From 0c653cef51534d6500807fba53e3e91f55f5108d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 14 May 2025 17:05:31 +0100 Subject: [PATCH] retrieving rotation id when pulling topology --- .../topology_control/nym_api_provider.rs | 67 +++++++--- common/client-core/src/init/helpers.rs | 5 +- .../validator-client/src/client.rs | 124 ++++++++++++++++-- .../client-libs/validator-client/src/error.rs | 3 + common/nymsphinx/params/src/key_rotation.rs | 6 +- common/topology/src/lib.rs | 50 ++++++- common/types/src/error.rs | 5 + nym-api/nym-api-requests/src/nym_nodes.rs | 75 +++++++++-- .../src/network_monitor/monitor/preparer.rs | 9 +- nym-api/src/network_monitor/test_route/mod.rs | 9 +- .../nym_nodes/handlers/unstable/skimmed.rs | 25 +++- nym-api/src/support/nyxd/mod.rs | 4 +- nym-node/src/node/shared_network.rs | 41 ++++-- 13 files changed, 354 insertions(+), 69 deletions(-) diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 69a4a8107d..3c060f36ea 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use log::{debug, error, warn}; use nym_topology::provider_trait::TopologyProvider; -use nym_topology::NymTopology; +use nym_topology::{NymTopology, NymTopologyMetadata}; use nym_validator_client::UserAgent; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -89,55 +89,84 @@ impl NymApiTopologyProvider { let rewarded_set_fut = self.validator_client.get_current_rewarded_set(); let topology = if self.config.use_extended_topology { - let all_nodes_fut = self.validator_client.get_all_basic_nodes(); + let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata(); // Join rewarded_set_fut and all_nodes_fut concurrently - let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut) + let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut) .inspect_err(|err| error!("failed to get network nodes: {err}")) .ok()?; + let metadata = all_nodes_res.metadata; + let all_nodes = all_nodes_res.nodes; + debug!( "there are {} nodes on the network (before filtering)", all_nodes.len() ); - let mut topology = NymTopology::new_empty(rewarded_set); - topology.add_additional_nodes(all_nodes.iter().filter(|n| { - n.performance.round_to_integer() >= self.config.min_node_performance() - })); + let nodes_filtered = all_nodes + .into_iter() + .filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance()) + .collect::>(); - topology + NymTopology::new( + NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id), + rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes_filtered) } else { // if we're not using extended topology, we're only getting active set mixnodes and gateways let mixnodes_fut = self .validator_client - .get_all_basic_active_mixing_assigned_nodes(); + .get_all_basic_active_mixing_assigned_nodes_with_metadata(); // TODO: we really should be getting ACTIVE gateways only - let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes(); + let gateways_fut = self + .validator_client + .get_all_basic_entry_assigned_nodes_with_metadata(); - let (rewarded_set, mixnodes, gateways) = + let (rewarded_set, mixnodes_res, gateways_res) = futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut) .inspect_err(|err| { error!("failed to get network nodes: {err}"); }) .ok()?; + let metadata = mixnodes_res.metadata; + let mixnodes = mixnodes_res.nodes; + + if gateways_res.metadata != metadata { + warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata); + return None; + } + + let gateways = gateways_res.nodes; + debug!( "there are {} mixnodes and {} gateways in total (before performance filtering)", mixnodes.len(), gateways.len() ); - let mut topology = NymTopology::new_empty(rewarded_set); - topology.add_additional_nodes(mixnodes.iter().filter(|m| { - m.performance.round_to_integer() >= self.config.min_mixnode_performance - })); - topology.add_additional_nodes(gateways.iter().filter(|m| { - m.performance.round_to_integer() >= self.config.min_gateway_performance - })); + let mut nodes = Vec::new(); + for mix in mixnodes { + if mix.performance.round_to_integer() >= self.config.min_mixnode_performance { + nodes.push(mix) + } + } + for gateway in gateways { + if gateway.performance.round_to_integer() >= self.config.min_gateway_performance { + nodes.push(gateway) + } + } - topology + NymTopology::new( + NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id), + rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes) }; if !topology.is_minimally_routable() { diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 0848b27f86..da92fe093a 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -107,7 +107,10 @@ pub async fn gateways_for_init( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_all_basic_entry_assigned_nodes().await?; + let gateways = client + .get_all_basic_entry_assigned_nodes_with_metadata() + .await? + .nodes; info!("nym api reports {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 93062570b5..c018574c2c 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -25,7 +25,9 @@ use nym_api_requests::models::{ NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; -use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_api_requests::nym_nodes::{ + NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata, +}; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_mixnet_contract_common::EpochRewardedSet; @@ -425,12 +427,34 @@ impl NymApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")] pub async fn get_all_basic_entry_assigned_nodes( &self, ) -> Result, ValidatorClientError> { + self.get_all_basic_entry_assigned_nodes_with_metadata() + .await + .map(|res| res.nodes) + } + + pub async fn get_all_basic_entry_assigned_nodes_with_metadata( + &self, + ) -> Result { // 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(); + + // unroll first loop iteration in order to obtain the metadata + let res = self + .nym_api + .get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; loop { let mut res = self @@ -438,6 +462,10 @@ impl NymApiClient { .get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode) .await?; + if metadata != res.metadata { + return Err(ValidatorClientError::InconsistentPagedMetadata); + } + nodes.append(&mut res.nodes.data); if nodes.len() < res.nodes.pagination.total { page += 1 @@ -446,17 +474,39 @@ impl NymApiClient { } } - Ok(nodes) + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")] pub async fn get_all_basic_active_mixing_assigned_nodes( &self, ) -> Result, ValidatorClientError> { + self.get_all_basic_active_mixing_assigned_nodes_with_metadata() + .await + .map(|res| res.nodes) + } + + pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata( + &self, + ) -> Result { // 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(); + + // unroll first loop iteration in order to obtain the metadata + let res = self + .nym_api + .get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; loop { let mut res = self @@ -464,6 +514,10 @@ impl NymApiClient { .get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode) .await?; + if metadata != res.metadata { + return Err(ValidatorClientError::InconsistentPagedMetadata); + } + nodes.append(&mut res.nodes.data); if nodes.len() < res.nodes.pagination.total { page += 1 @@ -472,17 +526,39 @@ impl NymApiClient { } } - Ok(nodes) + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } /// retrieve basic information for nodes are capable of operating as a mixnode /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")] pub async fn get_all_basic_mixing_capable_nodes( &self, ) -> Result, ValidatorClientError> { + self.get_all_basic_mixing_capable_nodes_with_metadata() + .await + .map(|res| res.nodes) + } + + pub async fn get_all_basic_mixing_capable_nodes_with_metadata( + &self, + ) -> Result { // 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(); + + // unroll first loop iteration in order to obtain the metadata + let res = self + .nym_api + .get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; loop { let mut res = self @@ -490,6 +566,10 @@ impl NymApiClient { .get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode) .await?; + if metadata != res.metadata { + return Err(ValidatorClientError::InconsistentPagedMetadata); + } + nodes.append(&mut res.nodes.data); if nodes.len() < res.nodes.pagination.total { page += 1 @@ -498,14 +578,36 @@ impl NymApiClient { } } - Ok(nodes) + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } /// retrieve basic information for all bonded nodes on the network + #[deprecated(note = "use get_all_basic_nodes_with_metadata instead")] pub async fn get_all_basic_nodes(&self) -> Result, ValidatorClientError> { + self.get_all_basic_nodes_with_metadata() + .await + .map(|res| res.nodes) + } + + pub async fn get_all_basic_nodes_with_metadata( + &self, + ) -> Result { // 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(); + + // unroll first loop iteration in order to obtain the metadata + let res = self + .nym_api + .get_basic_nodes(false, Some(page), None, self.use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; loop { let mut res = self @@ -513,6 +615,10 @@ impl NymApiClient { .get_basic_nodes(false, Some(page), None, self.use_bincode) .await?; + if metadata != res.metadata { + return Err(ValidatorClientError::InconsistentPagedMetadata); + } + nodes.append(&mut res.nodes.data); if nodes.len() < res.nodes.pagination.total { page += 1 @@ -521,7 +627,7 @@ impl NymApiClient { } } - Ok(nodes) + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } pub async fn health(&self) -> Result { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index 11bdb3d745..6acbc73c78 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -22,6 +22,9 @@ pub enum ValidatorClientError { #[error("nyxd request failed: {0}")] NyxdError(#[from] crate::nyxd::error::NyxdError), + #[error("the response metadata has changed between pages")] + InconsistentPagedMetadata, + #[error("No validator API url has been provided")] NoAPIUrlAvailable, } diff --git a/common/nymsphinx/params/src/key_rotation.rs b/common/nymsphinx/params/src/key_rotation.rs index b57c3e49c6..7c6e6179af 100644 --- a/common/nymsphinx/params/src/key_rotation.rs +++ b/common/nymsphinx/params/src/key_rotation.rs @@ -21,9 +21,12 @@ pub struct InvalidSphinxKeyRotation { received: u8, } +// convert from particular rotation id into SphinxKeyRotation variant impl From for SphinxKeyRotation { fn from(value: u32) -> Self { - if value % 2 == 0 { + if value == 0 || value == u32::MAX { + SphinxKeyRotation::Unknown + } else if value % 2 == 0 { SphinxKeyRotation::EvenRotation } else { SphinxKeyRotation::OddRotation @@ -31,6 +34,7 @@ impl From for SphinxKeyRotation { } } +// convert from an encoded SphinxKeyRotation into particular variant // if value is actually provided, it MUST be one of the two. otherwise is invalid impl TryFrom for SphinxKeyRotation { type Error = InvalidSphinxKeyRotation; diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 261d1de54d..53886ed5fd 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,6 +3,8 @@ use ::serde::{Deserialize, Serialize}; use nym_api_requests::nym_nodes::SkimmedNode; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::EpochId; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::IteratorRandom; @@ -15,7 +17,6 @@ use tracing::{debug, trace, warn}; pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; pub use error::NymTopologyError; -use nym_crypto::asymmetric::ed25519; pub use nym_mixnet_contract_common::nym_node::Role; pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId}; pub use rewarded_set::CachedEpochRewardedSet; @@ -92,8 +93,39 @@ mod deprecated_network_address_impls { pub type MixLayer = u8; +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct NymTopologyMetadata { + key_rotation_id: u32, + // we have to keep track of key rotation id anyway, so we might as well also include the epoch id + // to keep track of the data staleness + absolute_epoch_id: EpochId, +} + +impl NymTopologyMetadata { + pub fn new(key_rotation_id: u32, absolute_epoch_id: EpochId) -> Self { + NymTopologyMetadata { + key_rotation_id, + absolute_epoch_id, + } + } +} + +impl Default for NymTopologyMetadata { + fn default() -> Self { + // that's not ideal, but we don't want to break backwards compatibility : / + NymTopologyMetadata { + key_rotation_id: u32::MAX, + absolute_epoch_id: 0, + } + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct NymTopology { + // while this is not ideal, use empty values as default to not break backwards compatibility + #[serde(default)] + metadata: NymTopologyMetadata, + // for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering // so we use the same 'master' rewarded set information for that // @@ -130,8 +162,11 @@ impl NymRouteProvider { } pub fn current_key_rotation(&self) -> u32 { - todo!() - // self.topology.rewarded_set.epoch_id + self.topology.metadata.key_rotation_id + } + + pub fn absolute_epoch_id(&self) -> EpochId { + self.topology.metadata.absolute_epoch_id } pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider { @@ -207,18 +242,22 @@ impl NymRouteProvider { } impl NymTopology { + #[deprecated] pub fn new_empty(rewarded_set: impl Into) -> Self { NymTopology { + metadata: NymTopologyMetadata::default(), rewarded_set: rewarded_set.into(), node_details: Default::default(), } } pub fn new( + metadata: NymTopologyMetadata, rewarded_set: impl Into, node_details: Vec, ) -> Self { NymTopology { + metadata, rewarded_set: rewarded_set.into(), node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), } @@ -234,6 +273,11 @@ impl NymTopology { self.add_additional_nodes(nodes.iter()) } + pub fn with_skimmed_nodes(mut self, nodes: &[SkimmedNode]) -> Self { + self.add_skimmed_nodes(nodes); + self + } + pub fn add_routing_nodes>( &mut self, nodes: impl IntoIterator, diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 2a4595a495..823ec8b375 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -84,6 +84,8 @@ pub enum TypesError { NotADelegationEvent, #[error("Unknown network - {0}")] UnknownNetwork(String), + #[error("the response metadata has changed between pages")] + InconsistentPagedMetadata, } impl Serialize for TypesError { @@ -103,6 +105,9 @@ impl From for TypesError { ValidatorClientError::NyxdError(e) => e.into(), ValidatorClientError::NoAPIUrlAvailable => TypesError::NoNymApiUrlConfigured, ValidatorClientError::TendermintErrorRpc(err) => err.into(), + ValidatorClientError::InconsistentPagedMetadata => { + TypesError::InconsistentPagedMetadata + } } } } diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index ffe2a82f4e..b4dc015d13 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -8,14 +8,28 @@ 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::{Interval, NodeId}; +use nym_mixnet_contract_common::{EpochId, Interval, NodeId}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::IpAddr; use time::OffsetDateTime; use utoipa::ToSchema; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct SkimmedNodesWithMetadata { + pub nodes: Vec, + pub metadata: NodesResponseMetadata, +} + +impl SkimmedNodesWithMetadata { + pub fn new(nodes: Vec, metadata: NodesResponseMetadata) -> Self { + SkimmedNodesWithMetadata { nodes, metadata } + } +} + +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq, +)] #[serde(rename_all = "kebab-case")] pub enum TopologyRequestStatus { NoUpdates, @@ -43,22 +57,52 @@ impl CachedNodesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] -pub struct PaginatedCachedNodesResponse { +#[derive( + Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq, +)] +pub struct NodesResponseMetadata { pub status: Option, + #[schema(value_type = u32)] + pub absolute_epoch_id: EpochId, pub rotation_id: u32, pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, +} + +impl NodesResponseMetadata { + pub fn refreshed_at(&self) -> OffsetDateTime { + self.refreshed_at.into() + } +} + +const TODO: &str = + "create new endpoints with metadata after poc works for backwards bincode compat..."; + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +// can't add any new fields here, even with #[serde(default)] and whatnot, +// because it will break all clients using bincode : ( +pub struct LegacyPaginatedCachedNodesResponse { + pub status: Option, + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + pub nodes: PaginatedResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaginatedCachedNodesResponse { + // make sure to flatten it to preserve backwards compatibility!! + #[serde(flatten)] + pub metadata: NodesResponseMetadata, + pub nodes: PaginatedResponse, } impl PaginatedCachedNodesResponse { pub fn new_full( + absolute_epoch_id: EpochId, rotation_id: u32, refreshed_at: impl Into, nodes: Vec, ) -> Self { PaginatedCachedNodesResponse { - refreshed_at: refreshed_at.into(), nodes: PaginatedResponse { pagination: Pagination { total: nodes.len(), @@ -67,19 +111,22 @@ impl PaginatedCachedNodesResponse { }, data: nodes, }, - status: None, - rotation_id, + metadata: NodesResponseMetadata { + refreshed_at: refreshed_at.into(), + status: None, + absolute_epoch_id, + rotation_id, + }, } } pub fn fresh(mut self, interval: Interval) -> Self { - self.status = Some(TopologyRequestStatus::Fresh(interval)); + self.metadata.status = Some(TopologyRequestStatus::Fresh(interval)); self } - pub fn no_updates(rotation_id: u32) -> Self { + pub fn no_updates(absolute_epoch_id: EpochId, rotation_id: u32) -> Self { PaginatedCachedNodesResponse { - refreshed_at: OffsetDateTime::now_utc().into(), nodes: PaginatedResponse { pagination: Pagination { total: 0, @@ -88,8 +135,12 @@ impl PaginatedCachedNodesResponse { }, data: Vec::new(), }, - status: Some(TopologyRequestStatus::NoUpdates), - rotation_id, + metadata: NodesResponseMetadata { + refreshed_at: OffsetDateTime::now_utc().into(), + status: Some(TopologyRequestStatus::NoUpdates), + absolute_epoch_id, + rotation_id, + }, } } } diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 4075ff5696..df622105e1 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -416,7 +416,14 @@ impl PacketPreparer { let node_3 = rand_l3[i].clone(); let gateway = rand_gateways[i].clone(); - routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway)) + routes.push(TestRoute::new( + rng.gen(), + current_rotation_id, + node_1, + node_2, + node_3, + gateway, + )) } info!("The following routes will be used for testing: {routes:#?}"); Some(routes) diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index fd59a6419b..6280ba9f01 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -7,7 +7,7 @@ use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, RewardedSet}; use nym_topology::node::RoutingNode; -use nym_topology::{NymRouteProvider, NymTopology}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyMetadata}; use std::fmt::{Debug, Formatter}; #[derive(Clone)] @@ -19,6 +19,7 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, + key_rotation_id: u32, l1_mix: RoutingNode, l2_mix: RoutingNode, l3_mix: RoutingNode, @@ -40,7 +41,11 @@ impl TestRoute { TestRoute { id, - nodes: NymTopology::new(fake_rewarded_set, nodes), + nodes: NymTopology::new( + NymTopologyMetadata::new(key_rotation_id, 0), + fake_rewarded_set, + nodes, + ), } } diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 93e644246e..b467892c70 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -143,6 +143,7 @@ where if let Some(client_known_epoch) = query_params.epoch_id { if client_known_epoch == interval.current_epoch_id() { return Ok(output.to_response(PaginatedCachedNodesResponse::no_updates( + interval.current_epoch_absolute_id(), current_key_rotation, ))); } @@ -167,8 +168,13 @@ where ]); return Ok(output.to_response( - PaginatedCachedNodesResponse::new_full(current_key_rotation, refreshed_at, nodes) - .fresh(interval), + PaginatedCachedNodesResponse::new_full( + interval.current_epoch_absolute_id(), + current_key_rotation, + refreshed_at, + nodes, + ) + .fresh(interval), )); } @@ -193,8 +199,13 @@ where ]); let base_response = output.to_response( - PaginatedCachedNodesResponse::new_full(current_key_rotation, refreshed_at, nodes) - .fresh(interval), + PaginatedCachedNodesResponse::new_full( + interval.current_epoch_absolute_id(), + current_key_rotation, + refreshed_at, + nodes, + ) + .fresh(interval), ); if !active_only { @@ -237,7 +248,7 @@ pub(super) async fn deprecated_gateways_basic( // 3. return result Ok(output.to_response(CachedNodesResponse { - refreshed_at: all_gateways.refreshed_at, + refreshed_at: all_gateways.metadata.refreshed_at, // 2. remove pagination nodes: all_gateways.nodes.data, })) @@ -272,7 +283,7 @@ pub(super) async fn deprecated_mixnodes_basic( // 3. return result Ok(output.to_response(CachedNodesResponse { - refreshed_at: active_mixnodes.refreshed_at, + refreshed_at: active_mixnodes.metadata.refreshed_at, // 2. remove pagination nodes: active_mixnodes.nodes.data, })) @@ -296,6 +307,7 @@ async fn nodes_basic( let legacy_mixnodes = state.legacy_mixnode_annotations().await?; let legacy_gateways = state.legacy_gateways_annotations().await?; + let interval = state.nym_contract_cache().current_interval().await?; let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?; let mut nodes = build_nym_nodes_response( @@ -337,6 +349,7 @@ async fn nodes_basic( Ok(output.to_response(PaginatedCachedNodesResponse::new_full( current_key_rotation, + interval.current_epoch_absolute_id(), refreshed_at, nodes, ))) diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 9f0dfd46ad..41ef7d2bc4 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -30,8 +30,8 @@ use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ ConfigScoreParams, CurrentIntervalResponse, Delegation, EpochRewardedSet, EpochStatus, - ExecuteMsg, GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, KeyRotationState, NymNodeDetails, - RewardedSet, RoleAssignment, + ExecuteMsg, GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, KeyRotationState, + NymNodeDetails, RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index d7d6ebe8d1..0197b27018 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -10,9 +10,14 @@ use nym_gateway::node::UserAgent; use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; use nym_task::ShutdownToken; use nym_topology::node::RoutingNode; -use nym_topology::{EntryDetails, EpochRewardedSet, NodeId, NymTopology, Role, TopologyProvider}; +use nym_topology::{ + EntryDetails, EpochRewardedSet, NodeId, NymTopology, NymTopologyMetadata, Role, + TopologyProvider, +}; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_validator_client::nym_nodes::{ + NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata, +}; use nym_validator_client::{NymApiClient, ValidatorClientError}; use std::collections::HashSet; use std::net::{IpAddr, SocketAddr}; @@ -58,10 +63,10 @@ impl NodesQuerier { res } - async fn current_nymnodes(&mut self) -> Result, ValidatorClientError> { + async fn current_nymnodes(&mut self) -> Result { let res = self .client - .get_all_basic_nodes() + .get_all_basic_nodes_with_metadata() .await .inspect_err(|err| error!("failed to get network nodes: {err}")); @@ -140,14 +145,18 @@ impl TopologyProvider for CachedTopologyProvider { let network_guard = self.cached_network.inner.read().await; let self_node = self.gateway_node.identity_key; - let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone()) - .with_additional_nodes(network_guard.network_nodes.iter().filter(|node| { - if node.supported_roles.mixnode { - node.performance.round_to_integer() >= self.min_mix_performance - } else { - true - } - })); + let mut topology = NymTopology::new( + network_guard.topology_metadata, + network_guard.rewarded_set.clone(), + Vec::new(), + ) + .with_additional_nodes(network_guard.network_nodes.iter().filter(|node| { + if node.supported_roles.mixnode { + node.performance.round_to_integer() >= self.min_mix_performance + } else { + true + } + })); if !topology.has_node(self.gateway_node.identity_key) { debug!("{self_node} didn't exist in topology. inserting it.",); @@ -169,6 +178,7 @@ impl CachedNetwork { CachedNetwork { inner: Arc::new(RwLock::new(CachedNetworkInner { rewarded_set: Default::default(), + topology_metadata: Default::default(), network_nodes: vec![], })), } @@ -177,6 +187,7 @@ impl CachedNetwork { struct CachedNetworkInner { rewarded_set: EpochRewardedSet, + topology_metadata: NymTopologyMetadata, network_nodes: Vec, } @@ -264,7 +275,9 @@ impl NetworkRefresher { async fn refresh_network_nodes_inner(&mut self) -> Result<(), ValidatorClientError> { let rewarded_set = self.querier.rewarded_set().await?; - let nodes = self.querier.current_nymnodes().await?; + let res = self.querier.current_nymnodes().await?; + let nodes = res.nodes; + let metadata = res.metadata; // collect all known/allowed nodes information let known_nodes = nodes @@ -293,6 +306,8 @@ impl NetworkRefresher { self.routing_filter.pending.clear().await; let mut network_guard = self.network.inner.write().await; + network_guard.topology_metadata = + NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id); network_guard.network_nodes = nodes; network_guard.rewarded_set = rewarded_set;