diff --git a/Cargo.lock b/Cargo.lock index 4d30ae54a3d..42fe98baef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6241,6 +6241,7 @@ dependencies = [ "nym-crypto", "nym-gateway", "nym-gateway-stats-storage", + "nym-http-api-client", "nym-http-api-common", "nym-ip-packet-router", "nym-metrics", @@ -6932,6 +6933,7 @@ version = "0.1.0" dependencies = [ "async-trait", "nym-api-requests", + "nym-client-core-config-types", "nym-config", "nym-crypto", "nym-mixnet-contract-common", @@ -6943,6 +6945,8 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.12", + "time", + "tokio", "tracing", "tsify", "wasm-bindgen", diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 38c976097f7..4edb130dd10 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -22,7 +22,7 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon use crate::client::replies::reply_storage::{ CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, }; -use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider; +use crate::client::topology_control::smart_api_provider::NymApiTopologyProvider; use crate::client::topology_control::{ TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, }; @@ -54,8 +54,7 @@ use nym_statistics_common::clients::ClientStatsSender; use nym_statistics_common::generate_client_stats_id; use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use nym_task::{TaskClient, TaskHandle}; -use nym_topology::provider_trait::TopologyProvider; -use nym_topology::HardcodedTopologyProvider; +use nym_topology::providers::{HardcodedTopologyProvider, TopologyProvider}; use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent}; use rand::rngs::OsRng; use std::fmt::Debug; @@ -557,6 +556,7 @@ where config_topology, nym_api_urls, user_agent, + None, )), config::TopologyStructure::GeoAware(group_by) => { warn!("using deprecated 'GeoAware' topology provider - this option will be removed very soon"); diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 459209a977c..4979f2c2023 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -3,7 +3,7 @@ use log::{debug, error}; use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ - provider_trait::{async_trait, TopologyProvider}, + providers::{async_trait, TopologyProvider}, NymTopology, }; use nym_validator_client::client::NodeId; diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index 42d096c54fa..f15f8b59f9f 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -19,11 +19,12 @@ use wasmtimer::tokio::sleep; mod accessor; pub mod geo_aware_provider; pub mod nym_api_provider; +pub mod smart_api_provider; #[allow(deprecated)] pub use geo_aware_provider::GeoAwareTopologyProvider; -pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; -pub use nym_topology::provider_trait::TopologyProvider; +pub use nym_topology::providers::TopologyProvider; +pub use smart_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; // TODO: move it to config later const MAX_FAILURE_COUNT: usize = 10; 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 30d2461abd7..b2f0a395780 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 @@ -3,8 +3,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, TopologyProvider}; use nym_validator_client::UserAgent; use rand::prelude::SliceRandom; use rand::thread_rng; diff --git a/common/client-core/src/client/topology_control/smart_api_provider.rs b/common/client-core/src/client/topology_control/smart_api_provider.rs new file mode 100644 index 00000000000..d75bfc9bf71 --- /dev/null +++ b/common/client-core/src/client/topology_control/smart_api_provider.rs @@ -0,0 +1,230 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! Caching, piecewise API Topology Provider +//! + +#![warn(missing_docs)] + +use async_trait::async_trait; +use log::{debug, error, warn}; +pub use nym_topology::providers::piecewise::Config; +use nym_topology::{ + providers::piecewise::{NymTopologyProvider, PiecewiseTopologyProvider}, + EpochRewardedSet, NymTopology, RoutingNode, TopologyProvider, +}; +use nym_validator_client::UserAgent; +use rand::{prelude::SliceRandom, thread_rng}; +use url::Url; + +/// Topology Provider build around a cached piecewise provider that uses the Nym API to +/// fetch changes and node details. +#[derive(Clone)] +pub struct NymApiTopologyProvider { + inner: NymTopologyProvider, +} + +impl NymApiTopologyProvider { + /// Construct a new thread safe Cached topology provider using the Nym API + pub fn new( + config: impl Into, + nym_api_urls: Vec, + user_agent: Option, + initial_topology: Option, + ) -> Self { + let manager = NymApiPiecewiseProvider::new(nym_api_urls, user_agent); + let inner = NymTopologyProvider::new(manager, config.into(), initial_topology); + + Self { inner } + } +} + +impl AsRef> for NymApiTopologyProvider { + fn as_ref(&self) -> &NymTopologyProvider { + &self.inner + } +} + +impl AsMut> for NymApiTopologyProvider { + fn as_mut(&mut self) -> &mut NymTopologyProvider { + &mut self.inner + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl TopologyProvider for NymApiTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + self.as_mut().get_new_topology().await + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl TopologyProvider for NymApiTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + self.as_mut().get_new_topology().await + } +} + +#[derive(Clone)] +struct NymApiPiecewiseProvider { + validator_client: nym_validator_client::client::NymApiClient, + nym_api_urls: Vec, + currently_used_api: usize, +} + +impl NymApiPiecewiseProvider { + fn new(mut nym_api_urls: Vec, user_agent: Option) -> Self { + nym_api_urls.shuffle(&mut thread_rng()); + + let validator_client = if let Some(user_agent) = user_agent { + nym_validator_client::client::NymApiClient::new_with_user_agent( + nym_api_urls[0].clone(), + user_agent, + ) + } else { + nym_validator_client::client::NymApiClient::new(nym_api_urls[0].clone()) + }; + + Self { + validator_client, + nym_api_urls, + currently_used_api: 0, + } + } + + fn use_next_nym_api(&mut self) { + if self.nym_api_urls.len() == 1 { + warn!("There's only a single nym API available - it won't be possible to use a different one"); + return; + } + + self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); + self.validator_client + .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + } + + async fn get_full_topology_inner(&mut self) -> Option { + let layer_assignments = self.get_layer_assignments().await?; + + let mut topology = NymTopology::new_empty(layer_assignments); + + let all_nodes = self + .validator_client + .get_all_basic_nodes() + .await + .inspect_err(|err| { + self.use_next_nym_api(); + error!("failed to get network nodes: {err}"); + }) + .ok()?; + + debug!("there are {} nodes on the network", all_nodes.len()); + topology.add_additional_nodes(all_nodes.iter()); + + if !topology.is_minimally_routable() { + error!("the current filtered active topology can't be used to construct any packets"); + return None; + } + + Some(topology) + } + + async fn get_descriptor_batch_inner(&mut self, ids: &[u32]) -> Option> { + // Does this need to return a hashmap of RoutingNodes? that is moderately inconvenient + // especially when the nodes themselves contain their node_id unless we expect to directly + // use the result of this fn for lookups where we would otherwise for example, have to + // iterate over a whole vec to find a specific node_id. + let descriptor_vec = self + .validator_client + .retrieve_basic_nodes_batch(ids) + .await + .inspect_err(|err| { + self.use_next_nym_api(); + error!("failed to get current rewarded set: {err}"); + }) + .ok()?; + + let mut out = Vec::new(); + for node in descriptor_vec { + if let Ok(routing_node) = RoutingNode::try_from(&node) { + out.push(routing_node); + } + } + Some(out) + } + + async fn get_layer_assignments_inner(&mut self) -> Option { + self.validator_client + .get_current_rewarded_set() + .await + .inspect_err(|err| { + self.use_next_nym_api(); + error!("failed to get current rewarded set: {err}"); + }) + .ok() + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl PiecewiseTopologyProvider for NymApiPiecewiseProvider { + async fn get_full_topology(&mut self) -> Option { + self.get_full_topology_inner().await + } + + async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option> { + self.get_descriptor_batch_inner(ids).await + } + + async fn get_layer_assignments(&mut self) -> Option { + self.get_layer_assignments_inner().await + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl PiecewiseTopologyProvider for NymApiPiecewiseProvider { + async fn get_full_topology(&mut self) -> Option { + self.get_full_topology_inner().await + } + + async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option> { + self.get_descriptor_batch_inner(ids).await + } + + async fn get_layer_assignments(&mut self) -> Option { + self.get_layer_assignments_inner().await + } +} + +// // Test requires running a local instance of the nym-api binary, for example using: +// // `RUST_LOG="info" ./target/debug/nym-api run --nyxd-validator "https://rpc.nymtech.net"` + +// #[cfg(test)] +// mod test { +// use std::time::Duration; + +// use super::*; +// use nym_bin_common::logging::setup_tracing_logger; + +// #[tokio::test] +// async fn local_api_provider_test() { +// setup_tracing_logger(); +// let mut provider = NymApiTopologyProvider::new( +// Config::default(), +// vec!["http://localhost:8000" +// .parse() +// .expect("failed to parse api url")], +// None, +// None, +// ); + +// for _ in 0..180 { +// let topo = provider.get_new_topology().await; +// assert!(topo.is_some()); +// tokio::time::sleep(Duration::from_secs(30)).await; +// } +// } +// } diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index ba1d80f4c45..dec66cfb809 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -14,7 +14,8 @@ pub mod error; pub mod init; pub use nym_topology::{ - HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError, TopologyProvider, + providers::HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError, + TopologyProvider, }; #[cfg(target_arch = "wasm32")] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c41ba519d45..d91c4cd99d4 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -498,6 +498,21 @@ impl NymApiClient { Ok(nodes) } + /// Batch request for node descriptors in the current topology. + /// + /// Given the set of node IDs included in the request body, provide the descriptor for each + /// associated node in if it is available in the current topology. + pub async fn retrieve_basic_nodes_batch( + &self, + node_ids: &[u32], + ) -> Result, ValidatorClientError> { + Ok(self + .nym_api + .retrieve_basic_nodes_batch(node_ids) + .await? + .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 a9b1bc54d52..de15c43a656 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -270,10 +270,10 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, "mixnodes", - "skimmed", + routes::SKIMMED, ], NO_PARAMS, ) @@ -286,10 +286,10 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, "gateways", - "skimmed", + routes::SKIMMED, ], NO_PARAMS, ) @@ -335,9 +335,9 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, - "skimmed", + routes::SKIMMED, "entry-gateways", "all", ], @@ -372,9 +372,9 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, - "skimmed", + routes::SKIMMED, "mixnodes", "active", ], @@ -409,9 +409,9 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, - "skimmed", + routes::SKIMMED, "mixnodes", "all", ], @@ -420,6 +420,31 @@ pub trait NymApiClientExt: ApiClient { .await } + /// Send a Post request with a set of node ids. A successful response will contain descriptors + /// for all nodes associated with those node IDs available in the current full topology. + /// + /// If a provided node ID is not present there will be no descriptor for that node in the response. + /// + /// If no node IDs are provided the response will contain no descriptors. + #[instrument(level = "debug", skip(self))] + async fn retrieve_basic_nodes_batch( + &self, + node_ids: &[NodeId], + ) -> Result, NymAPIError> { + self.post_json( + &[ + routes::API_VERSION, + routes::UNSTABLE, + routes::NYM_NODES_ROUTES, + routes::SKIMMED, + routes::BATCH, + ], + NO_PARAMS, + node_ids, + ) + .await + } + #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, @@ -444,9 +469,9 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - "unstable", + routes::UNSTABLE, routes::NYM_NODES_ROUTES, - "skimmed", + routes::SKIMMED, ], ¶ms, ) diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index 8787264c4a3..084755550a5 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -74,3 +74,7 @@ pub const SERVICE_PROVIDERS: &str = "services"; pub const DETAILS: &str = "details"; pub const CHAIN_STATUS: &str = "chain-status"; pub const NETWORK: &str = "network"; + +pub const UNSTABLE: &str = "unstable"; +pub const SKIMMED: &str = "skimmed"; +pub const BATCH: &str = "batch"; diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index ba34832b045..77729a7548f 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -17,6 +17,9 @@ rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["macros", "sync"] } + # 'serde' feature serde_json = { workspace = true, optional = true } @@ -35,7 +38,9 @@ nym-sphinx-types = { path = "../nymsphinx/types", features = [ "outfox", ] } nym-sphinx-routing = { path = "../nymsphinx/routing" } - +nym-client-core-config-types = { path = "../client-core/config-types", features = [ + "disk-persistence", +] } # I'm not sure how to feel about pulling in this dependency here... nym-api-requests = { path = "../../nym-api/nym-api-requests" } @@ -45,8 +50,8 @@ nym-api-requests = { path = "../../nym-api/nym-api-requests" } wasm-utils = { path = "../wasm/utils", default-features = false, optional = true } [features] -default = ["provider-trait"] -provider-trait = ["async-trait"] +default = ["providers"] +providers = ["async-trait"] wasm-serde-types = ["tsify", "wasm-bindgen", "wasm-utils"] persistence = ["serde_json"] outfox = [] diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 4354721bc5e..c99a5a1d679 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -23,13 +23,13 @@ pub mod error; pub mod node; pub mod rewarded_set; -#[cfg(feature = "provider-trait")] -pub mod provider_trait; #[cfg(feature = "wasm-serde-types")] pub mod wasm_helpers; -#[cfg(feature = "provider-trait")] -pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider}; +#[cfg(feature = "providers")] +pub mod providers; +#[cfg(feature = "providers")] +pub use providers::TopologyProvider; #[deprecated] #[derive(Debug, Clone)] @@ -91,18 +91,6 @@ mod deprecated_network_address_impls { pub type MixLayer = u8; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct NymTopology { - // 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 - // - // how do we solve the problem of "we have to go through a node that we want to filter out?" - // ¯\_(ツ)_/¯ that's a future problem - rewarded_set: CachedEpochRewardedSet, - - node_details: HashMap, -} - #[derive(Clone, Debug, Default)] pub struct NymRouteProvider { pub topology: NymTopology, @@ -189,6 +177,18 @@ impl NymRouteProvider { } } +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct NymTopology { + // 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 + // + // how do we solve the problem of "we have to go through a node that we want to filter out?" + // ¯\_(ツ)_/¯ that's a future problem + rewarded_set: CachedEpochRewardedSet, + + node_details: HashMap, +} + impl NymTopology { pub fn new_empty(rewarded_set: impl Into) -> Self { NymTopology { @@ -546,4 +546,19 @@ impl NymTopology { .values() .filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id)) } + + pub fn all_nodes(&self) -> impl Iterator { + self.node_details.values() + } + + pub fn all_node_ids(&self) -> impl Iterator { + self.node_details.keys() + } + + pub fn gateways(&self) -> impl Iterator { + self.node_details.values().filter(|n| { + self.rewarded_set.entry_gateways.contains(&n.node_id) + || self.rewarded_set.exit_gateways.contains(&n.node_id) + }) + } } diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 47193e6438a..a6ce8e8566b 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; use thiserror::Error; +pub use nym_mixnet_contract_common::reward_params::Performance; pub use nym_mixnet_contract_common::LegacyMixLayer; #[derive(Error, Debug)] @@ -19,7 +20,7 @@ pub enum RoutingNodeError { NoIpAddressesProvided { node_id: NodeId, identity: String }, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct EntryDetails { // to allow client to choose ipv6 preference, if available pub ip_addresses: Vec, @@ -28,7 +29,7 @@ pub struct EntryDetails { pub clients_wss_port: Option, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub struct SupportedRoles { pub mixnode: bool, pub mixnet_entry: bool, @@ -45,7 +46,7 @@ impl From for SupportedRoles { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RoutingNode { pub node_id: NodeId, @@ -56,6 +57,7 @@ pub struct RoutingNode { pub sphinx_key: x25519::PublicKey, pub supported_roles: SupportedRoles, + pub performance: Performance, } impl RoutingNode { @@ -109,6 +111,12 @@ impl<'a> From<&'a RoutingNode> for SphinxNode { } } +impl<'a> From<&'a RoutingNode> for RoutingNode { + fn from(node: &'a RoutingNode) -> Self { + node.clone() + } +} + impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { type Error = RoutingNodeError; @@ -138,6 +146,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { identity_key: value.ed25519_identity_pubkey, sphinx_key: value.x25519_sphinx_pubkey, supported_roles: value.supported_roles.into(), + performance: value.performance, }) } } diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/providers/mod.rs similarity index 93% rename from common/topology/src/provider_trait.rs rename to common/topology/src/providers/mod.rs index ad8381fa7db..8f3c6edcf2c 100644 --- a/common/topology/src/provider_trait.rs +++ b/common/topology/src/providers/mod.rs @@ -4,7 +4,9 @@ use crate::NymTopology; pub use async_trait::async_trait; -// hehe, wasm +/// Cached Topology Provider built using efficient piecewise requests. +pub mod piecewise; + #[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait TopologyProvider: Send { diff --git a/common/topology/src/providers/piecewise.rs b/common/topology/src/providers/piecewise.rs new file mode 100644 index 00000000000..0b46ead8d41 --- /dev/null +++ b/common/topology/src/providers/piecewise.rs @@ -0,0 +1,482 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! +//! +#![warn(missing_docs)] + +use crate::{EpochRewardedSet, NymTopology, RoutingNode, TopologyProvider}; + +use async_trait::async_trait; +use time::OffsetDateTime; +use tokio::sync::Mutex; +use tracing::{debug, warn}; + +use std::{cmp::min, collections::HashSet, sync::Arc, time::Duration}; + +/// Topology filtering and caching configuration +#[derive(Debug)] +pub struct Config { + /// Specifies a minimum performance of a mixnode that is used on route construction. + /// This setting is only applicable when `NymApi` topology is used. + pub min_mixnode_performance: u8, + + /// Specifies a minimum performance of a gateway that is used on route construction. + /// This setting is only applicable when `NymApi` topology is used. + pub min_gateway_performance: u8, + + /// Specifies whether this client should attempt to retrieve all available network nodes + /// as opposed to just active mixnodes/gateways. + pub use_extended_topology: bool, + + /// Specifies whether this client should ignore the current epoch role of the target egress node + /// when constructing the final hop packets. + pub ignore_egress_epoch_role: bool, + + /// Minimum duration during which querying the topology will NOT attempt to re-fetch data, and + /// will be served from cache. + pub cache_ttl: Duration, +} + +impl Default for Config { + fn default() -> Self { + Self { + min_mixnode_performance: 50, + min_gateway_performance: 0, + use_extended_topology: false, + ignore_egress_epoch_role: true, + cache_ttl: Self::DEFAULT_TOPOLOGY_CACHE_TTL, + } + } +} + +impl From for Config { + fn from(value: nym_client_core_config_types::Topology) -> Self { + Config { + min_mixnode_performance: value.minimum_mixnode_performance, + min_gateway_performance: value.minimum_gateway_performance, + use_extended_topology: value.use_extended_topology, + ignore_egress_epoch_role: value.ignore_egress_epoch_role, + cache_ttl: value.topology_refresh_rate, + } + } +} + +impl Config { + /// Default duration during which the topology will be reproduced from cache. + pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(120); + + // if we're using 'extended' topology, filter the nodes based on the lowest set performance + fn min_node_performance(&self) -> u8 { + min(self.min_mixnode_performance, self.min_gateway_performance) + } +} + +/// Topology Provider build around a cached piecewise provider that uses the Nym API to +/// fetch changes and node details. +#[derive(Clone)] +pub struct NymTopologyProvider { + inner: Arc>>, +} + +impl NymTopologyProvider { + /// Construct a new thread safe Cached topology provider + pub fn new( + manager: M, + config: Config, + initial_topology: Option, + ) -> NymTopologyProvider { + let inner = NymTopologyProviderInner::new(config, manager, initial_topology); + + NymTopologyProvider { + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Bypass the caching for the topology and force a check for the latest updates next time the + /// topology is requested. This fn requires async to get lock in case other threads have access + /// to the cached topology state. + pub async fn force_refresh(&self) { + let mut guard = self.inner.lock().await; + guard.cached_at = OffsetDateTime::UNIX_EPOCH; + } + + /// Remove all stored topology state. The next time the topology is requested this will force a + /// pull of all topology information. This fn requires async to get lock in case other threads + /// have access to the cached topology state. + /// + /// WARNING: This may be slow / require non-trivial bandwidth. + pub async fn force_clear(&self) { + let mut guard = self.inner.lock().await; + guard.cached = None; + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl TopologyProvider for NymTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + let mut guard = self.inner.lock().await; + // check the cache + if let Some(cached) = guard.get_current_compatible_topology().await { + return Some(cached); + } + + // not cached, or cache expired. try update. + guard.update_cache().await; + guard.get_current_compatible_topology().await + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl TopologyProvider for NymTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + let mut guard = self.inner.lock().await; + // check the cache + if let Some(cached) = guard.get_current_compatible_topology().await { + return Some(cached); + } + + // not cached, or cache expired. try update. + guard.update_cache().await; + guard.get_current_compatible_topology().await + } +} + +struct NymTopologyProviderInner { + config: Config, + + cached: Option, + cached_at: OffsetDateTime, + + topology_manager: M, +} + +impl NymTopologyProviderInner { + pub fn new( + config: impl Into, + manager: M, + initial_topology: Option, + ) -> Self { + Self { + config: config.into(), + cached_at: OffsetDateTime::UNIX_EPOCH, + cached: initial_topology, + topology_manager: manager, + } + } + + fn cached_topology(&self) -> Option { + if let Some(cached_topology) = &self.cached { + if self.cached_at + self.config.cache_ttl > OffsetDateTime::now_utc() { + return Some(cached_topology.clone()); + } + } + + None + } + + async fn update_cache(&mut self) { + if let Some(ref mut cached_topology) = self.cached { + // get layer assignment map + let response = self.topology_manager.get_layer_assignments().await; + if response.is_none() { + warn!("pulled layer assignments and got no response"); + self.cached_at = OffsetDateTime::now_utc(); + return; + } + + let layer_assignments = response.unwrap(); + // Check if we already know about the epoch + if cached_topology.rewarded_set.epoch_id == layer_assignments.epoch_id { + debug!("pulled layer assignments, epoch already known"); + self.cached_at = OffsetDateTime::now_utc(); + return; + } + + cached_topology.rewarded_set = layer_assignments.into(); + + // get the set of node IDs + let new_id_set = cached_topology.rewarded_set.all_ids(); + let known_id_set = HashSet::::from_iter(cached_topology.all_node_ids().copied()); + let unknown_node_ids: Vec<_> = new_id_set.difference(&known_id_set).copied().collect(); + + // Pull node descriptors for unknown IDs + let response = self + .topology_manager + .get_descriptor_batch(&unknown_node_ids[..]) + .await; + + // Add the new nodes to our cached topology + if let Some(new_descriptors) = response { + cached_topology.add_routing_nodes(new_descriptors); + } + + // double check that we have the expected nodes + let known_id_set = HashSet::::from_iter(cached_topology.all_node_ids().copied()); + let unknown_node_ids: Vec<_> = new_id_set.difference(&known_id_set).collect(); + if !unknown_node_ids.is_empty() { + warn!( + "still missing descriptors for nodes in the assigned set: {:?}", + unknown_node_ids + ); + } + } else { + self.cached = self.topology_manager.get_full_topology().await; + } + + self.cached_at = OffsetDateTime::now_utc(); + } + + /// Gets the current topology state using `Self::cached_topology` and then applies any filters + /// defined in the provided Config. + async fn get_current_compatible_topology(&mut self) -> Option { + let full_topology = self.cached_topology()?; + + let mut topology = NymTopology::new_empty(full_topology.rewarded_set().clone()); + + if self.config.use_extended_topology { + topology.add_additional_nodes(full_topology.all_nodes().filter(|n| { + n.performance.round_to_integer() >= self.config.min_node_performance() + })); + + return Some(full_topology); + } + + topology.add_additional_nodes( + full_topology.mixnodes().filter(|m| { + m.performance.round_to_integer() >= self.config.min_mixnode_performance + }), + ); + topology.add_additional_nodes( + full_topology.gateways().filter(|m| { + m.performance.round_to_integer() >= self.config.min_gateway_performance + }), + ); + + Some(topology) + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl TopologyProvider for NymTopologyProviderInner

{ + async fn get_new_topology(&mut self) -> Option { + self.get_current_compatible_topology().await + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl TopologyProvider for NymTopologyProviderInner

{ + async fn get_new_topology(&mut self) -> Option { + self.get_current_compatible_topology().await + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Trait allowing construction and upkeep of a +#[async_trait] +pub trait PiecewiseTopologyProvider: Send { + /// Pull a copy of the full topology. + /// + /// This is intended to be used sparingly as repeated usage could result in fetching duplicate + /// information more often than necessary. + async fn get_full_topology(&mut self) -> Option; + + /// Fetch a node descriptors for the set of provided IDs if available. + async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option>; + + /// Fetch the latest mapping of node IDs to Nym Network layer. + async fn get_layer_assignments(&mut self) -> Option; +} + +#[cfg(target_arch = "wasm32")] +/// Trait allowing construction and upkeep of a +#[async_trait(?Send)] +pub trait PiecewiseTopologyProvider: Send { + /// Pull a copy of the full topology. + /// + /// This is intended to be used sparingly as repeated usage could result in fetching duplicate + /// information more often than necessary. + async fn get_full_topology(&mut self) -> Option; + + /// Fetch a node descriptors for the set of provided IDs if available. + async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option>; + + /// Fetch the latest mapping of node IDs to Nym Network layer. + async fn get_layer_assignments(&mut self) -> Option; +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod test { + use super::*; + use crate::SupportedRoles; + use nym_crypto::asymmetric::encryption::PublicKey as SphinxPubkey; + use nym_crypto::asymmetric::identity::PublicKey as IdentityPubkey; + use nym_mixnet_contract_common::Percent; + + #[derive(Clone)] + struct PassthroughPiecewiseTopologyProvider { + topo: NymTopology, + } + + #[async_trait] + impl PiecewiseTopologyProvider for PassthroughPiecewiseTopologyProvider { + async fn get_full_topology(&mut self) -> Option { + Some(self.topo.clone()) + } + + async fn get_descriptor_batch(&mut self, ids: &[u32]) -> Option> { + let mut nodes = Vec::new(); + ids.iter().for_each(|id| { + if let Some(node) = self.topo.node_details.get(id) { + nodes.push(node.clone()); + } + }); + + Some(nodes) + } + + async fn get_layer_assignments(&mut self) -> Option { + return Some(self.topo.rewarded_set.clone().into()); + } + } + + #[tokio::test] + async fn test_topology_provider() -> Result<(), Box> { + let mut topo_mgr = PassthroughPiecewiseTopologyProvider { + topo: NymTopology::default(), + }; + + let mut topo_provider = + NymTopologyProviderInner::new(Config::default(), topo_mgr.clone(), None); + + // No initial topology was provided, No update has run yet, None should be returned + assert_eq!(topo_provider.cached_topology(), None); + + // force an update of the cached topology + topo_provider.update_cache().await; + + let topo = topo_provider.cached_topology(); + assert!(topo.is_some()); + let topo = topo.unwrap(); + assert!(topo.is_empty()); + + // create a change in the manager to make sure it is propogated to the provider cache on update + topo_mgr.topo.rewarded_set.epoch_id += 1; + topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123]); + assert_eq!(topo_mgr.topo.node_details.insert(123, fake_node(123)), None); + topo_provider.topology_manager = topo_mgr.clone(); + + // force an update of the cached topology + topo_provider.update_cache().await; + + let topo = topo_provider.cached_topology(); + assert!(topo.is_some()); + let topo1 = topo.unwrap(); + assert!(!topo1.is_empty()); + assert!(topo1.node_details.contains_key(&123)); + + // try forcing an update even though the epoch has not changed. Should result in no change + topo_provider.update_cache().await; + let topo2 = topo_provider.cached_topology().unwrap(); + assert_eq!(topo1, topo2); + + // Add a node without a descriptor to make sure warning is printed. + topo_mgr.topo.rewarded_set.epoch_id += 1; + topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123, 456]); + topo_provider.topology_manager = topo_mgr.clone(); + + // try forcing an update even though the epoch has not changed. Should result in no change + topo_provider.update_cache().await; + let _ = topo_provider.cached_topology().unwrap(); + + Ok(()) + } + + #[tokio::test] + async fn test_topology_provider_by_trait() -> Result<(), Box> { + let mut topo_mgr = PassthroughPiecewiseTopologyProvider { + topo: NymTopology::default(), + }; + + let mut topo_provider = NymTopologyProvider::new(topo_mgr.clone(), Config::default(), None); + + // No initial topology was provided, the NymTopologyProvider should do an update from the + // manager to build its cache. This should be our empty topology initialized in the manage + // above + let maybe_topo = topo_provider.get_new_topology().await; + assert!(maybe_topo.is_some()); + let topo1 = maybe_topo.unwrap(); + assert!(topo1.is_empty()); + + // Try pulling again, should give response from cache because we are under ttl + let maybe_topo = topo_provider.get_new_topology().await; + assert!(maybe_topo.is_some()); + let topo2 = maybe_topo.unwrap(); + assert_eq!(topo1, topo2); + + // create a change in the manager + topo_mgr.topo.rewarded_set.epoch_id += 1; + topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123]); + assert_eq!(topo_mgr.topo.node_details.insert(123, fake_node(123)), None); + { + let mut guard = topo_provider.inner.lock().await; + guard.topology_manager = topo_mgr.clone(); + drop(guard) + } + + // The NymTopologyProvider should still serve from cache because we haven't crossed ttl + // despite updates being available in the manager + let maybe_topo = topo_provider.get_new_topology().await; + assert!(maybe_topo.is_some()); + let topo3 = maybe_topo.unwrap(); + assert_eq!(topo2, topo3); + + // force ttl timeout should allow refresh that includes latest changes from manager + topo_provider.force_refresh().await; + let maybe_topo = topo_provider.get_new_topology().await; + assert!(maybe_topo.is_some()); + let topo4 = maybe_topo.unwrap(); + assert_ne!(topo3, topo4); + assert!(topo4.node_details.contains_key(&123)); + + // create another change in the manager + topo_mgr.topo.rewarded_set.epoch_id += 1; + topo_mgr.topo.rewarded_set.entry_gateways = HashSet::from([123, 456]); + assert_eq!(topo_mgr.topo.node_details.insert(456, fake_node(456)), None); + { + let mut guard = topo_provider.inner.lock().await; + guard.topology_manager = topo_mgr.clone(); + drop(guard) + } + + // force clear cache should also pull latest full topology + topo_provider.force_clear().await; + let maybe_topo = topo_provider.get_new_topology().await; + assert!(maybe_topo.is_some()); + let topo5 = maybe_topo.unwrap(); + assert!(topo5.node_details.contains_key(&456)); + + Ok(()) + } + + fn fake_node(node_id: u32) -> RoutingNode { + RoutingNode { + node_id, + mix_host: "127.0.0.1:2345".parse().unwrap(), + entry: None, + identity_key: IdentityPubkey::from_bytes(&[0u8; 32][..]).unwrap(), + sphinx_key: SphinxPubkey::from_bytes(&[0u8; 32][..]).unwrap(), + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: true, + mixnet_exit: true, + }, + performance: Percent::hundred(), + } + } +} diff --git a/common/topology/src/rewarded_set.rs b/common/topology/src/rewarded_set.rs index 0d06239be6f..73c57b9cac4 100644 --- a/common/topology/src/rewarded_set.rs +++ b/common/topology/src/rewarded_set.rs @@ -6,7 +6,7 @@ use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, NodeId, RewardedSet} use serde::{Deserialize, Serialize}; use std::collections::HashSet; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct CachedEpochRewardedSet { pub epoch_id: EpochId, @@ -119,4 +119,22 @@ impl CachedEpochRewardedSet { mixnodes.extend(&self.layer3); mixnodes } + + pub fn all_ids(&self) -> HashSet { + let mut mixnodes = HashSet::with_capacity( + self.entry_gateways.len() + + self.exit_gateways.len() + + self.layer1.len() + + self.layer2.len() + + self.layer3.len() + + self.standby.len(), + ); + mixnodes.extend(&self.entry_gateways); + mixnodes.extend(&self.exit_gateways); + mixnodes.extend(&self.layer1); + mixnodes.extend(&self.layer2); + mixnodes.extend(&self.layer3); + mixnodes.extend(&self.standby); + mixnodes + } } diff --git a/common/topology/src/wasm_helpers.rs b/common/topology/src/wasm_helpers.rs index ecb451e8beb..7787a3f9624 100644 --- a/common/topology/src/wasm_helpers.rs +++ b/common/topology/src/wasm_helpers.rs @@ -6,6 +6,7 @@ use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles}; use crate::{CachedEpochRewardedSet, NymTopology}; +use nym_mixnet_contract_common::reward_params::Performance; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; @@ -105,6 +106,8 @@ impl TryFrom for RoutingNode { } })?, supported_roles: value.supported_roles, + + performance: Performance::hundred(), }) } } diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index 8569dd7ad97..09fbcc1442a 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -28,7 +28,9 @@ pub use nym_sphinx::{ }; pub use nym_statistics_common::clients::ClientStatsSender; pub use nym_task; -pub use nym_topology::{HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider}; +pub use nym_topology::{ + providers::HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider, +}; pub use nym_validator_client::nym_api::Client as ApiClient; pub use nym_validator_client::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient}; // TODO: that's a very nasty import path. it should come from contracts instead! diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 7ee98884782..6417b5a92ce 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -10,7 +10,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; -use nym_contracts_common::NaiveFloat; +use nym_contracts_common::{NaiveFloat, Percent}; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_tester_utils::node::{NodeType, TestableNode}; @@ -231,6 +231,8 @@ impl PacketPreparer { mixnet_entry: false, mixnet_exit: false, }, + // We have no information about performance in legacy node formats + performance: Percent::hundred(), }) } @@ -263,6 +265,8 @@ impl PacketPreparer { mixnet_entry: true, mixnet_exit: false, }, + // We have no information about performance in legacy node formats + performance: Percent::hundred(), }) } diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index f858065d2b5..b199cf5429e 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -26,7 +26,7 @@ use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; use crate::nym_nodes::handlers::unstable::skimmed::{ entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, - nodes_basic_all, + nodes_basic_all, nodes_basic_batch, }; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; @@ -52,6 +52,7 @@ pub(crate) fn nym_node_routes_unstable() -> Router { "/skimmed", Router::new() .route("/", get(nodes_basic_all)) + .route("/batch", post(nodes_basic_batch)) .route("/active", get(nodes_basic_active)) .nest( "/mixnodes", diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 8eb5388deae..158f6ff57f8 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -25,6 +25,7 @@ use tracing::trace; use utoipa::ToSchema; pub type PaginatedSkimmedNodes = AxumResult>>; +type SkimmedNodes = AxumResult>>; /// Given all relevant caches, build part of response for JUST Nym Nodes fn build_nym_nodes_response<'a, NI>( @@ -196,7 +197,7 @@ where pub(super) async fn deprecated_gateways_basic( state: State, query_params: Query, -) -> AxumResult>> { +) -> SkimmedNodes { // 1. call '/v1/unstable/skimmed/entry-gateways/all' let all_gateways = entry_gateways_basic_all(state, query_params).await?; @@ -223,7 +224,7 @@ pub(super) async fn deprecated_gateways_basic( pub(super) async fn deprecated_mixnodes_basic( state: State, query_params: Query, -) -> AxumResult>> { +) -> SkimmedNodes { // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' let active_mixnodes = mixnodes_basic_active(state, query_params).await?; @@ -239,7 +240,7 @@ async fn nodes_basic( state: State, Query(_query_params): Query, active_only: bool, -) -> PaginatedSkimmedNodes { +) -> AxumResult> { // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes // 1. grab all relevant described nym-nodes @@ -281,10 +282,10 @@ async fn nodes_basic( legacy_gateways.timestamp(), ]); - Ok(Json(PaginatedCachedNodesResponse::new_full( + Ok(CachedNodesResponse { refreshed_at, nodes, - ))) + }) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -326,7 +327,47 @@ pub(super) async fn nodes_basic_all( }; } - nodes_basic(state, Query(query_params.into()), false).await + let nodes = nodes_basic(state, Query(query_params.into()), false).await?; + // We are never using pagination (always one page) anyways so just build it here. + Ok(Json(PaginatedCachedNodesResponse::new_full( + nodes.refreshed_at, + nodes.nodes, + ))) +} + +/// Post request handler taking a json array of NodeId (u32) values and returning descriptors for +/// the provided NodeId values. A successful response will contain descriptors for all nodes +/// associated with those node IDs available in the current full topology. +/// +/// If a provided node ID is not present in the current topology there will be no descriptor for +/// that node in the response. +/// +/// If no node IDs are provided the response will contain no descriptors. +#[utoipa::path( + tag = "Unstable Nym Nodes batch by Node ID", + get, + params(NodesParamsWithRole), + path = "batch", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponseSchema) + ) +)] +pub(super) async fn nodes_basic_batch( + state: State, + Query(query_params): Query, + Json(ids): Json>, +) -> SkimmedNodes { + let nodes = nodes_basic(state, Query(query_params.into()), false).await?; + let requested_nodes = nodes + .nodes + .into_iter() + .filter(|node| ids.contains(&node.node_id)) + .collect(); + Ok(Json(CachedNodesResponse { + nodes: requested_nodes, + refreshed_at: nodes.refreshed_at, + })) } /// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) @@ -359,7 +400,12 @@ pub(super) async fn nodes_basic_active( }; } - nodes_basic(state, Query(query_params.into()), true).await + let nodes = nodes_basic(state, Query(query_params.into()), true).await?; + // We are never using pagination (always one page) anyways so just build it here. + Ok(Json(PaginatedCachedNodesResponse::new_full( + nodes.refreshed_at, + nodes.nodes, + ))) } async fn mixnodes_basic( diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index e38fcefe8f1..068f94a2f23 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -10,7 +10,7 @@ use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; use nym_sphinx::chunking::monitoring; -use nym_topology::{HardcodedTopologyProvider, NymTopology}; +use nym_topology::{providers::HardcodedTopologyProvider, NymTopology}; use std::fs::File; use std::io::Write; use std::sync::LazyLock; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index f68a2d5553c..3d250c2fec0 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -46,6 +46,7 @@ use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{ShutdownManager, ShutdownToken, TaskClient}; +use nym_topology::node::Performance; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::NodeRefreshBody; use nym_validator_client::{NymApiClient, UserAgent}; @@ -579,6 +580,8 @@ impl NymNode { mixnet_entry: true, mixnet_exit: true, }, + // Perf metrics are not meaningful in this context. + performance: Performance::hundred(), }) } diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 7cd5a6f50cd..4c5b2f2494f 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::provider_trait::{async_trait, TopologyProvider}; +use nym_topology::providers::{async_trait, TopologyProvider}; use nym_topology::NymTopology; use url::Url; diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 05ce8ebf018..82ea48f6f73 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,10 +1,10 @@ +use async_trait::async_trait; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_gateway_requests::SharedSymmetricKey; use nym_sdk::mixnet::{ self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage, GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender, }; -use nym_topology::provider_trait::async_trait; #[tokio::main] async fn main() { diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index 38c2b3d7f31..21d9c9ad128 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::{NymTopology, RoutingNode, SupportedRoles}; +use nym_topology::{node::Performance, NymTopology, RoutingNode, SupportedRoles}; #[tokio::main] async fn main() { @@ -30,6 +30,7 @@ async fn main() { mixnet_entry: false, mixnet_exit: false, }, + performance: Performance::hundred(), }, RoutingNode { node_id: 23, @@ -46,6 +47,7 @@ async fn main() { mixnet_entry: false, mixnet_exit: false, }, + performance: Performance::hundred(), }, RoutingNode { node_id: 66, @@ -62,6 +64,7 @@ async fn main() { mixnet_entry: false, mixnet_exit: false, }, + performance: Performance::hundred(), }, ]; diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index b540aa752df..d352a9e6f99 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -83,7 +83,7 @@ pub use nym_statistics_common::clients::{ connection::ConnectionStatsEvent, ClientStatsEvents, ClientStatsSender, }; pub use nym_task::connections::{LaneQueueLengths, TransmissionLane}; -pub use nym_topology::{provider_trait::TopologyProvider, NymTopology}; +pub use nym_topology::{providers::TopologyProvider, NymTopology}; pub use paths::StoragePaths; pub use socks5_client::Socks5MixnetClient; pub use traits::MixnetMessageSender; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 6b5076d6474..c4129060c39 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -32,7 +32,7 @@ use nym_credentials_interface::TicketType; use nym_crypto::hkdf::DerivationMaterial; use nym_socks5_client_core::config::Socks5; use nym_task::{TaskClient, TaskHandle, TaskStatus}; -use nym_topology::provider_trait::TopologyProvider; +use nym_topology::providers::TopologyProvider; use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient, UserAgent}; use rand::rngs::OsRng; use std::path::Path;