diff --git a/Cargo.lock b/Cargo.lock index 7da72e6d4d..311a864b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4892,18 +4892,18 @@ dependencies = [ "getset", "hex", "humantime-serde", - "nym-compact-ecash 0.1.0", - "nym-config 0.1.0", - "nym-contracts-common 0.5.0", - "nym-credentials-interface 0.1.0", - "nym-crypto 0.4.0", - "nym-ecash-time 0.1.0", - "nym-mixnet-contract-common 0.6.0", - "nym-network-defaults 0.1.0", - "nym-node-requests 0.1.0", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-node-requests", "nym-noise-keys", - "nym-serde-helpers 0.1.0", - "nym-ticketbooks-merkle 0.1.0", + "nym-serde-helpers", + "nym-ticketbooks-merkle", "rand_chacha 0.3.1", "schemars", "serde", @@ -6530,7 +6530,7 @@ dependencies = [ "nym-mixnet-client", "nym-network-requester", "nym-node-metrics", - "nym-node-requests 0.1.0", + "nym-node-requests", "nym-noise", "nym-noise-keys", "nym-nonexhaustive-delayqueue", @@ -6593,12 +6593,12 @@ dependencies = [ "celes", "humantime", "humantime-serde", - "nym-bin-common 0.6.0", - "nym-crypto 0.4.0", - "nym-exit-policy 0.1.0", - "nym-http-api-client 0.1.0", + "nym-bin-common", + "nym-crypto", + "nym-exit-policy", + "nym-http-api-client", "nym-noise-keys", - "nym-wireguard-types 0.1.0", + "nym-wireguard-types", "rand_chacha 0.3.1", "schemars", "serde", @@ -6785,7 +6785,7 @@ dependencies = [ "nym-noise-keys", "pin-project", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "snow", "thiserror 2.0.12", "tokio", @@ -10095,7 +10095,7 @@ dependencies = [ "curve25519-dalek", "rand_core 0.6.4", "rustc_version 0.4.1", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", ] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 363296f15d..fcf5a75bc9 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -25,9 +25,7 @@ use nym_api_requests::models::{ NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; -use nym_api_requests::nym_nodes::{ - NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, -}; +use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode}; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_mixnet_contract_common::EpochRewardedSet; @@ -567,6 +565,31 @@ impl NymApiClient { Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)) } + /// retrieve expanded information for all bonded nodes on the network + pub async fn get_all_expanded_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_expanded_nodes(false, Some(page), None) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + pub async fn health(&self) -> Result { Ok(self.nym_api.health().await?) } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 8cdaadf763..7124ef951f 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -18,8 +18,8 @@ use nym_api_requests::models::{ NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, }; use nym_api_requests::nym_nodes::{ - NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1, - PaginatedCachedNodesResponseV2, + NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, + SemiSkimmedNode, }; use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ @@ -676,6 +676,39 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] + async fn get_expanded_nodes( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "semi-skimmed", + ], + ¶ms, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a9c5c2ccd4..4b1df5ccfe 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -896,29 +896,7 @@ pub struct HostKeys { pub current_x25519_sphinx_key: SphinxKey, #[serde(default)] - pub pre_announced_x25519_sphinx_key: Option, - - #[serde(default)] - pub x25519_versioned_noise: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct SphinxKey { - pub rotation_id: u32, - - #[serde(with = "bs58_x25519_pubkey")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub public_key: x25519::PublicKey, -} - -impl From for SphinxKey { - fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self { - SphinxKey { - rotation_id: value.rotation_id, - public_key: value.public_key, - } - } + pub x25519_noise: Option, } impl From for HostKeys { @@ -1108,19 +1086,14 @@ impl NymNodeDescription { pub fn to_semi_skimmed_node( &self, - current_rotation_id: u32, role: NodeRole, performance: Performance, ) -> SemiSkimmedNode { - let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance); + let skimmed_node = self.to_skimmed_node(role, performance); SemiSkimmedNode { basic: skimmed_node, - x25519_noise_versioned_key: self - .description - .host_information - .keys - .x25519_versioned_noise, + x25519_noise_versioned_key: self.description.host_information.keys.x25519_noise, } } } diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 6a19eb76d9..cc58b6e9a8 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -8,7 +8,7 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{EpochId, Interval, NodeId}; +use nym_mixnet_contract_common::{Interval, NodeId}; use nym_noise_keys::VersionedNoiseKey; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs index e83c220321..dd406e32fb 100644 --- a/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs @@ -1,13 +1,92 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::models::AxumResult; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation}; +use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole; use axum::extract::{Query, State}; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; +use axum::Json; +use nym_api_requests::models::{ + NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode}; +use nym_api_requests::pagination::PaginatedResponse; use nym_http_api_common::FormattedResponse; +use nym_mixnet_contract_common::NodeId; +use nym_topology::CachedEpochRewardedSet; +use std::collections::HashMap; +use tracing::trace; +use utoipa::ToSchema; +pub type PaginatedSemiSkimmedNodes = + AxumResult>>; + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedEpochRewardedSet, + nym_nodes_subset: NI, + annotations: &HashMap, +) -> Vec +where + NI: Iterator + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // 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_semi_skimmed_node(role, annotation.last_24h_performance)); + } + nodes +} + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy( + nodes: &mut Vec, + rewarded_set: &CachedEpochRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // 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_semi_skimmed_node(role, legacy.performance())) + } else { + match legacy.try_to_semi_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +#[allow(dead_code)] // not dead, used in OpenAPI docs +#[derive(ToSchema)] +#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")] +pub struct PaginatedCachedNodesExpandedResponseSchema { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + #[schema(value_type = SemiSkimmedNode)] + pub nodes: PaginatedResponse, +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. #[utoipa::path( tag = "Unstable Nym Nodes", get, @@ -15,13 +94,41 @@ use nym_http_api_common::FormattedResponse; path = "", context_path = "/v1/unstable/nym-nodes/semi-skimmed", responses( - // (status = 200, body = CachedNodesResponse) - (status = 501) + (status = 200, body = PaginatedCachedNodesExpandedResponseSchema) ) )] -pub(crate) async fn nodes_expanded( - _state: State, +pub(super) async fn nodes_expanded( + state: State, _query_params: Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) +) -> PaginatedSemiSkimmedNodes { + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations); + + // add legacy gateways to the response + add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways); + + // add legacy mixnodes to the response + add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) }