diff --git a/Cargo.lock b/Cargo.lock index def19342ded..9dc7a9c6b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,6 +493,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-client-ip" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +dependencies = [ + "axum 0.7.7", + "forwarded-header-value", + "serde", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -2638,6 +2649,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -4275,6 +4296,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "notify" version = "5.2.0" @@ -5534,9 +5561,11 @@ dependencies = [ "nym-network-requester", "nym-node-http-api", "nym-pemstore 0.3.0", + "nym-sdk", "nym-sphinx", "nym-statistics-common", "nym-task", + "nym-topology", "nym-types", "nym-validator-client 0.1.0", "nym-wireguard", @@ -5729,6 +5758,7 @@ name = "nym-http-api-common" version = "0.1.0" dependencies = [ "axum 0.7.7", + "axum-client-ip", "bytes", "colored", "mime", diff --git a/Cargo.toml b/Cargo.toml index d99e3d75320..42eb03e8fb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ aead = "0.5.2" anyhow = "1.0.90" argon2 = "0.5.0" async-trait = "0.1.83" +axum-client-ip = "0.6.1" axum = "0.7.5" axum-extra = "0.9.4" base64 = "0.22.1" diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index bbf32f377c6..4e60278a22f 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -6,7 +6,6 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; use futures::StreamExt; use log::*; use nym_sphinx::addressing::nodes::NodeIdentity; -use nym_topology::provider_trait::TopologyProvider; use nym_topology::NymTopologyError; use std::time::Duration; @@ -18,7 +17,11 @@ use wasmtimer::tokio::sleep; mod accessor; pub mod geo_aware_provider; -pub(crate) mod nym_api_provider; +pub mod nym_api_provider; + +pub use geo_aware_provider::GeoAwareTopologyProvider; +pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; +pub use nym_topology::provider_trait::TopologyProvider; // 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 c2b2dd9003c..d3b4713c446 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 @@ -14,9 +14,10 @@ use url::Url; pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50; pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; -pub(crate) struct Config { - pub(crate) min_mixnode_performance: u8, - pub(crate) min_gateway_performance: u8, +#[derive(Debug)] +pub struct Config { + pub min_mixnode_performance: u8, + pub min_gateway_performance: u8, } impl Default for Config { @@ -29,7 +30,7 @@ impl Default for Config { } } -pub(crate) struct NymApiTopologyProvider { +pub struct NymApiTopologyProvider { config: Config, validator_client: nym_validator_client::client::NymApiClient, @@ -40,7 +41,7 @@ pub(crate) struct NymApiTopologyProvider { } impl NymApiTopologyProvider { - pub(crate) fn new( + pub fn new( config: Config, mut nym_api_urls: Vec, client_version: String, diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 472a6a99330..2ed43607cd6 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -19,7 +19,7 @@ use nym_api_requests::ecash::{ }; use nym_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; @@ -30,10 +30,10 @@ use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; +use nym_mixnet_contract_common::NymNodeDetails; pub use nym_mixnet_contract_common::{ mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, }; - // re-export the type to not break existing imports pub use crate::coconut::EcashApiClient; @@ -106,7 +106,9 @@ impl Config { pub struct Client { // ideally they would have been read-only, but unfortunately rust doesn't have such features + // #[deprecated(note = "please use `nym_api_client` instead")] pub nym_api: nym_api::Client, + // pub nym_api_client: NymApiClient, pub nyxd: NyxdClient, } @@ -243,6 +245,50 @@ impl Client { Ok(self.nym_api.get_gateways().await?) } + // TODO: combine with NymApiClient... + pub async fn get_all_cached_described_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 descriptions = Vec::new(); + + loop { + let mut res = self.nym_api.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + + // TODO: combine with NymApiClient... + pub async fn get_all_cached_bonded_nym_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 bonds = Vec::new(); + + loop { + let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -327,7 +373,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_all_basic_entry_assigned_nodes( + .get_basic_entry_assigned_nodes( semver_compatibility.clone(), false, Some(page), @@ -404,6 +450,48 @@ impl NymApiClient { Ok(self.nym_api.get_gateways_described().await?) } + pub async fn get_all_described_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 descriptions = Vec::new(); + + loop { + let mut res = self.nym_api.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + + pub async fn get_all_bonded_nym_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 bonds = Vec::new(); + + loop { + let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, 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 2e5ce565f98..7fc676f144e 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -11,9 +11,10 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ - AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, + AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, NymNodeDescription, }; use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; +use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ ecash::{ models::{ @@ -38,7 +39,7 @@ use nym_contracts_common::IdentityKey; pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails}; use time::format_description::BorrowedFormatItem; use time::Date; use tracing::instrument; @@ -127,6 +128,44 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_nodes_described( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + 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, "nym-nodes", "described"], ¶ms) + .await + } + + async fn get_nym_nodes( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + 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, "nym-nodes", "bonded"], ¶ms) + .await + } + #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes( &self, @@ -178,7 +217,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes #[instrument(level = "debug", skip(self))] - async fn get_all_basic_entry_assigned_nodes( + async fn get_basic_entry_assigned_nodes( &self, semver_compatibility: Option, no_legacy: bool, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 43a33c5d3e8..fb03a1034b3 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -17,6 +17,7 @@ use crate::{ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; /// Full details associated with given mixnode. @@ -647,14 +648,39 @@ impl From for u8 { export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts" ) )] -#[cw_serde] -#[derive(Default, Copy)] +// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want +// with the addition of .cost_params_change field +#[derive( + ::cosmwasm_schema::serde::Serialize, + ::cosmwasm_schema::serde::Deserialize, + ::std::clone::Clone, + ::std::fmt::Debug, + ::std::cmp::PartialEq, + ::cosmwasm_schema::schemars::JsonSchema, + Default, + Copy, +)] +#[schemars(crate = "::cosmwasm_schema::schemars")] pub struct PendingMixNodeChanges { pub pledge_change: Option, + #[serde(default)] pub cost_params_change: Option, } +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct LegacyPendingMixNodeChanges { + pub pledge_change: Option, +} + +impl From for LegacyPendingMixNodeChanges { + fn from(value: PendingMixNodeChanges) -> Self { + LegacyPendingMixNodeChanges { + pledge_change: value.pledge_change, + } + } +} + impl PendingMixNodeChanges { pub fn new_empty() -> PendingMixNodeChanges { PendingMixNodeChanges { diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index fb841541fcf..7d7b988fc85 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -112,12 +112,18 @@ impl PemStorableKeyPair for KeyPair { } } -#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +#[derive(PartialEq, Eq, Hash, Copy, Clone)] pub struct PublicKey(x25519_dalek::PublicKey); impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_base58_string()) + Display::fmt(&self.to_base58_string(), f) + } +} + +impl Debug for PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.to_base58_string(), f) } } diff --git a/common/crypto/src/asymmetric/encryption/serde_helpers.rs b/common/crypto/src/asymmetric/encryption/serde_helpers.rs index 374afbd40d5..02a5282cddc 100644 --- a/common/crypto/src/asymmetric/encryption/serde_helpers.rs +++ b/common/crypto/src/asymmetric/encryption/serde_helpers.rs @@ -31,8 +31,16 @@ pub mod option_bs58_x25519_pubkey { pub fn deserialize<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { - let s = Option::::deserialize(deserializer)?; - s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom)) - .transpose() + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(s) => { + if s.is_empty() { + Ok(None) + } else { + Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom)) + .transpose() + } + } + } } } diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index e2d3df624d5..4b51aa2f641 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError; use ed25519_dalek::{Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -119,12 +119,18 @@ impl PemStorableKeyPair for KeyPair { } /// ed25519 EdDSA Public Key -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq)] pub struct PublicKey(ed25519_dalek::VerifyingKey); impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_base58_string()) + Display::fmt(&self.to_base58_string(), f) + } +} + +impl Debug for PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.to_base58_string(), f) } } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index f35c662e88b..a01358ded27 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -35,6 +35,9 @@ pub enum HttpClientError { source: reqwest::Error, }, + #[error("failed to deserialise received response: {source}")] + ResponseDeserialisationFailure { source: serde_json::Error }, + #[error("provided url is malformed: {source}")] MalformedUrl { #[from] @@ -531,19 +534,17 @@ where } if res.status().is_success() { - #[cfg(debug_assertions)] - { - let text = res.text().await.inspect_err(|err| { - tracing::error!("Couldn't even get response text: {err}"); - })?; - tracing::trace!("Result:\n{:#?}", text); - - serde_json::from_str(&text) - .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + let text = res.text().await?; + match serde_json::from_str(&text) { + Ok(res) => Ok(res), + Err(source) => { + #[cfg(debug_assertions)] + { + tracing::trace!("Result:\n{:#?}", text); + } + Err(HttpClientError::ResponseDeserialisationFailure { source }) + }, } - - #[cfg(not(debug_assertions))] - Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index b9252c2b051..c7b6db2ff6a 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum-client-ip.workspace = true axum.workspace = true bytes = { workspace = true } colored.workspace = true diff --git a/common/http-api-common/src/logging.rs b/common/http-api-common/src/logging.rs index ca2f5c63cef..8de60b338f0 100644 --- a/common/http-api-common/src/logging.rs +++ b/common/http-api-common/src/logging.rs @@ -1,18 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::{ConnectInfo, Request}; +use axum::extract::Request; use axum::http::header::{HOST, USER_AGENT}; use axum::http::HeaderValue; use axum::middleware::Next; use axum::response::IntoResponse; +use axum_client_ip::InsecureClientIp; use colored::Colorize; -use std::net::SocketAddr; use std::time::Instant; use tracing::info; pub async fn logger( - ConnectInfo(addr): ConnectInfo, + InsecureClientIp(addr): InsecureClientIp, request: Request, next: Next, ) -> impl IntoResponse { diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 62c0c378bc5..6e89cf27535 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -286,6 +286,10 @@ impl NymTopology { self.get_gateway(gateway_identity).is_some() } + pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) { + self.gateways.push(gateway) + } + pub fn set_gateways(&mut self, gateways: Vec) { self.gateways = gateways } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index edfbe8740f1..170f1000a55 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -116,7 +116,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { }); } - let layer = match value.epoch_role { + let layer = match value.role { NodeRole::Mixnode { layer } => layer .try_into() .map_err(|_| MixnodeConversionError::InvalidLayer)?, diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 23dbd3a01cf..0c77bcf0108 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -158,10 +158,13 @@ impl PeerController { .ok_or(Error::MissingClientBandwidthEntry)? .client_id { - storage.create_bandwidth_entry(client_id).await?; + let bandwidth = storage + .get_available_bandwidth(client_id) + .await? + .ok_or(Error::MissingClientBandwidthEntry)?; Ok(Some(BandwidthStorageManager::new( storage, - ClientBandwidth::new(Default::default()), + ClientBandwidth::new(bandwidth.into()), client_id, BandwidthFlushingBehaviourConfig::default(), true, diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index cd91d99b3c6..9b737c53d0d 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -84,12 +84,13 @@ impl PeerHandle { .ok_or(Error::InconsistentConsumedBytes)? .try_into() .map_err(|_| Error::InconsistentConsumedBytes)?; - if bandwidth_manager - .write() - .await - .try_use_bandwidth(spent_bandwidth) - .await - .is_err() + if spent_bandwidth > 0 + && bandwidth_manager + .write() + .await + .try_use_bandwidth(spent_bandwidth) + .await + .is_err() { let success = self.remove_peer().await?; return Ok(!success); diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 7d43c712283..4fa3465b4b3 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -3689,8 +3689,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", @@ -5239,8 +5238,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", @@ -5575,8 +5573,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", @@ -7595,8 +7592,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json index 8bde1a57329..239f0fc353f 100644 --- a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json +++ b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json @@ -315,8 +315,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json index 4bcb8772acf..8db9d629b62 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json @@ -323,8 +323,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json index 68d3856e11a..bee2b305229 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json @@ -317,8 +317,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json index 94867ce877f..94499e2fd06 100644 --- a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json +++ b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json @@ -319,8 +319,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs index ab59cd2617a..1d15c6b03ae 100644 --- a/contracts/mixnet/src/nodes/storage/helpers.rs +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -52,8 +52,8 @@ pub(crate) fn save_assignment( // update metadata let mut metadata = ROLES_METADATA.load(storage, inactive)?; - let last = assignment.nodes.last().copied().unwrap_or_default(); - metadata.set_highest_id(last, assignment.role); + let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default(); + metadata.set_highest_id(highest_id, assignment.role); metadata.set_role_count(assignment.role, assignment.nodes.len() as u32); if assignment.is_final_assignment() { metadata.fully_assigned = true @@ -140,6 +140,7 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet mod tests { use super::*; use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::TestSetup; #[test] fn next_id() { @@ -149,4 +150,33 @@ mod tests { assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap()); } } + + #[test] + fn assigning_role_uses_highest_id_even_if_not_sorted() { + let mut test = TestSetup::new(); + let deps = test.deps_mut(); + + let sorted = RoleAssignment { + role: Role::EntryGateway, + nodes: vec![1, 2, 3], + }; + + let unsorted = RoleAssignment { + role: Role::Layer1, + nodes: vec![8, 5, 4], + }; + + save_assignment(deps.storage, sorted).unwrap(); + save_assignment(deps.storage, unsorted).unwrap(); + + let storage = deps.as_ref().storage; + + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap(); + let inactive = active_bucket.other() as u8; + let metadata = ROLES_METADATA.load(storage, inactive).unwrap(); + + assert_eq!(metadata.entry_gateway_metadata.highest_id, 3); + assert_eq!(metadata.layer1_metadata.highest_id, 8); + assert_eq!(metadata.highest_rewarded_id(), 8) + } } diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 50c5431c25f..806cecef7d8 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -13,6 +13,8 @@ pub use nym_explorer_api_requests::{ // Paths const API_VERSION: &str = "v1"; +const TMP: &str = "tmp"; +const UNSTABLE: &str = "unstable"; const MIXNODES: &str = "mix-nodes"; const GATEWAYS: &str = "gateways"; @@ -60,6 +62,12 @@ impl ExplorerClient { Ok(Self { client, url }) } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result { + let client = reqwest::Client::builder().timeout(timeout).build()?; + Ok(Self { client, url }) + } + async fn send_get_request( &self, paths: &[&str], @@ -96,6 +104,13 @@ impl ExplorerClient { pub async fn get_gateways(&self) -> Result, ExplorerApiError> { self.query_explorer_api(&[API_VERSION, GATEWAYS]).await } + + pub async fn unstable_get_gateways( + &self, + ) -> Result, ExplorerApiError> { + self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS]) + .await + } } fn combine_url(mut base_url: Url, paths: &[&str]) -> Result { diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 858f593986f..18442fc6d79 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -4,6 +4,7 @@ use crate::state::ExplorerApiStateContext; use log::{info, warn}; use nym_explorer_api_requests::Location; +use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; use nym_task::TaskClient; pub(crate) struct GeoLocateTask { @@ -25,6 +26,7 @@ impl GeoLocateTask { _ = interval_timer.tick() => { self.locate_mix_nodes().await; self.locate_gateways().await; + self.locate_nym_nodes().await; } _ = self.shutdown.recv() => { trace!("Listener: Received shutdown"); @@ -109,6 +111,83 @@ impl GeoLocateTask { trace!("All mix nodes located"); } + async fn locate_nym_nodes(&mut self) { + // I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...` + // If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway + let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await; + + let geo_ip = self.state.inner.geo_ip.0.clone(); + + for (i, cache_item) in nym_nodes.values().enumerate() { + if self + .state + .inner + .nymnodes + .is_location_valid(cache_item.node_id()) + .await + { + // when the cached location is valid, don't locate and continue to next mix node + continue; + } + + let bonded_host = &cache_item.bond_information.node.host; + + match geo_ip.query( + bonded_host, + Some( + cache_item + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + ), + ) { + Ok(opt) => match opt { + Some(location) => { + let location: Location = location.into(); + + trace!( + "{} mix nodes already located. host {} is located in {:#?}", + i, + bonded_host, + location.three_letter_iso_country_code, + ); + + if i > 0 && (i % 100) == 0 { + info!("Located {} nym-nodes...", i + 1,); + } + + self.state + .inner + .nymnodes + .set_location(cache_item.node_id(), Some(location)) + .await; + + // one node has been located, so return out of the loop + return; + } + None => { + warn!("❌ Location for {bonded_host} not found."); + self.state + .inner + .nymnodes + .set_location(cache_item.node_id(), None) + .await; + } + }, + Err(_e) => { + // warn!( + // "❌ Oh no! Location for {} failed. Error: {:#?}", + // cache_item.mix_node().host, + // e + // ); + } + }; + } + + trace!("All nym-nodes nodes located"); + } + async fn locate_gateways(&mut self) { let gateways = self.state.inner.gateways.get_gateways().await; diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 559093671e6..2d4e2d9fabf 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes; use crate::http::swagger::get_docs; use crate::mix_node::http::mix_node_make_default_routes; use crate::mix_nodes::http::mix_nodes_make_default_routes; +use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes; use crate::overview::http::overview_make_default_routes; use crate::ping::http::ping_make_default_routes; use crate::service_providers::http::service_providers_make_default_routes; @@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket { "/ping" => ping_make_default_routes(&openapi_settings), "/validators" => validators_make_default_routes(&openapi_settings), "/service-providers" => service_providers_make_default_routes(&openapi_settings), + "/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings), }; building_rocket diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 8cb98280afe..ecaae3f4922 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -22,6 +22,7 @@ mod http; mod location; mod mix_node; pub(crate) mod mix_nodes; +mod nym_nodes; mod overview; mod ping; pub(crate) mod service_providers; diff --git a/explorer-api/src/nym_nodes/http.rs b/explorer-api/src/nym_nodes/http.rs new file mode 100644 index 00000000000..378fcc78d79 --- /dev/null +++ b/explorer-api/src/nym_nodes/http.rs @@ -0,0 +1,26 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::PrettyDetailedGatewayBond; +use okapi::openapi3::OpenApi; +use rocket::serde::json::Json; +use rocket::{Route, State}; +use rocket_okapi::settings::OpenApiSettings; + +pub fn unstable_temp_nymnodes_make_default_routes( + settings: &OpenApiSettings, +) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: all_gateways] +} + +#[openapi(tag = "UNSTABLE")] +#[get("/gateways")] +pub(crate) async fn all_gateways( + state: &State, +) -> Json> { + let mut gateways = state.inner.gateways.get_detailed_gateways().await; + gateways.append(&mut state.inner.nymnodes.pretty_gateways().await); + + Json(gateways) +} diff --git a/explorer-api/src/nym_nodes/location.rs b/explorer-api/src/nym_nodes/location.rs new file mode 100644 index 00000000000..134023f3caa --- /dev/null +++ b/explorer-api/src/nym_nodes/location.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_mixnet_contract_common::NodeId; + +use crate::location::LocationCache; + +pub(crate) type NymNodeLocationCache = LocationCache; diff --git a/explorer-api/src/nym_nodes/mod.rs b/explorer-api/src/nym_nodes/mod.rs new file mode 100644 index 00000000000..2ceb85885a7 --- /dev/null +++ b/explorer-api/src/nym_nodes/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +pub(crate) mod http; +pub(crate) mod location; +pub(crate) mod models; + +pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200); diff --git a/explorer-api/src/nym_nodes/models.rs b/explorer-api/src/nym_nodes/models.rs new file mode 100644 index 00000000000..7cdbbbdee37 --- /dev/null +++ b/explorer-api/src/nym_nodes/models.rs @@ -0,0 +1,154 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::location::{LocationCache, LocationCacheItem}; +use crate::nym_nodes::location::NymNodeLocationCache; +use crate::nym_nodes::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; +use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails}; +use nym_validator_client::models::NymNodeDescription; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; +use tokio::sync::{RwLock, RwLockReadGuard}; + +pub(crate) struct NymNodesCache { + pub(crate) valid_until: SystemTime, + pub(crate) bonded_nym_nodes: HashMap, + pub(crate) described_nodes: HashMap, +} + +impl NymNodesCache { + fn new() -> Self { + NymNodesCache { + valid_until: SystemTime::now() - Duration::from_secs(60), // in the past + bonded_nym_nodes: Default::default(), + described_nodes: Default::default(), + } + } + + // fn is_valid(&self) -> bool { + // self.valid_until >= SystemTime::now() + // } +} + +#[derive(Clone)] +pub(crate) struct ThreadSafeNymNodesCache { + nymnodes: Arc>, + locations: Arc>>, +} + +impl ThreadSafeNymNodesCache { + pub(crate) fn new() -> Self { + ThreadSafeNymNodesCache { + nymnodes: Arc::new(RwLock::new(NymNodesCache::new())), + locations: Arc::new(RwLock::new(NymNodeLocationCache::new())), + } + } + + pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self { + ThreadSafeNymNodesCache { + nymnodes: Arc::new(RwLock::new(NymNodesCache::new())), + locations: Arc::new(RwLock::new(locations)), + } + } + + pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool { + self.locations + .read() + .await + .get(&node_id) + .map_or(false, |cache_item| { + cache_item.valid_until > SystemTime::now() + }) + } + + pub(crate) async fn get_bonded_nymnodes( + &self, + ) -> RwLockReadGuard> { + let guard = self.nymnodes.read().await; + RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes) + } + + pub(crate) async fn get_locations(&self) -> NymNodeLocationCache { + self.locations.read().await.clone() + } + + pub(crate) async fn set_location(&self, node_id: NodeId, location: Option) { + // cache the location for this mix node so that it can be used when the mix node list is refreshed + self.locations + .write() + .await + .insert(node_id, LocationCacheItem::new_from_location(location)); + } + + pub(crate) async fn update_cache( + &self, + all_bonds: Vec, + descriptions: Vec, + ) { + let mut guard = self.nymnodes.write().await; + guard.bonded_nym_nodes = all_bonds + .into_iter() + .map(|details| (details.node_id(), details)) + .collect(); + guard.described_nodes = descriptions + .into_iter() + .map(|description| (description.node_id, description)) + .collect(); + + guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL; + } + + pub(crate) async fn pretty_gateways(&self) -> Vec { + let nodes_guard = self.nymnodes.read().await; + let location_guard = self.locations.read().await; + + let mut pretty_gateways = vec![]; + + for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes { + let Some(description) = nodes_guard.described_nodes.get(node_id) else { + continue; + }; + + if description.description.declared_role.entry { + let location = location_guard.get(node_id); + let bond = &native_nymnode.bond_information; + + pretty_gateways.push(PrettyDetailedGatewayBond { + pledge_amount: bond.original_pledge.clone(), + owner: bond.owner.clone(), + block_height: bond.bonding_height, + gateway: Gateway { + host: bond.node.host.clone(), + mix_port: description.description.mix_port(), + clients_port: description.description.mixnet_websockets.ws_port, + location: description + .description + .auxiliary_details + .location + .as_ref() + .map(|l| l.to_string()) + .unwrap_or_default(), + sphinx_key: description + .description + .host_information + .keys + .x25519 + .to_base58_string(), + identity_key: bond.node.identity_key.clone(), + version: description + .description + .build_information + .build_version + .clone(), + }, + proxy: None, + location: location.and_then(|l| l.location.clone()), + }) + } + } + + pretty_gateways + } +} diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index b007fae76df..28373aef809 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache; use crate::mix_node::models::ThreadsafeMixNodeCache; use crate::mix_nodes::location::MixnodeLocationCache; use crate::mix_nodes::models::ThreadsafeMixNodesCache; +use crate::nym_nodes::location::NymNodeLocationCache; +use crate::nym_nodes::models::ThreadSafeNymNodesCache; use crate::ping::models::ThreadsafePingCache; use crate::validators::models::ThreadsafeValidatorCache; @@ -30,6 +32,7 @@ pub struct ExplorerApiState { pub(crate) gateways: ThreadsafeGatewayCache, pub(crate) mixnode: ThreadsafeMixNodeCache, pub(crate) mixnodes: ThreadsafeMixNodesCache, + pub(crate) nymnodes: ThreadSafeNymNodesCache, pub(crate) ping: ThreadsafePingCache, pub(crate) validators: ThreadsafeValidatorCache, pub(crate) geo_ip: ThreadsafeGeoIp, @@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk { pub(crate) country_node_distribution: CountryNodesDistribution, pub(crate) mixnode_location_cache: MixnodeLocationCache, pub(crate) gateway_location_cache: GatewayLocationCache, + pub(crate) nymnode_location_cache: NymNodeLocationCache, pub(crate) as_at: DateTime, } @@ -85,6 +89,9 @@ impl ExplorerApiStateContext { mixnodes: ThreadsafeMixNodesCache::new_with_location_cache( state.mixnode_location_cache, ), + nymnodes: ThreadSafeNymNodesCache::new_with_location_cache( + state.nymnode_location_cache, + ), ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), @@ -101,6 +108,7 @@ impl ExplorerApiStateContext { gateways: ThreadsafeGatewayCache::new(), mixnode: ThreadsafeMixNodeCache::new(), mixnodes: ThreadsafeMixNodesCache::new(), + nymnodes: ThreadSafeNymNodesCache::new(), ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), @@ -117,6 +125,7 @@ impl ExplorerApiStateContext { country_node_distribution: self.inner.country_node_distribution.get_all().await, mixnode_location_cache: self.inner.mixnodes.get_locations().await, gateway_location_cache: self.inner.gateways.get_locations().await, + nymnode_location_cache: self.inner.nymnodes.get_locations().await, as_at: Utc::now(), }; serde_json::to_writer(file, &state).expect("error writing state to disk"); diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index f14408f1c88..efb03f34a64 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -1,16 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::GatewayBond; +use crate::mix_nodes::CACHE_REFRESH_RATE; +use crate::state::ExplorerApiStateContext; +use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails}; use nym_task::TaskClient; -use nym_validator_client::models::MixNodeBondAnnotated; +use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError}; use std::future::Future; - -use crate::mix_nodes::CACHE_REFRESH_RATE; -use crate::state::ExplorerApiStateContext; +use tokio::time::MissedTickBehavior; pub(crate) struct ExplorerApiTasks { state: ExplorerApiStateContext, @@ -39,6 +39,28 @@ impl ExplorerApiTasks { bonds } + async fn retrieve_bonded_nymnodes(&self) -> Result, ValidatorClientError> { + info!("About to retrieve all nymnode bonds..."); + self.state + .inner + .validator_client + .0 + .get_all_cached_bonded_nym_nodes() + .await + } + + async fn retrieve_node_descriptions( + &self, + ) -> Result, ValidatorClientError> { + info!("About to retrieve node descriptions..."); + self.state + .inner + .validator_client + .0 + .get_all_cached_described_nodes() + .await + } + async fn retrieve_all_mixnodes(&self) -> Vec { info!("About to retrieve all mixnode bonds..."); self.retrieve_mixnodes( @@ -130,10 +152,33 @@ impl ExplorerApiTasks { } } + async fn update_nymnodes_cache(&self) { + let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| { + error!("failed to retrieve nym node bonds: {err}"); + Vec::new() + }); + + let all_descriptions = self + .retrieve_node_descriptions() + .await + .unwrap_or_else(|err| { + error!("failed to retrieve node descriptions: {err}"); + Vec::new() + }); + + self.state + .inner + .nymnodes + .update_cache(nym_node_bonds, all_descriptions) + .await + } + pub(crate) fn start(mut self) { info!("Spawning mix nodes task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE); + interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip); + while !self.shutdown.is_shutdown() { tokio::select! { _ = interval_timer.tick() => { @@ -147,6 +192,10 @@ impl ExplorerApiTasks { info!("Updating mix node cache..."); self.update_mixnode_cache().await; + + info!("Updating nymnode cache..."); + self.update_nymnodes_cache().await; + info!("Done"); } _ = self.shutdown.recv() => { trace!("Listener: Received shutdown"); diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index ccfabc8c96a..2e4efa79192 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -77,9 +77,11 @@ nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } nym-node-http-api = { path = "../nym-node/nym-node-http-api" } nym-pemstore = { path = "../common/pemstore" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } +nym-topology = { path = "../common/topology" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs index 80f504dae12..1c54e0a1b3f 100644 --- a/gateway/src/node/helpers.rs +++ b/gateway/src/node/helpers.rs @@ -3,14 +3,19 @@ use crate::config::Config; use crate::error::GatewayError; - +use async_trait::async_trait; use nym_crypto::asymmetric::encryption; use nym_gateway_stats_storage::PersistentStatsStorage; use nym_gateway_storage::PersistentStorage; use nym_pemstore::traits::PemStorableKeyPair; use nym_pemstore::KeyPairPath; - +use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +use nym_topology::{gateway, NymTopology, TopologyProvider}; use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::debug; +use url::Url; pub async fn load_network_requester_config>( id: &str, @@ -102,3 +107,56 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result>, +} + +impl GatewayTopologyProvider { + pub fn new( + gateway_node: gateway::LegacyNode, + user_agent: UserAgent, + nym_api_url: Vec, + ) -> GatewayTopologyProvider { + GatewayTopologyProvider { + inner: Arc::new(Mutex::new(GatewayTopologyProviderInner { + inner: NymApiTopologyProvider::new( + NymApiTopologyProviderConfig { + min_mixnode_performance: 50, + min_gateway_performance: 0, + }, + nym_api_url, + env!("CARGO_PKG_VERSION").to_string(), + Some(user_agent), + ), + gateway_node, + })), + } + } +} + +struct GatewayTopologyProviderInner { + inner: NymApiTopologyProvider, + gateway_node: gateway::LegacyNode, +} + +#[async_trait] +impl TopologyProvider for GatewayTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + let mut guard = self.inner.lock().await; + match guard.inner.get_new_topology().await { + None => None, + Some(mut base) => { + if !base.gateway_exists(&guard.gateway_node.identity_key) { + debug!( + "{} didn't exist in topology. inserting it.", + guard.gateway_node.identity_key + ); + base.insert_gateway(guard.gateway_node.clone()); + } + Some(base) + } + } + } +} diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 738d6f30830..2ae82ccb32c 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -13,10 +13,11 @@ use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; use crate::node::helpers::{ - initialise_main_storage, initialise_stats_storage, load_network_requester_config, + initialise_main_storage, initialise_stats_storage, load_network_requester_config, GatewayTopologyProvider }; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use futures::channel::{mpsc, oneshot}; +use nym_bin_common::bin_info; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; @@ -27,13 +28,15 @@ use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilte use nym_node_http_api::state::metrics::SharedSessionStats; use nym_statistics_common::events::{self, StatsEventSender}; use nym_task::{TaskClient, TaskHandle, TaskManager}; +use nym_topology::NetworkAddress; use nym_types::gateway::GatewayNodeDetailsResponse; +use nym_validator_client::client::NodeId; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; use statistics::GatewayStatisticsCollector; -use std::net::SocketAddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use tracing::*; @@ -236,6 +239,39 @@ impl Gateway { crate::helpers::node_details(&self.config).await } + fn gateway_topology_provider(&self) -> GatewayTopologyProvider { + GatewayTopologyProvider::new( + self.as_topology_node(), + bin_info!().into(), + self.config.gateway.nym_api_urls.clone(), + ) + } + + fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode { + let ip = self + .config + .host + .public_ips + .first() + .copied() + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); + let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port); + + nym_topology::gateway::LegacyNode { + // those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate. + // the only thing that matters is the identity key (and maybe version) + node_id: NodeId::MAX, + mix_host, + host: NetworkAddress::IpAddr(ip), + clients_ws_port: self.config.gateway.clients_port, + clients_wss_port: self.config.gateway.clients_wss_port, + sphinx_key: *self.sphinx_keypair.public_key(), + + identity_key: *self.identity_keypair.public_key(), + version: env!("CARGO_PKG_VERSION").into(), + } + } + fn start_mix_socket_listener( &self, ack_sender: MixForwardingSender, @@ -268,6 +304,7 @@ impl Gateway { async fn start_authenticator( &mut self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ecash_verifier: Arc>, ) -> Result> @@ -315,6 +352,7 @@ impl Gateway { .with_shutdown(shutdown.fork("authenticator")) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &opts.custom_mixnet_path { @@ -363,6 +401,7 @@ impl Gateway { async fn start_authenticator( &self, _forwarding_channel: MixForwardingSender, + _topology_provider: GatewayTopologyProvider, _shutdown: TaskClient, _ecash_verifier: Arc>, ) -> Result> { @@ -435,6 +474,7 @@ impl Gateway { async fn start_network_requester( &self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ) -> Result { info!("Starting network requester..."); @@ -462,6 +502,7 @@ impl Gateway { .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path { @@ -499,6 +540,7 @@ impl Gateway { async fn start_ip_packet_router( &self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ) -> Result { info!("Starting IP packet provider..."); @@ -527,6 +569,7 @@ impl Gateway { .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path { @@ -643,6 +686,8 @@ impl Gateway { shutdown.fork("statistics::GatewayStatisticsCollector"), ); + let topology_provider = self.gateway_topology_provider(); + let handler_config = CredentialHandlerConfig { revocation_bandwidth_penalty: self .config @@ -691,6 +736,7 @@ impl Gateway { let embedded_nr = self .start_network_requester( mix_forwarding_channel.clone(), + topology_provider.clone(), shutdown.fork("NetworkRequester"), ) .await?; @@ -706,6 +752,7 @@ impl Gateway { let embedded_ip_sp = self .start_ip_packet_router( mix_forwarding_channel.clone(), + topology_provider.clone(), shutdown.fork("ip_service_provider"), ) .await?; @@ -718,6 +765,7 @@ impl Gateway { let embedded_auth = self .start_authenticator( mix_forwarding_channel, + topology_provider, shutdown.fork("authenticator"), ecash_verifier, ) diff --git a/nym-api/nym-api-requests/src/legacy.rs b/nym-api/nym-api-requests/src/legacy.rs index f7af56252c6..f0810ffa8ee 100644 --- a/nym-api/nym-api-requests/src/legacy.rs +++ b/nym-api/nym-api-requests/src/legacy.rs @@ -2,10 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges; -use nym_mixnet_contract_common::{ - GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, -}; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Deref; @@ -64,7 +62,7 @@ pub struct LegacyMixNodeDetailsWithLayer { /// Adjustments to the mixnode that are ought to happen during future epoch transitions. #[serde(default)] - pub pending_changes: PendingMixNodeChanges, + pub pending_changes: LegacyPendingMixNodeChanges, } impl LegacyMixNodeDetailsWithLayer { @@ -80,13 +78,3 @@ impl LegacyMixNodeDetailsWithLayer { self.bond_information.is_unbonding } } - -impl From for MixNodeDetails { - fn from(value: LegacyMixNodeDetailsWithLayer) -> Self { - MixNodeDetails { - bond_information: value.bond_information.into(), - rewarding_details: value.rewarding_details, - pending_changes: value.pending_changes, - } - } -} diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index ce84b7a53ef..93e9be195d2 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{ use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent}; use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; @@ -138,6 +138,48 @@ pub struct NodePerformance { pub last_24h: Performance, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts") +)] +pub enum DisplayRole { + EntryGateway, + Layer1, + Layer2, + Layer3, + ExitGateway, + Standby, +} + +impl From for DisplayRole { + fn from(role: Role) -> Self { + match role { + Role::EntryGateway => DisplayRole::EntryGateway, + Role::Layer1 => DisplayRole::Layer1, + Role::Layer2 => DisplayRole::Layer2, + Role::Layer3 => DisplayRole::Layer3, + Role::ExitGateway => DisplayRole::ExitGateway, + Role::Standby => DisplayRole::Standby, + } + } +} + +impl From for Role { + fn from(role: DisplayRole) -> Self { + match role { + DisplayRole::EntryGateway => Role::EntryGateway, + DisplayRole::Layer1 => Role::Layer1, + DisplayRole::Layer2 => Role::Layer2, + DisplayRole::Layer3 => Role::Layer3, + DisplayRole::ExitGateway => Role::ExitGateway, + DisplayRole::Standby => Role::Standby, + } + } +} + // imo for now there's no point in exposing more than that, // nym-api shouldn't be calculating apy or stake saturation for you. // it should just return its own metrics (performance) and then you can do with it as you wish @@ -153,7 +195,7 @@ pub struct NodePerformance { pub struct NodeAnnotation { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub last_24h_performance: Performance, - pub current_role: Option, + pub current_role: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] @@ -286,7 +328,7 @@ impl MixNodeBondAnnotated { .sphinx_key .parse() .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - epoch_role: role, + role, supported_roles: DeclaredRoles { mixnode: true, entry: false, @@ -345,7 +387,7 @@ impl GatewayBondAnnotated { .sphinx_key .parse() .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - epoch_role: role, + role, supported_roles: DeclaredRoles { mixnode: false, entry: true, @@ -827,7 +869,7 @@ impl NymNodeDescription { // we can't use the declared roles, we have to take whatever was provided in the contract. // why? say this node COULD operate as an exit, but it might be the case the contract decided // to assign it an ENTRY role only. we have to use that one instead. - epoch_role: role, + role, supported_roles: self.description.declared_role, entry, performance, @@ -935,14 +977,14 @@ impl NymNodeData { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct LegacyDescribedGateway { - pub bond: LegacyGatewayBondWithId, + pub bond: GatewayBond, pub self_described: Option, } impl From for LegacyDescribedGateway { fn from(bond: LegacyGatewayBondWithId) -> Self { LegacyDescribedGateway { - bond, + bond: bond.bond, self_described: None, } } diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 27d3e6ba3d2..fcfd09c2be3 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -141,8 +141,8 @@ pub struct SkimmedNode { #[schemars(with = "String")] pub x25519_sphinx_pubkey: x25519::PublicKey, - #[serde(alias = "role")] - pub epoch_role: NodeRole, + #[serde(alias = "epoch_role")] + pub role: NodeRole, // needed for the purposes of sending appropriate test packets #[serde(default)] @@ -157,7 +157,7 @@ pub struct SkimmedNode { impl SkimmedNode { pub fn get_mix_layer(&self) -> Option { - match self.epoch_role { + match self.role { NodeRole::Mixnode { layer } => Some(layer), _ => None, } diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index d956f2897d3..89478fbd09a 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -24,21 +24,21 @@ use utoipa::IntoParams; pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { Router::new() .route( - "/master-verification-key:epoch_id", + "/master-verification-key", axum::routing::get({ let ecash_state = Arc::clone(&ecash_state); |epoch_id| master_verification_key(epoch_id, ecash_state) }), ) .route( - "/aggregated-expiration-date-signatures:expiration_date", + "/aggregated-expiration-date-signatures", axum::routing::get({ let ecash_state = Arc::clone(&ecash_state); |expiration_date| expiration_date_signatures(expiration_date, ecash_state) }), ) .route( - "/aggregated-coin-indices-signatures:epoch_id", + "/aggregated-coin-indices-signatures", axum::routing::get({ let ecash_state = Arc::clone(&ecash_state); |epoch_id| coin_indices_signatures(epoch_id, ecash_state) @@ -52,7 +52,7 @@ pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router) -> Router) -> Router { Router::new() .route( @@ -242,6 +243,7 @@ async fn batch_redeem_tickets( (status = 500, body = ErrorResponse, description = "bloomfilters got disabled"), ) )] +#[deprecated] async fn double_spending_filter_v1( _state: Arc, ) -> AxumResult> { diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 789053d0d19..0d5c97eacd8 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -169,7 +169,7 @@ impl EpochAdvancer { let standby_eligible = all_choices .iter() .filter(|node| { - exit_gateways.contains(&node.0.node_id) + !exit_gateways.contains(&node.0.node_id) && !entry_gateways.contains(&node.0.node_id) && !mixnodes.contains(&node.0.node_id) }) @@ -228,14 +228,24 @@ impl EpochAdvancer { ) } - Ok(RewardedSet { + let mut rewarded_set = RewardedSet { entry_gateways: entry_gateways.into_iter().collect(), exit_gateways: exit_gateways.into_iter().collect(), layer1, layer2, layer3, standby, - }) + }; + + // make sure to sort the rewarded set values + rewarded_set.entry_gateways.sort(); + rewarded_set.exit_gateways.sort(); + rewarded_set.layer1.sort(); + rewarded_set.layer2.sort(); + rewarded_set.layer3.sort(); + rewarded_set.standby.sort(); + + Ok(rewarded_set) } async fn attach_performance_to_eligible_nodes( diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 1fdb0ad6012..0f04d971c68 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -274,8 +274,8 @@ impl Monitor { info!("Received {}/{} packets", total_received, total_sent); let summary = self.summary_producer.produce_summary( - prepared_packets.tested_mixnodes, - prepared_packets.tested_gateways, + prepared_packets.mixnodes_under_test, + prepared_packets.gateways_under_test, received, prepared_packets.invalid_mixnodes, prepared_packets.invalid_gateways, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 69a0ae3e4d0..8650cf257f7 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -25,7 +25,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; -use tracing::{error, info, trace}; +use tracing::{debug, error, info, trace}; const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); @@ -59,10 +59,10 @@ pub(crate) struct PreparedPackets { pub(super) packets: Vec, /// Vector containing list of public keys and owners of all nodes mixnodes being tested. - pub(super) tested_mixnodes: Vec, + pub(super) mixnodes_under_test: Vec, /// Vector containing list of public keys and owners of all gateways being tested. - pub(super) tested_gateways: Vec, + pub(super) gateways_under_test: Vec, /// All mixnodes that failed to get parsed correctly or were not version compatible. /// They will be marked to the validator as being down for the test. @@ -151,34 +151,38 @@ impl PacketPreparer { self.contract_cache.wait_for_initial_values().await; self.described_cache.naive_wait_for_initial_values().await; + let described_nodes = self + .described_cache + .get() + .await + .expect("the self-describe cache should have been initialised!"); + // now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); loop { let gateways = self.contract_cache.legacy_gateways_all().await; let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; - - if gateways.len() < minimum_full_routes { - self.topology_wait_backoff(initialisation_backoff).await; - continue; - } - - let mut layer1_count = 0; - let mut layer2_count = 0; - let mut layer3_count = 0; - - for mix in mixnodes { - match mix.layer { - LegacyMixLayer::One => layer1_count += 1, - LegacyMixLayer::Two => layer2_count += 1, - LegacyMixLayer::Three => layer3_count += 1, + let nym_nodes = self.contract_cache.nym_nodes().await; + + let mut gateways_count = gateways.len(); + let mut mixnodes_count = mixnodes.len(); + + for nym_node in nym_nodes { + if let Some(described) = described_nodes.get_description(&nym_node.node_id()) { + if described.declared_role.mixnode { + mixnodes_count += 1; + } else if described.declared_role.entry { + gateways_count += 1; + } } } - if layer1_count >= minimum_full_routes - && layer2_count >= minimum_full_routes - && layer3_count >= minimum_full_routes - { + debug!( + "we have {mixnodes_count} possible mixnodes and {gateways_count} possible gateways" + ); + + if gateways_count >= minimum_full_routes && mixnodes_count * 3 >= minimum_full_routes { break; } @@ -529,30 +533,41 @@ impl PacketPreparer { let mixing_nym_nodes = descriptions.mixing_nym_nodes(); let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); - let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes); - let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways); - - let mut tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); - let mut tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + let (mut mixnodes_to_test_details, invalid_mixnodes) = + self.filter_outdated_and_malformed_mixnodes(mixnodes); + let (mut gateways_to_test_details, invalid_gateways) = + self.filter_outdated_and_malformed_gateways(gateways); + + // summary of nodes that got tested + let mut mixnodes_under_test = mixnodes_to_test_details + .iter() + .map(|node| node.into()) + .collect::>(); + let mut gateways_under_test = gateways_to_test_details + .iter() + .map(|node| node.into()) + .collect::>(); // try to add nym-nodes into the fold if let Some(rewarded_set) = rewarded_set { let mut rng = thread_rng(); for mix in mixing_nym_nodes { if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, mix) { - tested_mixnodes.push(TestableNode::from(&parsed)); + mixnodes_under_test.push(TestableNode::from(&parsed)); + mixnodes_to_test_details.push(parsed); } } } for gateway in gateway_capable_nym_nodes { if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) { - tested_gateways.push((&parsed, gateway.node_id).into()) + gateways_under_test.push((&parsed, gateway.node_id).into()); + gateways_to_test_details.push((parsed, gateway.node_id)); } } let packets_to_create = (test_routes.len() * self.per_node_test_packets) - * (tested_mixnodes.len() + tested_gateways.len()); + * (mixnodes_under_test.len() + gateways_under_test.len()); info!("Need to create {} mix packets", packets_to_create); let mut all_gateway_packets = HashMap::new(); @@ -574,7 +589,7 @@ impl PacketPreparer { #[allow(clippy::unwrap_used)] let mixnode_test_packets = mix_tester .mixnodes_test_packets( - &mixnodes, + &mixnodes_to_test_details, route_ext, self.per_node_test_packets as u32, None, @@ -588,7 +603,7 @@ impl PacketPreparer { gateway_packets.push_packets(mix_packets); // and generate test packets for gateways (note the variable recipient) - for (gateway, node_id) in &gateways { + for (gateway, node_id) in &gateways_to_test_details { let recipient = self.create_packet_sender(gateway); let gateway_identity = gateway.identity_key; let gateway_address = gateway.clients_address(); @@ -624,8 +639,8 @@ impl PacketPreparer { PreparedPackets { packets, - tested_mixnodes, - tested_gateways, + mixnodes_under_test, + gateways_under_test, invalid_mixnodes, invalid_gateways, } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index d8e0b3c32b5..8368e58a97d 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -145,6 +145,7 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { } } +#[derive(Debug, Clone)] pub struct DescribedNodes { nodes: HashMap, } @@ -290,6 +291,7 @@ async fn try_get_description( }) } +#[derive(Debug)] struct RefreshData { host: String, node_id: NodeId, diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 1752345f2c4..78611ca6240 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -210,6 +210,7 @@ impl ResolvedNodeDescribedInfo { } } +#[derive(Debug)] pub(crate) struct UnwrappedResolvedNodeDescribedInfo { pub(crate) build_info: BinaryBuildInformationOwned, pub(crate) roles: DeclaredRoles, diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index d532308c68b..a13508b9dfd 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -209,7 +209,7 @@ pub(crate) async fn produce_node_annotations( legacy_mix.mix_id(), NodeAnnotation { last_24h_performance: perf, - current_role: rewarded_set.role(legacy_mix.mix_id()), + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), }, ); } @@ -229,7 +229,7 @@ pub(crate) async fn produce_node_annotations( legacy_gateway.node_id, NodeAnnotation { last_24h_performance: perf, - current_role: rewarded_set.role(legacy_gateway.node_id), + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), }, ); } @@ -249,7 +249,7 @@ pub(crate) async fn produce_node_annotations( nym_node.node_id(), NodeAnnotation { last_24h_performance: perf, - current_role: rewarded_set.role(nym_node.node_id()), + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), }, ); } diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 92e788047ed..bce5d6eea6d 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -16,6 +16,7 @@ use nym_serde_helpers::date::DATE_FORMAT; use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use sqlx::Error; use std::fmt::Display; use thiserror::Error; use time::{Date, OffsetDateTime}; @@ -438,7 +439,7 @@ pub enum NymApiStorageError { // I don't think we want to expose errors to the user about what really happened #[error("experienced internal database error")] - InternalDatabaseError(#[from] sqlx::Error), + InternalDatabaseError(sqlx::Error), // the same is true here (also note that the message is subtly different so we would be able to distinguish them) #[error("experienced internal storage error")] @@ -449,6 +450,14 @@ pub enum NymApiStorageError { StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), } +impl From for NymApiStorageError { + fn from(err: Error) -> Self { + // those should realistically never be happening so an `error!` is warranted + error!("storage failure: {err}"); + NymApiStorageError::InternalDatabaseError(err) + } +} + impl NymApiStorageError { pub fn database_inconsistency>(reason: S) -> NymApiStorageError { NymApiStorageError::DatabaseInconsistency { diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index 0e9e6f45f96..badf0e23449 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -173,7 +173,7 @@ impl NymContractCacheRefresher { layer, }, rewarding_details: detail.rewarding_details, - pending_changes: detail.pending_changes, + pending_changes: detail.pending_changes.into(), }) } diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index 1ebd9260c77..cae94d52547 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -49,7 +49,7 @@ async fn get_gateways_described( .into_iter() .map(|bond| LegacyDescribedGateway { self_described: self_descriptions.get_description(&bond.node_id).cloned(), - bond, + bond: bond.bond, }) .collect(), ) diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index ad92a689c08..4ccb3c02de0 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -9,7 +9,7 @@ use axum::routing::get; use axum::{Json, Router}; use nym_api_requests::models::{ AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NoiseDetails, - NymNodeData, PerformanceHistoryResponse, UptimeHistoryResponse, + NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -125,32 +125,27 @@ async fn get_bonded_nodes( path = "/described", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse) + (status = 200, body = PaginatedResponse) ), params(PaginationRequest) )] async fn get_described_nodes( State(state): State, Query(pagination): Query, -) -> AxumResult>> { +) -> AxumResult>> { // TODO: implement it let _ = pagination; let cache = state.described_nodes_cache.get().await?; - let descriptions = cache.all_nodes(); - - let data = descriptions - .map(|n| &n.description) - .cloned() - .collect::>(); + let descriptions = cache.all_nodes().cloned().collect::>(); Ok(Json(PaginatedResponse { pagination: Pagination { - total: data.len(), + total: descriptions.len(), page: 0, - size: data.len(), + size: descriptions.len(), }, - data, + data: descriptions, })) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 8d1809668f6..36f17593ae7 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -234,9 +234,9 @@ impl Config { fn default_http_socket_addr() -> SocketAddr { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080) + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8000) } else { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080) + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8000) } } } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 5b4093ecbe7..f41eabf5083 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -119,9 +119,8 @@ impl StorageManager { start_ts_secs: i64, end_ts_secs: i64, ) -> Result, sqlx::Error> { - // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. - // some (like v3 directory) happens at runtime - let result = sqlx::query_as( + let result = sqlx::query_as!( + AvgGatewayReliability, r#" SELECT d.node_id as "node_id: NodeId", @@ -135,9 +134,9 @@ impl StorageManager { timestamp <= ? GROUP BY 1 "#, + start_ts_secs, + end_ts_secs ) - .bind(start_ts_secs) - .bind(end_ts_secs) .fetch_all(&self.connection_pool) .await?; Ok(result) diff --git a/nym-api/src/v3_migration.rs b/nym-api/src/v3_migration.rs index c4e920247b8..42401ad6fd7 100644 --- a/nym-api/src/v3_migration.rs +++ b/nym-api/src/v3_migration.rs @@ -30,10 +30,6 @@ pub async fn migrate_v3_database( let contract_gateways = nyxd_client.get_gateways().await?; let nym_nodes = nyxd_client.get_nymnodes().await?; - if preassigned_ids.len() != contract_gateways.len() { - bail!("CONTRACT DATA CORRUPTION: THE NUMBER OF PREASSIGNED GATEWAY IDS IS DIFFERENT THAN THE NUMBER OF GATEWAYS") - } - // assign node_id to every gateway let all_known = storage.get_all_known_gateways().await?; for gateway in all_known { diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 4db9d492973..6bf586f71b0 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::api::v1::node::models::{HostInformation, LegacyHostInformation}; +use crate::api::v1::node::models::{ + HostInformation, LegacyHostInformation, LegacyHostInformationV2, +}; use crate::error::Error; use nym_crypto::asymmetric::identity; use schemars::JsonSchema; @@ -61,9 +63,18 @@ impl SignedHostInformation { return true; } - // attempt to verify legacy signature + // attempt to verify legacy signatures + let legacy_v2 = SignedData { + data: LegacyHostInformationV2::from(self.data.clone()), + signature: self.signature.clone(), + }; + + if legacy_v2.verify(&self.keys.ed25519_identity) { + return true; + } + SignedData { - data: LegacyHostInformation::from(self.data.clone()), + data: LegacyHostInformation::from(legacy_v2.data), signature: self.signature.clone(), } .verify(&self.keys.ed25519_identity) @@ -133,6 +144,81 @@ mod tests { assert!(signed_info.verify_host_information()); } + #[test] + fn dummy_legacy_v2_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV2 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV2 { + ed25519_identity: ed22519.public_key().to_base58_string(), + x25519_sphinx: x25519_sphinx.public_key().to_base58_string(), + x25519_noise: "".to_string(), + }, + }; + + let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV2 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV2 { + ed25519_identity: ed22519.public_key().to_base58_string(), + x25519_sphinx: x25519_sphinx.public_key().to_base58_string(), + x25519_noise: x25519_noise.public_key().to_base58_string(), + }, + }; + + let host_info_no_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_no_noise.ip_address.clone(), + hostname: legacy_info_no_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(), + x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(), + x25519_noise: None, + }, + }; + + let host_info_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_noise.ip_address.clone(), + hostname: legacy_info_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), + x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), + x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()), + }, + }; + + // signature on legacy data + let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key()) + .unwrap() + .signature; + + let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + let current_struct_no_noise = SignedData { + data: host_info_no_noise, + signature: signature_no_noise, + }; + + let current_struct_noise = SignedData { + data: host_info_noise, + signature: signature_noise, + }; + + assert!(!current_struct_no_noise.verify(ed22519.public_key())); + assert!(current_struct_no_noise.verify_host_information()); + + // if noise key is present, the signature is actually valid + assert!(current_struct_noise.verify(ed22519.public_key())); + assert!(current_struct_noise.verify_host_information()) + } + #[test] fn dummy_legacy_signed_host_verification() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index dcaa12435f5..6979beb9494 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -59,6 +59,13 @@ pub struct HostInformation { pub keys: HostKeys, } +#[derive(Serialize)] +pub struct LegacyHostInformationV2 { + pub ip_address: Vec, + pub hostname: Option, + pub keys: LegacyHostKeysV2, +} + #[derive(Serialize)] pub struct LegacyHostInformation { pub ip_address: Vec, @@ -66,8 +73,18 @@ pub struct LegacyHostInformation { pub keys: LegacyHostKeys, } -impl From for LegacyHostInformation { +impl From for LegacyHostInformationV2 { fn from(value: HostInformation) -> Self { + LegacyHostInformationV2 { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +impl From for LegacyHostInformation { + fn from(value: LegacyHostInformationV2) -> Self { LegacyHostInformation { ip_address: value.ip_address, hostname: value.hostname, @@ -99,13 +116,11 @@ pub struct HostKeys { pub x25519_noise: Option, } -impl From for LegacyHostKeys { - fn from(value: HostKeys) -> Self { - LegacyHostKeys { - ed25519: value.ed25519_identity.to_base58_string(), - x25519: value.x25519_sphinx.to_base58_string(), - } - } +#[derive(Serialize)] +pub struct LegacyHostKeysV2 { + pub ed25519_identity: String, + pub x25519_sphinx: String, + pub x25519_noise: String, } #[derive(Serialize)] @@ -114,6 +129,28 @@ pub struct LegacyHostKeys { pub x25519: String, } +impl From for LegacyHostKeysV2 { + fn from(value: HostKeys) -> Self { + LegacyHostKeysV2 { + ed25519_identity: value.ed25519_identity.to_base58_string(), + x25519_sphinx: value.x25519_sphinx.to_base58_string(), + x25519_noise: value + .x25519_noise + .map(|k| k.to_base58_string()) + .unwrap_or_default(), + } + } +} + +impl From for LegacyHostKeys { + fn from(value: LegacyHostKeysV2) -> Self { + LegacyHostKeys { + ed25519: value.ed25519_identity, + x25519: value.x25519_sphinx, + } + } +} + #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostSystem { diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/entry_gateway.rs index 6807e55d58b..f7c0a51c06b 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/entry_gateway.rs @@ -154,7 +154,7 @@ pub fn ephemeral_entry_gateway_config( config: Config, mnemonic: &bip39::Mnemonic, ) -> Result { - let auth_opts = LocalAuthenticatorOpts { + let mut auth_opts = LocalAuthenticatorOpts { config: nym_authenticator::Config { base: nym_client_core_config_types::Config { client: base_client_config(&config), @@ -173,6 +173,10 @@ pub fn ephemeral_entry_gateway_config( custom_mixnet_path: None, }; + if config.authenticator.debug.disable_poisson_rate { + auth_opts.config.base.set_no_poisson_process(); + } + let wg_opts = LocalWireguardOpts { config: super::Wireguard { enabled: config.wireguard.enabled, diff --git a/nym-node/src/config/exit_gateway.rs b/nym-node/src/config/exit_gateway.rs index 4c3c55fad2d..0fc6b9e8eed 100644 --- a/nym-node/src/config/exit_gateway.rs +++ b/nym-node/src/config/exit_gateway.rs @@ -243,7 +243,7 @@ pub fn ephemeral_exit_gateway_config( ipr_opts.config.base.set_no_poisson_process() } - let auth_opts = LocalAuthenticatorOpts { + let mut auth_opts = LocalAuthenticatorOpts { config: nym_authenticator::Config { base: nym_client_core_config_types::Config { client: base_client_config(&config), @@ -262,6 +262,10 @@ pub fn ephemeral_exit_gateway_config( custom_mixnet_path: None, }; + if config.authenticator.debug.disable_poisson_rate { + auth_opts.config.base.set_no_poisson_process(); + } + let pub_id_path = config .storage_paths .keys diff --git a/nym-node/src/config/mixnode.rs b/nym-node/src/config/mixnode.rs index f5a56e128af..c938610255d 100644 --- a/nym-node/src/config/mixnode.rs +++ b/nym-node/src/config/mixnode.rs @@ -46,6 +46,7 @@ pub struct Verloc { /// will use. /// Useful when the node is behind a proxy. #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] pub announce_port: Option, #[serde(default)] diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 60ed79ab204..65ca8983d72 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -421,6 +421,7 @@ pub struct Mixnet { /// will use. /// Useful when the node is behind a proxy. #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] pub announce_port: Option, /// Addresses to nym APIs from which the node gets the view of the network. diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 1fa8dc70c4e..8e2ce5a3279 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -4,13 +4,12 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ - AnnotationResponse, ComputeRewardEstParam, GatewayCoreStatusResponse, + AnnotationResponse, ComputeRewardEstParam, DisplayRole, GatewayCoreStatusResponse, GatewayStatusReportResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; @@ -110,7 +109,7 @@ pub async fn mixnode_inclusion_probability( pub async fn get_nymnode_role( node_id: NodeId, state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { +) -> Result, BackendError> { let annotation = get_nymnode_annotation(node_id, state).await?; Ok(annotation.annotation.and_then(|n| n.current_role)) } diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index ff35bd2b9b6..23144fd8712 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -110,7 +110,7 @@ export type TGatewayReport = { most_recent: number; }; -export type TNodeRole = 'entry' | 'exit' | 'layer1' | 'layer2' | 'layer3' | 'standby'; +export type TNodeRole = 'entryGateway' | 'exitGateway' | 'layer1' | 'layer2' | 'layer3' | 'standby'; export type MixnodeSaturationResponse = { saturation: string; diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index 0707730ec66..545090e3ae6 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -10,7 +10,13 @@ pub mod mixnet; pub mod tcp_proxy; pub use error::{Error, Result}; -pub use nym_client_core::client::mix_traffic::transceiver::*; +pub use nym_client_core::client::{ + mix_traffic::transceiver::*, + topology_control::{ + GeoAwareTopologyProvider, NymApiTopologyProvider, NymApiTopologyProviderConfig, + TopologyProvider, + }, +}; pub use nym_network_defaults::{ ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails, ValidatorDetails, diff --git a/service-providers/authenticator/src/authenticator.rs b/service-providers/authenticator/src/authenticator.rs index d5a69e7d446..f36ed252676 100644 --- a/service-providers/authenticator/src/authenticator.rs +++ b/service-providers/authenticator/src/authenticator.rs @@ -128,7 +128,9 @@ impl Authenticator { // Connect to the mixnet let mixnet_client = crate::mixnet_client::create_mixnet_client( &self.config.base, - task_handle.get_handle().named("nym_sdk::MixnetClient"), + task_handle + .get_handle() + .named("nym_sdk::MixnetClient[AUTH]"), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 009dab5427f..679d8a1b9e8 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -297,6 +297,10 @@ impl MixnetListener { credential: CredentialSpendingData, client_id: i64, ) -> Result { + ecash_verifier + .storage() + .create_bandwidth_entry(client_id) + .await?; let bandwidth = ecash_verifier .storage() .get_available_bandwidth(client_id) diff --git a/service-providers/ip-packet-router/src/ip_packet_router.rs b/service-providers/ip-packet-router/src/ip_packet_router.rs index 780a550f1af..e002559ca14 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -133,7 +133,7 @@ impl IpPacketRouter { // Connect to the mixnet let mixnet_client = crate::mixnet_client::create_mixnet_client( &self.config.base, - task_handle.get_handle().named("nym_sdk::MixnetClient"), + task_handle.get_handle().named("nym_sdk::MixnetClient[IPR]"), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index f9a6b3e5aa0..6ba73731c46 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -239,7 +239,7 @@ impl NRServiceProviderBuilder { // Connect to the mixnet let mixnet_client = create_mixnet_client( &self.config.base, - shutdown.get_handle().named("nym_sdk::MixnetClient"), + shutdown.get_handle().named("nym_sdk::MixnetClient[NR]"), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway,