From d91d01e6842033ffdb11953a501efcdce4161e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 10 Dec 2024 15:13:44 +0000 Subject: [PATCH 1/9] revamped NymTopology --- Cargo.lock | 1 - .../contract_traits/mixnet_query_client.rs | 27 +-- .../mixnet-contract/src/types.rs | 18 ++ .../acknowledgements/src/surb_ack.rs | 2 +- .../anonymous-replies/src/reply_surb.rs | 2 +- common/nymsphinx/cover/src/lib.rs | 2 +- common/nymsphinx/src/preparer/mod.rs | 4 +- common/topology/Cargo.toml | 1 - common/topology/src/error.rs | 11 +- common/topology/src/lib.rs | 196 ++++++++++++++++-- common/topology/src/node.rs | 113 ++++++++++ common/topology/src/random_route_provider.rs | 2 +- nym-api/nym-api-requests/src/models.rs | 8 +- nym-api/src/epoch_operations/helpers.rs | 7 +- nym-api/src/nym_contract_cache/cache/data.rs | 43 ++-- nym-api/src/nym_contract_cache/cache/mod.rs | 6 +- .../src/nym_contract_cache/cache/refresher.rs | 22 +- nym-api/src/support/nyxd/mod.rs | 7 +- nym-network-monitor/src/accounting.rs | 2 +- 19 files changed, 399 insertions(+), 75 deletions(-) create mode 100644 common/topology/src/node.rs diff --git a/Cargo.lock b/Cargo.lock index d84ab9916c1..dab3434cf40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6562,7 +6562,6 @@ dependencies = [ "bs58", "log", "nym-api-requests", - "nym-bin-common", "nym-config", "nym-crypto", "nym-mixnet-contract-common", diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 2e1095b3b8b..5060fe3df47 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -26,10 +26,10 @@ use nym_mixnet_contract_common::{ reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochStatus, GatewayBond, - GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, IdentityKey, - IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, - MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId, + CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus, + GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, + IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, + MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, @@ -670,7 +670,7 @@ impl PagedMixnetQueryClient for T where T: MixnetQueryClient {} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait MixnetQueryClientExt: MixnetQueryClient { - async fn get_rewarded_set(&self) -> Result { + async fn get_rewarded_set(&self) -> Result { let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message)); let metadata = self.get_rewarded_set_metadata().await?; @@ -711,13 +711,16 @@ pub trait MixnetQueryClientExt: MixnetQueryClient { return error_response("the nodes assigned for 'standby' returned unexpected epoch_id"); } - Ok(RewardedSet { - entry_gateways: entry.nodes, - exit_gateways: exit.nodes, - layer1: layer1.nodes, - layer2: layer2.nodes, - layer3: layer3.nodes, - standby: standby.nodes, + Ok(EpochRewardedSet { + epoch_id: expected_epoch_id, + assignment: RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + }, }) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index eb5f972e4b5..ba1546a1d40 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -3,6 +3,7 @@ use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams}; use crate::nym_node::Role; +use crate::EpochId; use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::Coin; @@ -32,6 +33,23 @@ impl RoleAssignment { } } +#[cw_serde] +#[derive(Default)] +pub struct EpochRewardedSet { + pub epoch_id: EpochId, + + pub assignment: RewardedSet, +} + +impl From<(EpochId, RewardedSet)> for EpochRewardedSet { + fn from((epoch_id, assignment): (EpochId, RewardedSet)) -> Self { + EpochRewardedSet { + epoch_id, + assignment, + } + } +} + #[cw_serde] #[derive(Default)] pub struct RewardedSet { diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index a9311be597b..d30768e1f62 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -50,7 +50,7 @@ impl SurbAck { R: RngCore + CryptoRng, { let route = - topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; + topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index f25871ba0be..8cbfc868190 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -95,7 +95,7 @@ impl ReplySurb { R: RngCore + CryptoRng, { let route = - topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; + topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 23c83c3ed11..86510d1de70 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -119,7 +119,7 @@ where .collect(); let route = - topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, full_address.gateway())?; + topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, full_address.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len()); let destination = full_address.as_sphinx_destination(); diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 8578b655b0a..33a8c6aae53 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -245,11 +245,11 @@ pub trait FragmentPreparer { log::trace!("using deterministic route selection"); let seed = fragment_header.seed().wrapping_mul(self.nonce()); let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); - topology.random_route_to_gateway(&mut rng, hops, destination)? + topology.random_route_to_egress(&mut rng, hops, destination)? } else { log::trace!("using pseudorandom route selection"); let mut rng = self.rng(); - topology.random_route_to_gateway(&mut rng, hops, destination)? + topology.random_route_to_egress(&mut rng, hops, destination)? }; let destination = packet_recipient.as_sphinx_destination(); diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 055175fd36d..b19fb2da8a0 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -28,7 +28,6 @@ tsify = { workspace = true, features = ["js"], optional = true } wasm-bindgen = { workspace = true, optional = true } ## internal -nym-bin-common = { path = "../bin-common" } nym-config = { path = "../config" } nym-crypto = { path = "../crypto", features = ["sphinx", "outfox"] } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 835ea37b1ff..bdea316ca30 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -4,18 +4,25 @@ use std::array::TryFromSliceError; use crate::MixLayer; +use nym_sphinx_addressing::NodeIdentity; use nym_sphinx_types::NymPacketError; use thiserror::Error; #[derive(Debug, Error)] pub enum NymTopologyError { - #[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")] + #[error("the provided network topology is empty - there are no valid nodes on it - the network request(s) probably failed")] EmptyNetworkTopology, + #[error("no node with identity {node_identity} is known")] + NonExistentNode { node_identity: NodeIdentity }, + + // + // + // #[error("The provided network topology has no gateways available")] NoGatewaysAvailable, - #[error("The provided network topology has no mixnodes available")] + #[error("The provided network topology has no valid mixnodes available")] NoMixnodesAvailable, #[error("Gateway with identity key {identity_key} doesn't exist")] diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 4133a8ae0e3..82b2e63104f 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,19 +1,16 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -#![allow(unknown_lints)] -// clippy::to_string_trait_impl is not on stable as of 1.77 - pub use error::NymTopologyError; use log::{debug, info, warn}; use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; use nym_config::defaults::var_names::NYM_API; -use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; +use nym_mixnet_contract_common::{EpochRewardedSet, IdentityKeyRef, NodeId, RewardedSet}; use nym_sphinx_addressing::nodes::NodeIdentity; -use nym_sphinx_types::Node as SphinxNode; +use nym_sphinx_types::{Node as SphinxNode, Node}; use rand::prelude::SliceRandom; use rand::{CryptoRng, Rng}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::Infallible; use std::fmt::{self, Display, Formatter}; use std::io; @@ -31,6 +28,7 @@ pub mod random_route_provider; #[cfg(feature = "provider-trait")] pub mod provider_trait; +mod node; #[cfg(feature = "serializable")] pub(crate) mod serde; @@ -39,9 +37,11 @@ pub use crate::serde::{ SerializableGateway, SerializableMixNode, SerializableNymTopology, SerializableTopologyError, }; +use crate::node::RoutingNode; #[cfg(feature = "provider-trait")] pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider}; +#[deprecated] #[derive(Debug, Default, Clone)] pub enum NodeVersion { Explicit(semver::Version), @@ -113,6 +113,160 @@ impl Display for NetworkAddress { pub type MixLayer = u8; +pub struct NymTopologyNew { + // 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: EpochRewardedSet, + + node_details: HashMap, +} + +const unused: &str = r#"there shall be a config setting, like debug.topology.use_extended = true/false (so that node_details would also include standby/inactive nodes) + and another one for debug.topology.ignore_epoch_roles = true/false (to say send final packet to epoch mixnode) + "#; + +impl NymTopologyNew { + pub fn new(rewarded_set: EpochRewardedSet, node_details: Vec) -> Self { + NymTopologyNew { + rewarded_set, + node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), + } + } + + pub fn add_additional_nodes(&mut self, nodes: impl Iterator) + where + N: TryInto, + >::Error: Display, + { + for node in nodes { + match node.try_into() { + Ok(node_details) => { + let node_id = node_details.node_id; + if self.node_details.insert(node_id, node_details).is_some() { + debug!("overwriting node details for node {node_id}") + } + } + Err(err) => { + debug!("malformed node details: {err}") + } + } + } + } + + fn get_sphinx_node(&self, node_id: NodeId) -> Option { + self.node_details.get(&node_id).map(Into::into) + } + + fn find_valid_mix_hop( + &self, + rng: &mut R, + id_choices: Vec, + ) -> Result + where + R: Rng + CryptoRng + ?Sized, + { + let mut id_choices = id_choices; + while !id_choices.is_empty() { + let index = rng.gen_range(0..id_choices.len()); + + // SAFETY: this is not run if the vector is empty + let candidate_id = id_choices[index]; + match self.get_sphinx_node(candidate_id) { + Some(node) => { + return Ok(node); + } + // this will mess with VRF, but that's a future problem + None => { + id_choices.remove(index); + continue; + } + } + } + + Err(NymTopologyError::NoMixnodesAvailable) + } + + fn choose_mixing_node( + &self, + rng: &mut R, + assigned_nodes: &[NodeId], + ) -> Result + where + R: Rng + CryptoRng + ?Sized, + { + // try first choice without cloning the ids (because I reckon, more often than not, it will actually work) + let Some(candidate) = assigned_nodes.choose(rng) else { + return Err(NymTopologyError::NoMixnodesAvailable); + }; + + match self.get_sphinx_node(*candidate) { + Some(node) => Ok(node), + None => { + let remaining_choices = assigned_nodes + .iter() + .filter(|&n| n != candidate) + .copied() + .collect(); + self.find_valid_mix_hop(rng, remaining_choices) + } + } + } + + fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> { + self.node_details + .values() + .find(|n| n.identity_key == node_identity) + } + + fn sphinx_node_by_identity( + &self, + node_identity: NodeIdentity, + ) -> Result { + let Some(node) = self.find_node_by_identity(node_identity) else { + return Err(NymTopologyError::NonExistentNode { node_identity }); + }; + + Ok(node.into()) + } + + pub fn random_mix_route(&self, rng: &mut R) -> Result, NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + if self.rewarded_set.assignment.is_empty() || self.node_details.is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); + } + + // we reserve an additional item in the route because we'll have to push an egress + let mut mix_route = Vec::with_capacity(4); + + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer1)?); + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer2)?); + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer3)?); + + Ok(mix_route) + } + + /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1, + /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node + pub fn random_route_to_egress( + &self, + rng: &mut R, + egress_identity: NodeIdentity, + ) -> Result, NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + let egress = self.sphinx_node_by_identity(egress_identity)?; + let mut mix_route = self.random_mix_route(rng)?; + mix_route.push(egress); + Ok(mix_route) + } +} + // the reason for those having `Legacy` prefix is that eventually they should be using // exactly the same types #[derive(Debug, Clone, Default)] @@ -122,6 +276,7 @@ pub struct NymTopology { } impl NymTopology { + #[deprecated] pub async fn new_from_env() -> Result { let api_url = std::env::var(NYM_API)?; @@ -157,6 +312,7 @@ impl NymTopology { NymTopology { mixes, gateways } } + #[deprecated] pub fn new_unordered( unordered_mixes: Vec, gateways: Vec, @@ -338,20 +494,20 @@ impl NymTopology { Ok(route) } - pub fn random_path_to_gateway( + pub fn random_path_to_egress( &self, rng: &mut R, num_mix_hops: u8, - gateway_identity: &NodeIdentity, + egress_identity: &NodeIdentity, ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> where R: Rng + CryptoRng + ?Sized, { - let gateway = self.get_gateway(gateway_identity).ok_or( - NymTopologyError::NonExistentGatewayError { - identity_key: gateway_identity.to_base58_string(), - }, - )?; + let gateway = + self.get_gateway(egress_identity) + .ok_or(NymTopologyError::NonExistentGatewayError { + identity_key: egress_identity.to_base58_string(), + })?; let path = self.random_mix_route(rng, num_mix_hops)?; @@ -360,20 +516,20 @@ impl NymTopology { /// Tries to create a route to the specified gateway, such that it goes through mixnode on layer 1, /// mixnode on layer2, .... mixnode on layer n and finally the target gateway - pub fn random_route_to_gateway( + pub fn random_route_to_egress( &self, rng: &mut R, num_mix_hops: u8, - gateway_identity: &NodeIdentity, + egress_identity: &NodeIdentity, ) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { - let gateway = self.get_gateway(gateway_identity).ok_or( - NymTopologyError::NonExistentGatewayError { - identity_key: gateway_identity.to_base58_string(), - }, - )?; + let gateway = + self.get_gateway(egress_identity) + .ok_or(NymTopologyError::NonExistentGatewayError { + identity_key: egress_identity.to_base58_string(), + })?; Ok(self .random_mix_route(rng, num_mix_hops)? diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs new file mode 100644 index 00000000000..050b34ffcdc --- /dev/null +++ b/common/topology/src/node.rs @@ -0,0 +1,113 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_api_requests::models::DeclaredRoles; +use nym_api_requests::nym_nodes::SkimmedNode; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_mixnet_contract_common::NodeId; +use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; +use nym_sphinx_types::Node as SphinxNode; +use std::net::SocketAddr; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RoutingNodeError { + #[error("this node has no mixing information available")] + NoMixingInformationAvailable, + + #[error("node {node_id} ('{identity}') has not provided any valid ip addresses")] + NoIpAddressesProvided { + node_id: NodeId, + identity: ed25519::PublicKey, + }, +} + +#[derive(Debug, Clone)] +pub struct EntryDetails { + pub clients_ws_port: u16, + pub hostname: Option, + pub clients_wss_port: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct SupportedRoles { + pub mixnode: bool, + pub mixnet_entry: bool, + pub mixnet_exit: bool, +} + +impl From for SupportedRoles { + fn from(value: DeclaredRoles) -> Self { + SupportedRoles { + mixnode: value.mixnode, + mixnet_entry: value.entry, + mixnet_exit: value.exit_nr && value.exit_ipr, + } + } +} + +#[derive(Debug, Clone)] +pub struct RoutingNode { + pub node_id: NodeId, + + pub mix_host: SocketAddr, + + pub entry: Option, + pub identity_key: ed25519::PublicKey, + pub sphinx_key: x25519::PublicKey, + + pub supported_roles: SupportedRoles, + pub performance: f64, +} + +impl RoutingNode { + pub fn ws_entry_address_tls(&self) -> Option { + todo!() + } + + pub fn ws_entry_address_no_tls(&self) -> Option { + todo!() + } + + pub fn ws_entry_address(&self) -> Option { + if let Some(tls) = self.ws_entry_address_tls() { + return Some(tls); + } + self.ws_entry_address_no_tls() + } + + pub fn identity(&self) -> ed25519::PublicKey { + self.identity_key + } + + pub fn mix_host(&self) -> Option { + todo!() + } +} + +impl<'a> From<&'a RoutingNode> for SphinxNode { + fn from(node: &'a RoutingNode) -> Self { + // SAFETY: this conversion is infallible as all versions of socket addresses have + // sufficiently small bytes representation to fit inside `NodeAddressBytes` + #[allow(clippy::unwrap_used)] + let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) + .try_into() + .unwrap(); + + SphinxNode::new(node_address_bytes, (&node.sphinx_key).into()) + } +} + +impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { + type Error = RoutingNodeError; + + fn try_from(value: &'a SkimmedNode) -> Result { + if value.ip_addresses.is_empty() { + return Err(RoutingNodeError::NoIpAddressesProvided { + node_id: value.node_id, + identity: value.ed25519_identity_pubkey, + }); + } + todo!() + } +} diff --git a/common/topology/src/random_route_provider.rs b/common/topology/src/random_route_provider.rs index 1771c83eb71..9b74dc118b1 100644 --- a/common/topology/src/random_route_provider.rs +++ b/common/topology/src/random_route_provider.rs @@ -25,6 +25,6 @@ where destination: &Recipient, ) -> Result, NymTopologyError> { self.inner - .random_route_to_gateway(&mut self.rng, hops, destination.gateway()) + .random_route_to_egress(&mut self.rng, hops, destination.gateway()) } } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 3b9c759620b..3e157d1f681 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -20,7 +20,9 @@ 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::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_mixnet_contract_common::{ + EpochId, 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; @@ -1342,6 +1344,10 @@ impl NodeRefreshBody { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct RewardedSetResponse { + #[serde(default)] + #[schema(value_type = u32)] + pub epoch_id: EpochId, + pub entry_gateways: Vec, pub exit_gateways: Vec, diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index fce29eda8d4..95caa10d030 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -6,7 +6,9 @@ use crate::support::caching::Cache; use cosmwasm_std::{Decimal, Fraction}; use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; -use nym_mixnet_contract_common::{ExecuteMsg, NodeId, RewardedSet, RewardingParams}; +use nym_mixnet_contract_common::{ + EpochRewardedSet, ExecuteMsg, NodeId, RewardedSet, RewardingParams, +}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tokio::sync::RwLockReadGuard; @@ -93,11 +95,12 @@ impl EpochAdvancer { pub(crate) async fn load_nodes_for_rewarding( &self, - nodes: &RewardedSet, + nodes: &EpochRewardedSet, // we only need reward parameters for active set work factor and rewarded/active set sizes; // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync global_rewarding_params: RewardingParams, ) -> Vec { + let nodes = &nodes.assignment; // currently we are using constant omega for nodes, but that will change with tickets // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere let active_node_work_factor = global_rewarding_params.active_node_work(); diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index d259109f988..b5b44de29aa 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -7,14 +7,16 @@ use nym_api_requests::models::{ConfigScoreDataResponse, RewardedSetResponse}; use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ - ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, - RewardedSet, RewardingParams, + ConfigScoreParams, EpochId, EpochRewardedSet, HistoricalNymNodeVersionEntry, Interval, NodeId, + NymNodeDetails, RewardedSet, RewardingParams, }; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; #[derive(Default, Clone)] pub(crate) struct CachedRewardedSet { + pub(crate) epoch_id: EpochId, + pub(crate) entry_gateways: HashSet, pub(crate) exit_gateways: HashSet, @@ -28,28 +30,32 @@ pub(crate) struct CachedRewardedSet { pub(crate) standby: HashSet, } -impl From for CachedRewardedSet { - fn from(value: RewardedSet) -> Self { +impl From for CachedRewardedSet { + fn from(value: EpochRewardedSet) -> Self { CachedRewardedSet { - entry_gateways: value.entry_gateways.into_iter().collect(), - exit_gateways: value.exit_gateways.into_iter().collect(), - layer1: value.layer1.into_iter().collect(), - layer2: value.layer2.into_iter().collect(), - layer3: value.layer3.into_iter().collect(), - standby: value.standby.into_iter().collect(), + epoch_id: value.epoch_id, + entry_gateways: value.assignment.entry_gateways.into_iter().collect(), + exit_gateways: value.assignment.exit_gateways.into_iter().collect(), + layer1: value.assignment.layer1.into_iter().collect(), + layer2: value.assignment.layer2.into_iter().collect(), + layer3: value.assignment.layer3.into_iter().collect(), + standby: value.assignment.standby.into_iter().collect(), } } } -impl From for RewardedSet { +impl From for EpochRewardedSet { fn from(value: CachedRewardedSet) -> Self { - RewardedSet { - entry_gateways: value.entry_gateways.into_iter().collect(), - exit_gateways: value.exit_gateways.into_iter().collect(), - layer1: value.layer1.into_iter().collect(), - layer2: value.layer2.into_iter().collect(), - layer3: value.layer3.into_iter().collect(), - standby: value.standby.into_iter().collect(), + EpochRewardedSet { + epoch_id: value.epoch_id, + assignment: RewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + }, } } } @@ -57,6 +63,7 @@ impl From for RewardedSet { impl From<&CachedRewardedSet> for RewardedSetResponse { fn from(value: &CachedRewardedSet) -> Self { RewardedSetResponse { + epoch_id: value.epoch_id, entry_gateways: value.entry_gateways.iter().copied().collect(), exit_gateways: value.exit_gateways.iter().copied().collect(), layer1: value.layer1.iter().copied().collect(), diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 1738901550e..a720dce4ffb 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -11,8 +11,8 @@ use nym_api_requests::legacy::{ use nym_api_requests::models::MixnodeStatus; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ - ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, - RewardedSet, RewardingParams, + ConfigScoreParams, EpochRewardedSet, HistoricalNymNodeVersionEntry, Interval, NodeId, + NymNodeDetails, RewardedSet, RewardingParams, }; use std::{ collections::HashSet, @@ -80,7 +80,7 @@ impl NymContractCache { mixnodes: Vec, gateways: Vec, nym_nodes: Vec, - rewarded_set: RewardedSet, + rewarded_set: EpochRewardedSet, config_score_params: ConfigScoreParams, nym_node_version_history: Vec, rewarding_params: RewardingParams, diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index 6681ac669e7..a3e911d10fa 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -9,7 +9,7 @@ use anyhow::Result; use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use nym_mixnet_contract_common::{LegacyMixLayer, RewardedSet}; +use nym_mixnet_contract_common::{EpochRewardedSet, LegacyMixLayer, RewardedSet}; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, NymContractsProvider, VestingQueryClient, @@ -141,9 +141,21 @@ impl NymContractCacheRefresher { } let rewarded_set = self.get_rewarded_set().await; - let layer1 = rewarded_set.layer1.iter().collect::>(); - let layer2 = rewarded_set.layer2.iter().collect::>(); - let layer3 = rewarded_set.layer3.iter().collect::>(); + let layer1 = rewarded_set + .assignment + .layer1 + .iter() + .collect::>(); + let layer2 = rewarded_set + .assignment + .layer2 + .iter() + .collect::>(); + let layer3 = rewarded_set + .assignment + .layer3 + .iter() + .collect::>(); let layer_choices = [ LegacyMixLayer::One, @@ -209,7 +221,7 @@ impl NymContractCacheRefresher { Ok(()) } - async fn get_rewarded_set(&self) -> RewardedSet { + async fn get_rewarded_set(&self) -> EpochRewardedSet { self.nyxd_client .get_rewarded_set_nodes() .await diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index cb97f758340..180d1810e42 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -29,8 +29,9 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - ConfigScoreParams, CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, - HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet, RoleAssignment, + ConfigScoreParams, CurrentIntervalResponse, EpochRewardedSet, EpochStatus, ExecuteMsg, + GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet, + RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; @@ -252,7 +253,7 @@ impl Client { nyxd_query!(self, get_rewarding_parameters().await) } - pub(crate) async fn get_rewarded_set_nodes(&self) -> Result { + pub(crate) async fn get_rewarded_set_nodes(&self) -> Result { nyxd_query!(self, get_rewarded_set().await) } diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index 2fcacee75a3..139e5e8dccf 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -162,7 +162,7 @@ impl NetworkAccount { fn hydrate_route(&self, fragment: SentFragment) -> anyhow::Result { let mut rng = ChaCha8Rng::seed_from_u64(fragment.seed() as u64); - let (nodes, gw) = self.topology.random_path_to_gateway( + let (nodes, gw) = self.topology.random_path_to_egress( &mut rng, fragment.mixnet_params().hops(), fragment.mixnet_params().destination(), From 2596b770492e1093a94cd5f4f60152b85c0d1538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 12 Dec 2024 15:36:54 +0000 Subject: [PATCH 2/9] wip --- Cargo.lock | 1 - common/client-core/Cargo.toml | 2 +- .../src/client/topology_control/accessor.rs | 23 +- .../topology_control/nym_api_provider.rs | 139 ++++++------ .../validator-client/src/client.rs | 6 +- .../validator-client/src/nym_api/mod.rs | 11 +- .../mixnet-contract/src/types.rs | 23 ++ common/nymsphinx/addressing/src/clients.rs | 4 +- common/nymsphinx/routing/src/lib.rs | 34 +-- common/topology/Cargo.toml | 5 +- common/topology/src/error.rs | 6 + common/topology/src/lib.rs | 204 +++++++++++++----- common/topology/src/node.rs | 18 +- common/topology/src/provider_trait.rs | 18 +- common/topology/src/random_route_provider.rs | 30 --- common/topology/src/serde.rs | 54 ++--- common/wasm/client-core/Cargo.toml | 4 +- nym-api/nym-api-requests/src/models.rs | 16 ++ nym-api/nym-api-requests/src/nym_nodes.rs | 1 - 19 files changed, 344 insertions(+), 255 deletions(-) delete mode 100644 common/topology/src/random_route_provider.rs diff --git a/Cargo.lock b/Cargo.lock index dab3434cf40..eddc68ddb0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6559,7 +6559,6 @@ name = "nym-topology" version = "0.1.0" dependencies = [ "async-trait", - "bs58", "log", "nym-api-requests", "nym-config", diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index b5191eef802..b43c6e2dbfd 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -45,7 +45,7 @@ nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } nym-pemstore = { path = "../pemstore" } -nym-topology = { path = "../topology", features = ["serializable"] } +nym-topology = { path = "../topology", features = ["serde"] } nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } diff --git a/common/client-core/src/client/topology_control/accessor.rs b/common/client-core/src/client/topology_control/accessor.rs index 6b12d64562c..db380f8b563 100644 --- a/common/client-core/src/client/topology_control/accessor.rs +++ b/common/client-core/src/client/topology_control/accessor.rs @@ -53,6 +53,7 @@ impl<'a> TopologyReadPermit<'a> { &'a self, ack_recipient: &Recipient, packet_recipient: Option<&Recipient>, + ignore_epoch_roles: bool, ) -> Result<&'a NymTopology, NymTopologyError> { // 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through? let topology = self @@ -60,25 +61,15 @@ impl<'a> TopologyReadPermit<'a> { .as_ref() .ok_or(NymTopologyError::EmptyNetworkTopology)?; - // 2. does it have any mixnode at all? - // 3. does it have any gateways at all? - // 4. does it have a mixnode on each layer? - topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?; + // 2. does the topology have a node on each mixing layer? + topology.ensure_minimally_routable()?; - // 5. does it contain OUR gateway (so that we could create an ack packet)? - if !topology.gateway_exists(ack_recipient.gateway()) { - return Err(NymTopologyError::NonExistentGatewayError { - identity_key: ack_recipient.gateway().to_base58_string(), - }); - } + // 3. does it contain OUR gateway (so that we could create an ack packet)? + let _ = topology.egress_by_identity(ack_recipient.gateway(), ignore_epoch_roles)?; - // 6. for our target recipient, does it contain THEIR gateway (so that we could create + // 4. for our target recipient, does it contain THEIR gateway (so that we send anything over?) if let Some(recipient) = packet_recipient { - if !topology.gateway_exists(recipient.gateway()) { - return Err(NymTopologyError::NonExistentGatewayError { - identity_key: recipient.gateway().to_base58_string(), - }); - } + let _ = topology.egress_by_identity(recipient.gateway(), ignore_epoch_roles)?; } Ok(topology) 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 3b87086f59a..f4db6590420 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -4,10 +4,11 @@ use async_trait::async_trait; use log::{debug, error, warn}; use nym_topology::provider_trait::TopologyProvider; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymTopologyError, NymTopologyNew}; use nym_validator_client::UserAgent; use rand::prelude::SliceRandom; use rand::thread_rng; +use std::cmp::min; use url::Url; // the same values as our current (10.06.24) blacklist @@ -18,6 +19,15 @@ pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; pub struct Config { pub min_mixnode_performance: u8, pub min_gateway_performance: u8, + pub use_extended_topology: bool, + pub ignore_epoch_roles: bool, +} + +impl Config { + // 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) + } } impl Default for Config { @@ -26,6 +36,8 @@ impl Default for Config { Config { min_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, min_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, + use_extended_topology: false, + ignore_epoch_roles: false, } } } @@ -70,70 +82,69 @@ impl NymApiTopologyProvider { .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) } - /// Verifies whether nodes a reasonably distributed among all mix layers. - /// - /// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3. - /// However, this is a rather unrealistic expectation, instead we check whether there exists - /// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure. - /// - /// # Arguments - /// - /// * `topology`: active topology constructed from validator api data - fn check_layer_distribution( - &self, - active_topology: &NymTopology, - ) -> Result<(), NymTopologyError> { - let lower_threshold = 0.15; - let upper_threshold = 0.66; - active_topology.ensure_even_layer_distribution(lower_threshold, upper_threshold) - } - - async fn get_current_compatible_topology(&mut self) -> Option { - let mixnodes = match self + async fn get_current_compatible_topology(&mut self) -> Option { + let rewarded_set = self .validator_client - .get_all_basic_active_mixing_assigned_nodes() + .get_current_rewarded_set() .await - { - Err(err) => { - error!("failed to get network mixnodes - {err}"); - return None; - } - Ok(mixes) => mixes, - }; - - let gateways = match self - .validator_client - .get_all_basic_entry_assigned_nodes() - .await - { - Err(err) => { - error!("failed to get network gateways - {err}"); - return None; - } - Ok(gateways) => gateways, + .inspect_err(|err| error!("failed to get current rewarded set: {err}")) + .ok()?; + + let mut topology = NymTopologyNew::new_empty(rewarded_set); + + if self.config.use_extended_topology { + let all_nodes = self + .validator_client + .get_all_basic_nodes() + .await + .inspect_err(|err| error!("failed to get network nodes: {err}")) + .ok()?; + + debug!( + "there are {} nodes on the network (before filtering)", + all_nodes.len() + ); + topology.add_additional_nodes(all_nodes.iter().filter(|n| { + n.performance.round_to_integer() >= self.config.min_node_performance() + })); + } else { + // if we're not using extended topology, we're only getting active set mixnodes and gateways + + let mixnodes = self + .validator_client + .get_all_basic_active_mixing_assigned_nodes() + .await + .inspect_err(|err| error!("failed to get network mixnodes: {err}")) + .ok()?; + + // TODO: we really should be getting ACTIVE gateways only + let gateways = self + .validator_client + .get_all_basic_entry_assigned_nodes() + .await + .inspect_err(|err| error!("failed to get network gateways: {err}")) + .ok()?; + + debug!( + "there are {} mixnodes and {} gateways in total (before performance filtering)", + mixnodes.len(), + gateways.len() + ); + + topology.add_additional_nodes(mixnodes.iter().filter(|m| { + m.performance.round_to_integer() >= self.config.min_mixnode_performance + })); + topology.add_additional_nodes(gateways.iter().filter(|m| { + m.performance.round_to_integer() >= self.config.min_gateway_performance + })); }; - debug!( - "there are {} mixnodes and {} gateways in total (before performance filtering)", - mixnodes.len(), - gateways.len() - ); - - let topology = NymTopology::from_unordered( - mixnodes.iter().filter(|m| { - m.performance.round_to_integer() >= self.config.min_mixnode_performance - }), - gateways.iter().filter(|g| { - g.performance.round_to_integer() >= self.config.min_gateway_performance - }), - ); - if let Err(err) = self.check_layer_distribution(&topology) { - warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}"); - self.use_next_nym_api(); - None - } else { - Some(topology) + if !topology.is_minimally_routable() { + error!("the current filtered active topology can't be used to construct any packets"); + return None; } + + Some(topology) } } @@ -141,8 +152,12 @@ impl NymApiTopologyProvider { #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for NymApiTopologyProvider { - async fn get_new_topology(&mut self) -> Option { - self.get_current_compatible_topology().await + async fn get_new_topology(&mut self) -> Option { + let Some(topology) = self.get_current_compatible_topology().await else { + self.use_next_nym_api(); + return None; + }; + Some(topology) } } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index cae61b0d07a..82807016441 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -32,10 +32,10 @@ use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; +use nym_mixnet_contract_common::EpochRewardedSet; pub use nym_mixnet_contract_common::{ mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, NymNodeDetails, }; - // re-export the type to not break existing imports pub use crate::coconut::EcashApiClient; @@ -367,6 +367,10 @@ impl NymApiClient { Ok(self.nym_api.get_basic_gateways().await?.nodes) } + pub async fn get_current_rewarded_set(&self) -> Result { + Ok(self.nym_api.get_rewarded_set().await?.into()) + } + /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes pub async fn get_all_basic_entry_assigned_nodes( 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 ebfb85ba97c..bf630f3914f 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -13,7 +13,7 @@ use nym_api_requests::ecash::models::{ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, - NodeRefreshBody, NymNodeDescription, + NodeRefreshBody, NymNodeDescription, RewardedSetResponse, }; use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; use nym_api_requests::pagination::PaginatedResponse; @@ -235,6 +235,15 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] + async fn get_rewarded_set(&self) -> Result { + self.get_json( + &[routes::API_VERSION, "nym-nodes", "rewarded-set"], + NO_PARAMS, + ) + .await + } + /// 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))] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index ba1546a1d40..d722f5af938 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -87,6 +87,29 @@ impl RewardedSet { pub fn rewarded_set_size(&self) -> usize { self.active_set_size() + self.standby.len() } + + pub fn get_role(&self, node_id: NodeId) -> Option { + // given each role has ~100 entries in them, doing linear lookup with vec should be fine + if self.entry_gateways.contains(&node_id) { + return Some(Role::EntryGateway); + } + if self.exit_gateways.contains(&node_id) { + return Some(Role::ExitGateway); + } + if self.layer1.contains(&node_id) { + return Some(Role::Layer1); + } + if self.layer2.contains(&node_id) { + return Some(Role::Layer2); + } + if self.layer3.contains(&node_id) { + return Some(Role::Layer3); + } + if self.standby.contains(&node_id) { + return Some(Role::Standby); + } + None + } } #[cw_serde] diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index 743302edbe1..d60581bf216 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -131,8 +131,8 @@ impl Recipient { &self.client_encryption_key } - pub fn gateway(&self) -> &NodeIdentity { - &self.gateway + pub fn gateway(&self) -> NodeIdentity { + self.gateway } pub fn to_bytes(self) -> RecipientBytes { diff --git a/common/nymsphinx/routing/src/lib.rs b/common/nymsphinx/routing/src/lib.rs index e8881b409dc..b468fd97542 100644 --- a/common/nymsphinx/routing/src/lib.rs +++ b/common/nymsphinx/routing/src/lib.rs @@ -1,19 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_sphinx_types::{delays, Delay}; use std::time::Duration; - -use nym_sphinx_addressing::clients::Recipient; -use nym_sphinx_types::{delays, Delay, Node}; use thiserror::Error; -pub trait SphinxRouteMaker { - type Error; - - fn sphinx_route(&mut self, hops: u8, destination: &Recipient) - -> Result, Self::Error>; -} - #[derive(Debug, Error, Clone, Copy)] #[error("the route vector contains {available} nodes while {requested} hops are required")] pub struct InvalidNumberOfHops { @@ -21,29 +12,6 @@ pub struct InvalidNumberOfHops { requested: u8, } -// if one wants to provide a hardcoded route, they can -impl SphinxRouteMaker for Vec { - type Error = InvalidNumberOfHops; - - fn sphinx_route( - &mut self, - hops: u8, - _destination: &Recipient, - ) -> Result, InvalidNumberOfHops> { - // it's the responsibility of the caller to ensure the hardcoded route has correct number of hops - // and that it's final hop include the recipient's gateway. - - if self.len() != hops as usize { - Err(InvalidNumberOfHops { - available: self.len(), - requested: hops, - }) - } else { - Ok(self.clone()) - } - } -} - pub fn generate_hop_delays(average_packet_delay: Duration, num_hops: usize) -> Vec { if average_packet_delay.is_zero() { vec![nym_sphinx_types::Delay::new_from_millis(0); num_hops] diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index b19fb2da8a0..c07f8ffe3d2 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -12,14 +12,13 @@ documentation = { workspace = true } [dependencies] async-trait = { workspace = true, optional = true } -bs58 = { workspace = true } log = { workspace = true } rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } semver = { workspace = true } thiserror = { workspace = true } -# 'serializable' feature +# 'serde' feature serde = { workspace = true, features = ["derive"], optional = true } serde_json = { workspace = true, optional = true } @@ -50,5 +49,5 @@ wasm-utils = { path = "../wasm/utils", default-features = false, optional = true default = ["provider-trait"] provider-trait = ["async-trait"] wasm-serde-types = ["tsify", "wasm-bindgen", "wasm-utils"] -serializable = ["serde", "serde_json"] +serde = ["dep:serde", "serde_json"] outfox = [] diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index bdea316ca30..1c52f1be8ce 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -16,6 +16,12 @@ pub enum NymTopologyError { #[error("no node with identity {node_identity} is known")] NonExistentNode { node_identity: NodeIdentity }, + #[error("could not use node with identity {node_identity} as egress since it didn't get assigned valid role in the current epoch")] + InvalidEgressRole { node_identity: NodeIdentity }, + + #[error("one (or more) of mixing layers does not have any valid nodes available")] + InsufficientMixingNodes, + // // // diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 82b2e63104f..8b72e7d5760 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -17,22 +17,23 @@ use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::str::FromStr; -#[cfg(feature = "serializable")] +#[cfg(feature = "serde")] use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; +use nym_mixnet_contract_common::nym_node::Role; pub mod error; pub mod gateway; pub mod mix; +pub mod node; pub mod random_route_provider; #[cfg(feature = "provider-trait")] pub mod provider_trait; -mod node; -#[cfg(feature = "serializable")] +#[cfg(feature = "serde")] pub(crate) mod serde; -#[cfg(feature = "serializable")] +#[cfg(feature = "serde")] pub use crate::serde::{ SerializableGateway, SerializableMixNode, SerializableNymTopology, SerializableTopologyError, }; @@ -73,6 +74,7 @@ impl<'a> From<&'a str> for NodeVersion { } } +#[deprecated] #[derive(Debug, Clone)] pub enum NetworkAddress { IpAddr(IpAddr), @@ -113,7 +115,8 @@ impl Display for NetworkAddress { pub type MixLayer = u8; -pub struct NymTopologyNew { +#[derive(Clone, Debug)] +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 // @@ -128,14 +131,27 @@ const unused: &str = r#"there shall be a config setting, like debug.topology.use and another one for debug.topology.ignore_epoch_roles = true/false (to say send final packet to epoch mixnode) "#; -impl NymTopologyNew { +impl NymTopology { + pub fn new_empty(rewarded_set: EpochRewardedSet) -> Self { + NymTopology { + rewarded_set, + node_details: Default::default(), + } + } + pub fn new(rewarded_set: EpochRewardedSet, node_details: Vec) -> Self { - NymTopologyNew { + NymTopology { rewarded_set, node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), } } + #[cfg(feature = "serde")] + pub fn new_from_file>(path: P) -> std::io::Result { + let file = std::fs::File::open(path)?; + serde_json::from_reader(file).map_err(Into::into) + } + pub fn add_additional_nodes(&mut self, nodes: impl Iterator) where N: TryInto, @@ -156,6 +172,30 @@ impl NymTopologyNew { } } + fn node_exists(&self, ids: &[NodeId]) -> bool { + for id in ids { + if self.node_details.contains_key(id) { + return true; + } + } + false + } + + pub fn is_minimally_routable(&self) -> bool { + self.node_exists(&self.rewarded_set.assignment.layer1) + && self.node_exists(&self.rewarded_set.assignment.layer2) + && self.node_exists(&self.rewarded_set.assignment.layer3) + + // TODO: we should also include gateways in that check, but right now we're allowing ALL gateways, even inactive + } + + pub fn ensure_minimally_routable(&self) -> Result<(), NymTopologyError> { + if !self.is_minimally_routable() { + return Err(NymTopologyError::InsufficientMixingNodes); + } + Ok(()) + } + fn get_sphinx_node(&self, node_id: NodeId) -> Option { self.node_details.get(&node_id).map(Into::into) } @@ -221,15 +261,35 @@ impl NymTopologyNew { .find(|n| n.identity_key == node_identity) } - fn sphinx_node_by_identity( + pub fn egress_by_identity( &self, node_identity: NodeIdentity, - ) -> Result { + ignore_epoch_roles: bool, + ) -> Result<&RoutingNode, NymTopologyError> { let Some(node) = self.find_node_by_identity(node_identity) else { return Err(NymTopologyError::NonExistentNode { node_identity }); }; - Ok(node.into()) + // a 'valid' egress is one assigned to either entry role (i.e. entry for another client) + // or exit role (as a service provider) + if !ignore_epoch_roles { + let Some(role) = self.rewarded_set.assignment.get_role(node.node_id) else { + return Err(NymTopologyError::InvalidEgressRole { node_identity }); + }; + if !matches!(role, Role::EntryGateway | Role::ExitGateway) { + return Err(NymTopologyError::InvalidEgressRole { node_identity }); + } + } + Ok(node) + } + + fn egress_node_by_identity( + &self, + node_identity: NodeIdentity, + ignore_epoch_roles: bool, + ) -> Result { + self.egress_by_identity(node_identity, ignore_epoch_roles) + .map(Into::into) } pub fn random_mix_route(&self, rng: &mut R) -> Result, NymTopologyError> @@ -256,26 +316,50 @@ impl NymTopologyNew { &self, rng: &mut R, egress_identity: NodeIdentity, + ignore_epoch_roles: bool, ) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { - let egress = self.sphinx_node_by_identity(egress_identity)?; + let egress = self.egress_node_by_identity(egress_identity, ignore_epoch_roles)?; let mut mix_route = self.random_mix_route(rng)?; mix_route.push(egress); Ok(mix_route) } } +#[cfg(feature = "serde")] +impl Serialize for NymTopology { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + todo!() + // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for NymTopology { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + todo!() + // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; + // serializable.try_into().map_err(::serde::de::Error::custom) + } +} + // the reason for those having `Legacy` prefix is that eventually they should be using // exactly the same types #[derive(Debug, Clone, Default)] -pub struct NymTopology { +pub struct NymTopologyOld { mixes: BTreeMap>, gateways: Vec, } -impl NymTopology { +impl NymTopologyOld { #[deprecated] pub async fn new_from_env() -> Result { let api_url = std::env::var(NYM_API)?; @@ -301,7 +385,7 @@ impl NymTopology { .map(gateway::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; - let topology = NymTopology::new_unordered(mixnodes, gateways); + let topology = Self::new_unordered(mixnodes, gateways); Ok(topology) } @@ -309,7 +393,7 @@ impl NymTopology { mixes: BTreeMap>, gateways: Vec, ) -> Self { - NymTopology { mixes, gateways } + NymTopologyOld { mixes, gateways } } #[deprecated] @@ -324,7 +408,7 @@ impl NymTopology { layer_entry.push(node) } - NymTopology { mixes, gateways } + NymTopologyOld { mixes, gateways } } pub fn from_unordered(unordered_mixes: MI, unordered_gateways: GI) -> Self @@ -356,17 +440,19 @@ impl NymTopology { } } - NymTopology::new(mixes, gateways) + NymTopologyOld::new(mixes, gateways) } - #[cfg(feature = "serializable")] + #[cfg(feature = "serde")] pub fn new_from_file>(path: P) -> std::io::Result { - let file = std::fs::File::open(path)?; - serde_json::from_reader(file).map_err(Into::into) + todo!() + // let file = std::fs::File::open(path)?; + // serde_json::from_reader(file).map_err(Into::into) } pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { - nym_topology_from_basic_info(basic_mixes, basic_gateways) + todo!() + // nym_topology_from_basic_info(basic_mixes, basic_gateways) } pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { @@ -621,24 +707,26 @@ impl NymTopology { } } -#[cfg(feature = "serializable")] -impl Serialize for NymTopology { +#[cfg(feature = "serde")] +impl Serialize for NymTopologyOld { fn serialize(&self, serializer: S) -> Result where S: Serializer, { - crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) + todo!() + // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) } } -#[cfg(feature = "serializable")] -impl<'de> Deserialize<'de> for NymTopology { +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for NymTopologyOld { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; - serializable.try_into().map_err(::serde::de::Error::custom) + todo!() + // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; + // serializable.try_into().map_err(::serde::de::Error::custom) } } @@ -646,35 +734,37 @@ pub fn nym_topology_from_basic_info( basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode], ) -> NymTopology { - let mut mixes = BTreeMap::new(); - for mix in basic_mixes { - let Some(layer) = mix.get_mix_layer() else { - warn!("node {} doesn't have any assigned mix layer!", mix.node_id); - continue; - }; - - let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - match mix.try_into() { - Ok(mix) => layer_entry.push(mix), - Err(err) => { - warn!("node (mixnode) {} is malformed: {err}", mix.node_id); - continue; - } - } - } - - let mut gateways = Vec::with_capacity(basic_gateways.len()); - for gateway in basic_gateways { - match gateway.try_into() { - Ok(gate) => gateways.push(gate), - Err(err) => { - warn!("node (gateway) {} is malformed: {err}", gateway.node_id); - continue; - } - } - } - - NymTopology::new(mixes, gateways) + todo!() + // let mut mixes = BTreeMap::new(); + // for mix in basic_mixes { + // let Some(layer) = mix.get_mix_layer() else { + // warn!("node {} doesn't have any assigned mix layer!", mix.node_id); + // continue; + // }; + // + // let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); + // match mix.try_into() { + // Ok(mix) => layer_entry.push(mix), + // Err(err) => { + // warn!("node (mixnode) {} is malformed: {err}", mix.node_id); + // continue; + // } + // } + // } + // + // let mut gateways = Vec::with_capacity(basic_gateways.len()); + // for gateway in basic_gateways { + // match gateway.try_into() { + // Ok(gate) => gateways.push(gate), + // Err(err) => { + // warn!("node (gateway) {} is malformed: {err}", gateway.node_id); + // continue; + // } + // } + // } + // + // // NymTopology::new(mixes, gateways) + // todo!() } #[cfg(test)] diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 050b34ffcdc..b604ad1d72f 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -7,7 +7,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use thiserror::Error; #[derive(Error, Debug)] @@ -22,8 +22,10 @@ pub enum RoutingNodeError { }, } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct EntryDetails { + // to allow client to choose ipv6 preference, if available + pub ip_addresses: Vec, pub clients_ws_port: u16, pub hostname: Option, pub clients_wss_port: Option, @@ -46,7 +48,7 @@ impl From for SupportedRoles { } } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct RoutingNode { pub node_id: NodeId, @@ -65,24 +67,20 @@ impl RoutingNode { todo!() } - pub fn ws_entry_address_no_tls(&self) -> Option { + pub fn ws_entry_address_no_tls(&self, prefer_ipv6: bool) -> Option { todo!() } - pub fn ws_entry_address(&self) -> Option { + pub fn ws_entry_address(&self, prefer_ipv6: bool) -> Option { if let Some(tls) = self.ws_entry_address_tls() { return Some(tls); } - self.ws_entry_address_no_tls() + self.ws_entry_address_no_tls(prefer_ipv6) } pub fn identity(&self) -> ed25519::PublicKey { self.identity_key } - - pub fn mix_host(&self) -> Option { - todo!() - } } impl<'a> From<&'a RoutingNode> for SphinxNode { diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/provider_trait.rs index 0dddecf2cb7..9e156354d8d 100644 --- a/common/topology/src/provider_trait.rs +++ b/common/topology/src/provider_trait.rs @@ -1,33 +1,33 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::NymTopology; +use crate::NymTopologyNew; pub use async_trait::async_trait; // hehe, wasm #[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait TopologyProvider: Send { - async fn get_new_topology(&mut self) -> Option; + async fn get_new_topology(&mut self) -> Option; } #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] pub trait TopologyProvider { - async fn get_new_topology(&mut self) -> Option; + async fn get_new_topology(&mut self) -> Option; } pub struct HardcodedTopologyProvider { - topology: NymTopology, + topology: NymTopologyNew, } impl HardcodedTopologyProvider { - #[cfg(feature = "serializable")] + #[cfg(feature = "serde")] pub fn new_from_file>(path: P) -> std::io::Result { - NymTopology::new_from_file(path).map(Self::new) + NymTopologyNew::new_from_file(path).map(Self::new) } - pub fn new(topology: NymTopology) -> Self { + pub fn new(topology: NymTopologyNew) -> Self { HardcodedTopologyProvider { topology } } } @@ -35,7 +35,7 @@ impl HardcodedTopologyProvider { #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for HardcodedTopologyProvider { - async fn get_new_topology(&mut self) -> Option { + async fn get_new_topology(&mut self) -> Option { Some(self.topology.clone()) } } @@ -43,7 +43,7 @@ impl TopologyProvider for HardcodedTopologyProvider { #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] impl TopologyProvider for HardcodedTopologyProvider { - async fn get_new_topology(&mut self) -> Option { + async fn get_new_topology(&mut self) -> Option { Some(self.topology.clone()) } } diff --git a/common/topology/src/random_route_provider.rs b/common/topology/src/random_route_provider.rs deleted file mode 100644 index 9b74dc118b1..00000000000 --- a/common/topology/src/random_route_provider.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{NymTopology, NymTopologyError}; -use nym_sphinx_addressing::clients::Recipient; -use nym_sphinx_routing::SphinxRouteMaker; -use nym_sphinx_types::Node; -use rand::{CryptoRng, Rng}; - -#[allow(dead_code)] -pub struct NymTopologyRouteProvider { - rng: R, - inner: NymTopology, -} - -impl SphinxRouteMaker for NymTopologyRouteProvider -where - R: Rng + CryptoRng, -{ - type Error = NymTopologyError; - - fn sphinx_route( - &mut self, - hops: u8, - destination: &Recipient, - ) -> Result, NymTopologyError> { - self.inner - .random_route_to_egress(&mut self.rng, hops, destination.gateway()) - } -} diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs index 601b78dfd34..61be24399f1 100644 --- a/common/topology/src/serde.rs +++ b/common/topology/src/serde.rs @@ -66,37 +66,39 @@ impl TryFrom for NymTopology { type Error = SerializableTopologyError; fn try_from(value: SerializableNymTopology) -> Result { - let mut converted_mixes = BTreeMap::new(); - - for (layer, nodes) in value.mixnodes { - let layer_nodes = nodes - .into_iter() - .map(TryInto::try_into) - .collect::>()?; - - converted_mixes.insert(layer, layer_nodes); - } - - let gateways = value - .gateways - .into_iter() - .map(TryInto::try_into) - .collect::>()?; - - Ok(NymTopology::new(converted_mixes, gateways)) + todo!() + // let mut converted_mixes = BTreeMap::new(); + // + // for (layer, nodes) in value.mixnodes { + // let layer_nodes = nodes + // .into_iter() + // .map(TryInto::try_into) + // .collect::>()?; + // + // converted_mixes.insert(layer, layer_nodes); + // } + // + // let gateways = value + // .gateways + // .into_iter() + // .map(TryInto::try_into) + // .collect::>()?; + // + // Ok(NymTopology::new(converted_mixes, gateways)) } } impl From for SerializableNymTopology { fn from(value: NymTopology) -> Self { - SerializableNymTopology { - mixnodes: value - .mixes() - .iter() - .map(|(&l, nodes)| (l, nodes.iter().map(Into::into).collect())) - .collect(), - gateways: value.gateways().iter().map(Into::into).collect(), - } + todo!() + // SerializableNymTopology { + // mixnodes: value + // .mixes() + // .iter() + // .map(|(&l, nodes)| (l, nodes.iter().map(Into::into).collect())) + // .collect(), + // gateways: value.gateways().iter().map(Into::into).collect(), + // } } } diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index 1b0b3df67f7..a2bcda86def 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -29,10 +29,10 @@ nym-credential-storage = { path = "../../credential-storage" } nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] } nym-gateway-client = { path = "../../client-libs/gateway-client", default-features = false, features = ["wasm"] } nym-sphinx = { path = "../../nymsphinx" } -nym-sphinx-acknowledgements = { path = "../../nymsphinx/acknowledgements", features = ["serde"]} +nym-sphinx-acknowledgements = { path = "../../nymsphinx/acknowledgements", features = ["serde"] } nym-statistics-common = { path = "../../statistics" } nym-task = { path = "../../task" } -nym-topology = { path = "../../topology", features = ["serializable", "wasm-serde-types"] } +nym-topology = { path = "../../topology", features = ["serde", "wasm-serde-types"] } nym-validator-client = { path = "../../client-libs/validator-client", default-features = false } wasm-utils = { path = "../utils" } wasm-storage = { path = "../storage" } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 3e157d1f681..f5d223eb630 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1361,6 +1361,22 @@ pub struct RewardedSetResponse { pub standby: Vec, } +impl From for nym_mixnet_contract_common::EpochRewardedSet { + fn from(res: RewardedSetResponse) -> Self { + nym_mixnet_contract_common::EpochRewardedSet { + epoch_id: res.epoch_id, + assignment: nym_mixnet_contract_common::RewardedSet { + entry_gateways: res.entry_gateways, + exit_gateways: res.exit_gateways, + layer1: res.layer1, + layer2: res.layer2, + layer3: res.layer3, + standby: res.standby, + }, + } + } +} + pub use config_score::*; pub mod config_score { use nym_contracts_common::NaiveFloat; diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index fcfd09c2be3..61789debcb0 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -134,7 +134,6 @@ pub struct SkimmedNode { #[schema(value_type = Vec)] pub ip_addresses: Vec, - // TODO: to be deprecated in favour of well-known hardcoded port for everyone pub mix_port: u16, #[serde(with = "bs58_x25519_pubkey")] From 6098330cfe93a3d5b9907e9824587615f4f0d6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 13 Dec 2024 15:16:44 +0000 Subject: [PATCH 3/9] working e2e client --- common/client-core/config-types/src/lib.rs | 11 +++ .../client-core/src/client/base_client/mod.rs | 14 ++-- .../src/client/cover_traffic_stream.rs | 1 + .../src/client/inbound_messages.rs | 28 ------- .../input_message_listener.rs | 43 ++-------- .../acknowledgement_control/mod.rs | 6 -- .../retransmission_request_listener.rs | 9 +- .../real_messages_control/message_handler.rs | 19 ++--- .../real_traffic_stream.rs | 1 + .../client/replies/reply_controller/mod.rs | 1 - .../src/client/topology_control/accessor.rs | 84 +++++++++++-------- .../topology_control/geo_aware_provider.rs | 16 +--- .../src/client/topology_control/mod.rs | 18 ++-- .../topology_control/nym_api_provider.rs | 45 +++++----- common/ip-packet-requests/src/v7/request.rs | 3 + .../acknowledgements/src/surb_ack.rs | 9 +- .../anonymous-replies/src/reply_surb.rs | 7 +- common/nymsphinx/chunking/src/lib.rs | 19 +---- common/nymsphinx/cover/src/lib.rs | 13 ++- common/nymsphinx/src/preparer/mod.rs | 30 +++---- common/topology/src/lib.rs | 79 +++++++++++++++-- common/topology/src/node.rs | 29 +++++-- common/topology/src/provider_trait.rs | 16 ++-- nym-node/src/node/shared_topology.rs | 2 + .../src/connected_client_handler.rs | 7 +- .../ip-packet-router/src/mixnet_listener.rs | 3 - .../src/util/create_message.rs | 9 +- 27 files changed, 254 insertions(+), 268 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 992559bc92e..2031c5c34e8 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -550,6 +550,15 @@ pub struct Topology { /// Specifies a minimum performance of a gateway that is used on route construction. /// This setting is only applicable when `NymApi` topology is used. pub minimum_gateway_performance: u8, + + /// Specifies whether this client should attempt to retrieve all available network nodes + /// as opposed to just active mixnodes/gateways. + /// Useless without `ignore_epoch_roles = true` + 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, } #[allow(clippy::large_enum_variant)] @@ -586,6 +595,8 @@ impl Default for Topology { topology_structure: TopologyStructure::default(), minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, + use_extended_topology: false, + ignore_egress_epoch_role: false, } } } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index bb0ffd5d03b..5dd57155a50 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -24,7 +24,7 @@ use crate::client::replies::reply_storage::{ }; use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider; use crate::client::topology_control::{ - nym_api_provider, TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, }; use crate::config::{Config, DebugConfig}; use crate::error::ClientCoreError; @@ -539,10 +539,7 @@ where // if no custom provider was ... provided ..., create one using nym-api custom_provider.unwrap_or_else(|| match config_topology.topology_structure { config::TopologyStructure::NymApi => Box::new(NymApiTopologyProvider::new( - nym_api_provider::Config { - min_mixnode_performance: config_topology.minimum_mixnode_performance, - min_gateway_performance: config_topology.minimum_gateway_performance, - }, + config_topology, nym_api_urls, user_agent, )), @@ -558,7 +555,7 @@ where topology_provider: Box, topology_config: config::Topology, topology_accessor: TopologyAccessor, - local_gateway: &NodeIdentity, + local_gateway: NodeIdentity, wait_for_gateway: bool, mut shutdown: TaskClient, ) -> Result<(), ClientCoreError> { @@ -590,7 +587,7 @@ where }; if let Err(err) = topology_refresher - .ensure_contains_gateway(local_gateway) + .ensure_contains_routable_egress(local_gateway) .await { if let Some(waiting_timeout) = gateway_wait_timeout { @@ -740,7 +737,8 @@ where // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::new(); + let shared_topology_accessor = + TopologyAccessor::new(self.config.debug.topology.ignore_egress_epoch_role); // Shutdown notifier for signalling tasks to stop let shutdown = self diff --git a/common/client-core/src/client/cover_traffic_stream.rs b/common/client-core/src/client/cover_traffic_stream.rs index 8efea56e5a7..1059e22e0f1 100644 --- a/common/client-core/src/client/cover_traffic_stream.rs +++ b/common/client-core/src/client/cover_traffic_stream.rs @@ -163,6 +163,7 @@ impl LoopCoverTrafficStream { // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) + let topology_ref = match topology_permit.try_get_valid_topology_ref( &self.our_full_destination, Some(&self.our_full_destination), diff --git a/common/client-core/src/client/inbound_messages.rs b/common/client-core/src/client/inbound_messages.rs index baf163913f5..b14a4d7ed5c 100644 --- a/common/client-core/src/client/inbound_messages.rs +++ b/common/client-core/src/client/inbound_messages.rs @@ -28,7 +28,6 @@ pub enum InputMessage { recipient: Recipient, data: Vec, lane: TransmissionLane, - mix_hops: Option, }, /// Creates a message used for a duplex anonymous communication where the recipient @@ -44,7 +43,6 @@ pub enum InputMessage { data: Vec, reply_surbs: u32, lane: TransmissionLane, - mix_hops: Option, }, /// Attempt to use our internally received and stored `ReplySurb` to send the message back @@ -94,29 +92,6 @@ impl InputMessage { recipient, data, lane, - mix_hops: None, - }; - if let Some(packet_type) = packet_type { - InputMessage::new_wrapper(message, packet_type) - } else { - message - } - } - - // IMHO `new_regular` should take `mix_hops: Option` as an argument instead of creating - // this function, but that would potentially break backwards compatibility with the current API - pub fn new_regular_with_custom_hops( - recipient: Recipient, - data: Vec, - lane: TransmissionLane, - packet_type: Option, - mix_hops: Option, - ) -> Self { - let message = InputMessage::Regular { - recipient, - data, - lane, - mix_hops, }; if let Some(packet_type) = packet_type { InputMessage::new_wrapper(message, packet_type) @@ -137,7 +112,6 @@ impl InputMessage { data, reply_surbs, lane, - mix_hops: None, }; if let Some(packet_type) = packet_type { InputMessage::new_wrapper(message, packet_type) @@ -154,14 +128,12 @@ impl InputMessage { reply_surbs: u32, lane: TransmissionLane, packet_type: Option, - mix_hops: Option, ) -> Self { let message = InputMessage::Anonymous { recipient, data, reply_surbs, lane, - mix_hops, }; if let Some(packet_type) = packet_type { InputMessage::new_wrapper(message, packet_type) diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 00a7abe4e7b..19ba2d1caea 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -73,11 +73,10 @@ where content: Vec, lane: TransmissionLane, packet_type: PacketType, - mix_hops: Option, ) { if let Err(err) = self .message_handler - .try_send_plain_message(recipient, content, lane, packet_type, mix_hops) + .try_send_plain_message(recipient, content, lane, packet_type) .await { warn!("failed to send a plain message - {err}") @@ -91,18 +90,10 @@ where reply_surbs: u32, lane: TransmissionLane, packet_type: PacketType, - mix_hops: Option, ) { if let Err(err) = self .message_handler - .try_send_message_with_reply_surbs( - recipient, - content, - reply_surbs, - lane, - packet_type, - mix_hops, - ) + .try_send_message_with_reply_surbs(recipient, content, reply_surbs, lane, packet_type) .await { warn!("failed to send a repliable message - {err}") @@ -115,9 +106,8 @@ where recipient, data, lane, - mix_hops, } => { - self.handle_plain_message(recipient, data, lane, PacketType::Mix, mix_hops) + self.handle_plain_message(recipient, data, lane, PacketType::Mix) .await } InputMessage::Anonymous { @@ -125,17 +115,9 @@ where data, reply_surbs, lane, - mix_hops, } => { - self.handle_repliable_message( - recipient, - data, - reply_surbs, - lane, - PacketType::Mix, - mix_hops, - ) - .await + self.handle_repliable_message(recipient, data, reply_surbs, lane, PacketType::Mix) + .await } InputMessage::Reply { recipient_tag, @@ -153,9 +135,8 @@ where recipient, data, lane, - mix_hops, } => { - self.handle_plain_message(recipient, data, lane, packet_type, mix_hops) + self.handle_plain_message(recipient, data, lane, packet_type) .await } InputMessage::Anonymous { @@ -163,17 +144,9 @@ where data, reply_surbs, lane, - mix_hops, } => { - self.handle_repliable_message( - recipient, - data, - reply_surbs, - lane, - packet_type, - mix_hops, - ) - .await + self.handle_repliable_message(recipient, data, reply_surbs, lane, packet_type) + .await } InputMessage::Reply { recipient_tag, diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index 2b65df9036c..24efc7a5609 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -70,7 +70,6 @@ pub(crate) struct PendingAcknowledgement { message_chunk: Fragment, delay: SphinxDelay, destination: PacketDestination, - mix_hops: Option, retransmissions: u32, } @@ -80,13 +79,11 @@ impl PendingAcknowledgement { message_chunk: Fragment, delay: SphinxDelay, recipient: Recipient, - mix_hops: Option, ) -> Self { PendingAcknowledgement { message_chunk, delay, destination: PacketDestination::KnownRecipient(recipient.into()), - mix_hops, retransmissions: 0, } } @@ -104,9 +101,6 @@ impl PendingAcknowledgement { recipient_tag, extra_surb_request, }, - // Messages sent using SURBs are using the number of mix hops set by the recipient when - // they provided the SURBs, so it doesn't make sense to include it here. - mix_hops: None, retransmissions: 0, } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 6eda1c8adc0..a55a9ac2264 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -52,18 +52,12 @@ where packet_recipient: Recipient, chunk_data: Fragment, packet_type: PacketType, - mix_hops: Option, ) -> Result { debug!("retransmitting normal packet..."); // TODO: Figure out retransmission packet type signaling self.message_handler - .try_prepare_single_chunk_for_sending( - packet_recipient, - chunk_data, - packet_type, - mix_hops, - ) + .try_prepare_single_chunk_for_sending(packet_recipient, chunk_data, packet_type) .await } @@ -110,7 +104,6 @@ where **recipient, timed_out_ack.message_chunk.clone(), packet_type, - timed_out_ack.mix_hops, ) .await } diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 75cca06f768..f272a8c0921 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -19,7 +19,7 @@ use nym_sphinx::params::{PacketSize, PacketType, DEFAULT_NUM_MIX_HOPS}; use nym_sphinx::preparer::{MessagePreparer, PreparedFragment}; use nym_sphinx::Delay; use nym_task::connections::TransmissionLane; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; use rand::{CryptoRng, Rng}; use std::collections::HashMap; use std::sync::Arc; @@ -216,7 +216,7 @@ where fn get_topology<'a>( &self, permit: &'a TopologyReadPermit<'a>, - ) -> Result<&'a NymTopology, PreparationError> { + ) -> Result<&'a NymRouteProvider, PreparationError> { match permit.try_get_valid_topology_ref(&self.config.sender_address, None) { Ok(topology_ref) => Ok(topology_ref), Err(err) => { @@ -424,10 +424,9 @@ where message: Vec, lane: TransmissionLane, packet_type: PacketType, - mix_hops: Option, ) -> Result<(), PreparationError> { let message = NymMessage::new_plain(message); - self.try_split_and_send_non_reply_message(message, recipient, lane, packet_type, mix_hops) + self.try_split_and_send_non_reply_message(message, recipient, lane, packet_type) .await } @@ -437,7 +436,6 @@ where recipient: Recipient, lane: TransmissionLane, packet_type: PacketType, - mix_hops: Option, ) -> Result<(), PreparationError> { debug!("Sending non-reply message with packet type {packet_type}"); // TODO: I really dislike existence of this assertion, it implies code has to be re-organised @@ -470,7 +468,6 @@ where &self.config.ack_key, &recipient, packet_type, - mix_hops, )?; let real_message = RealMessage::new( @@ -478,8 +475,7 @@ where Some(fragment.fragment_identifier()), ); let delay = prepared_fragment.total_delay; - let pending_ack = - PendingAcknowledgement::new_known(fragment, delay, recipient, mix_hops); + let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient); real_messages.push(real_message); pending_acks.push(pending_ack); @@ -496,7 +492,6 @@ where recipient: Recipient, amount: u32, packet_type: PacketType, - mix_hops: Option, ) -> Result<(), PreparationError> { debug!("Sending additional reply SURBs with packet type {packet_type}"); let sender_tag = self.get_or_create_sender_tag(&recipient); @@ -513,7 +508,6 @@ where recipient, TransmissionLane::AdditionalReplySurbs, packet_type, - mix_hops, ) .await?; @@ -530,7 +524,6 @@ where num_reply_surbs: u32, lane: TransmissionLane, packet_type: PacketType, - mix_hops: Option, ) -> Result<(), SurbWrappedPreparationError> { debug!("Sending message with reply SURBs with packet type {packet_type}"); let sender_tag = self.get_or_create_sender_tag(&recipient); @@ -541,7 +534,7 @@ where let message = NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs)); - self.try_split_and_send_non_reply_message(message, recipient, lane, packet_type, mix_hops) + self.try_split_and_send_non_reply_message(message, recipient, lane, packet_type) .await?; log::trace!("storing {} reply keys", reply_keys.len()); @@ -555,7 +548,6 @@ where recipient: Recipient, chunk: Fragment, packet_type: PacketType, - mix_hops: Option, ) -> Result { debug!("Sending single chunk with packet type {packet_type}"); let topology_permit = self.topology_access.get_read_permit().await; @@ -569,7 +561,6 @@ where &self.config.ack_key, &recipient, packet_type, - mix_hops, ) .unwrap(); diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index d39c1731b62..09b55eb633d 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -230,6 +230,7 @@ where // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) + let topology_ref = match topology_permit.try_get_valid_topology_ref( &self.config.our_full_destination, Some(&self.config.our_full_destination), diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index da58f377c6f..4ba9ce3ac9d 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -516,7 +516,6 @@ where recipient, to_send, nym_sphinx::params::PacketType::Mix, - self.config.reply_surbs.surb_mix_hops, ) .await { diff --git a/common/client-core/src/client/topology_control/accessor.rs b/common/client-core/src/client/topology_control/accessor.rs index db380f8b563..b1f74c4f249 100644 --- a/common/client-core/src/client/topology_control/accessor.rs +++ b/common/client-core/src/client/topology_control/accessor.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -17,29 +16,36 @@ pub struct TopologyAccessorInner { // few seconds, while reads are needed every single packet generated. // However, proper benchmarks will be needed to determine if `RwLock` is indeed a better // approach than a `Mutex` - topology: RwLock>, + topology: RwLock, } impl TopologyAccessorInner { - fn new() -> Self { + fn new(initial: NymRouteProvider) -> Self { TopologyAccessorInner { controlled_manually: AtomicBool::new(false), released_manual_control: Notify::new(), - topology: RwLock::new(None), + topology: RwLock::new(initial), } } async fn update(&self, new: Option) { - *self.topology.write().await = new; + let mut guard = self.topology.write().await; + + match new { + Some(updated) => { + guard.update(updated); + } + None => guard.clear_topology(), + } } } pub struct TopologyReadPermit<'a> { - permit: RwLockReadGuard<'a, Option>, + permit: RwLockReadGuard<'a, NymRouteProvider>, } impl Deref for TopologyReadPermit<'_> { - type Target = Option; + type Target = NymRouteProvider; fn deref(&self) -> &Self::Target { &self.permit @@ -53,34 +59,31 @@ impl<'a> TopologyReadPermit<'a> { &'a self, ack_recipient: &Recipient, packet_recipient: Option<&Recipient>, - ignore_epoch_roles: bool, - ) -> Result<&'a NymTopology, NymTopologyError> { + ) -> Result<&'a NymRouteProvider, NymTopologyError> { + let route_provider = self.permit.deref(); + let topology = &route_provider.topology; + // 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through? - let topology = self - .permit - .as_ref() - .ok_or(NymTopologyError::EmptyNetworkTopology)?; + topology.ensure_not_empty()?; // 2. does the topology have a node on each mixing layer? topology.ensure_minimally_routable()?; // 3. does it contain OUR gateway (so that we could create an ack packet)? - let _ = topology.egress_by_identity(ack_recipient.gateway(), ignore_epoch_roles)?; + let _ = route_provider.egress_by_identity(ack_recipient.gateway())?; // 4. for our target recipient, does it contain THEIR gateway (so that we send anything over?) if let Some(recipient) = packet_recipient { - let _ = topology.egress_by_identity(recipient.gateway(), ignore_epoch_roles)?; + let _ = route_provider.egress_by_identity(recipient.gateway())?; } - Ok(topology) + Ok(route_provider) } } -impl<'a> From>> for TopologyReadPermit<'a> { - fn from(read_permit: RwLockReadGuard<'a, Option>) -> Self { - TopologyReadPermit { - permit: read_permit, - } +impl<'a> From> for TopologyReadPermit<'a> { + fn from(permit: RwLockReadGuard<'a, NymRouteProvider>) -> Self { + TopologyReadPermit { permit } } } @@ -90,9 +93,11 @@ pub struct TopologyAccessor { } impl TopologyAccessor { - pub fn new() -> Self { + pub fn new(ignore_egress_epoch_roles: bool) -> Self { TopologyAccessor { - inner: Arc::new(TopologyAccessorInner::new()), + inner: Arc::new(TopologyAccessorInner::new(NymRouteProvider::new_empty( + ignore_egress_epoch_roles, + ))), } } @@ -112,8 +117,21 @@ impl TopologyAccessor { self.inner.released_manual_control.notified().await } + #[deprecated(note = "use .current_route_provider instead")] pub async fn current_topology(&self) -> Option { - self.inner.topology.read().await.clone() + self.current_route_provider() + .await + .as_ref() + .map(|p| p.topology.clone()) + } + + pub async fn current_route_provider(&self) -> Option> { + let provider = self.inner.topology.read().await; + if provider.topology.is_empty() { + None + } else { + Some(provider) + } } pub async fn manually_change_topology(&self, new_topology: NymTopology) { @@ -131,15 +149,11 @@ impl TopologyAccessor { // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because health checker is disabled due to required changes) pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> { - match self.inner.topology.read().await.deref() { - None => Err(NymTopologyError::EmptyNetworkTopology), - Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS), - } - } -} - -impl Default for TopologyAccessor { - fn default() -> Self { - TopologyAccessor::new() + self.inner + .topology + .read() + .await + .topology + .ensure_minimally_routable() } } 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 d3fabd9a938..d9a0519e0b1 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 @@ -15,8 +15,6 @@ use url::Url; pub use nym_country_group::CountryGroup; -const MIN_NODES_PER_LAYER: usize = 1; - fn create_explorer_client() -> Option { let Ok(explorer_api_url) = std::env::var(EXPLORER_API) else { error!("Missing EXPLORER_API"); @@ -63,22 +61,10 @@ fn log_mixnode_distribution(mixnodes: &HashMap>) { } fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { - let mixes = topology.mixes(); - if mixes.keys().len() < 3 { + if topology.ensure_minimally_routable().is_err() { error!("Layer is missing in topology!"); return Err(()); } - for (layer, mixnodes) in mixes { - debug!("Layer {:?} has {} mixnodes", layer, mixnodes.len()); - if mixnodes.len() < MIN_NODES_PER_LAYER { - error!( - "There are only {} mixnodes in layer {:?}", - mixnodes.len(), - layer - ); - return Err(()); - } - } Ok(()) } diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index 4e60278a22f..121e08e98ed 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -27,7 +27,7 @@ pub use nym_topology::provider_trait::TopologyProvider; const MAX_FAILURE_COUNT: usize = 10; pub struct TopologyRefresherConfig { - refresh_rate: Duration, + pub refresh_rate: Duration, } impl TopologyRefresherConfig { @@ -96,28 +96,24 @@ impl TopologyRefresher { self.topology_accessor.ensure_is_routable().await } - pub async fn ensure_contains_gateway( + pub async fn ensure_contains_routable_egress( &self, - gateway: &NodeIdentity, + egress: NodeIdentity, ) -> Result<(), NymTopologyError> { let topology = self .topology_accessor - .current_topology() + .current_route_provider() .await .ok_or(NymTopologyError::EmptyNetworkTopology)?; - if !topology.gateway_exists(gateway) { - return Err(NymTopologyError::NonExistentGatewayError { - identity_key: gateway.to_base58_string(), - }); - } + let _ = topology.egress_by_identity(egress)?; Ok(()) } pub async fn wait_for_gateway( &mut self, - gateway: &NodeIdentity, + gateway: NodeIdentity, timeout_duration: Duration, ) -> Result<(), NymTopologyError> { info!( @@ -135,7 +131,7 @@ impl TopologyRefresher { }) } _ = self.try_refresh() => { - if self.ensure_contains_gateway(gateway).await.is_ok() { + if self.ensure_contains_routable_egress(gateway).await.is_ok() { return Ok(()) } info!("gateway '{gateway}' is still not online..."); 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 f4db6590420..4f41d27671a 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -4,23 +4,30 @@ use async_trait::async_trait; use log::{debug, error, warn}; use nym_topology::provider_trait::TopologyProvider; -use nym_topology::{NymTopologyError, NymTopologyNew}; +use nym_topology::NymTopology; use nym_validator_client::UserAgent; use rand::prelude::SliceRandom; use rand::thread_rng; use std::cmp::min; use url::Url; -// the same values as our current (10.06.24) blacklist -pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50; -pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; - #[derive(Debug)] pub struct Config { pub min_mixnode_performance: u8, pub min_gateway_performance: u8, pub use_extended_topology: bool, - pub ignore_epoch_roles: bool, + pub ignore_egress_epoch_role: bool, +} + +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, + } + } } impl Config { @@ -30,18 +37,6 @@ impl Config { } } -impl Default for Config { - fn default() -> Self { - // old values that decided on blacklist membership - Config { - min_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, - min_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, - use_extended_topology: false, - ignore_epoch_roles: false, - } - } -} - pub struct NymApiTopologyProvider { config: Config, @@ -51,7 +46,11 @@ pub struct NymApiTopologyProvider { } impl NymApiTopologyProvider { - pub fn new(config: Config, mut nym_api_urls: Vec, user_agent: Option) -> Self { + pub fn new( + config: impl Into, + 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 { @@ -64,7 +63,7 @@ impl NymApiTopologyProvider { }; NymApiTopologyProvider { - config, + config: config.into(), validator_client, nym_api_urls, currently_used_api: 0, @@ -82,7 +81,7 @@ impl NymApiTopologyProvider { .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) } - async fn get_current_compatible_topology(&mut self) -> Option { + async fn get_current_compatible_topology(&mut self) -> Option { let rewarded_set = self .validator_client .get_current_rewarded_set() @@ -90,7 +89,7 @@ impl NymApiTopologyProvider { .inspect_err(|err| error!("failed to get current rewarded set: {err}")) .ok()?; - let mut topology = NymTopologyNew::new_empty(rewarded_set); + let mut topology = NymTopology::new_empty(rewarded_set); if self.config.use_extended_topology { let all_nodes = self @@ -152,7 +151,7 @@ impl NymApiTopologyProvider { #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for NymApiTopologyProvider { - async fn get_new_topology(&mut self) -> Option { + async fn get_new_topology(&mut self) -> Option { let Some(topology) = self.get_current_compatible_topology().await else { self.use_next_nym_api(); return None; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index dc125068b79..89c2756c64b 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -285,6 +285,9 @@ pub struct DynamicConnectRequest { // The number of mix node hops that responses should take, in addition to the entry and exit // node. Zero means only client -> entry -> exit -> client. + #[deprecated( + note = "clients can no longer control number of hops to use. this field is scheduled for removal in V8" + )] pub reply_to_hops: Option, // The average delay at each mix node, in milliseconds. Currently this is not supported by the diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index d30768e1f62..a11715268f6 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -8,10 +8,10 @@ use nym_sphinx_addressing::nodes::{ NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN, }; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketType, DEFAULT_NUM_MIX_HOPS}; +use nym_sphinx_params::PacketType; use nym_sphinx_types::delays::Delay; use nym_sphinx_types::{NymPacket, NymPacketError, MIN_PACKET_SIZE}; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use std::time; @@ -43,14 +43,13 @@ impl SurbAck { ack_key: &AckKey, marshaled_fragment_id: [u8; 5], average_delay: time::Duration, - topology: &NymTopology, + topology: &NymRouteProvider, packet_type: PacketType, ) -> Result where R: RngCore + CryptoRng, { - let route = - topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; + let route = topology.random_route_to_egress(rng, recipient.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 8cbfc868190..4ddeb4c8e77 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -8,7 +8,7 @@ use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPAD use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -89,13 +89,12 @@ impl ReplySurb { rng: &mut R, recipient: &Recipient, average_delay: time::Duration, - topology: &NymTopology, + topology: &NymRouteProvider, ) -> Result where R: RngCore + CryptoRng, { - let route = - topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, recipient.gateway())?; + let route = topology.random_route_to_egress(rng, recipient.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 802d2325a66..5500363594e 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -69,11 +69,11 @@ pub mod monitoring { } } - pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey, hops: u8) { + pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey) { if enabled() { let id = fragment.fragment_identifier().set_id(); let mut entry = FRAGMENTS_SENT.entry(id).or_default(); - let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination, hops); + let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination); entry.push(s); } } @@ -82,17 +82,12 @@ pub mod monitoring { #[derive(Debug, Clone)] pub struct FragmentMixParams { destination: PublicKey, - hops: u8, } impl FragmentMixParams { pub fn destination(&self) -> &PublicKey { &self.destination } - - pub fn hops(&self) -> u8 { - self.hops - } } #[derive(Debug, Clone, Serialize, ToSchema)] @@ -105,14 +100,8 @@ pub struct SentFragment { } impl SentFragment { - fn new( - header: FragmentHeader, - at: u64, - client_nonce: i32, - destination: PublicKey, - hops: u8, - ) -> Self { - let mixnet_params = FragmentMixParams { destination, hops }; + fn new(header: FragmentHeader, at: u64, client_nonce: i32, destination: PublicKey) -> Self { + let mixnet_params = FragmentMixParams { destination }; SentFragment { header, at, diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 86510d1de70..41bb6150edb 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -10,11 +10,9 @@ use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_chunking::fragment::COVER_FRAG_ID; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{ - PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, DEFAULT_NUM_MIX_HOPS, -}; +use nym_sphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType}; use nym_sphinx_types::NymPacket; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use std::time; @@ -36,7 +34,7 @@ pub enum CoverMessageError { pub fn generate_loop_cover_surb_ack( rng: &mut R, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, full_address: &Recipient, average_ack_delay: time::Duration, @@ -59,7 +57,7 @@ where #[allow(clippy::too_many_arguments)] pub fn generate_loop_cover_packet( rng: &mut R, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, full_address: &Recipient, average_ack_delay: time::Duration, @@ -118,8 +116,7 @@ where .chain(cover_content) .collect(); - let route = - topology.random_route_to_egress(rng, DEFAULT_NUM_MIX_HOPS, full_address.gateway())?; + let route = topology.random_route_to_egress(rng, full_address.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len()); let destination = full_address.as_sphinx_destination(); diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 33a8c6aae53..3013de41a49 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -16,7 +16,7 @@ use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; use nym_sphinx_types::{Delay, NymPacket}; -use nym_topology::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -61,7 +61,7 @@ pub trait FragmentPreparer { fn generate_reply_surbs( &mut self, amount: usize, - topology: &NymTopology, + topology: &NymRouteProvider, reply_recipient: &Recipient, ) -> Result, NymTopologyError> { let mut reply_surbs = Vec::with_capacity(amount); @@ -79,7 +79,7 @@ pub trait FragmentPreparer { &mut self, recipient: &Recipient, fragment_id: FragmentIdentifier, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_type: PacketType, ) -> Result { @@ -109,7 +109,7 @@ pub trait FragmentPreparer { fn prepare_reply_chunk_for_sending( &mut self, fragment: Fragment, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, reply_surb: ReplySurb, packet_sender: &Recipient, @@ -190,12 +190,11 @@ pub trait FragmentPreparer { fn prepare_chunk_for_sending( &mut self, fragment: Fragment, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_sender: &Recipient, packet_recipient: &Recipient, packet_type: PacketType, - mix_hops: Option, ) -> Result { debug!("Preparing chunk for sending"); // each plain or repliable packet (i.e. not a reply) attaches an ephemeral public key so that the recipient @@ -204,8 +203,7 @@ pub trait FragmentPreparer { let fragment_header = fragment.header(); let destination = packet_recipient.gateway(); - let hops = mix_hops.unwrap_or(self.num_mix_hops()); - monitoring::fragment_sent(&fragment, self.nonce(), *destination, hops); + monitoring::fragment_sent(&fragment, self.nonce(), destination); let non_reply_overhead = encryption::PUBLIC_KEY_SIZE; let expected_plaintext = match packet_type { @@ -240,16 +238,16 @@ pub trait FragmentPreparer { }; // generate pseudorandom route for the packet - log::trace!("Preparing chunk for sending with {hops} mix hops"); + log::trace!("Preparing chunk for sending"); let route = if self.deterministic_route_selection() { log::trace!("using deterministic route selection"); let seed = fragment_header.seed().wrapping_mul(self.nonce()); let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); - topology.random_route_to_egress(&mut rng, hops, destination)? + topology.random_route_to_egress(&mut rng, destination)? } else { log::trace!("using pseudorandom route selection"); let mut rng = self.rng(); - topology.random_route_to_egress(&mut rng, hops, destination)? + topology.random_route_to_egress(&mut rng, destination)? }; let destination = packet_recipient.as_sphinx_destination(); @@ -380,7 +378,7 @@ where pub fn generate_reply_surbs( &mut self, amount: usize, - topology: &NymTopology, + topology: &NymRouteProvider, ) -> Result, NymTopologyError> { let mut reply_surbs = Vec::with_capacity(amount); for _ in 0..amount { @@ -399,7 +397,7 @@ where pub fn prepare_reply_chunk_for_sending( &mut self, fragment: Fragment, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, reply_surb: ReplySurb, packet_type: PacketType, @@ -420,11 +418,10 @@ where pub fn prepare_chunk_for_sending( &mut self, fragment: Fragment, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_recipient: &Recipient, packet_type: PacketType, - mix_hops: Option, ) -> Result { let sender = self.sender_address; @@ -436,7 +433,6 @@ where &sender, packet_recipient, packet_type, - mix_hops, ) } @@ -444,7 +440,7 @@ where pub fn generate_surb_ack( &mut self, fragment_id: FragmentIdentifier, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_type: PacketType, ) -> Result { diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 8b72e7d5760..9bafc5fb035 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -25,7 +25,6 @@ pub mod error; pub mod gateway; pub mod mix; pub mod node; -pub mod random_route_provider; #[cfg(feature = "provider-trait")] pub mod provider_trait; @@ -115,7 +114,7 @@ impl Display for NetworkAddress { pub type MixLayer = u8; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] 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 @@ -127,9 +126,68 @@ pub struct NymTopology { node_details: HashMap, } -const unused: &str = r#"there shall be a config setting, like debug.topology.use_extended = true/false (so that node_details would also include standby/inactive nodes) - and another one for debug.topology.ignore_epoch_roles = true/false (to say send final packet to epoch mixnode) - "#; +#[derive(Clone, Debug)] +pub struct NymRouteProvider { + pub topology: NymTopology, + + /// Allow constructing routes with final hop at nodes that are not entry/exit gateways in the current epoch + pub ignore_egress_epoch_roles: bool, +} + +impl From for NymRouteProvider { + fn from(topology: NymTopology) -> Self { + NymRouteProvider { + topology, + ignore_egress_epoch_roles: false, + } + } +} + +impl NymRouteProvider { + pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider { + let this: Self = NymTopology::default().into(); + this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles) + } + + pub fn update(&mut self, new_topology: NymTopology) { + self.topology = new_topology; + } + + pub fn clear_topology(&mut self) { + self.topology = Default::default(); + } + + pub fn with_ignore_egress_epoch_roles(mut self, ignore_egress_epoch_roles: bool) -> Self { + self.ignore_egress_epoch_roles(ignore_egress_epoch_roles); + self + } + + pub fn ignore_egress_epoch_roles(&mut self, ignore_egress_epoch_roles: bool) { + self.ignore_egress_epoch_roles = ignore_egress_epoch_roles; + } + + pub fn egress_by_identity( + &self, + node_identity: NodeIdentity, + ) -> Result<&RoutingNode, NymTopologyError> { + self.topology + .egress_by_identity(node_identity, self.ignore_egress_epoch_roles) + } + + /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1, + /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node + pub fn random_route_to_egress( + &self, + rng: &mut R, + egress_identity: NodeIdentity, + ) -> Result, NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + self.topology + .random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles) + } +} impl NymTopology { pub fn new_empty(rewarded_set: EpochRewardedSet) -> Self { @@ -196,6 +254,17 @@ impl NymTopology { Ok(()) } + pub fn is_empty(&self) -> bool { + self.rewarded_set.assignment.is_empty() || self.node_details.is_empty() + } + + pub fn ensure_not_empty(&self) -> Result<(), NymTopologyError> { + if self.is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); + } + Ok(()) + } + fn get_sphinx_node(&self, node_id: NodeId) -> Option { self.node_details.get(&node_id).map(Into::into) } diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index b604ad1d72f..bf281d3bfb4 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use nym_api_requests::models::DeclaredRoles; -use nym_api_requests::nym_nodes::SkimmedNode; +use nym_api_requests::nym_nodes::{BasicEntryInformation, SkimmedNode}; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_mixnet_contract_common::NodeId; +use nym_mixnet_contract_common::{NaiveFloat, NodeId}; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; use std::net::{IpAddr, SocketAddr}; @@ -100,12 +100,31 @@ impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { type Error = RoutingNodeError; fn try_from(value: &'a SkimmedNode) -> Result { - if value.ip_addresses.is_empty() { + let Some(first_ip) = value.ip_addresses.first() else { return Err(RoutingNodeError::NoIpAddressesProvided { node_id: value.node_id, identity: value.ed25519_identity_pubkey, }); - } - todo!() + }; + + let entry = match &value.entry { + None => None, + Some(entry) => Some(EntryDetails { + ip_addresses: value.ip_addresses.clone(), + clients_ws_port: entry.ws_port, + hostname: entry.hostname.clone(), + clients_wss_port: entry.wss_port, + }), + }; + + Ok(RoutingNode { + node_id: value.node_id, + mix_host: SocketAddr::new(*first_ip, value.mix_port), + entry, + identity_key: value.ed25519_identity_pubkey, + sphinx_key: value.x25519_sphinx_pubkey, + supported_roles: value.supported_roles.into(), + performance: value.performance.naive_to_f64(), + }) } } diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/provider_trait.rs index 9e156354d8d..eb4d09b52c8 100644 --- a/common/topology/src/provider_trait.rs +++ b/common/topology/src/provider_trait.rs @@ -1,33 +1,33 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::NymTopologyNew; +use crate::NymTopology; pub use async_trait::async_trait; // hehe, wasm #[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait TopologyProvider: Send { - async fn get_new_topology(&mut self) -> Option; + async fn get_new_topology(&mut self) -> Option; } #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] pub trait TopologyProvider { - async fn get_new_topology(&mut self) -> Option; + async fn get_new_topology(&mut self) -> Option; } pub struct HardcodedTopologyProvider { - topology: NymTopologyNew, + topology: NymTopology, } impl HardcodedTopologyProvider { #[cfg(feature = "serde")] pub fn new_from_file>(path: P) -> std::io::Result { - NymTopologyNew::new_from_file(path).map(Self::new) + NymTopology::new_from_file(path).map(Self::new) } - pub fn new(topology: NymTopologyNew) -> Self { + pub fn new(topology: NymTopology) -> Self { HardcodedTopologyProvider { topology } } } @@ -35,7 +35,7 @@ impl HardcodedTopologyProvider { #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for HardcodedTopologyProvider { - async fn get_new_topology(&mut self) -> Option { + async fn get_new_topology(&mut self) -> Option { Some(self.topology.clone()) } } @@ -43,7 +43,7 @@ impl TopologyProvider for HardcodedTopologyProvider { #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] impl TopologyProvider for HardcodedTopologyProvider { - async fn get_new_topology(&mut self) -> Option { + async fn get_new_topology(&mut self) -> Option { Some(self.topology.clone()) } } diff --git a/nym-node/src/node/shared_topology.rs b/nym-node/src/node/shared_topology.rs index 594fb5f4fb3..a4a96676155 100644 --- a/nym-node/src/node/shared_topology.rs +++ b/nym-node/src/node/shared_topology.rs @@ -31,6 +31,8 @@ impl NymNodeTopologyProvider { NymApiTopologyProviderConfig { min_mixnode_performance: 50, min_gateway_performance: 0, + use_extended_topology: false, + ignore_egress_epoch_role: true, }, nym_api_url, Some(user_agent), diff --git a/service-providers/ip-packet-router/src/connected_client_handler.rs b/service-providers/ip-packet-router/src/connected_client_handler.rs index 8754fb83943..a1904eba5fb 100644 --- a/service-providers/ip-packet-router/src/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/connected_client_handler.rs @@ -22,9 +22,6 @@ pub(crate) struct ConnectedClientHandler { // The address of the client that this handler is connected to nym_address: Recipient, - // The number of hops the packet should take before reaching the client - mix_hops: Option, - // Channel to receive packets from the tun_listener forward_from_tun_rx: tokio::sync::mpsc::UnboundedReceiver>, @@ -47,7 +44,6 @@ pub(crate) struct ConnectedClientHandler { impl ConnectedClientHandler { pub(crate) fn start( reply_to: Recipient, - reply_to_hops: Option, buffer_timeout: std::time::Duration, client_version: SupportedClientVersion, mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, @@ -67,7 +63,6 @@ impl ConnectedClientHandler { let connected_client_handler = ConnectedClientHandler { nym_address: reply_to, - mix_hops: reply_to_hops, forward_from_tun_rx, mixnet_client_sender, close_rx, @@ -98,7 +93,7 @@ impl ConnectedClientHandler { } .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })?; - let input_message = create_input_message(self.nym_address, response_packet, self.mix_hops); + let input_message = create_input_message(self.nym_address, response_packet); self.mixnet_client_sender .send(input_message) diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 914cd03b36a..fe029c0fb78 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -464,7 +464,6 @@ impl MixnetListener { let (forward_from_tun_tx, close_tx, handle) = connected_client_handler::ConnectedClientHandler::start( reply_to, - reply_to_hops, buffer_timeout, client_version, self.mixnet_client.split_sender(), @@ -474,7 +473,6 @@ impl MixnetListener { self.connected_clients.connect( requested_ips, reply_to, - reply_to_hops, forward_from_tun_tx, close_tx, handle, @@ -518,7 +516,6 @@ impl MixnetListener { let request_id = connect_request.request_id; let reply_to = connect_request.reply_to; - let reply_to_hops = connect_request.reply_to_hops; // TODO: add to connect request let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; // TODO: ignoring reply_to_avg_mix_delays for now diff --git a/service-providers/ip-packet-router/src/util/create_message.rs b/service-providers/ip-packet-router/src/util/create_message.rs index d04d85bd223..dca63c21dfc 100644 --- a/service-providers/ip-packet-router/src/util/create_message.rs +++ b/service-providers/ip-packet-router/src/util/create_message.rs @@ -4,15 +4,8 @@ use nym_task::connections::TransmissionLane; pub(crate) fn create_input_message( nym_address: Recipient, response_packet: Vec, - mix_hops: Option, ) -> InputMessage { let lane = TransmissionLane::General; let packet_type = None; - InputMessage::new_regular_with_custom_hops( - nym_address, - response_packet, - lane, - packet_type, - mix_hops, - ) + InputMessage::new_regular(nym_address, response_packet, lane, packet_type) } From 19b0f8d113e742e0188a617661d04641c5ce3cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 13 Dec 2024 16:53:33 +0000 Subject: [PATCH 4/9] updated nym-api --- common/client-core/Cargo.toml | 2 +- .../src/cli_helpers/client_add_gateway.rs | 4 +- .../src/cli_helpers/client_init.rs | 2 +- .../real_messages_control/message_handler.rs | 59 +- common/client-core/src/error.rs | 9 +- common/client-core/src/init/helpers.rs | 58 +- common/client-core/src/init/mod.rs | 3 +- common/client-core/src/init/types.rs | 5 +- common/client-core/src/lib.rs | 3 +- .../mixnet-contract/src/mixnode.rs | 11 + .../ip-packet-requests/src/v7/conversion.rs | 1 + common/ip-packet-requests/src/v7/request.rs | 1 + common/node-tester-utils/src/message.rs | 64 +- common/node-tester-utils/src/node.rs | 42 +- common/node-tester-utils/src/tester.rs | 131 +-- .../anonymous-replies/src/reply_surb.rs | 13 +- .../anonymous-replies/src/requests.rs | 148 +-- common/nymsphinx/params/src/lib.rs | 4 - common/nymsphinx/src/message.rs | 22 +- common/nymsphinx/src/preparer/mod.rs | 23 +- common/nymsphinx/src/receiver.rs | 26 +- common/topology/Cargo.toml | 4 +- common/topology/src/error.rs | 6 - common/topology/src/gateway.rs | 41 +- common/topology/src/lib.rs | 1011 +++++++++-------- common/topology/src/mix.rs | 39 +- common/topology/src/node.rs | 40 +- common/topology/src/provider_trait.rs | 2 +- common/topology/src/rewarded_set.rs | 122 ++ common/topology/src/serde.rs | 264 ----- common/topology/src/wasm_helpers.rs | 100 ++ common/wasm/client-core/Cargo.toml | 2 +- nym-api/nym-api-requests/src/models.rs | 14 + nym-api/nym-api-requests/src/nym_nodes.rs | 3 +- nym-api/src/epoch_operations/helpers.rs | 4 +- nym-api/src/network_monitor/monitor/mod.rs | 4 +- .../src/network_monitor/monitor/preparer.rs | 209 ++-- nym-api/src/network_monitor/monitor/sender.rs | 31 +- nym-api/src/network_monitor/test_packet.rs | 4 +- nym-api/src/network_monitor/test_route/mod.rs | 71 +- nym-api/src/node_describe_cache/mod.rs | 90 +- .../src/node_status_api/cache/node_sets.rs | 50 +- nym-api/src/nym_contract_cache/cache/data.rs | 131 +-- nym-api/src/nym_contract_cache/cache/mod.rs | 9 +- .../src/nym_contract_cache/cache/refresher.rs | 2 +- nym-api/src/nym_nodes/handlers/mod.rs | 9 +- .../nym_nodes/handlers/unstable/skimmed.rs | 6 +- nym-api/src/support/http/state.rs | 5 +- nym-api/src/support/legacy_helpers.rs | 19 + nym-api/src/support/storage/mod.rs | 6 +- .../examples/manually_overwrite_topology.rs | 5 +- sdk/rust/nym-sdk/src/mixnet/native_client.rs | 10 +- sdk/rust/nym-sdk/src/mixnet/socks5_client.rs | 5 - 53 files changed, 1316 insertions(+), 1633 deletions(-) create mode 100644 common/topology/src/rewarded_set.rs delete mode 100644 common/topology/src/serde.rs create mode 100644 common/topology/src/wasm_helpers.rs diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index b43c6e2dbfd..63b92f9b8b2 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -45,7 +45,7 @@ nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } nym-pemstore = { path = "../pemstore" } -nym-topology = { path = "../topology", features = ["serde"] } +nym-topology = { path = "../topology", features = ["persistence"] } nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs index 56b1c3ad8e8..dd1064f1353 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -112,7 +112,7 @@ where source, } })?; - hardcoded_topology.get_gateways() + hardcoded_topology.entry_capable_nodes().cloned().collect() } else { let mut rng = rand::thread_rng(); crate::init::helpers::current_gateways( @@ -128,7 +128,7 @@ where // make sure the list of available gateways doesn't overlap the list of known gateways let available_gateways = available_gateways .into_iter() - .filter(|g| !registered_gateways.contains(g.identity())) + .filter(|g| !registered_gateways.contains(&g.identity())) .collect::>(); if available_gateways.is_empty() { diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index 060c1192da8..0599f3a20e2 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -167,7 +167,7 @@ where source, } })?; - hardcoded_topology.get_gateways() + hardcoded_topology.entry_capable_nodes().cloned().collect() } else { let mut rng = rand::thread_rng(); crate::init::helpers::current_gateways( diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index f272a8c0921..4dcce654948 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -15,11 +15,11 @@ use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessa use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey}; use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx::message::NymMessage; -use nym_sphinx::params::{PacketSize, PacketType, DEFAULT_NUM_MIX_HOPS}; +use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::preparer::{MessagePreparer, PreparedFragment}; use nym_sphinx::Delay; use nym_task::connections::TransmissionLane; -use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng}; use std::collections::HashMap; use std::sync::Arc; @@ -100,10 +100,6 @@ pub(crate) struct Config { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, - /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. - /// Note that it does not include gateway hops. - num_mix_hops: u8, - /// Primary predefined packet size used for the encapsulated messages. primary_packet_size: PacketSize, @@ -125,19 +121,11 @@ impl Config { deterministic_route_selection, average_packet_delay, average_ack_delay, - num_mix_hops: DEFAULT_NUM_MIX_HOPS, primary_packet_size: PacketSize::default(), secondary_packet_size: None, } } - /// Allows setting non-default number of expected mix hops in the network. - #[allow(dead_code)] - pub fn with_mix_hops(mut self, hops: u8) -> Self { - self.num_mix_hops = hops; - self - } - /// Allows setting non-default size of the sphinx packets sent out. pub fn with_custom_primary_packet_size(mut self, packet_size: PacketSize) -> Self { self.primary_packet_size = packet_size; @@ -185,9 +173,7 @@ where config.sender_address, config.average_packet_delay, config.average_ack_delay, - ) - .with_mix_hops(config.num_mix_hops); - + ); MessageHandler { config, rng, @@ -233,9 +219,8 @@ where return self.config.primary_packet_size; }; - let primary_count = - msg.required_packets(self.config.primary_packet_size, self.config.num_mix_hops); - let secondary_count = msg.required_packets(secondary_packet, self.config.num_mix_hops); + let primary_count = msg.required_packets(self.config.primary_packet_size); + let secondary_count = msg.required_packets(secondary_packet); trace!("This message would require: {primary_count} primary packets or {secondary_count} secondary packets..."); // if there would be no benefit in using the secondary packet - use the primary (duh) @@ -553,16 +538,13 @@ where let topology_permit = self.topology_access.get_read_permit().await; let topology = self.get_topology(&topology_permit)?; - let prepared_fragment = self - .message_preparer - .prepare_chunk_for_sending( - chunk, - topology, - &self.config.ack_key, - &recipient, - packet_type, - ) - .unwrap(); + let prepared_fragment = self.message_preparer.prepare_chunk_for_sending( + chunk, + topology, + &self.config.ack_key, + &recipient, + packet_type, + )?; Ok(prepared_fragment) } @@ -615,16 +597,13 @@ where Err(err) => return Err(err.return_surbs(vec![reply_surb])), }; - let prepared_fragment = self - .message_preparer - .prepare_reply_chunk_for_sending( - chunk, - topology, - &self.config.ack_key, - reply_surb, - PacketType::Mix, - ) - .unwrap(); + let prepared_fragment = self.message_preparer.prepare_reply_chunk_for_sending( + chunk, + topology, + &self.config.ack_key, + reply_surb, + PacketType::Mix, + )?; Ok(prepared_fragment) } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 0cc9e04d753..fae13a0d7e3 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::mix_traffic::transceiver::ErasedGatewayError; +use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; use nym_topology::gateway::GatewayConversionError; -use nym_topology::NymTopologyError; +use nym_topology::{NodeId, NymTopologyError}; use nym_validator_client::ValidatorClientError; use std::error::Error; use std::path::PathBuf; @@ -159,6 +160,12 @@ pub enum ClientCoreError { #[error("the specified gateway '{gateway}' does not support the wss protocol")] UnsupportedWssProtocol { gateway: String }, + #[error("node {id} ({identity}) does not support mixnet entry mode")] + UnsupportedEntry { + id: NodeId, + identity: ed25519::PublicKey, + }, + #[error( "failed to load custom topology using path '{}'. detailed message: {source}", file_path.display() )] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 68b3b8d4570..5c35be69262 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -7,7 +7,7 @@ use futures::{SinkExt, StreamExt}; use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; -use nym_topology::gateway; +use nym_topology::node::RoutingNode; use nym_validator_client::client::IdentityKeyRef; use nym_validator_client::UserAgent; use rand::{seq::SliceRandom, Rng}; @@ -15,6 +15,7 @@ use std::{sync::Arc, time::Duration}; use tungstenite::Message; use url::Url; +use nym_topology::NodeId; #[cfg(not(target_arch = "wasm32"))] use tokio::net::TcpStream; #[cfg(not(target_arch = "wasm32"))] @@ -25,7 +26,6 @@ use tokio::time::Instant; use tokio_tungstenite::connect_async; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; - #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] @@ -48,22 +48,30 @@ const PING_TIMEOUT: Duration = Duration::from_millis(1000); // The abstraction that some of these helpers use pub trait ConnectableGateway { - fn identity(&self) -> &identity::PublicKey; - fn clients_address(&self) -> String; + fn node_id(&self) -> NodeId; + fn identity(&self) -> identity::PublicKey; + fn clients_address(&self, prefer_ipv6: bool) -> Option; fn is_wss(&self) -> bool; } -impl ConnectableGateway for gateway::LegacyNode { - fn identity(&self) -> &identity::PublicKey { - self.identity() +impl ConnectableGateway for RoutingNode { + fn node_id(&self) -> NodeId { + self.node_id + } + + fn identity(&self) -> identity::PublicKey { + self.identity_key } - fn clients_address(&self) -> String { - self.clients_address() + fn clients_address(&self, prefer_ipv6: bool) -> Option { + self.ws_entry_address(prefer_ipv6) } fn is_wss(&self) -> bool { - self.clients_wss_port.is_some() + self.entry + .as_ref() + .map(|e| e.clients_wss_port.is_some()) + .unwrap_or_default() } } @@ -83,7 +91,7 @@ pub async fn current_gateways( nym_apis: &[Url], user_agent: Option, minimum_performance: u8, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -104,7 +112,7 @@ pub async fn current_gateways( .iter() .filter(|g| g.performance.round_to_integer() >= minimum_performance) .filter_map(|gateway| gateway.try_into().ok()) - .collect::>(); + .collect::>(); log::debug!("After checking validity: {}", valid_gateways.len()); log::trace!("Valid gateways: {:#?}", valid_gateways); @@ -134,7 +142,12 @@ async fn measure_latency(gateway: &G) -> Result, Client where G: ConnectableGateway, { - let addr = gateway.clients_address(); + let Some(addr) = gateway.clients_address(false) else { + return Err(ClientCoreError::UnsupportedEntry { + id: gateway.node_id(), + identity: gateway.identity(), + }); + }; trace!( "establishing connection to {} ({addr})...", gateway.identity(), @@ -205,7 +218,7 @@ pub async fn choose_gateway_by_latency<'a, R: Rng, G: ConnectableGateway + Clone let gateways_with_latency = Arc::new(tokio::sync::Mutex::new(Vec::new())); futures::stream::iter(gateways) .for_each_concurrent(CONCURRENT_GATEWAYS_MEASURED, |gateway| async { - let id = *gateway.identity(); + let id = gateway.identity(); trace!("measuring latency to {id}..."); match measure_latency(gateway).await { Ok(with_latency) => { @@ -252,9 +265,9 @@ fn filter_by_tls( pub(super) fn uniformly_random_gateway( rng: &mut R, - gateways: &[gateway::LegacyNode], + gateways: &[RoutingNode], must_use_tls: bool, -) -> Result { +) -> Result { filter_by_tls(gateways, must_use_tls)? .choose(rng) .ok_or(ClientCoreError::NoGatewaysOnNetwork) @@ -263,9 +276,9 @@ pub(super) fn uniformly_random_gateway( pub(super) fn get_specified_gateway( gateway_identity: IdentityKeyRef, - gateways: &[gateway::LegacyNode], + gateways: &[RoutingNode], must_use_tls: bool, -) -> Result { +) -> Result { log::debug!("Requesting specified gateway: {}", gateway_identity); let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; @@ -275,7 +288,14 @@ pub(super) fn get_specified_gateway( .find(|gateway| gateway.identity_key == user_gateway) .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string()))?; - if must_use_tls && gateway.clients_wss_port.is_none() { + let Some(entry_details) = gateway.entry.as_ref() else { + return Err(ClientCoreError::UnsupportedEntry { + id: gateway.node_id, + identity: gateway.identity(), + }); + }; + + if must_use_tls && entry_details.clients_wss_port.is_none() { return Err(ClientCoreError::UnsupportedWssProtocol { gateway: gateway_identity.to_string(), }); diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 8e93babbf23..d4f843a76c1 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -20,6 +20,7 @@ use nym_client_core_gateways_storage::GatewaysDetailsStore; use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration}; use nym_gateway_client::client::InitGatewayClient; use nym_topology::gateway; +use nym_topology::node::RoutingNode; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; use serde::Serialize; @@ -50,7 +51,7 @@ async fn setup_new_gateway( key_store: &K, details_store: &D, selection_specification: GatewaySelectionSpecification, - available_gateways: Vec, + available_gateways: Vec, ) -> Result where K: KeyStore, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 1aa4a0d24bb..3f4e4762baf 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -14,6 +14,7 @@ use nym_gateway_client::client::InitGatewayClient; use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_sphinx::addressing::clients::Recipient; use nym_topology::gateway; +use nym_topology::node::RoutingNode; use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; use serde::Serialize; @@ -38,7 +39,7 @@ pub enum SelectedGateway { impl SelectedGateway { pub fn from_topology_node( - node: gateway::LegacyNode, + node: RoutingNode, must_use_tls: bool, ) -> Result { let gateway_listener = if must_use_tls { @@ -200,7 +201,7 @@ pub enum GatewaySetup { specification: GatewaySelectionSpecification, // TODO: seems to be a bit inefficient to pass them by value - available_gateways: Vec, + available_gateways: Vec, }, ReuseConnection { diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 12ea3f7d5cb..5154d192f10 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -14,8 +14,7 @@ pub mod error; pub mod init; pub use nym_topology::{ - HardcodedTopologyProvider, NymTopology, NymTopologyError, SerializableNymTopology, - SerializableTopologyError, TopologyProvider, + HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError, TopologyProvider, }; #[cfg(target_arch = "wasm32")] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index fb03a1034b3..bbae58a9358 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -7,6 +7,7 @@ use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE}; use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; +use crate::nym_node::Role; use crate::reward_params::{NodeRewardingParameters, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; @@ -611,6 +612,16 @@ pub enum LegacyMixLayer { Three = 3, } +impl From for Role { + fn from(layer: LegacyMixLayer) -> Self { + match layer { + LegacyMixLayer::One => Role::Layer1, + LegacyMixLayer::Two => Role::Layer2, + LegacyMixLayer::Three => Role::Layer3, + } + } +} + impl From for String { fn from(layer: LegacyMixLayer) -> Self { (layer as u8).to_string() diff --git a/common/ip-packet-requests/src/v7/conversion.rs b/common/ip-packet-requests/src/v7/conversion.rs index 16923b7a3b6..04b0244b057 100644 --- a/common/ip-packet-requests/src/v7/conversion.rs +++ b/common/ip-packet-requests/src/v7/conversion.rs @@ -63,6 +63,7 @@ impl From for v7::request::StaticConnectReque } } +#[allow(deprecated)] impl From for v7::request::DynamicConnectRequest { fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self { Self { diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 89c2756c64b..43b71bb3623 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -51,6 +51,7 @@ impl IpPacketRequest { ) } + #[allow(deprecated)] pub fn new_dynamic_connect_request( reply_to: Recipient, reply_to_hops: Option, diff --git a/common/node-tester-utils/src/message.rs b/common/node-tester-utils/src/message.rs index b17515d5b61..8f09095c223 100644 --- a/common/node-tester-utils/src/message.rs +++ b/common/node-tester-utils/src/message.rs @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::NetworkTestingError; -use crate::node::TestableNode; -use crate::NodeId; +use crate::node::{NodeType, TestableNode}; use nym_sphinx::message::NymMessage; -use nym_topology::{gateway, mix}; +use nym_topology::node::RoutingNode; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -26,73 +25,76 @@ pub struct TestMessage { } impl TestMessage { - pub fn new>(node: N, msg_id: u32, total_msgs: u32, ext: T) -> Self { + pub fn new(tested_node: TestableNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { TestMessage { - tested_node: node.into(), + tested_node, msg_id, total_msgs, ext, } } - pub fn new_mix(node: &mix::LegacyNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { - Self::new(node, msg_id, total_msgs, ext) + pub fn new_mix(node: &RoutingNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { + Self::new( + TestableNode::new_routing(node, NodeType::Mixnode), + msg_id, + total_msgs, + ext, + ) } - // pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { - // Self::new(node, msg_id, total_msgs, ext) - // } - - pub fn new_serialized( - node: N, - msg_id: u32, - total_msgs: u32, - ext: T, - ) -> Result, NetworkTestingError> - where - N: Into, - T: Serialize, - { - Self::new(node, msg_id, total_msgs, ext).as_bytes() + pub fn new_gateway(node: &RoutingNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { + Self::new( + TestableNode::new_routing(node, NodeType::Gateway), + msg_id, + total_msgs, + ext, + ) } - pub fn new_plaintexts( - node: &N, + pub fn new_plaintexts( + node: TestableNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where - for<'a> &'a N: Into, T: Serialize + Clone, { let mut msgs = Vec::with_capacity(total_msgs as usize); for msg_id in 1..=total_msgs { - msgs.push(Self::new(node, msg_id, total_msgs, ext.clone()).as_bytes()?) + msgs.push(Self::new(node.clone(), msg_id, total_msgs, ext.clone()).as_bytes()?) } Ok(msgs) } pub fn mix_plaintexts( - node: &mix::LegacyNode, + node: &RoutingNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where T: Serialize + Clone, { - Self::new_plaintexts(node, total_msgs, ext) + Self::new_plaintexts( + TestableNode::new_routing(node, NodeType::Mixnode), + total_msgs, + ext, + ) } pub fn legacy_gateway_plaintexts( - node: &gateway::LegacyNode, - node_id: NodeId, + node: &RoutingNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where T: Serialize + Clone, { - Self::new_plaintexts(&(node, node_id), total_msgs, ext) + Self::new_plaintexts( + TestableNode::new_routing(node, NodeType::Gateway), + total_msgs, + ext, + ) } pub fn as_json_string(&self) -> Result diff --git a/common/node-tester-utils/src/node.rs b/common/node-tester-utils/src/node.rs index d60623a6e2c..3ec4d806702 100644 --- a/common/node-tester-utils/src/node.rs +++ b/common/node-tester-utils/src/node.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::NodeId; -use nym_topology::{gateway, mix}; +use nym_topology::node::RoutingNode; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -24,6 +24,14 @@ impl TestableNode { } } + pub fn new_routing(routing_node: &RoutingNode, typ: NodeType) -> Self { + TestableNode::new( + routing_node.identity_key.to_base58_string(), + typ, + routing_node.node_id, + ) + } + pub fn new_mixnode(encoded_identity: String, node_id: NodeId) -> Self { TestableNode::new(encoded_identity, NodeType::Mixnode, node_id) } @@ -37,38 +45,6 @@ impl TestableNode { } } -impl<'a> From<&'a mix::LegacyNode> for TestableNode { - fn from(value: &'a mix::LegacyNode) -> Self { - TestableNode { - encoded_identity: value.identity_key.to_base58_string(), - typ: NodeType::Mixnode, - node_id: value.mix_id, - } - } -} - -impl<'a> From<(&'a gateway::LegacyNode, NodeId)> for TestableNode { - fn from((gateway, node_id): (&'a gateway::LegacyNode, NodeId)) -> Self { - (&(gateway, node_id)).into() - } -} - -impl<'a> From<&'a (gateway::LegacyNode, NodeId)> for TestableNode { - fn from((gateway, node_id): &'a (gateway::LegacyNode, NodeId)) -> Self { - (gateway, *node_id).into() - } -} - -impl<'a, 'b> From<&'a (&'b gateway::LegacyNode, NodeId)> for TestableNode { - fn from((gateway, node_id): &'a (&'b gateway::LegacyNode, NodeId)) -> Self { - TestableNode { - encoded_identity: gateway.identity_key.to_base58_string(), - typ: NodeType::Gateway, - node_id: *node_id, - } - } -} - impl Display for TestableNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index d60ec55e67c..97e17b67b21 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -2,16 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::NetworkTestingError; -use crate::Empty; -use crate::NodeId; use crate::TestMessage; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::message::NymMessage; -use nym_sphinx::params::{PacketSize, DEFAULT_NUM_MIX_HOPS}; +use nym_sphinx::params::PacketSize; use nym_sphinx::preparer::{FragmentPreparer, PreparedFragment}; use nym_sphinx_params::PacketType; -use nym_topology::{gateway, mix, NymTopology}; +use nym_topology::node::LegacyMixLayer; +use nym_topology::node::RoutingNode; +use nym_topology::{NymRouteProvider, NymTopology, Role}; use rand::{CryptoRng, Rng}; use serde::Serialize; use std::sync::Arc; @@ -38,10 +38,6 @@ pub struct NodeTester { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, - /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. - /// Note that it does not include gateway hops. - num_mix_hops: u8, - // while acks are going to be ignored they still need to be constructed // so that the gateway would be able to correctly process and forward the message ack_key: Arc, @@ -70,41 +66,27 @@ where deterministic_route_selection, average_packet_delay, average_ack_delay, - num_mix_hops: DEFAULT_NUM_MIX_HOPS, ack_key, } } - /// Allows setting non-default number of expected mix hops in the network. - #[allow(dead_code)] - pub fn with_mix_hops(mut self, hops: u8) -> Self { - self.num_mix_hops = hops; - self - } - - pub fn testable_mix_topology(&self, node: &mix::LegacyNode) -> NymTopology { + pub fn testable_mix_topology(&self, layer: LegacyMixLayer, node: &RoutingNode) -> NymTopology { let mut topology = self.base_topology.clone(); - topology.set_mixes_in_layer(node.layer as u8, vec![node.clone()]); + topology.set_testable_node(layer.into(), node.clone()); topology } - pub fn testable_gateway_topology(&self, gateway: &gateway::LegacyNode) -> NymTopology { + pub fn testable_gateway_topology(&self, node: &RoutingNode) -> NymTopology { let mut topology = self.base_topology.clone(); - topology.set_gateways(vec![gateway.clone()]); + topology.set_testable_node(Role::EntryGateway, node.clone()); + topology.set_testable_node(Role::ExitGateway, node.clone()); topology } - pub fn simple_mixnode_test_packets( - &mut self, - mix: &mix::LegacyNode, - test_packets: u32, - ) -> Result, NetworkTestingError> { - self.mixnode_test_packets(mix, Empty, test_packets, None) - } - pub fn mixnode_test_packets( &mut self, - mix: &mix::LegacyNode, + mix: &RoutingNode, + legacy_mix_layer: LegacyMixLayer, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -112,7 +94,9 @@ where where T: Serialize + Clone, { - let ephemeral_topology = self.testable_mix_topology(mix); + let ephemeral_topology = + NymRouteProvider::from(self.testable_mix_topology(legacy_mix_layer, mix)) + .with_ignore_egress_epoch_roles(true); let mut packets = Vec::with_capacity(test_packets as usize); for plaintext in TestMessage::mix_plaintexts(mix, test_packets, msg_ext)? { @@ -128,7 +112,7 @@ where pub fn mixnodes_test_packets( &mut self, - nodes: &[mix::LegacyNode], + nodes: &[(LegacyMixLayer, RoutingNode)], msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -137,9 +121,10 @@ where T: Serialize + Clone, { let mut packets = Vec::new(); - for node in nodes { + for (layer, node) in nodes { packets.append(&mut self.mixnode_test_packets( node, + *layer, msg_ext.clone(), test_packets, custom_recipient, @@ -149,26 +134,10 @@ where Ok(packets) } - pub fn existing_mixnode_test_packets( - &mut self, - mix_id: NodeId, - msg_ext: T, - test_packets: u32, - custom_recipient: Option, - ) -> Result, NetworkTestingError> - where - T: Serialize + Clone, - { - let Some(node) = self.base_topology.find_mix(mix_id) else { - return Err(NetworkTestingError::NonExistentMixnode { mix_id }); - }; - - self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) - } - pub fn existing_identity_mixnode_test_packets( &mut self, encoded_mix_identity: String, + layer: LegacyMixLayer, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -176,22 +145,30 @@ where where T: Serialize + Clone, { - let Some(node) = self - .base_topology - .find_mix_by_identity(&encoded_mix_identity) - else { + let Ok(identity) = encoded_mix_identity.parse() else { return Err(NetworkTestingError::NonExistentMixnodeIdentity { mix_identity: encoded_mix_identity, }); }; - self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) + let Some(node) = self.base_topology.find_node_by_identity(identity) else { + return Err(NetworkTestingError::NonExistentMixnodeIdentity { + mix_identity: encoded_mix_identity, + }); + }; + + self.mixnode_test_packets( + &node.clone(), + layer, + msg_ext, + test_packets, + custom_recipient, + ) } pub fn legacy_gateway_test_packets( &mut self, - gateway: &gateway::LegacyNode, - node_id: NodeId, + gateway: &RoutingNode, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -199,12 +176,11 @@ where where T: Serialize + Clone, { - let ephemeral_topology = self.testable_gateway_topology(gateway); + let ephemeral_topology = NymRouteProvider::from(self.testable_gateway_topology(gateway)) + .with_ignore_egress_epoch_roles(true); let mut packets = Vec::with_capacity(test_packets as usize); - for plaintext in - TestMessage::legacy_gateway_plaintexts(gateway, node_id, test_packets, msg_ext)? - { + for plaintext in TestMessage::legacy_gateway_plaintexts(gateway, test_packets, msg_ext)? { packets.push(self.wrap_plaintext_data( plaintext, &ephemeral_topology, @@ -215,36 +191,10 @@ where Ok(packets) } - pub fn existing_gateway_test_packets( - &mut self, - node_id: NodeId, - encoded_gateway_identity: String, - msg_ext: T, - test_packets: u32, - custom_recipient: Option, - ) -> Result, NetworkTestingError> - where - T: Serialize + Clone, - { - let Some(node) = self.base_topology.find_gateway(&encoded_gateway_identity) else { - return Err(NetworkTestingError::NonExistentGateway { - gateway_identity: encoded_gateway_identity, - }); - }; - - self.legacy_gateway_test_packets( - &node.clone(), - node_id, - msg_ext, - test_packets, - custom_recipient, - ) - } - pub fn wrap_plaintext_data( &mut self, plaintext: Vec, - topology: &NymTopology, + topology: &NymRouteProvider, custom_recipient: Option, ) -> Result { let message = NymMessage::new_plain(plaintext); @@ -274,14 +224,13 @@ where &address, &address, PacketType::Mix, - None, )?) } pub fn create_test_packet( &mut self, message: &TestMessage, - topology: &NymTopology, + topology: &NymRouteProvider, custom_recipient: Option, ) -> Result where @@ -307,10 +256,6 @@ impl FragmentPreparer for NodeTester { 1 } - fn num_mix_hops(&self) -> u8 { - self.num_mix_hops - } - fn average_packet_delay(&self) -> Duration { self.average_packet_delay } diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 4ddeb4c8e77..3ac7af8571c 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -6,9 +6,9 @@ use nym_crypto::{generic_array::typenum::Unsigned, Digest}; use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; +use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; -use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -109,15 +109,12 @@ impl ReplySurb { /// Returns the expected number of bytes the [`ReplySURB`] will take after serialization. /// Useful for deserialization from a bytes stream. - pub fn serialized_len(mix_hops: u8) -> usize { + pub fn serialized_len() -> usize { use nym_sphinx_types::{HEADER_SIZE, NODE_ADDRESS_LENGTH, PAYLOAD_KEY_SIZE}; // the SURB itself consists of SURB_header, first hop address and set of payload keys - // (note extra 1 for the gateway) - SurbEncryptionKeySize::USIZE - + HEADER_SIZE - + NODE_ADDRESS_LENGTH - + (1 + mix_hops as usize) * PAYLOAD_KEY_SIZE + // for each hop (3x mix + egress) + SurbEncryptionKeySize::USIZE + HEADER_SIZE + NODE_ADDRESS_LENGTH + 4 * PAYLOAD_KEY_SIZE } pub fn encryption_key(&self) -> &SurbEncryptionKey { diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests.rs index 9dd4c84dc04..39457534957 100644 --- a/common/nymsphinx/anonymous-replies/src/requests.rs +++ b/common/nymsphinx/anonymous-replies/src/requests.rs @@ -169,10 +169,7 @@ impl RepliableMessage { .collect() } - pub fn try_from_bytes( - bytes: &[u8], - num_mix_hops: u8, - ) -> Result { + pub fn try_from_bytes(bytes: &[u8]) -> Result { if bytes.len() < SENDER_TAG_SIZE + 1 { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } @@ -180,11 +177,8 @@ impl RepliableMessage { AnonymousSenderTag::from_bytes(bytes[..SENDER_TAG_SIZE].try_into().unwrap()); let content_tag = RepliableMessageContentTag::try_from(bytes[SENDER_TAG_SIZE])?; - let content = RepliableMessageContent::try_from_bytes( - &bytes[SENDER_TAG_SIZE + 1..], - num_mix_hops, - content_tag, - )?; + let content = + RepliableMessageContent::try_from_bytes(&bytes[SENDER_TAG_SIZE + 1..], content_tag)?; Ok(RepliableMessage { sender_tag, @@ -192,23 +186,20 @@ impl RepliableMessage { }) } - pub fn serialized_size(&self, num_mix_hops: u8) -> usize { + pub fn serialized_size(&self) -> usize { let content_type_size = 1; - SENDER_TAG_SIZE + content_type_size + self.content.serialized_size(num_mix_hops) + SENDER_TAG_SIZE + content_type_size + self.content.serialized_size() } } // this recovery code is shared between all variants containing reply surbs -fn recover_reply_surbs( - bytes: &[u8], - num_mix_hops: u8, -) -> Result<(Vec, usize), InvalidReplyRequestError> { +fn recover_reply_surbs(bytes: &[u8]) -> Result<(Vec, usize), InvalidReplyRequestError> { let mut consumed = mem::size_of::(); if bytes.len() < consumed { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - let surb_size = ReplySurb::serialized_len(num_mix_hops); + let surb_size = ReplySurb::serialized_len(); if bytes[consumed..].len() < num_surbs as usize * surb_size { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } @@ -307,14 +298,13 @@ impl RepliableMessageContent { fn try_from_bytes( bytes: &[u8], - num_mix_hops: u8, tag: RepliableMessageContentTag, ) -> Result { if bytes.is_empty() { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } - let (reply_surbs, n) = recover_reply_surbs(bytes, num_mix_hops)?; + let (reply_surbs, n) = recover_reply_surbs(bytes)?; match tag { RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data { @@ -340,7 +330,7 @@ impl RepliableMessageContent { } } - fn serialized_size(&self, num_mix_hops: u8) -> usize { + fn serialized_size(&self) -> usize { match self { RepliableMessageContent::Data { message, @@ -348,19 +338,18 @@ impl RepliableMessageContent { } => { let num_reply_surbs_tag = mem::size_of::(); num_reply_surbs_tag - + reply_surbs.len() * ReplySurb::serialized_len(num_mix_hops) + + reply_surbs.len() * ReplySurb::serialized_len() + message.len() } RepliableMessageContent::AdditionalSurbs { reply_surbs } => { let num_reply_surbs_tag = mem::size_of::(); - num_reply_surbs_tag + reply_surbs.len() * ReplySurb::serialized_len(num_mix_hops) + num_reply_surbs_tag + reply_surbs.len() * ReplySurb::serialized_len() } RepliableMessageContent::Heartbeat { additional_reply_surbs, } => { let num_reply_surbs_tag = mem::size_of::(); - num_reply_surbs_tag - + additional_reply_surbs.len() * ReplySurb::serialized_len(num_mix_hops) + num_reply_surbs_tag + additional_reply_surbs.len() * ReplySurb::serialized_len() } } } @@ -578,11 +567,11 @@ mod tests { } } - pub(super) fn reply_surb(rng: &mut ChaCha20Rng, num_mix_hops: u8) -> ReplySurb { + pub(super) fn reply_surb(rng: &mut ChaCha20Rng) -> ReplySurb { // due to gateway - let num_hops = num_mix_hops + 1; - let route = (0..num_hops).map(|_| node(rng)).collect(); - let delays = (0..num_hops) + const HOPS: u8 = 4; + let route = (0..HOPS).map(|_| node(rng)).collect(); + let delays = (0..HOPS) .map(|_| Delay::new_from_nanos(rng.next_u64())) .collect(); let mut destination_bytes = [0u8; 32]; @@ -605,47 +594,40 @@ mod tests { } } - pub(super) fn reply_surbs( - rng: &mut ChaCha20Rng, - num_mix_hops: u8, - n: usize, - ) -> Vec { + pub(super) fn reply_surbs(rng: &mut ChaCha20Rng, n: usize) -> Vec { let mut surbs = Vec::with_capacity(n); for _ in 0..n { - surbs.push(reply_surb(rng, num_mix_hops)) + surbs.push(reply_surb(rng)) } surbs } pub(super) fn repliable_content_data( rng: &mut ChaCha20Rng, - num_mix_hops: u8, msg_len: usize, surbs: usize, ) -> RepliableMessageContent { RepliableMessageContent::Data { message: random_vec_u8(rng, msg_len), - reply_surbs: reply_surbs(rng, num_mix_hops, surbs), + reply_surbs: reply_surbs(rng, surbs), } } pub(super) fn repliable_content_surbs( rng: &mut ChaCha20Rng, - num_mix_hops: u8, surbs: usize, ) -> RepliableMessageContent { RepliableMessageContent::AdditionalSurbs { - reply_surbs: reply_surbs(rng, num_mix_hops, surbs), + reply_surbs: reply_surbs(rng, surbs), } } pub(super) fn repliable_content_heartbeat( rng: &mut ChaCha20Rng, - num_mix_hops: u8, surbs: usize, ) -> RepliableMessageContent { RepliableMessageContent::Heartbeat { - additional_reply_surbs: reply_surbs(rng, num_mix_hops, surbs), + additional_reply_surbs: reply_surbs(rng, surbs), } } @@ -676,70 +658,54 @@ mod tests { #[test] fn serialized_size_matches_actual_serialization() { let mut rng = fixtures::test_rng(); - let num_mix_hops = 3; let data1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, num_mix_hops, 10000, 0), + content: fixtures::repliable_content_data(&mut rng, 10000, 0), }; - assert_eq!( - data1.serialized_size(num_mix_hops), - data1.into_bytes().len() - ); + assert_eq!(data1.serialized_size(), data1.into_bytes().len()); let data2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, num_mix_hops, 10, 100), + content: fixtures::repliable_content_data(&mut rng, 10, 100), }; - assert_eq!( - data2.serialized_size(num_mix_hops), - data2.into_bytes().len() - ); + assert_eq!(data2.serialized_size(), data2.into_bytes().len()); let data3 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, num_mix_hops, 100000, 1000), + content: fixtures::repliable_content_data(&mut rng, 100000, 1000), }; - assert_eq!( - data3.serialized_size(num_mix_hops), - data3.into_bytes().len() - ); + assert_eq!(data3.serialized_size(), data3.into_bytes().len()); let additional_surbs1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_surbs(&mut rng, num_mix_hops, 1), + content: fixtures::repliable_content_surbs(&mut rng, 1), }; assert_eq!( - additional_surbs1.serialized_size(num_mix_hops), + additional_surbs1.serialized_size(), additional_surbs1.into_bytes().len() ); let additional_surbs2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_surbs(&mut rng, num_mix_hops, 1000), + content: fixtures::repliable_content_surbs(&mut rng, 1000), }; assert_eq!( - additional_surbs2.serialized_size(num_mix_hops), + additional_surbs2.serialized_size(), additional_surbs2.into_bytes().len() ); let heartbeat1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_heartbeat(&mut rng, num_mix_hops, 1), + content: fixtures::repliable_content_heartbeat(&mut rng, 1), }; - assert_eq!( - heartbeat1.serialized_size(num_mix_hops), - heartbeat1.into_bytes().len() - ); + assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); let heartbeat2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_heartbeat(&mut rng, num_mix_hops, 1000), + content: fixtures::repliable_content_heartbeat(&mut rng, 1000), }; - assert_eq!( - heartbeat2.serialized_size(num_mix_hops), - heartbeat2.into_bytes().len() - ); + assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); } } @@ -750,49 +716,33 @@ mod tests { #[test] fn serialized_size_matches_actual_serialization() { let mut rng = fixtures::test_rng(); - let num_mix_hops = 3; - let data1 = fixtures::repliable_content_data(&mut rng, num_mix_hops, 10000, 0); - assert_eq!( - data1.serialized_size(num_mix_hops), - data1.into_bytes().len() - ); + let data1 = fixtures::repliable_content_data(&mut rng, 10000, 0); + assert_eq!(data1.serialized_size(), data1.into_bytes().len()); - let data2 = fixtures::repliable_content_data(&mut rng, num_mix_hops, 10, 100); - assert_eq!( - data2.serialized_size(num_mix_hops), - data2.into_bytes().len() - ); + let data2 = fixtures::repliable_content_data(&mut rng, 10, 100); + assert_eq!(data2.serialized_size(), data2.into_bytes().len()); - let data3 = fixtures::repliable_content_data(&mut rng, num_mix_hops, 100000, 1000); - assert_eq!( - data3.serialized_size(num_mix_hops), - data3.into_bytes().len() - ); + let data3 = fixtures::repliable_content_data(&mut rng, 100000, 1000); + assert_eq!(data3.serialized_size(), data3.into_bytes().len()); - let additional_surbs1 = fixtures::repliable_content_surbs(&mut rng, num_mix_hops, 1); + let additional_surbs1 = fixtures::repliable_content_surbs(&mut rng, 1); assert_eq!( - additional_surbs1.serialized_size(num_mix_hops), + additional_surbs1.serialized_size(), additional_surbs1.into_bytes().len() ); - let additional_surbs2 = fixtures::repliable_content_surbs(&mut rng, num_mix_hops, 1000); + let additional_surbs2 = fixtures::repliable_content_surbs(&mut rng, 1000); assert_eq!( - additional_surbs2.serialized_size(num_mix_hops), + additional_surbs2.serialized_size(), additional_surbs2.into_bytes().len() ); - let heartbeat1 = fixtures::repliable_content_heartbeat(&mut rng, num_mix_hops, 1); - assert_eq!( - heartbeat1.serialized_size(num_mix_hops), - heartbeat1.into_bytes().len() - ); + let heartbeat1 = fixtures::repliable_content_heartbeat(&mut rng, 1); + assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); - let heartbeat2 = fixtures::repliable_content_heartbeat(&mut rng, num_mix_hops, 1000); - assert_eq!( - heartbeat2.serialized_size(num_mix_hops), - heartbeat2.into_bytes().len() - ); + let heartbeat2 = fixtures::repliable_content_heartbeat(&mut rng, 1000); + assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); } } diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index 9d899a426b1..f5d3fd7afbe 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -16,10 +16,6 @@ pub mod packet_sizes; pub mod packet_types; pub mod packet_version; -// If somebody can provide an argument why it might be reasonable to have more than 255 mix hops, -// I will change this to [`usize`] -pub const DEFAULT_NUM_MIX_HOPS: u8 = 3; - // TODO: not entirely sure how to feel about those being defined here, ideally it'd be where [`Fragment`] // is defined, but that'd introduce circular dependencies as the acknowledgements crate also needs // access to that diff --git a/common/nymsphinx/src/message.rs b/common/nymsphinx/src/message.rs index ed1aa252687..8124989339c 100644 --- a/common/nymsphinx/src/message.rs +++ b/common/nymsphinx/src/message.rs @@ -149,7 +149,7 @@ impl NymMessage { .collect() } - fn try_from_bytes(bytes: &[u8], num_mix_hops: u8) -> Result { + fn try_from_bytes(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(NymMessageError::EmptyMessage); } @@ -158,7 +158,7 @@ impl NymMessage { match typ_tag { NymMessageType::Plain => Ok(NymMessage::Plain(bytes[1..].to_vec())), NymMessageType::Repliable => Ok(NymMessage::Repliable( - RepliableMessage::try_from_bytes(&bytes[1..], num_mix_hops)?, + RepliableMessage::try_from_bytes(&bytes[1..])?, )), NymMessageType::Reply => Ok(NymMessage::Reply(ReplyMessage::try_from_bytes( &bytes[1..], @@ -166,10 +166,10 @@ impl NymMessage { } } - fn serialized_size(&self, num_mix_hops: u8) -> usize { + fn serialized_size(&self) -> usize { let inner_size = match self { NymMessage::Plain(msg) => msg.len(), - NymMessage::Repliable(msg) => msg.serialized_size(num_mix_hops), + NymMessage::Repliable(msg) => msg.serialized_size(), NymMessage::Reply(msg) => msg.serialized_size(), }; let message_type_size = 1; @@ -207,9 +207,9 @@ impl NymMessage { } /// Determines the number of required packets of the provided size for the split message. - pub fn required_packets(&self, packet_size: PacketSize, num_mix_hops: u8) -> usize { + pub fn required_packets(&self, packet_size: PacketSize) -> usize { let plaintext_per_packet = self.true_available_plaintext_per_packet(packet_size); - let serialized_len = self.serialized_size(num_mix_hops); + let serialized_len = self.serialized_size(); let (num_fragments, _) = chunking::number_of_required_fragments(serialized_len, plaintext_per_packet); @@ -279,11 +279,11 @@ impl PaddedMessage { } // reverse of NymMessage::pad_to_full_packet_lengths - pub fn remove_padding(self, num_mix_hops: u8) -> Result { + pub fn remove_padding(self) -> Result { // we are looking for first occurrence of 1 in the tail and we get its index if let Some(padding_end) = self.0.iter().rposition(|b| *b == 1) { // and now we only take bytes until that point (but not including it) - NymMessage::try_from_bytes(&self.0[..padding_end], num_mix_hops) + NymMessage::try_from_bytes(&self.0[..padding_end]) } else { Err(NymMessageError::InvalidMessagePadding) } @@ -304,7 +304,7 @@ mod tests { fn serialized_size_matches_actual_serialization() { // plain let plain = NymMessage::new_plain(vec![1, 2, 3, 4, 5]); - assert_eq!(plain.serialized_size(3), plain.into_bytes().len()); + assert_eq!(plain.serialized_size(), plain.into_bytes().len()); // a single variant for each repliable and reply is enough as they are more thoroughly tested // internally @@ -313,9 +313,9 @@ mod tests { [42u8; 16].into(), vec![], )); - assert_eq!(repliable.serialized_size(3), repliable.into_bytes().len()); + assert_eq!(repliable.serialized_size(), repliable.into_bytes().len()); let reply = NymMessage::new_reply(ReplyMessage::new_data_message(vec![1, 2, 3, 4, 5])); - assert_eq!(reply.serialized_size(3), reply.into_bytes().len()); + assert_eq!(reply.serialized_size(), reply.into_bytes().len()); } } diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 3013de41a49..e4e0e3e7bae 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -14,7 +14,7 @@ use nym_sphinx_anonymous_replies::reply_surb::ReplySurb; use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; +use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; use nym_sphinx_types::{Delay, NymPacket}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; @@ -54,7 +54,6 @@ pub trait FragmentPreparer { fn deterministic_route_selection(&self) -> bool; fn rng(&mut self) -> &mut Self::Rng; fn nonce(&self) -> i32; - fn num_mix_hops(&self) -> u8; fn average_packet_delay(&self) -> Duration; fn average_ack_delay(&self) -> Duration; @@ -130,9 +129,8 @@ pub trait FragmentPreparer { .expect("the message has been incorrectly fragmented"); // this is not going to be accurate by any means. but that's the best estimation we can do - let expected_forward_delay = Delay::new_from_millis( - (self.average_packet_delay().as_millis() * self.num_mix_hops() as u128) as u64, - ); + let expected_forward_delay = + Delay::new_from_millis((self.average_packet_delay().as_millis() * 3) as u64); let fragment_identifier = fragment.fragment_identifier(); @@ -333,10 +331,6 @@ pub struct MessagePreparer { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, - /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. - /// Note that it does not include gateway hops. - num_mix_hops: u8, - nonce: i32, } @@ -359,17 +353,10 @@ where sender_address, average_packet_delay, average_ack_delay, - num_mix_hops: DEFAULT_NUM_MIX_HOPS, nonce, } } - /// Allows setting non-default number of expected mix hops in the network. - pub fn with_mix_hops(mut self, hops: u8) -> Self { - self.num_mix_hops = hops; - self - } - /// Overwrites existing sender address with the provided value. pub fn set_sender_address(&mut self, sender_address: Recipient) { self.sender_address = sender_address; @@ -479,10 +466,6 @@ impl FragmentPreparer for MessagePreparer { self.nonce } - fn num_mix_hops(&self) -> u8 { - self.num_mix_hops - } - fn average_packet_delay(&self) -> Duration { self.average_packet_delay } diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 8def09db67b..bc904fd05d6 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -14,7 +14,6 @@ use nym_sphinx_chunking::reconstruction::MessageReconstructor; use nym_sphinx_chunking::ChunkingError; use nym_sphinx_params::{ PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, - DEFAULT_NUM_MIX_HOPS, }; use thiserror::Error; @@ -79,7 +78,6 @@ pub enum MessageRecoveryError { pub trait MessageReceiver { fn new() -> Self; fn reconstructor(&mut self) -> &mut MessageReconstructor; - fn num_mix_hops(&self) -> u8; fn decrypt_raw_message( &self, @@ -143,7 +141,7 @@ pub trait MessageReceiver { fragment: Fragment, ) -> Result)>, MessageRecoveryError> { if let Some((message, used_sets)) = self.reconstructor().insert_new_fragment(fragment) { - match PaddedMessage::new_reconstructed(message).remove_padding(self.num_mix_hops()) { + match PaddedMessage::new_reconstructed(message).remove_padding() { Ok(message) => Ok(Some((message, used_sets))), Err(err) => Err(MessageRecoveryError::MalformedReconstructedMessage { source: err, @@ -161,23 +159,6 @@ pub struct SphinxMessageReceiver { /// High level public structure used to buffer all received data [`Fragment`]s and eventually /// returning original messages that they encapsulate. reconstructor: MessageReconstructor, - - /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. - /// Note that it does not include gateway hops. - num_mix_hops: u8, -} - -impl SphinxMessageReceiver { - /// Allows setting non-default number of expected mix hops in the network. - // IMPORTANT NOTE: this is among others used to deserialize SURBs. Meaning that this is a - // global setting and currently always set to the default value. The implication is that it is - // not currently possible to have different number of hops for different SURB messages. So, - // don't try to use <3 mix hops for SURBs until this is refactored. - #[must_use] - pub fn with_mix_hops(mut self, hops: u8) -> Self { - self.num_mix_hops = hops; - self - } } impl MessageReceiver for SphinxMessageReceiver { @@ -201,17 +182,12 @@ impl MessageReceiver for SphinxMessageReceiver { fn reconstructor(&mut self) -> &mut MessageReconstructor { &mut self.reconstructor } - - fn num_mix_hops(&self) -> u8 { - self.num_mix_hops - } } impl Default for SphinxMessageReceiver { fn default() -> Self { SphinxMessageReceiver { reconstructor: Default::default(), - num_mix_hops: DEFAULT_NUM_MIX_HOPS, } } } diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index c07f8ffe3d2..e96881ed111 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -16,10 +16,10 @@ log = { workspace = true } rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } semver = { workspace = true } +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } # 'serde' feature -serde = { workspace = true, features = ["derive"], optional = true } serde_json = { workspace = true, optional = true } # 'wasm-serde-types' feature @@ -49,5 +49,5 @@ wasm-utils = { path = "../wasm/utils", default-features = false, optional = true default = ["provider-trait"] provider-trait = ["async-trait"] wasm-serde-types = ["tsify", "wasm-bindgen", "wasm-utils"] -serde = ["dep:serde", "serde_json"] +persistence = ["serde_json"] outfox = [] diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 1c52f1be8ce..02e900f47ca 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -68,12 +68,6 @@ pub enum NymTopologyError { #[error("{0}")] ReqwestError(#[from] reqwest::Error), - #[error("{0}")] - MixnodeConversionError(#[from] crate::mix::MixnodeConversionError), - - #[error("{0}")] - GatewayConversionError(#[from] crate::gateway::GatewayConversionError), - #[error("{0}")] VarError(#[from] std::env::VarError), } diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index 545f47dd211..1fd09f0aa7a 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; @@ -68,6 +69,28 @@ pub struct LegacyNode { pub version: NodeVersion, } +impl From for RoutingNode { + fn from(gateway: LegacyNode) -> Self { + RoutingNode { + node_id: gateway.node_id, + mix_host: gateway.mix_host, + entry: Some(EntryDetails { + ip_addresses: vec![gateway.mix_host.ip()], + clients_ws_port: gateway.clients_ws_port, + hostname: gateway.host.as_hostname(), + clients_wss_port: gateway.clients_wss_port, + }), + identity_key: gateway.identity_key, + sphinx_key: gateway.sphinx_key, + supported_roles: SupportedRoles { + mixnode: false, + mixnet_entry: true, + mixnet_exit: false, + }, + } + } +} + impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("gateway::Node") @@ -83,24 +106,6 @@ impl std::fmt::Debug for LegacyNode { } impl LegacyNode { - pub fn parse_host(raw: &str) -> Result { - // safety: this conversion is infallible - // (but we retain result return type for legacy reasons) - Ok(raw.parse().unwrap()) - } - - pub fn extract_mix_host( - host: &NetworkAddress, - mix_port: u16, - ) -> Result { - Ok(host.to_socket_addrs(mix_port).map_err(|err| { - GatewayConversionError::InvalidAddress { - value: host.to_string(), - source: err, - } - })?[0]) - } - pub fn identity(&self) -> &NodeIdentity { &self.identity_key } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 9bafc5fb035..478f34f2cc0 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,78 +1,41 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub use error::NymTopologyError; -use log::{debug, info, warn}; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; -use nym_config::defaults::var_names::NYM_API; -use nym_mixnet_contract_common::{EpochRewardedSet, IdentityKeyRef, NodeId, RewardedSet}; +use crate::node::RoutingNode; +use ::serde::{Deserialize, Serialize}; +use log::{debug, warn}; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_sphinx_addressing::nodes::NodeIdentity; -use nym_sphinx_types::{Node as SphinxNode, Node}; -use rand::prelude::SliceRandom; +use nym_sphinx_types::Node as SphinxNode; +use rand::prelude::IteratorRandom; use rand::{CryptoRng, Rng}; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::convert::Infallible; -use std::fmt::{self, Display, Formatter}; -use std::io; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; -use std::str::FromStr; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::net::IpAddr; -#[cfg(feature = "serde")] -use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; -use nym_mixnet_contract_common::nym_node::Role; +pub use error::NymTopologyError; +pub use nym_mixnet_contract_common::nym_node::Role; +pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId}; +pub use rewarded_set::CachedEpochRewardedSet; pub mod error; -pub mod gateway; -pub mod mix; + +// #[deprecated] +// pub mod gateway; +// +// #[deprecated] +// pub mod mix; pub mod node; +pub mod rewarded_set; #[cfg(feature = "provider-trait")] pub mod provider_trait; +#[cfg(feature = "wasm-serde-types")] +pub(crate) mod wasm_helpers; -#[cfg(feature = "serde")] -pub(crate) mod serde; - -#[cfg(feature = "serde")] -pub use crate::serde::{ - SerializableGateway, SerializableMixNode, SerializableNymTopology, SerializableTopologyError, -}; - -use crate::node::RoutingNode; #[cfg(feature = "provider-trait")] pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider}; -#[deprecated] -#[derive(Debug, Default, Clone)] -pub enum NodeVersion { - Explicit(semver::Version), - - #[default] - Unknown, -} - -// this is only implemented for backwards compatibility so we wouldn't need to change everything at once -// (also I intentionally implemented `ToString` as opposed to `Display`) -#[allow(clippy::to_string_trait_impl)] -impl ToString for NodeVersion { - fn to_string(&self) -> String { - match self { - NodeVersion::Explicit(semver) => semver.to_string(), - NodeVersion::Unknown => String::new(), - } - } -} - -// this is also for backwards compat. -impl<'a> From<&'a str> for NodeVersion { - fn from(value: &'a str) -> Self { - if let Ok(semver) = value.parse() { - NodeVersion::Explicit(semver) - } else { - NodeVersion::Unknown - } - } -} - #[deprecated] #[derive(Debug, Clone)] pub enum NetworkAddress { @@ -80,48 +43,67 @@ pub enum NetworkAddress { Hostname(String), } -impl NetworkAddress { - pub fn to_socket_addrs(&self, port: u16) -> io::Result> { - match self { - NetworkAddress::IpAddr(addr) => Ok(vec![SocketAddr::new(*addr, port)]), - NetworkAddress::Hostname(hostname) => { - Ok((hostname.as_str(), port).to_socket_addrs()?.collect()) +#[allow(deprecated)] +mod deprecated_network_address_impls { + use crate::NetworkAddress; + use std::convert::Infallible; + use std::fmt::{Display, Formatter}; + use std::net::{SocketAddr, ToSocketAddrs}; + use std::str::FromStr; + use std::{fmt, io}; + + impl NetworkAddress { + pub fn as_hostname(self) -> Option { + match self { + NetworkAddress::IpAddr(_) => None, + NetworkAddress::Hostname(s) => Some(s), + } + } + } + + impl NetworkAddress { + pub fn to_socket_addrs(&self, port: u16) -> io::Result> { + match self { + NetworkAddress::IpAddr(addr) => Ok(vec![SocketAddr::new(*addr, port)]), + NetworkAddress::Hostname(hostname) => { + Ok((hostname.as_str(), port).to_socket_addrs()?.collect()) + } } } } -} -impl FromStr for NetworkAddress { - type Err = Infallible; + impl FromStr for NetworkAddress { + type Err = Infallible; - fn from_str(s: &str) -> Result { - if let Ok(ip_addr) = s.parse() { - Ok(NetworkAddress::IpAddr(ip_addr)) - } else { - Ok(NetworkAddress::Hostname(s.to_string())) + fn from_str(s: &str) -> Result { + if let Ok(ip_addr) = s.parse() { + Ok(NetworkAddress::IpAddr(ip_addr)) + } else { + Ok(NetworkAddress::Hostname(s.to_string())) + } } } -} -impl Display for NetworkAddress { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - NetworkAddress::IpAddr(ip_addr) => ip_addr.fmt(f), - NetworkAddress::Hostname(hostname) => hostname.fmt(f), + impl Display for NetworkAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + NetworkAddress::IpAddr(ip_addr) => ip_addr.fmt(f), + NetworkAddress::Hostname(hostname) => hostname.fmt(f), + } } } } pub type MixLayer = u8; -#[derive(Clone, Debug, Default)] +#[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: EpochRewardedSet, + rewarded_set: CachedEpochRewardedSet, node_details: HashMap, } @@ -144,6 +126,13 @@ impl From for NymRouteProvider { } impl NymRouteProvider { + pub fn new(topology: NymTopology, ignore_egress_epoch_roles: bool) -> Self { + NymRouteProvider { + topology, + ignore_egress_epoch_roles, + } + } + pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider { let this: Self = NymTopology::default().into(); this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles) @@ -190,21 +179,24 @@ impl NymRouteProvider { } impl NymTopology { - pub fn new_empty(rewarded_set: EpochRewardedSet) -> Self { + pub fn new_empty(rewarded_set: impl Into) -> Self { NymTopology { - rewarded_set, + rewarded_set: rewarded_set.into(), node_details: Default::default(), } } - pub fn new(rewarded_set: EpochRewardedSet, node_details: Vec) -> Self { + pub fn new( + rewarded_set: impl Into, + node_details: Vec, + ) -> Self { NymTopology { - rewarded_set, + rewarded_set: rewarded_set.into(), node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), } } - #[cfg(feature = "serde")] + #[cfg(feature = "persistence")] pub fn new_from_file>(path: P) -> std::io::Result { let file = std::fs::File::open(path)?; serde_json::from_reader(file).map_err(Into::into) @@ -230,7 +222,7 @@ impl NymTopology { } } - fn node_exists(&self, ids: &[NodeId]) -> bool { + fn node_details_exists(&self, ids: &HashSet) -> bool { for id in ids { if self.node_details.contains_key(id) { return true; @@ -240,9 +232,11 @@ impl NymTopology { } pub fn is_minimally_routable(&self) -> bool { - self.node_exists(&self.rewarded_set.assignment.layer1) - && self.node_exists(&self.rewarded_set.assignment.layer2) - && self.node_exists(&self.rewarded_set.assignment.layer3) + self.node_details_exists(&self.rewarded_set.layer1) + && self.node_details_exists(&self.rewarded_set.layer2) + && self.node_details_exists(&self.rewarded_set.layer3) + && (!self.rewarded_set.exit_gateways.is_empty() + || !self.rewarded_set.entry_gateways.is_empty()) // TODO: we should also include gateways in that check, but right now we're allowing ALL gateways, even inactive } @@ -255,7 +249,7 @@ impl NymTopology { } pub fn is_empty(&self) -> bool { - self.rewarded_set.assignment.is_empty() || self.node_details.is_empty() + self.rewarded_set.is_empty() || self.node_details.is_empty() } pub fn ensure_not_empty(&self) -> Result<(), NymTopologyError> { @@ -301,13 +295,15 @@ impl NymTopology { fn choose_mixing_node( &self, rng: &mut R, - assigned_nodes: &[NodeId], + assigned_nodes: &HashSet, ) -> Result where R: Rng + CryptoRng + ?Sized, { // try first choice without cloning the ids (because I reckon, more often than not, it will actually work) - let Some(candidate) = assigned_nodes.choose(rng) else { + // HashSet's iterator implements `ExactSizeIterator` so choosing **one** random element + // is actually not that expensive + let Some(candidate) = assigned_nodes.iter().choose(rng) else { return Err(NymTopologyError::NoMixnodesAvailable); }; @@ -324,12 +320,16 @@ impl NymTopology { } } - fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> { + pub fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> { self.node_details .values() .find(|n| n.identity_key == node_identity) } + pub fn find_node(&self, node_id: NodeId) -> Option<&RoutingNode> { + self.node_details.get(&node_id) + } + pub fn egress_by_identity( &self, node_identity: NodeIdentity, @@ -342,7 +342,7 @@ impl NymTopology { // a 'valid' egress is one assigned to either entry role (i.e. entry for another client) // or exit role (as a service provider) if !ignore_epoch_roles { - let Some(role) = self.rewarded_set.assignment.get_role(node.node_id) else { + let Some(role) = self.rewarded_set.role(node.node_id) else { return Err(NymTopologyError::InvalidEgressRole { node_identity }); }; if !matches!(role, Role::EntryGateway | Role::ExitGateway) { @@ -365,16 +365,16 @@ impl NymTopology { where R: Rng + CryptoRng + ?Sized, { - if self.rewarded_set.assignment.is_empty() || self.node_details.is_empty() { + if self.rewarded_set.is_empty() || self.node_details.is_empty() { return Err(NymTopologyError::EmptyNetworkTopology); } // we reserve an additional item in the route because we'll have to push an egress let mut mix_route = Vec::with_capacity(4); - mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer1)?); - mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer2)?); - mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.assignment.layer3)?); + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer1)?); + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer2)?); + mix_route.push(self.choose_mixing_node(rng, &self.rewarded_set.layer3)?); Ok(mix_route) } @@ -395,409 +395,440 @@ impl NymTopology { mix_route.push(egress); Ok(mix_route) } -} - -#[cfg(feature = "serde")] -impl Serialize for NymTopology { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - todo!() - // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for NymTopology { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - todo!() - // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; - // serializable.try_into().map_err(::serde::de::Error::custom) - } -} - -// the reason for those having `Legacy` prefix is that eventually they should be using -// exactly the same types -#[derive(Debug, Clone, Default)] -pub struct NymTopologyOld { - mixes: BTreeMap>, - gateways: Vec, -} - -impl NymTopologyOld { - #[deprecated] - pub async fn new_from_env() -> Result { - let api_url = std::env::var(NYM_API)?; - - info!("Generating topology from {api_url}"); - - let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) - .await? - .json::>() - .await? - .nodes - .iter() - .map(mix::LegacyNode::try_from) - .filter(Result::is_ok) - .collect::, _>>()?; - - let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) - .await? - .json::>() - .await? - .nodes - .iter() - .map(gateway::LegacyNode::try_from) - .filter(Result::is_ok) - .collect::, _>>()?; - let topology = Self::new_unordered(mixnodes, gateways); - Ok(topology) - } - - pub fn new( - mixes: BTreeMap>, - gateways: Vec, - ) -> Self { - NymTopologyOld { mixes, gateways } - } - - #[deprecated] - pub fn new_unordered( - unordered_mixes: Vec, - gateways: Vec, - ) -> Self { - let mut mixes = BTreeMap::new(); - for node in unordered_mixes.into_iter() { - let layer = node.layer as MixLayer; - let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - layer_entry.push(node) - } - - NymTopologyOld { mixes, gateways } - } - - pub fn from_unordered(unordered_mixes: MI, unordered_gateways: GI) -> Self - where - MI: Iterator, - GI: Iterator, - G: TryInto, - M: TryInto, - >::Error: Display, - >::Error: Display, - { - let mut mixes = BTreeMap::new(); - let mut gateways = Vec::new(); - - for node in unordered_mixes.into_iter() { - match node.try_into() { - Ok(mixnode) => mixes - .entry(mixnode.layer as MixLayer) - .or_insert_with(Vec::new) - .push(mixnode), - Err(err) => debug!("malformed mixnode: {err}"), - } - } - - for node in unordered_gateways.into_iter() { - match node.try_into() { - Ok(gateway) => gateways.push(gateway), - Err(err) => debug!("malformed gateway: {err}"), - } - } - - NymTopologyOld::new(mixes, gateways) - } - - #[cfg(feature = "serde")] - pub fn new_from_file>(path: P) -> std::io::Result { - todo!() - // let file = std::fs::File::open(path)?; - // serde_json::from_reader(file).map_err(Into::into) - } - - pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { - todo!() - // nym_topology_from_basic_info(basic_mixes, basic_gateways) - } - pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { - for nodes in self.mixes.values() { - for node in nodes { - if node.mix_id == mix_id { - return Some(node); - } + pub fn nodes_with_role<'a>(&'a self, role: Role) -> impl Iterator + 'a { + self.node_details.values().filter(move |node| match role { + Role::EntryGateway => self.rewarded_set.entry_gateways.contains(&node.node_id), + Role::Layer1 => self.rewarded_set.layer1.contains(&node.node_id), + Role::Layer2 => self.rewarded_set.layer2.contains(&node.node_id), + Role::Layer3 => self.rewarded_set.layer3.contains(&node.node_id), + Role::ExitGateway => self.rewarded_set.exit_gateways.contains(&node.node_id), + Role::Standby => self.rewarded_set.standby.contains(&node.node_id), + }) + } + + pub fn set_testable_node(&mut self, role: Role, node: impl Into) { + fn init_set(node: NodeId) -> HashSet { + let mut set = HashSet::new(); + set.insert(node); + set + } + + let node = node.into(); + let node_id = node.node_id; + self.node_details.insert(node.node_id, node); + + match role { + Role::EntryGateway => self.rewarded_set.entry_gateways = init_set(node_id), + Role::Layer1 => self.rewarded_set.layer1 = init_set(node_id), + Role::Layer2 => self.rewarded_set.layer2 = init_set(node_id), + Role::Layer3 => self.rewarded_set.layer3 = init_set(node_id), + Role::ExitGateway => self.rewarded_set.exit_gateways = init_set(node_id), + Role::Standby => { + warn!("attempting to test node in 'standby' mode - are you sure that's what you meant to do?"); + self.rewarded_set.standby = init_set(node_id) } } - None - } - - pub fn find_mix_by_identity( - &self, - mixnode_identity: IdentityKeyRef, - ) -> Option<&mix::LegacyNode> { - for nodes in self.mixes.values() { - for node in nodes { - if node.identity_key.to_base58_string() == mixnode_identity { - return Some(node); - } - } - } - None - } - - pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { - self.gateways - .iter() - .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) - } - - pub fn mixes(&self) -> &BTreeMap> { - &self.mixes - } - - pub fn num_mixnodes(&self) -> usize { - self.mixes.values().map(|m| m.len()).sum() } - pub fn mixes_as_vec(&self) -> Vec { - let mut mixes: Vec = vec![]; - - for layer in self.mixes().values() { - mixes.extend(layer.to_owned()) - } - mixes - } - - pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { - assert!([1, 2, 3].contains(&layer)); - self.mixes.get(&layer).unwrap().to_owned() - } - - pub fn gateways(&self) -> &[gateway::LegacyNode] { - &self.gateways - } - - pub fn get_gateways(&self) -> Vec { - self.gateways.clone() - } - - pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { - self.gateways - .iter() - .find(|gateway| gateway.identity() == gateway_identity) - } - - pub fn gateway_exists(&self, gateway_identity: &NodeIdentity) -> bool { - 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 - } - - pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> - where - R: Rng + CryptoRng, - { - self.gateways - .choose(rng) - .ok_or(NymTopologyError::NoGatewaysAvailable) - } - - /// Returns a vec of size of `num_mix_hops` of mixnodes, such that each subsequent node is on - /// next layer, starting from layer 1 - pub fn random_mix_route( - &self, - rng: &mut R, - num_mix_hops: u8, - ) -> Result, NymTopologyError> - where - R: Rng + CryptoRng + ?Sized, - { - if self.mixes.len() < num_mix_hops as usize { - return Err(NymTopologyError::InvalidNumberOfHopsError { - available: self.mixes.len(), - requested: num_mix_hops as usize, - }); - } - let mut route = Vec::with_capacity(num_mix_hops as usize); - - // there is no "layer 0" - for layer in 1..=num_mix_hops { - // get all mixes on particular layer - let layer_mixes = self - .mixes - .get(&layer) - .ok_or(NymTopologyError::EmptyMixLayer { layer })?; - - // choose a random mix from the above list - // this can return a 'None' only if slice is empty - let random_mix = layer_mixes - .choose(rng) - .ok_or(NymTopologyError::EmptyMixLayer { layer })?; - route.push(random_mix.clone()); - } - - Ok(route) - } - - pub fn random_path_to_egress( - &self, - rng: &mut R, - num_mix_hops: u8, - egress_identity: &NodeIdentity, - ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> - where - R: Rng + CryptoRng + ?Sized, - { - let gateway = - self.get_gateway(egress_identity) - .ok_or(NymTopologyError::NonExistentGatewayError { - identity_key: egress_identity.to_base58_string(), - })?; - - let path = self.random_mix_route(rng, num_mix_hops)?; - - Ok((path, gateway.clone())) - } - - /// Tries to create a route to the specified gateway, such that it goes through mixnode on layer 1, - /// mixnode on layer2, .... mixnode on layer n and finally the target gateway - pub fn random_route_to_egress( - &self, - rng: &mut R, - num_mix_hops: u8, - egress_identity: &NodeIdentity, - ) -> Result, NymTopologyError> - where - R: Rng + CryptoRng + ?Sized, - { - let gateway = - self.get_gateway(egress_identity) - .ok_or(NymTopologyError::NonExistentGatewayError { - identity_key: egress_identity.to_base58_string(), - })?; - - Ok(self - .random_mix_route(rng, num_mix_hops)? - .into_iter() - .map(|node| SphinxNode::from(&node)) - .chain(std::iter::once(gateway.into())) - .collect()) - } - - /// Overwrites the existing nodes in the specified layer - pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { - self.mixes.insert(layer, mixes); - } - - /// Checks if a mixnet path can be constructed using the specified number of hops - pub fn ensure_can_construct_path_through( - &self, - num_mix_hops: u8, - ) -> Result<(), NymTopologyError> { - let mixnodes = self.mixes(); - // 1. is it completely empty? - if mixnodes.is_empty() && self.gateways().is_empty() { - return Err(NymTopologyError::EmptyNetworkTopology); - } - - // 2. does it have any mixnode at all? - if mixnodes.is_empty() { - return Err(NymTopologyError::NoMixnodesAvailable); - } - - // 3. does it have any gateways at all? - if self.gateways().is_empty() { - return Err(NymTopologyError::NoGatewaysAvailable); - } - - // 4. does it have a mixnode on each layer? - for layer in 1..=num_mix_hops { - match mixnodes.get(&layer) { - None => return Err(NymTopologyError::EmptyMixLayer { layer }), - Some(layer_nodes) => { - if layer_nodes.is_empty() { - return Err(NymTopologyError::EmptyMixLayer { layer }); - } - } - } - } - - Ok(()) + pub fn entry_gateways(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| self.rewarded_set.entry_gateways.contains(&n.node_id)) } - pub fn ensure_even_layer_distribution( - &self, - lower_threshold: f32, - upper_threshold: f32, - ) -> Result<(), NymTopologyError> { - let mixnodes_count = self.num_mixnodes(); - - let layers = self - .mixes - .iter() - .map(|(k, v)| (*k, v.len())) - .collect::>(); - - if self.gateways.is_empty() { - return Err(NymTopologyError::NoGatewaysAvailable); - } - - if layers.is_empty() { - return Err(NymTopologyError::NoMixnodesAvailable); - } - - let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize; - let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize; - - for (layer, nodes) in &layers { - if nodes < &lower_bound || nodes > &upper_bound { - return Err(NymTopologyError::UnevenLayerDistribution { - layer: *layer, - nodes: *nodes, - lower_bound, - upper_bound, - total_nodes: mixnodes_count, - layer_distribution: layers, - }); - } - } - - Ok(()) + // ideally this shouldn't exist... + pub fn entry_capable_nodes(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| n.supported_roles.mixnet_entry) } -} -#[cfg(feature = "serde")] -impl Serialize for NymTopologyOld { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - todo!() - // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) + pub fn mixnodes(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id)) } } -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for NymTopologyOld { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - todo!() - // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; - // serializable.try_into().map_err(::serde::de::Error::custom) - } -} +// // the reason for those having `Legacy` prefix is that eventually they should be using +// // exactly the same types +// #[derive(Debug, Clone, Default)] +// pub struct NymTopologyOld { +// mixes: BTreeMap>, +// gateways: Vec, +// } +// +// impl NymTopologyOld { +// #[deprecated] +// pub async fn new_from_env() -> Result { +// let api_url = std::env::var(NYM_API)?; +// +// info!("Generating topology from {api_url}"); +// +// let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) +// .await? +// .json::>() +// .await? +// .nodes +// .iter() +// .map(mix::LegacyNode::try_from) +// .filter(Result::is_ok) +// .collect::, _>>()?; +// +// let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) +// .await? +// .json::>() +// .await? +// .nodes +// .iter() +// .map(gateway::LegacyNode::try_from) +// .filter(Result::is_ok) +// .collect::, _>>()?; +// let topology = Self::new_unordered(mixnodes, gateways); +// Ok(topology) +// } +// +// pub fn new( +// mixes: BTreeMap>, +// gateways: Vec, +// ) -> Self { +// NymTopologyOld { mixes, gateways } +// } +// +// #[deprecated] +// pub fn new_unordered( +// unordered_mixes: Vec, +// gateways: Vec, +// ) -> Self { +// let mut mixes = BTreeMap::new(); +// for node in unordered_mixes.into_iter() { +// let layer = node.layer as MixLayer; +// let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); +// layer_entry.push(node) +// } +// +// NymTopologyOld { mixes, gateways } +// } +// +// pub fn from_unordered(unordered_mixes: MI, unordered_gateways: GI) -> Self +// where +// MI: Iterator, +// GI: Iterator, +// G: TryInto, +// M: TryInto, +// >::Error: Display, +// >::Error: Display, +// { +// let mut mixes = BTreeMap::new(); +// let mut gateways = Vec::new(); +// +// for node in unordered_mixes.into_iter() { +// match node.try_into() { +// Ok(mixnode) => mixes +// .entry(mixnode.layer as MixLayer) +// .or_insert_with(Vec::new) +// .push(mixnode), +// Err(err) => debug!("malformed mixnode: {err}"), +// } +// } +// +// for node in unordered_gateways.into_iter() { +// match node.try_into() { +// Ok(gateway) => gateways.push(gateway), +// Err(err) => debug!("malformed gateway: {err}"), +// } +// } +// +// NymTopologyOld::new(mixes, gateways) +// } +// +// #[cfg(feature = "serde")] +// pub fn new_from_file>(path: P) -> std::io::Result { +// todo!() +// // let file = std::fs::File::open(path)?; +// // serde_json::from_reader(file).map_err(Into::into) +// } +// +// pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { +// todo!() +// // nym_topology_from_basic_info(basic_mixes, basic_gateways) +// } +// +// pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { +// for nodes in self.mixes.values() { +// for node in nodes { +// if node.mix_id == mix_id { +// return Some(node); +// } +// } +// } +// None +// } +// +// pub fn find_mix_by_identity( +// &self, +// mixnode_identity: IdentityKeyRef, +// ) -> Option<&mix::LegacyNode> { +// for nodes in self.mixes.values() { +// for node in nodes { +// if node.identity_key.to_base58_string() == mixnode_identity { +// return Some(node); +// } +// } +// } +// None +// } +// +// pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { +// self.gateways +// .iter() +// .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) +// } +// +// pub fn mixes(&self) -> &BTreeMap> { +// &self.mixes +// } +// +// pub fn num_mixnodes(&self) -> usize { +// self.mixes.values().map(|m| m.len()).sum() +// } +// +// pub fn mixes_as_vec(&self) -> Vec { +// let mut mixes: Vec = vec![]; +// +// for layer in self.mixes().values() { +// mixes.extend(layer.to_owned()) +// } +// mixes +// } +// +// pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { +// assert!([1, 2, 3].contains(&layer)); +// self.mixes.get(&layer).unwrap().to_owned() +// } +// +// pub fn gateways(&self) -> &[gateway::LegacyNode] { +// &self.gateways +// } +// +// pub fn get_gateways(&self) -> Vec { +// self.gateways.clone() +// } +// +// pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { +// self.gateways +// .iter() +// .find(|gateway| gateway.identity() == gateway_identity) +// } +// +// pub fn gateway_exists(&self, gateway_identity: &NodeIdentity) -> bool { +// 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 +// } +// +// pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> +// where +// R: Rng + CryptoRng, +// { +// self.gateways +// .choose(rng) +// .ok_or(NymTopologyError::NoGatewaysAvailable) +// } +// +// /// Returns a vec of size of `num_mix_hops` of mixnodes, such that each subsequent node is on +// /// next layer, starting from layer 1 +// pub fn random_mix_route( +// &self, +// rng: &mut R, +// num_mix_hops: u8, +// ) -> Result, NymTopologyError> +// where +// R: Rng + CryptoRng + ?Sized, +// { +// if self.mixes.len() < num_mix_hops as usize { +// return Err(NymTopologyError::InvalidNumberOfHopsError { +// available: self.mixes.len(), +// requested: num_mix_hops as usize, +// }); +// } +// let mut route = Vec::with_capacity(num_mix_hops as usize); +// +// // there is no "layer 0" +// for layer in 1..=num_mix_hops { +// // get all mixes on particular layer +// let layer_mixes = self +// .mixes +// .get(&layer) +// .ok_or(NymTopologyError::EmptyMixLayer { layer })?; +// +// // choose a random mix from the above list +// // this can return a 'None' only if slice is empty +// let random_mix = layer_mixes +// .choose(rng) +// .ok_or(NymTopologyError::EmptyMixLayer { layer })?; +// route.push(random_mix.clone()); +// } +// +// Ok(route) +// } +// +// pub fn random_path_to_egress( +// &self, +// rng: &mut R, +// num_mix_hops: u8, +// egress_identity: &NodeIdentity, +// ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> +// where +// R: Rng + CryptoRng + ?Sized, +// { +// let gateway = +// self.get_gateway(egress_identity) +// .ok_or(NymTopologyError::NonExistentGatewayError { +// identity_key: egress_identity.to_base58_string(), +// })?; +// +// let path = self.random_mix_route(rng, num_mix_hops)?; +// +// Ok((path, gateway.clone())) +// } +// +// /// Tries to create a route to the specified gateway, such that it goes through mixnode on layer 1, +// /// mixnode on layer2, .... mixnode on layer n and finally the target gateway +// pub fn random_route_to_egress( +// &self, +// rng: &mut R, +// num_mix_hops: u8, +// egress_identity: &NodeIdentity, +// ) -> Result, NymTopologyError> +// where +// R: Rng + CryptoRng + ?Sized, +// { +// let gateway = +// self.get_gateway(egress_identity) +// .ok_or(NymTopologyError::NonExistentGatewayError { +// identity_key: egress_identity.to_base58_string(), +// })?; +// +// Ok(self +// .random_mix_route(rng, num_mix_hops)? +// .into_iter() +// .map(|node| SphinxNode::from(&node)) +// .chain(std::iter::once(gateway.into())) +// .collect()) +// } +// +// /// Overwrites the existing nodes in the specified layer +// pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { +// self.mixes.insert(layer, mixes); +// } +// +// /// Checks if a mixnet path can be constructed using the specified number of hops +// pub fn ensure_can_construct_path_through( +// &self, +// num_mix_hops: u8, +// ) -> Result<(), NymTopologyError> { +// let mixnodes = self.mixes(); +// // 1. is it completely empty? +// if mixnodes.is_empty() && self.gateways().is_empty() { +// return Err(NymTopologyError::EmptyNetworkTopology); +// } +// +// // 2. does it have any mixnode at all? +// if mixnodes.is_empty() { +// return Err(NymTopologyError::NoMixnodesAvailable); +// } +// +// // 3. does it have any gateways at all? +// if self.gateways().is_empty() { +// return Err(NymTopologyError::NoGatewaysAvailable); +// } +// +// // 4. does it have a mixnode on each layer? +// for layer in 1..=num_mix_hops { +// match mixnodes.get(&layer) { +// None => return Err(NymTopologyError::EmptyMixLayer { layer }), +// Some(layer_nodes) => { +// if layer_nodes.is_empty() { +// return Err(NymTopologyError::EmptyMixLayer { layer }); +// } +// } +// } +// } +// +// Ok(()) +// } +// +// pub fn ensure_even_layer_distribution( +// &self, +// lower_threshold: f32, +// upper_threshold: f32, +// ) -> Result<(), NymTopologyError> { +// let mixnodes_count = self.num_mixnodes(); +// +// let layers = self +// .mixes +// .iter() +// .map(|(k, v)| (*k, v.len())) +// .collect::>(); +// +// if self.gateways.is_empty() { +// return Err(NymTopologyError::NoGatewaysAvailable); +// } +// +// if layers.is_empty() { +// return Err(NymTopologyError::NoMixnodesAvailable); +// } +// +// let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize; +// let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize; +// +// for (layer, nodes) in &layers { +// if nodes < &lower_bound || nodes > &upper_bound { +// return Err(NymTopologyError::UnevenLayerDistribution { +// layer: *layer, +// nodes: *nodes, +// lower_bound, +// upper_bound, +// total_nodes: mixnodes_count, +// layer_distribution: layers, +// }); +// } +// } +// +// Ok(()) +// } +// } +// +// #[cfg(feature = "serde")] +// impl Serialize for NymTopologyOld { +// fn serialize(&self, serializer: S) -> Result +// where +// S: Serializer, +// { +// todo!() +// // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) +// } +// } +// +// #[cfg(feature = "serde")] +// impl<'de> Deserialize<'de> for NymTopologyOld { +// fn deserialize(deserializer: D) -> Result +// where +// D: Deserializer<'de>, +// { +// todo!() +// // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; +// // serializable.try_into().map_err(::serde::de::Error::custom) +// } +// } pub fn nym_topology_from_basic_info( basic_mixes: &[SkimmedNode], diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 40c61cff4b4..1171e839b65 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -1,10 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::node::{RoutingNode, SupportedRoles}; use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; -pub use nym_mixnet_contract_common::LegacyMixLayer; use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; @@ -55,6 +55,23 @@ pub struct LegacyNode { pub version: NodeVersion, } +impl From for RoutingNode { + fn from(node: LegacyNode) -> Self { + RoutingNode { + node_id: node.mix_id, + mix_host: node.mix_host, + entry: None, + identity_key: node.identity_key, + sphinx_key: node.sphinx_key, + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: false, + mixnet_exit: false, + }, + } + } +} + impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("mix::Node") @@ -69,26 +86,6 @@ impl std::fmt::Debug for LegacyNode { } } -impl LegacyNode { - pub fn parse_host(raw: &str) -> Result { - // safety: this conversion is infallible - // (but we retain result return type for legacy reasons) - Ok(raw.parse().unwrap()) - } - - pub fn extract_mix_host( - host: &NetworkAddress, - mix_port: u16, - ) -> Result { - Ok(host.to_socket_addrs(mix_port).map_err(|err| { - MixnodeConversionError::InvalidAddress { - value: host.to_string(), - source: err, - } - })?[0]) - } -} - impl<'a> From<&'a LegacyNode> for SphinxNode { fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index bf281d3bfb4..df749479a17 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use nym_api_requests::models::DeclaredRoles; -use nym_api_requests::nym_nodes::{BasicEntryInformation, SkimmedNode}; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_mixnet_contract_common::{NaiveFloat, NodeId}; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; +use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; use thiserror::Error; +pub use nym_mixnet_contract_common::LegacyMixLayer; + #[derive(Error, Debug)] pub enum RoutingNodeError { #[error("this node has no mixing information available")] @@ -22,7 +25,7 @@ pub enum RoutingNodeError { }, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct EntryDetails { // to allow client to choose ipv6 preference, if available pub ip_addresses: Vec, @@ -31,7 +34,7 @@ pub struct EntryDetails { pub clients_wss_port: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct SupportedRoles { pub mixnode: bool, pub mixnet_entry: bool, @@ -48,7 +51,7 @@ impl From for SupportedRoles { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RoutingNode { pub node_id: NodeId, @@ -59,16 +62,32 @@ pub struct RoutingNode { pub sphinx_key: x25519::PublicKey, pub supported_roles: SupportedRoles, - pub performance: f64, } impl RoutingNode { pub fn ws_entry_address_tls(&self) -> Option { - todo!() + let entry = self.entry.as_ref()?; + let hostname = entry.hostname.as_ref()?; + let wss_port = entry.clients_wss_port?; + + Some(format!("wss://{hostname}:{wss_port}")) } pub fn ws_entry_address_no_tls(&self, prefer_ipv6: bool) -> Option { - todo!() + let entry = self.entry.as_ref()?; + + if let Some(hostname) = entry.hostname.as_ref() { + return Some(format!("ws://{hostname}:{}", entry.clients_ws_port)); + } + + if prefer_ipv6 { + if let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { + return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port)); + } + } + + let any_ip = entry.ip_addresses.first()?; + Some(format!("ws://{any_ip}:{}", entry.clients_ws_port)) } pub fn ws_entry_address(&self, prefer_ipv6: bool) -> Option { @@ -100,6 +119,10 @@ impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { type Error = RoutingNodeError; fn try_from(value: &'a SkimmedNode) -> Result { + // IF YOU EVER ADD "performance" TO RoutingNode, + // MAKE SURE TO UPDATE THE LAZY IMPLEMENTATION OF + // `impl NodeDescriptionTopologyExt for NymNodeDescription`!!! + let Some(first_ip) = value.ip_addresses.first() else { return Err(RoutingNodeError::NoIpAddressesProvided { node_id: value.node_id, @@ -124,7 +147,6 @@ 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.naive_to_f64(), }) } } diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/provider_trait.rs index eb4d09b52c8..ad8381fa7db 100644 --- a/common/topology/src/provider_trait.rs +++ b/common/topology/src/provider_trait.rs @@ -22,7 +22,7 @@ pub struct HardcodedTopologyProvider { } impl HardcodedTopologyProvider { - #[cfg(feature = "serde")] + #[cfg(feature = "persistence")] pub fn new_from_file>(path: P) -> std::io::Result { NymTopology::new_from_file(path).map(Self::new) } diff --git a/common/topology/src/rewarded_set.rs b/common/topology/src/rewarded_set.rs new file mode 100644 index 00000000000..0d06239be6f --- /dev/null +++ b/common/topology/src/rewarded_set.rs @@ -0,0 +1,122 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, NodeId, RewardedSet}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct CachedEpochRewardedSet { + pub epoch_id: EpochId, + + pub entry_gateways: HashSet, + + pub exit_gateways: HashSet, + + pub layer1: HashSet, + + pub layer2: HashSet, + + pub layer3: HashSet, + + pub standby: HashSet, +} + +impl From for CachedEpochRewardedSet { + fn from(value: EpochRewardedSet) -> Self { + CachedEpochRewardedSet { + epoch_id: value.epoch_id, + entry_gateways: value.assignment.entry_gateways.into_iter().collect(), + exit_gateways: value.assignment.exit_gateways.into_iter().collect(), + layer1: value.assignment.layer1.into_iter().collect(), + layer2: value.assignment.layer2.into_iter().collect(), + layer3: value.assignment.layer3.into_iter().collect(), + standby: value.assignment.standby.into_iter().collect(), + } + } +} + +impl From for EpochRewardedSet { + fn from(value: CachedEpochRewardedSet) -> Self { + EpochRewardedSet { + epoch_id: value.epoch_id, + assignment: RewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + }, + } + } +} + +impl CachedEpochRewardedSet { + pub fn is_empty(&self) -> bool { + self.entry_gateways.is_empty() + && self.exit_gateways.is_empty() + && self.layer1.is_empty() + && self.layer2.is_empty() + && self.layer3.is_empty() + && self.standby.is_empty() + } + + pub fn role(&self, node_id: NodeId) -> Option { + if self.entry_gateways.contains(&node_id) { + Some(Role::EntryGateway) + } else if self.exit_gateways.contains(&node_id) { + Some(Role::ExitGateway) + } else if self.layer1.contains(&node_id) { + Some(Role::Layer1) + } else if self.layer2.contains(&node_id) { + Some(Role::Layer2) + } else if self.layer3.contains(&node_id) { + Some(Role::Layer3) + } else if self.standby.contains(&node_id) { + Some(Role::Standby) + } else { + None + } + } + + pub fn legacy_mix_layer(&self, node_id: &NodeId) -> Option { + if self.layer1.contains(node_id) { + Some(1) + } else if self.layer2.contains(node_id) { + Some(2) + } else if self.layer3.contains(node_id) { + Some(3) + } else { + None + } + } + + pub fn is_standby(&self, node_id: &NodeId) -> bool { + self.standby.contains(node_id) + } + + pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { + self.layer1.contains(node_id) + || self.layer2.contains(node_id) + || self.layer3.contains(node_id) + } + + pub fn gateways(&self) -> HashSet { + let mut gateways = + HashSet::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); + gateways.extend(&self.entry_gateways); + gateways.extend(&self.exit_gateways); + gateways + } + + pub fn active_mixnodes(&self) -> HashSet { + let mut mixnodes = + HashSet::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); + mixnodes.extend(&self.layer1); + mixnodes.extend(&self.layer2); + mixnodes.extend(&self.layer3); + mixnodes + } +} diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs deleted file mode 100644 index 61be24399f1..00000000000 --- a/common/topology/src/serde.rs +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#![allow(unknown_lints)] -// clippy::empty_docs is not on stable as of 1.77 - -// due to the code generated by Tsify -#![allow(clippy::empty_docs)] - -use crate::gateway::GatewayConversionError; -use crate::mix::MixnodeConversionError; -use crate::{gateway, mix, MixLayer, NymTopology}; -use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; -use nym_crypto::asymmetric::{encryption, identity}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::net::{IpAddr, SocketAddr}; -use thiserror::Error; - -#[cfg(feature = "wasm-serde-types")] -use tsify::Tsify; - -use nym_mixnet_contract_common::NodeId; -#[cfg(feature = "wasm-serde-types")] -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - -#[cfg(feature = "wasm-serde-types")] -use wasm_utils::error::simple_js_error; - -#[derive(Debug, Error)] -pub enum SerializableTopologyError { - #[error("got invalid mix layer {value}. Expected 1, 2 or 3.")] - InvalidMixLayer { value: u8 }, - - #[error(transparent)] - GatewayConversion(#[from] GatewayConversionError), - - #[error(transparent)] - MixnodeConversion(#[from] MixnodeConversionError), - - #[error("The provided mixnode map was malformed: {msg}")] - MalformedMixnodeMap { msg: String }, - - #[error("The provided gateway list was malformed: {msg}")] - MalformedGatewayList { msg: String }, -} - -#[cfg(feature = "wasm-serde-types")] -impl From for JsValue { - fn from(value: SerializableTopologyError) -> Self { - simple_js_error(value.to_string()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))] -#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct SerializableNymTopology { - pub mixnodes: BTreeMap>, - pub gateways: Vec, -} - -impl TryFrom for NymTopology { - type Error = SerializableTopologyError; - - fn try_from(value: SerializableNymTopology) -> Result { - todo!() - // let mut converted_mixes = BTreeMap::new(); - // - // for (layer, nodes) in value.mixnodes { - // let layer_nodes = nodes - // .into_iter() - // .map(TryInto::try_into) - // .collect::>()?; - // - // converted_mixes.insert(layer, layer_nodes); - // } - // - // let gateways = value - // .gateways - // .into_iter() - // .map(TryInto::try_into) - // .collect::>()?; - // - // Ok(NymTopology::new(converted_mixes, gateways)) - } -} - -impl From for SerializableNymTopology { - fn from(value: NymTopology) -> Self { - todo!() - // SerializableNymTopology { - // mixnodes: value - // .mixes() - // .iter() - // .map(|(&l, nodes)| (l, nodes.iter().map(Into::into).collect())) - // .collect(), - // gateways: value.gateways().iter().map(Into::into).collect(), - // } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))] -#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct SerializableMixNode { - // this is a `MixId` but due to typescript issue, we're using u32 directly. - #[serde(alias = "mix_id")] - pub mix_id: u32, - - pub host: String, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - #[serde(alias = "mix_port")] - pub mix_port: Option, - - #[serde(alias = "identity_key")] - pub identity_key: String, - - #[serde(alias = "sphinx_key")] - pub sphinx_key: String, - - // this is a `MixLayer` but due to typescript issue, we're using u8 directly. - pub layer: u8, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub version: Option, -} - -impl TryFrom for mix::LegacyNode { - type Error = SerializableTopologyError; - - fn try_from(value: SerializableMixNode) -> Result { - let host = mix::LegacyNode::parse_host(&value.host)?; - - let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); - let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = mix::LegacyNode::extract_mix_host(&host, mix_port)?; - - Ok(mix::LegacyNode { - mix_id: value.mix_id, - host, - mix_host, - identity_key: identity::PublicKey::from_base58_string(&value.identity_key) - .map_err(MixnodeConversionError::from)?, - sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) - .map_err(MixnodeConversionError::from)?, - layer: mix::LegacyMixLayer::try_from(value.layer) - .map_err(|_| SerializableTopologyError::InvalidMixLayer { value: value.layer })?, - version, - }) - } -} - -impl<'a> From<&'a mix::LegacyNode> for SerializableMixNode { - fn from(value: &'a mix::LegacyNode) -> Self { - SerializableMixNode { - mix_id: value.mix_id, - host: value.host.to_string(), - mix_port: Some(value.mix_host.port()), - identity_key: value.identity_key.to_base58_string(), - sphinx_key: value.sphinx_key.to_base58_string(), - layer: value.layer.into(), - version: Some(value.version.to_string()), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))] -#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct SerializableGateway { - pub host: String, - - pub node_id: NodeId, - - // optional ip address in the case of host being a hostname that can't be resolved - // (thank you wasm) - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - #[serde(alias = "explicit_ip")] - pub explicit_ip: Option, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - #[serde(alias = "mix_port")] - pub mix_port: Option, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - #[serde(alias = "clients_port")] - #[serde(alias = "clients_ws_port")] - pub clients_ws_port: Option, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - #[serde(alias = "clients_wss_port")] - pub clients_wss_port: Option, - - #[serde(alias = "identity_key")] - pub identity_key: String, - - #[serde(alias = "sphinx_key")] - pub sphinx_key: String, - - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub version: Option, -} - -impl TryFrom for gateway::LegacyNode { - type Error = SerializableTopologyError; - - fn try_from(value: SerializableGateway) -> Result { - let host = gateway::LegacyNode::parse_host(&value.host)?; - - let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); - let clients_ws_port = value - .clients_ws_port - .unwrap_or(DEFAULT_CLIENT_LISTENING_PORT); - let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = if let Some(explicit_ip) = value.explicit_ip { - SocketAddr::new(explicit_ip, mix_port) - } else { - gateway::LegacyNode::extract_mix_host(&host, mix_port)? - }; - - Ok(gateway::LegacyNode { - node_id: value.node_id, - host, - mix_host, - clients_ws_port, - clients_wss_port: value.clients_wss_port, - identity_key: identity::PublicKey::from_base58_string(&value.identity_key) - .map_err(GatewayConversionError::from)?, - sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) - .map_err(GatewayConversionError::from)?, - version, - }) - } -} - -impl<'a> From<&'a gateway::LegacyNode> for SerializableGateway { - fn from(value: &'a gateway::LegacyNode) -> Self { - SerializableGateway { - host: value.host.to_string(), - node_id: value.node_id, - explicit_ip: Some(value.mix_host.ip()), - mix_port: Some(value.mix_host.port()), - clients_ws_port: Some(value.clients_ws_port), - clients_wss_port: value.clients_wss_port, - identity_key: value.identity_key.to_base58_string(), - sphinx_key: value.sphinx_key.to_base58_string(), - version: Some(value.version.to_string()), - } - } -} diff --git a/common/topology/src/wasm_helpers.rs b/common/topology/src/wasm_helpers.rs new file mode 100644 index 00000000000..8c0d9476b16 --- /dev/null +++ b/common/topology/src/wasm_helpers.rs @@ -0,0 +1,100 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// due to the code generated by Tsify +#![allow(clippy::empty_docs)] + +use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles}; +use crate::NymTopology; +use nym_mixnet_contract_common::EpochRewardedSet; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::SocketAddr; +use thiserror::Error; +use tsify::Tsify; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use wasm_utils::error::simple_js_error; + +#[derive(Debug, Error)] +pub enum SerializableTopologyError { + #[error(transparent)] + NodeConversion(#[from] RoutingNodeError), + + #[error("{provided} is not a valid ed25519 public key")] + MalformedIdentity { provided: String }, + + #[error("{provided} is not a valid x25519 public key")] + MalformedSphinxKey { provided: String }, +} + +#[cfg(feature = "wasm-serde-types")] +impl From for JsValue { + fn from(value: SerializableTopologyError) -> Self { + simple_js_error(value.to_string()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct WasmFriendlyNymTopology { + pub rewarded_set: EpochRewardedSet, + + pub node_details: HashMap, +} + +impl TryFrom for NymTopology { + type Error = SerializableTopologyError; + + fn try_from(value: WasmFriendlyNymTopology) -> Result { + let node_details = value + .node_details + .into_iter() + .map(|(_, details)| details.try_into()) + .collect::>()?; + + Ok(NymTopology::new(value.rewarded_set, node_details)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct WasmFriendlyRoutingNode { + pub node_id: u32, + + pub mix_host: SocketAddr, + + pub entry: Option, + pub identity_key: String, + pub sphinx_key: String, + + pub supported_roles: SupportedRoles, + pub performance: f64, +} + +impl TryFrom for RoutingNode { + type Error = SerializableTopologyError; + + fn try_from(value: WasmFriendlyRoutingNode) -> Result { + Ok(RoutingNode { + node_id: value.node_id, + mix_host: value.mix_host, + entry: value.entry, + identity_key: value.identity_key.as_str().parse().map_err(|_| { + SerializableTopologyError::MalformedIdentity { + provided: value.identity_key, + } + })?, + sphinx_key: value.sphinx_key.as_str().parse().map_err(|_| { + SerializableTopologyError::MalformedIdentity { + provided: value.sphinx_key, + } + })?, + supported_roles: value.supported_roles, + performance: value.performance, + }) + } +} diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index a2bcda86def..d63e31e50a2 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -32,7 +32,7 @@ nym-sphinx = { path = "../../nymsphinx" } nym-sphinx-acknowledgements = { path = "../../nymsphinx/acknowledgements", features = ["serde"] } nym-statistics-common = { path = "../../statistics" } nym-task = { path = "../../task" } -nym-topology = { path = "../../topology", features = ["serde", "wasm-serde-types"] } +nym-topology = { path = "../../topology", features = ["wasm-serde-types"] } nym-validator-client = { path = "../../client-libs/validator-client", default-features = false } wasm-utils = { path = "../utils" } wasm-storage = { path = "../storage" } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index f5d223eb630..720553ccf3e 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1377,6 +1377,20 @@ impl From for nym_mixnet_contract_common::EpochRewardedSet } } +impl From for RewardedSetResponse { + fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self { + RewardedSetResponse { + epoch_id: r.epoch_id, + entry_gateways: r.assignment.entry_gateways, + exit_gateways: r.assignment.exit_gateways, + layer1: r.assignment.layer1, + layer2: r.assignment.layer2, + layer3: r.assignment.layer3, + standby: r.assignment.standby, + } + } +} + pub use config_score::*; pub mod config_score { use nym_contracts_common::NaiveFloat; diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 61789debcb0..d8574e36d5f 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -72,7 +72,7 @@ pub enum NodeRoleQueryParam { ExitGateway, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, Default)] pub enum NodeRole { // a properly active mixnode Mixnode { @@ -88,6 +88,7 @@ pub enum NodeRole { // equivalent of node that's in rewarded set but not in the inactive set Standby, + #[default] Inactive, } diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 95caa10d030..ed9abf30ce1 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -6,9 +6,7 @@ use crate::support::caching::Cache; use cosmwasm_std::{Decimal, Fraction}; use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; -use nym_mixnet_contract_common::{ - EpochRewardedSet, ExecuteMsg, NodeId, RewardedSet, RewardingParams, -}; +use nym_mixnet_contract_common::{EpochRewardedSet, ExecuteMsg, NodeId, RewardingParams}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tokio::sync::RwLockReadGuard; diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 1f659b86461..01452451977 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -181,8 +181,8 @@ impl Monitor { } fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { - for mix in route.topology().mixes_as_vec() { - blacklist.insert(mix.mix_id); + for mix in route.topology().mixnodes() { + blacklist.insert(mix.node_id); } blacklist.insert(route.gateway().node_id); } diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 50c3be80baa..7ae25478448 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -5,26 +5,26 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; +use crate::nym_contract_cache::cache::NymContractCache; 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_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; -use nym_node_tester_utils::node::TestableNode; +use nym_node_tester_utils::node::{NodeType, TestableNode}; use nym_node_tester_utils::NodeTester; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::{PacketSize, PacketType}; -use nym_topology::gateway::GatewayConversionError; -use nym_topology::mix::MixnodeConversionError; -use nym_topology::{gateway, mix}; +use nym_topology::node::{EntryDetails, RoutingNode, SupportedRoles}; use rand::prelude::SliceRandom; use rand::{rngs::ThreadRng, thread_rng, Rng}; use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tracing::{debug, error, info, trace}; @@ -135,7 +135,7 @@ impl PacketPreparer { // when we're testing mixnodes, the recipient is going to stay constant, so we can specify it ahead of time fn ephemeral_mix_tester(&self, test_route: &TestRoute) -> NodeTester { - let self_address = self.create_packet_sender(test_route.gateway()); + let self_address = self.create_packet_sender(&test_route.gateway()); self.ephemeral_tester(test_route, Some(self_address)) } @@ -210,75 +210,87 @@ impl PacketPreparer { (mixnodes, gateways) } - pub(crate) fn try_parse_mix_bond( + pub(crate) fn try_parse_legacy_mix_bond( &self, bond: &LegacyMixNodeBondWithLayer, - ) -> Result { - fn parse_bond( - bond: &LegacyMixNodeBondWithLayer, - ) -> Result { - let host = mix::LegacyNode::parse_host(&bond.mix_node.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = mix::LegacyNode::extract_mix_host(&host, bond.mix_node.mix_port)?; - - Ok(mix::LegacyNode { - mix_id: bond.mix_id, - host, - mix_host, - identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, - layer: bond.layer, - version: bond.mix_node.version.as_str().into(), + ) -> Result { + fn parse_bond(bond: &LegacyMixNodeBondWithLayer) -> Option { + let (ips, _) = legacy_host_to_ips_and_hostname(&bond.mix_node.host)?; + + Some(RoutingNode { + node_id: bond.mix_id, + mix_host: SocketAddr::new(*ips.first()?, bond.mix_node.mix_port), + entry: None, + identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key) + .ok()?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key) + .ok()?, + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: false, + mixnet_exit: false, + }, }) } let identity = bond.mix_node.identity_key.clone(); - parse_bond(bond).map_err(|_| identity) + parse_bond(bond).ok_or(identity) } - pub(crate) fn try_parse_gateway_bond( + pub(crate) fn try_parse_legacy_gateway_bond( &self, gateway: &LegacyGatewayBondWithId, - ) -> Result { - fn parse_bond( - bond: &LegacyGatewayBondWithId, - ) -> Result { - let host = gateway::LegacyNode::parse_host(&bond.gateway.host)?; + ) -> Result { + fn parse_bond(bond: &LegacyGatewayBondWithId) -> Option { + let (ips, hostname) = legacy_host_to_ips_and_hostname(&bond.gateway.host)?; - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = gateway::LegacyNode::extract_mix_host(&host, bond.gateway.mix_port)?; - - Ok(gateway::LegacyNode { + Some(RoutingNode { node_id: bond.node_id, - host, - mix_host, - clients_ws_port: bond.gateway.clients_port, - clients_wss_port: None, - identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, - version: bond.gateway.version.as_str().into(), + mix_host: SocketAddr::new(*ips.first()?, bond.gateway.mix_port), + entry: Some(EntryDetails { + ip_addresses: ips, + clients_ws_port: bond.gateway.clients_port, + hostname, + clients_wss_port: None, + }), + identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key) + .ok()?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key) + .ok()?, + supported_roles: SupportedRoles { + mixnode: false, + mixnet_entry: true, + mixnet_exit: false, + }, }) } let identity = gateway.gateway.identity_key.clone(); - parse_bond(gateway).map_err(|_| identity) + parse_bond(gateway).ok_or(identity) + } + + fn random_legacy_layer(&self, rng: &mut R) -> LegacyMixLayer { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(rng).copied().unwrap() } fn to_legacy_layered_mixes<'a, R: Rng>( &self, rng: &mut R, - rewarded_set: &CachedRewardedSet, node_statuses: &HashMap, mixing_nym_nodes: impl Iterator + 'a, - ) -> HashMap> { + ) -> HashMap> { let mut layered_mixes = HashMap::new(); for mixing_nym_node in mixing_nym_nodes { - let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) - else { + let Some(parsed_node) = self.nym_node_to_routing_node(mixing_nym_node) else { continue; }; // if the node is not present, default to 0.5 @@ -286,7 +298,7 @@ impl PacketPreparer { .get(&mixing_nym_node.node_id) .map(|node| node.last_24h_performance.naive_to_f64()) .unwrap_or(0.5); - let layer = parsed_node.layer; + let layer = self.random_legacy_layer(rng); let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); layer_mixes.push((parsed_node, weight)) } @@ -298,11 +310,11 @@ impl PacketPreparer { &self, node_statuses: &HashMap, gateway_capable_nym_nodes: impl Iterator + 'a, - ) -> Vec<(gateway::LegacyNode, f64)> { + ) -> Vec<(RoutingNode, f64)> { let mut gateways = Vec::new(); for gateway_capable_node in gateway_capable_nym_nodes { - let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { + let Some(parsed_node) = self.nym_node_to_routing_node(gateway_capable_node) else { continue; }; // if the node is not present, default to 0.5 @@ -321,8 +333,6 @@ impl PacketPreparer { // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused pub(crate) async fn prepare_test_routes(&self, n: usize) -> Option> { - let rewarded_set = self.contract_cache.rewarded_set().await?; - let descriptions = self.described_cache.get().await.ok()?; let statuses = self.node_status_cache.node_annotations().await?; @@ -333,8 +343,7 @@ impl PacketPreparer { let mut rng = thread_rng(); // separate mixes into layers for easier selection alongside the selection weights - let layered_mixes = - self.to_legacy_layered_mixes(&mut rng, &rewarded_set, &statuses, mixing_nym_nodes); + let layered_mixes = self.to_legacy_layered_mixes(&mut rng, &statuses, mixing_nym_nodes); let gateways = self.to_legacy_gateway_nodes(&statuses, gateway_capable_nym_nodes); // get all nodes from each layer... @@ -394,7 +403,7 @@ impl PacketPreparer { Some(routes) } - fn create_packet_sender(&self, gateway: &gateway::LegacyNode) -> Recipient { + fn create_packet_sender(&self, gateway: &RoutingNode) -> Recipient { Recipient::new( self.self_public_identity, self.self_public_encryption, @@ -410,7 +419,8 @@ impl PacketPreparer { _packet_type: PacketType, ) -> GatewayPackets { let mut tester = self.ephemeral_mix_tester(route); - let topology = route.topology(); + let topology = route.testable_route_provider(); + let plaintexts = route.self_test_messages(num); // the unwrap here is fine as: @@ -419,7 +429,7 @@ impl PacketPreparer { // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet let mix_packets = plaintexts .into_iter() - .map(|p| tester.wrap_plaintext_data(p, topology, None).unwrap()) + .map(|p| tester.wrap_plaintext_data(p, &topology, None).unwrap()) .map(MixPacket::from) .collect(); @@ -433,11 +443,11 @@ impl PacketPreparer { fn filter_outdated_and_malformed_mixnodes( &self, nodes: Vec, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for mixnode in nodes { - if let Ok(parsed_node) = self.try_parse_mix_bond(&mixnode) { + if let Ok(parsed_node) = self.try_parse_legacy_mix_bond(&mixnode) { parsed_nodes.push(parsed_node) } else { invalid_nodes.push(InvalidNode::Malformed { @@ -451,12 +461,12 @@ impl PacketPreparer { fn filter_outdated_and_malformed_gateways( &self, nodes: Vec, - ) -> (Vec<(gateway::LegacyNode, NodeId)>, Vec) { + ) -> (Vec, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for gateway in nodes { - if let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) { - parsed_nodes.push((parsed_node, gateway.node_id)) + if let Ok(parsed_node) = self.try_parse_legacy_gateway_bond(&gateway) { + parsed_nodes.push(parsed_node) } else { invalid_nodes.push(InvalidNode::Malformed { node: TestableNode::new_gateway( @@ -469,41 +479,8 @@ impl PacketPreparer { (parsed_nodes, invalid_nodes) } - fn nym_node_to_legacy_mix( - &self, - rng: &mut R, - rewarded_set: &CachedRewardedSet, - mixing_nym_node: &NymNodeDescription, - ) -> Option { - let maybe_explicit_layer = rewarded_set - .try_get_mix_layer(&mixing_nym_node.node_id) - .and_then(|layer| LegacyMixLayer::try_from(layer).ok()); - - let layer = match maybe_explicit_layer { - Some(layer) => layer, - None => { - let layer_choices = [ - LegacyMixLayer::One, - LegacyMixLayer::Two, - LegacyMixLayer::Three, - ]; - - // if nym-node doesn't have a layer assigned, since it's either standby or inactive, - // we have to choose one randomly for the testing purposes - // SAFETY: the slice is not empty so the unwrap is fine - #[allow(clippy::unwrap_used)] - layer_choices.choose(rng).copied().unwrap() - } - }; - - mixing_nym_node.try_to_topology_mix_node(layer).ok() - } - - fn nym_node_to_legacy_gateway( - &self, - gateway_capable_node: &NymNodeDescription, - ) -> Option { - gateway_capable_node.try_to_topology_gateway().ok() + fn nym_node_to_routing_node(&self, description: &NymNodeDescription) -> Option { + description.try_to_topology_node().ok() } pub(super) async fn prepare_test_packets( @@ -514,7 +491,6 @@ impl PacketPreparer { _packet_type: PacketType, ) -> PreparedPackets { let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; - let rewarded_set = self.contract_cache.rewarded_set().await; let descriptions = self .described_cache @@ -532,28 +508,32 @@ impl PacketPreparer { // summary of nodes that got tested let mut mixnodes_under_test = mixnodes_to_test_details .iter() - .map(|node| node.into()) + .map(|node| TestableNode::new_routing(node, NodeType::Mixnode)) .collect::>(); let mut gateways_under_test = gateways_to_test_details .iter() - .map(|node| node.into()) + .map(|node| TestableNode::new_routing(node, NodeType::Gateway)) .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) { - mixnodes_under_test.push(TestableNode::from(&parsed)); - mixnodes_to_test_details.push(parsed); - } + for mix in mixing_nym_nodes { + if let Some(parsed) = self.nym_node_to_routing_node(mix) { + mixnodes_under_test.push(TestableNode::new_routing(&parsed, NodeType::Mixnode)); + mixnodes_to_test_details.push(parsed); } } + // assign random layer to each node + let mut rng = thread_rng(); + let mixnodes_to_test_details = mixnodes_to_test_details + .into_iter() + .map(|node| (self.random_legacy_layer(&mut rng), node)) + .collect::>(); + for gateway in gateway_capable_nym_nodes { - if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) { - gateways_under_test.push((&parsed, gateway.node_id).into()); - gateways_to_test_details.push((parsed, gateway.node_id)); + if let Some(parsed) = self.nym_node_to_routing_node(gateway) { + gateways_under_test.push(TestableNode::new_routing(&parsed, NodeType::Gateway)); + gateways_to_test_details.push(parsed); } } @@ -594,10 +574,10 @@ impl PacketPreparer { gateway_packets.push_packets(mix_packets); // and generate test packets for gateways (note the variable recipient) - for (gateway, node_id) in &gateways_to_test_details { + for gateway in &gateways_to_test_details { let recipient = self.create_packet_sender(gateway); let gateway_identity = gateway.identity_key; - let gateway_address = gateway.clients_address(); + let gateway_address = gateway.ws_entry_address(false); // the unwrap here is fine as: // 1. the topology is definitely valid (otherwise we wouldn't be here) @@ -607,7 +587,6 @@ impl PacketPreparer { let gateway_test_packets = mix_tester .legacy_gateway_test_packets( gateway, - *node_id, route_ext, self.per_node_test_packets as u32, Some(recipient), diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index 172d6b3680b..420efabb7c0 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -35,7 +35,7 @@ const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); pub(crate) struct GatewayPackets { /// Network address of the target gateway if wanted to be accessed by the client. /// It is a websocket address. - pub(crate) clients_address: String, + pub(crate) clients_address: Option, /// Public key of the target gateway. pub(crate) pub_key: ed25519::PublicKey, @@ -46,7 +46,7 @@ pub(crate) struct GatewayPackets { impl GatewayPackets { pub(crate) fn new( - clients_address: String, + clients_address: Option, pub_key: ed25519::PublicKey, packets: Vec, ) -> Self { @@ -57,15 +57,17 @@ impl GatewayPackets { } } - pub(crate) fn gateway_config(&self) -> GatewayConfig { - GatewayConfig { - gateway_identity: self.pub_key, - gateway_owner: None, - gateway_listener: self.clients_address.clone(), - } + pub(crate) fn gateway_config(&self) -> Option { + self.clients_address + .clone() + .map(|gateway_listener| GatewayConfig { + gateway_identity: self.pub_key, + gateway_owner: None, + gateway_listener, + }) } - pub(crate) fn empty(clients_address: String, pub_key: ed25519::PublicKey) -> Self { + pub(crate) fn empty(clients_address: Option, pub_key: ed25519::PublicKey) -> Self { GatewayPackets { clients_address, pub_key, @@ -355,17 +357,22 @@ impl PacketSender { fresh_gateway_client_data: Arc, max_sending_rate: usize, ) -> Option { + let identity = packets.pub_key; + + let Some(gateway_config) = packets.gateway_config() else { + warn!("gateway {identity} didn't provide valid entry information"); + return None; + }; + let (mut client, gateway_channels) = Self::create_new_gateway_client_handle_and_authenticate( - packets.gateway_config(), + gateway_config, &fresh_gateway_client_data, gateway_connection_timeout, gateway_bandwidth_claim_timeout, ) .await?; - let identity = client.gateway_identity(); - let estimated_time = Duration::from_secs_f64(packets.packets.len() as f64 / max_sending_rate as f64); // give some leeway diff --git a/nym-api/src/network_monitor/test_packet.rs b/nym-api/src/network_monitor/test_packet.rs index 1b1bfbbeda9..73ace159adb 100644 --- a/nym-api/src/network_monitor/test_packet.rs +++ b/nym-api/src/network_monitor/test_packet.rs @@ -3,7 +3,7 @@ use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::TestMessage; -use nym_topology::mix; +use nym_topology::node::RoutingNode; use serde::{Deserialize, Serialize}; pub(crate) type NodeTestMessage = TestMessage; @@ -24,7 +24,7 @@ impl NymApiTestMessageExt { pub fn mix_plaintexts( &self, - node: &mix::LegacyNode, + node: &RoutingNode, test_packets: u32, ) -> Result>, NetworkTestingError> { NodeTestMessage::mix_plaintexts(node, test_packets, *self) diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index 224f7513573..1032baf5db3 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -4,7 +4,10 @@ use crate::network_monitor::test_packet::NymApiTestMessageExt; use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use nym_crypto::asymmetric::identity; -use nym_topology::{gateway, mix, NymTopology}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, RewardedSet}; +use nym_topology::node::RoutingNode; +use nym_topology::{NymRouteProvider, NymTopology}; use std::fmt::{Debug, Formatter}; #[derive(Clone)] @@ -16,22 +19,28 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, - l1_mix: mix::LegacyNode, - l2_mix: mix::LegacyNode, - l3_mix: mix::LegacyNode, - gateway: gateway::LegacyNode, + l1_mix: RoutingNode, + l2_mix: RoutingNode, + l3_mix: RoutingNode, + gateway: RoutingNode, ) -> Self { - let layered_mixes = [ - (1u8, vec![l1_mix]), - (2u8, vec![l2_mix]), - (3u8, vec![l3_mix]), - ] - .into_iter() - .collect(); + let fake_rewarded_set = EpochRewardedSet { + epoch_id: EpochId::MAX, + assignment: RewardedSet { + entry_gateways: vec![gateway.node_id], + exit_gateways: vec![], + layer1: vec![l1_mix.node_id], + layer2: vec![l2_mix.node_id], + layer3: vec![l3_mix.node_id], + standby: vec![], + }, + }; + + let nodes = vec![l1_mix, l2_mix, l3_mix, gateway]; TestRoute { id, - nodes: NymTopology::new(layered_mixes, vec![gateway]), + nodes: NymTopology::new(fake_rewarded_set, nodes), } } @@ -39,24 +48,36 @@ impl TestRoute { self.id } - pub(crate) fn gateway(&self) -> &gateway::LegacyNode { - &self.nodes.gateways()[0] + pub(crate) fn gateway(&self) -> RoutingNode { + // SAFETY: we inserted entry gateway at construction + #[allow(clippy::unwrap_used)] + self.nodes + .nodes_with_role(Role::EntryGateway) + .next() + .unwrap() + .clone() } - pub(crate) fn layer_one_mix(&self) -> &mix::LegacyNode { - &self.nodes.mixes().get(&1).unwrap()[0] + pub(crate) fn layer_one_mix(&self) -> &RoutingNode { + // SAFETY: we inserted layer1 node at construction + #[allow(clippy::unwrap_used)] + self.nodes.nodes_with_role(Role::Layer1).next().unwrap() } - pub(crate) fn layer_two_mix(&self) -> &mix::LegacyNode { - &self.nodes.mixes().get(&2).unwrap()[0] + pub(crate) fn layer_two_mix(&self) -> &RoutingNode { + // SAFETY: we inserted layer2 node at construction + #[allow(clippy::unwrap_used)] + self.nodes.nodes_with_role(Role::Layer2).next().unwrap() } - pub(crate) fn layer_three_mix(&self) -> &mix::LegacyNode { - &self.nodes.mixes().get(&3).unwrap()[0] + pub(crate) fn layer_three_mix(&self) -> &RoutingNode { + // SAFETY: we inserted layer3 node at construction + #[allow(clippy::unwrap_used)] + self.nodes.nodes_with_role(Role::Layer3).next().unwrap() } - pub(crate) fn gateway_clients_address(&self) -> String { - self.gateway().clients_address() + pub(crate) fn gateway_clients_address(&self) -> Option { + self.gateway().ws_entry_address(false) } pub(crate) fn gateway_identity(&self) -> identity::PublicKey { @@ -67,6 +88,10 @@ impl TestRoute { &self.nodes } + pub(crate) fn testable_route_provider(&self) -> NymRouteProvider { + self.nodes.clone().into() + } + pub(crate) fn test_message_ext(&self, test_nonce: u64) -> NymApiTestMessageExt { NymApiTestMessageExt::new(self.id, test_nonce) } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index adf25c29f14..0a0aeabc2f7 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -12,13 +12,10 @@ use futures::{stream, StreamExt}; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, NymNodeDetails}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; -use nym_topology::gateway::GatewayConversionError; -use nym_topology::mix::MixnodeConversionError; -use nym_topology::{gateway, mix, NetworkAddress}; +use nym_topology::node::{RoutingNode, RoutingNodeError}; use std::collections::HashMap; -use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; use tracing::{debug, error, info}; @@ -65,87 +62,14 @@ pub enum NodeDescribeCacheError { // this exists because I've been moving things around quite a lot and now the place that holds the type // doesn't have relevant dependencies for proper impl pub(crate) trait NodeDescriptionTopologyExt { - fn try_to_topology_mix_node( - &self, - layer: LegacyMixLayer, - ) -> Result; - - fn try_to_topology_gateway(&self) -> Result; + fn try_to_topology_node(&self) -> Result; } impl NodeDescriptionTopologyExt for NymNodeDescription { - // TODO: this might have to be moved around - fn try_to_topology_mix_node( - &self, - layer: LegacyMixLayer, - ) -> Result { - let keys = &self.description.host_information.keys; - let ips = &self.description.host_information.ip_address; - if ips.is_empty() { - return Err(MixnodeConversionError::NoIpAddressesProvided { - mixnode: keys.ed25519.to_base58_string(), - }); - } - - let host = match &self.description.host_information.hostname { - None => NetworkAddress::IpAddr(ips[0]), - Some(hostname) => NetworkAddress::Hostname(hostname.clone()), - }; - - // get ip from the self-reported values so we wouldn't need to do any hostname resolution - // (which doesn't really work in wasm) - let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); - - Ok(mix::LegacyNode { - mix_id: self.node_id, - host, - mix_host, - identity_key: keys.ed25519, - sphinx_key: keys.x25519, - layer, - version: self - .description - .build_information - .build_version - .as_str() - .into(), - }) - } - - fn try_to_topology_gateway(&self) -> Result { - let keys = &self.description.host_information.keys; - - let ips = &self.description.host_information.ip_address; - if ips.is_empty() { - return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: keys.ed25519.to_base58_string(), - }); - } - - let host = match &self.description.host_information.hostname { - None => NetworkAddress::IpAddr(ips[0]), - Some(hostname) => NetworkAddress::Hostname(hostname.clone()), - }; - - // get ip from the self-reported values so we wouldn't need to do any hostname resolution - // (which doesn't really work in wasm) - let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); - - Ok(gateway::LegacyNode { - node_id: self.node_id, - host, - mix_host, - clients_ws_port: self.description.mixnet_websockets.ws_port, - clients_wss_port: self.description.mixnet_websockets.wss_port, - identity_key: self.description.host_information.keys.ed25519, - sphinx_key: self.description.host_information.keys.x25519, - version: self - .description - .build_information - .build_version - .as_str() - .into(), - }) + fn try_to_topology_node(&self) -> Result { + // for the purposes of routing, performance is completely ignored, + // so add dummy value and piggyback on existing conversion + (&self.to_skimmed_node(Default::default(), Default::default())).try_into() } } 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 e03dd2807da..a0c4df4ee9e 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -6,7 +6,7 @@ use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::models::Uptime; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; use crate::nym_contract_cache::cache::data::ConfigScoreData; -use crate::nym_contract_cache::cache::CachedRewardedSet; +use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::support::storage::NymApiStorage; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::DescribedNodeType::{LegacyGateway, LegacyMixnode, NymNode}; @@ -17,10 +17,8 @@ use nym_api_requests::models::{ use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; -use nym_topology::NetworkAddress; +use nym_topology::CachedEpochRewardedSet; use std::collections::{HashMap, HashSet}; -use std::net::ToSocketAddrs; -use std::str::FromStr; use tracing::trace; pub(super) async fn get_mixnode_reliability_from_storage( @@ -148,7 +146,10 @@ fn calculate_config_score( } // TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status(rewarded_set: &CachedRewardedSet, node_id: NodeId) -> RewardedSetStatus { +fn get_rewarded_set_status( + rewarded_set: &CachedEpochRewardedSet, + node_id: NodeId, +) -> RewardedSetStatus { if rewarded_set.is_standby(&node_id) { RewardedSetStatus::Standby } else if rewarded_set.is_active_mixnode(&node_id) { @@ -164,7 +165,7 @@ pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, - rewarded_set: &CachedRewardedSet, + rewarded_set: &CachedEpochRewardedSet, blacklist: &HashSet, ) -> HashMap { let mut annotated = HashMap::new(); @@ -203,21 +204,11 @@ pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( .ok() .unwrap_or_default(); - // safety: this conversion is infallible - let ip_addresses = - match NetworkAddress::from_str(&mixnode.bond_information.mix_node.host).unwrap() { - NetworkAddress::IpAddr(ip) => vec![ip], - NetworkAddress::Hostname(hostname) => { - // try to resolve it - ( - hostname.as_str(), - mixnode.bond_information.mix_node.mix_port, - ) - .to_socket_addrs() - .map(|iter| iter.map(|s| s.ip()).collect::>()) - .unwrap_or_default() - } - }; + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) + else { + continue; + }; let (estimated_operator_apy, estimated_delegators_apy) = compute_apy_from_reward(&mixnode, reward_estimate, current_interval); @@ -263,17 +254,10 @@ pub(crate) async fn annotate_legacy_gateways_with_details( .ok() .unwrap_or_default(); - // safety: this conversion is infallible - let ip_addresses = match NetworkAddress::from_str(&gateway_bond.bond.gateway.host).unwrap() - { - NetworkAddress::IpAddr(ip) => vec![ip], - NetworkAddress::Hostname(hostname) => { - // try to resolve it - (hostname.as_str(), gateway_bond.bond.gateway.mix_port) - .to_socket_addrs() - .map(|iter| iter.map(|s| s.ip()).collect::>()) - .unwrap_or_default() - } + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) + else { + continue; }; annotated.insert( @@ -298,7 +282,7 @@ pub(crate) async fn produce_node_annotations( legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], - rewarded_set: &CachedRewardedSet, + rewarded_set: &CachedEpochRewardedSet, current_interval: Interval, described_nodes: &DescribedNodes, ) -> HashMap { diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index b5b44de29aa..ba2c7d91154 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -3,137 +3,16 @@ use crate::support::caching::Cache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{ConfigScoreDataResponse, RewardedSetResponse}; +use nym_api_requests::models::ConfigScoreDataResponse; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ - ConfigScoreParams, EpochId, EpochRewardedSet, HistoricalNymNodeVersionEntry, Interval, NodeId, - NymNodeDetails, RewardedSet, RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, + RewardingParams, }; +use nym_topology::CachedEpochRewardedSet; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; -#[derive(Default, Clone)] -pub(crate) struct CachedRewardedSet { - pub(crate) epoch_id: EpochId, - - pub(crate) entry_gateways: HashSet, - - pub(crate) exit_gateways: HashSet, - - pub(crate) layer1: HashSet, - - pub(crate) layer2: HashSet, - - pub(crate) layer3: HashSet, - - pub(crate) standby: HashSet, -} - -impl From for CachedRewardedSet { - fn from(value: EpochRewardedSet) -> Self { - CachedRewardedSet { - epoch_id: value.epoch_id, - entry_gateways: value.assignment.entry_gateways.into_iter().collect(), - exit_gateways: value.assignment.exit_gateways.into_iter().collect(), - layer1: value.assignment.layer1.into_iter().collect(), - layer2: value.assignment.layer2.into_iter().collect(), - layer3: value.assignment.layer3.into_iter().collect(), - standby: value.assignment.standby.into_iter().collect(), - } - } -} - -impl From for EpochRewardedSet { - fn from(value: CachedRewardedSet) -> Self { - EpochRewardedSet { - epoch_id: value.epoch_id, - assignment: RewardedSet { - entry_gateways: value.entry_gateways.into_iter().collect(), - exit_gateways: value.exit_gateways.into_iter().collect(), - layer1: value.layer1.into_iter().collect(), - layer2: value.layer2.into_iter().collect(), - layer3: value.layer3.into_iter().collect(), - standby: value.standby.into_iter().collect(), - }, - } - } -} - -impl From<&CachedRewardedSet> for RewardedSetResponse { - fn from(value: &CachedRewardedSet) -> Self { - RewardedSetResponse { - epoch_id: value.epoch_id, - entry_gateways: value.entry_gateways.iter().copied().collect(), - exit_gateways: value.exit_gateways.iter().copied().collect(), - layer1: value.layer1.iter().copied().collect(), - layer2: value.layer2.iter().copied().collect(), - layer3: value.layer3.iter().copied().collect(), - standby: value.standby.iter().copied().collect(), - } - } -} - -impl CachedRewardedSet { - pub(crate) fn role(&self, node_id: NodeId) -> Option { - if self.entry_gateways.contains(&node_id) { - Some(Role::EntryGateway) - } else if self.exit_gateways.contains(&node_id) { - Some(Role::ExitGateway) - } else if self.layer1.contains(&node_id) { - Some(Role::Layer1) - } else if self.layer2.contains(&node_id) { - Some(Role::Layer2) - } else if self.layer3.contains(&node_id) { - Some(Role::Layer3) - } else if self.standby.contains(&node_id) { - Some(Role::Standby) - } else { - None - } - } - - pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { - if self.layer1.contains(node_id) { - Some(1) - } else if self.layer2.contains(node_id) { - Some(2) - } else if self.layer3.contains(node_id) { - Some(3) - } else { - None - } - } - - pub fn is_standby(&self, node_id: &NodeId) -> bool { - self.standby.contains(node_id) - } - - pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { - self.layer1.contains(node_id) - || self.layer2.contains(node_id) - || self.layer3.contains(node_id) - } - - #[allow(dead_code)] - pub(crate) fn gateways(&self) -> HashSet { - let mut gateways = - HashSet::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); - gateways.extend(&self.entry_gateways); - gateways.extend(&self.exit_gateways); - gateways - } - - pub(crate) fn active_mixnodes(&self) -> HashSet { - let mut mixnodes = - HashSet::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); - mixnodes.extend(&self.layer1); - mixnodes.extend(&self.layer2); - mixnodes.extend(&self.layer3); - mixnodes - } -} - #[derive(Clone)] pub(crate) struct ConfigScoreData { pub(crate) config_score_params: ConfigScoreParams, @@ -157,7 +36,7 @@ pub(crate) struct ContractCacheData { pub(crate) legacy_mixnodes: Cache>, pub(crate) legacy_gateways: Cache>, pub(crate) nym_nodes: Cache>, - pub(crate) rewarded_set: Cache, + pub(crate) rewarded_set: Cache, // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. // instead clients are meant to be filtering out them themselves based on the provided scores. diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index a720dce4ffb..b4c184d4b6d 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -12,8 +12,9 @@ use nym_api_requests::models::MixnodeStatus; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ ConfigScoreParams, EpochRewardedSet, HistoricalNymNodeVersionEntry, Interval, NodeId, - NymNodeDetails, RewardedSet, RewardingParams, + NymNodeDetails, RewardingParams, }; +use nym_topology::CachedEpochRewardedSet; use std::{ collections::HashSet, sync::{ @@ -29,8 +30,6 @@ use tracing::{debug, error}; pub(crate) mod data; pub(crate) mod refresher; -pub(crate) use self::data::CachedRewardedSet; - const CACHE_TIMEOUT_MS: u64 = 100; #[derive(Clone)] @@ -264,11 +263,11 @@ impl NymContractCache { .into_inner() } - pub async fn rewarded_set(&self) -> Option>> { + pub async fn rewarded_set(&self) -> Option>> { self.get(|cache| &cache.rewarded_set).await } - pub async fn rewarded_set_owned(&self) -> Cache { + pub async fn rewarded_set_owned(&self) -> Cache { self.get_owned(|cache| cache.rewarded_set.clone_cache()) .await .unwrap_or_default() diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index a3e911d10fa..e4f57dae63c 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -9,7 +9,7 @@ use anyhow::Result; use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use nym_mixnet_contract_common::{EpochRewardedSet, LegacyMixLayer, RewardedSet}; +use nym_mixnet_contract_common::{EpochRewardedSet, LegacyMixLayer}; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, NymContractsProvider, VestingQueryClient, diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index a3646e8b50b..fe3e3326c4b 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -19,7 +19,6 @@ use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::ops::Deref; use std::time::Duration; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToSchema}; @@ -63,9 +62,13 @@ async fn rewarded_set(State(state): State) -> AxumResult( - rewarded_set: &CachedRewardedSet, + rewarded_set: &CachedEpochRewardedSet, nym_nodes_subset: NI, annotations: &HashMap, active_only: bool, @@ -55,7 +55,7 @@ where /// Given all relevant caches, add appropriate legacy nodes to the part of the response fn add_legacy( nodes: &mut Vec, - rewarded_set: &CachedRewardedSet, + rewarded_set: &CachedEpochRewardedSet, describe_cache: &DescribedNodes, annotated_legacy_nodes: &HashMap, active_only: bool, diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index 6c277441eaa..9d55ab3bf17 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -8,7 +8,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; +use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::caching::Cache; @@ -17,6 +17,7 @@ use axum::extract::FromRef; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_mixnet_contract_common::NodeId; use nym_task::TaskManager; +use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::sync::Arc; use time::OffsetDateTime; @@ -166,7 +167,7 @@ impl AppState { pub(crate) async fn rewarded_set( &self, - ) -> Result>, AxumErrorResponse> { + ) -> Result>, AxumErrorResponse> { self.nym_contract_cache() .rewarded_set() .await diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs index d5135c76439..abc95b1b6d4 100644 --- a/nym-api/src/support/legacy_helpers.rs +++ b/nym-api/src/support/legacy_helpers.rs @@ -10,6 +10,25 @@ use nym_mixnet_contract_common::{ Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, }; use rand::prelude::SliceRandom; +use std::net::{IpAddr, ToSocketAddrs}; +use std::str::FromStr; + +pub(crate) fn legacy_host_to_ips_and_hostname( + legacy: &str, +) -> Option<(Vec, Option)> { + if let Ok(ip) = IpAddr::from_str(legacy) { + return Some((vec![ip], None)); + } + + let resolved = (legacy, 1789u16) + .to_socket_addrs() + .ok()? + .collect::>(); + Some(( + resolved.into_iter().map(|s| s.ip()).collect(), + Some(legacy.to_string()), + )) +} pub(crate) fn to_legacy_mixnode( nym_node: &NymNodeDetails, diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index dc5e4c25602..e1d6b729c7c 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -623,21 +623,21 @@ impl NymApiStorage { // we MUST have those entries in the database, otherwise the route wouldn't have been chosen // in the first place let layer1_mix_db_id = self - .get_mixnode_database_id(test_route.layer_one_mix().mix_id) + .get_mixnode_database_id(test_route.layer_one_mix().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!("could not get db id for layer1 mixnode from network monitor run {monitor_run_db_id}"), })?; let layer2_mix_db_id = self - .get_mixnode_database_id(test_route.layer_two_mix().mix_id) + .get_mixnode_database_id(test_route.layer_two_mix().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!("could not get db id for layer2 mixnode from network monitor run {monitor_run_db_id}"), })?; let layer3_mix_db_id = self - .get_mixnode_database_id(test_route.layer_three_mix().mix_id) + .get_mixnode_database_id(test_route.layer_three_mix().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!("could not get db id for layer3 mixnode from network monitor run {monitor_run_db_id}"), diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index 67f4a773a77..e889c4d9bea 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,8 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::mix::LegacyMixLayer; -use nym_topology::{mix, NymTopology}; +use nym_topology::node::LegacyMixLayer; use std::collections::BTreeMap; #[tokio::main] @@ -13,7 +12,7 @@ async fn main() { // Passing no config makes the client fire up an ephemeral session and figure shit out on its own let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); - let starting_topology = client.read_current_topology().await.unwrap(); + let starting_topology = client.read_current_route_provider().await.unwrap(); // but we don't like our default topology, we want to use only those very specific, hardcoded, nodes: let mut mixnodes = BTreeMap::new(); diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 3da173a6053..14b537753e7 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -18,10 +18,11 @@ use nym_task::{ connections::{ConnectionCommandSender, LaneQueueLengths}, TaskHandle, }; -use nym_topology::NymTopology; +use nym_topology::{NymRouteProvider, NymTopology}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use tokio::sync::RwLockReadGuard; /// Client connected to the Nym mixnet. pub struct MixnetClient { @@ -159,8 +160,11 @@ impl MixnetClient { } /// Gets the value of the currently used network topology. - pub async fn read_current_topology(&self) -> Option { - self.client_state.topology_accessor.current_topology().await + pub async fn read_current_route_provider(&self) -> Option> { + self.client_state + .topology_accessor + .current_route_provider() + .await } /// Restore default topology refreshing behaviour of this client. diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 6bc2a46a065..6cbf837c296 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -74,11 +74,6 @@ impl Socks5MixnetClient { .await } - /// Gets the value of the currently used network topology. - pub async fn read_current_topology(&self) -> Option { - self.client_state.topology_accessor.current_topology().await - } - /// Restore default topology refreshing behaviour of this client. pub fn restore_automatic_topology_refreshing(&self) { self.client_state.topology_accessor.release_manual_control() From 28d9317aecfc8fbb8da25e8ece2fa584823e08f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Dec 2024 13:30:52 +0000 Subject: [PATCH 5/9] updated nym-node --- common/client-core/src/error.rs | 6 ++--- common/client-core/src/init/mod.rs | 1 - common/client-core/src/init/types.rs | 12 +++++++--- common/nymsphinx/src/receiver.rs | 1 + common/topology/src/lib.rs | 21 +++++++++++++++- nym-node/src/node/mod.rs | 24 ++++++++++++------- nym-node/src/node/shared_topology.rs | 12 ++++++---- .../network-requester/src/reply.rs | 1 - 8 files changed, 56 insertions(+), 22 deletions(-) diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index fae13a0d7e3..b115acb82c4 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -5,7 +5,7 @@ use crate::client::mix_traffic::transceiver::ErasedGatewayError; use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; -use nym_topology::gateway::GatewayConversionError; +use nym_topology::node::RoutingNodeError; use nym_topology::{NodeId, NymTopologyError}; use nym_validator_client::ValidatorClientError; use std::error::Error; @@ -75,10 +75,10 @@ pub enum ClientCoreError { #[error("the gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), - #[error("The gateway is malformed: {source}")] + #[error("the node is malformed: {source}")] MalformedGateway { #[from] - source: GatewayConversionError, + source: RoutingNodeError, }, #[error("failed to establish connection to gateway: {source}")] diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index d4f843a76c1..4954efbb3d4 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -19,7 +19,6 @@ use crate::init::types::{ use nym_client_core_gateways_storage::GatewaysDetailsStore; use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration}; use nym_gateway_client::client::InitGatewayClient; -use nym_topology::gateway; use nym_topology::node::RoutingNode; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 3f4e4762baf..2f9b9893b5a 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -13,7 +13,6 @@ use nym_crypto::asymmetric::identity; use nym_gateway_client::client::InitGatewayClient; use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_sphinx::addressing::clients::Recipient; -use nym_topology::gateway; use nym_topology::node::RoutingNode; use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; @@ -42,13 +41,20 @@ impl SelectedGateway { node: RoutingNode, must_use_tls: bool, ) -> Result { + // for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : ) + let prefer_ipv6 = false; + let gateway_listener = if must_use_tls { - node.clients_address_tls() + node.ws_entry_address_tls() .ok_or(ClientCoreError::UnsupportedWssProtocol { gateway: node.identity_key.to_base58_string(), })? } else { - node.clients_address() + node.ws_entry_address(prefer_ipv6) + .ok_or(ClientCoreError::UnsupportedEntry { + id: node.node_id, + identity: node.identity_key, + })? }; let gateway_listener = diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index bc904fd05d6..89cc01a9b4e 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -194,6 +194,7 @@ impl Default for SphinxMessageReceiver { #[cfg(test)] mod message_receiver { + use super::*; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::LegacyMixLayer; diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 478f34f2cc0..0b6c9570bd2 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,7 +1,6 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::node::RoutingNode; use ::serde::{Deserialize, Serialize}; use log::{debug, warn}; use nym_api_requests::nym_nodes::SkimmedNode; @@ -13,6 +12,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::net::IpAddr; +pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; pub use error::NymTopologyError; pub use nym_mixnet_contract_common::nym_node::Role; pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId}; @@ -222,6 +222,25 @@ impl NymTopology { } } + pub fn has_node_details(&self, node_id: NodeId) -> bool { + self.node_details.contains_key(&node_id) + } + + pub fn insert_node_details(&mut self, node_details: RoutingNode) { + self.node_details.insert(node_details.node_id, node_details); + } + + pub fn force_set_active(&mut self, node_id: NodeId, role: Role) { + match role { + Role::EntryGateway => self.rewarded_set.entry_gateways.insert(node_id), + Role::Layer1 => self.rewarded_set.layer1.insert(node_id), + Role::Layer2 => self.rewarded_set.layer2.insert(node_id), + Role::Layer3 => self.rewarded_set.layer3.insert(node_id), + Role::ExitGateway => self.rewarded_set.exit_gateways.insert(node_id), + Role::Standby => self.rewarded_set.standby.insert(node_id), + }; + } + fn node_details_exists(&self, ids: &HashSet) -> bool { for id in ids { if self.node_details.contains_key(id) { diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 1311bb4f574..86087582e18 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -41,7 +41,6 @@ use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{TaskClient, TaskManager}; -use nym_topology::NetworkAddress; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::NodeRefreshBody; use nym_validator_client::{NymApiClient, UserAgent}; @@ -535,8 +534,10 @@ impl NymNode { )) } - fn as_gateway_topology_node(&self) -> Result { - let Some(ip) = self.config.host.public_ips.first() else { + fn as_gateway_topology_node(&self) -> Result { + let ip_addresses = self.config.host.public_ips.clone(); + + let Some(ip) = ip_addresses.first() else { return Err(NymNodeError::NoPublicIps); }; @@ -553,15 +554,22 @@ impl NymNode { .announce_ws_port .unwrap_or(self.config.gateway_tasks.bind_address.port()); - Ok(nym_topology::gateway::LegacyNode { + Ok(nym_topology::RoutingNode { node_id: u32::MAX, mix_host, - host: NetworkAddress::IpAddr(*ip), - clients_ws_port, - clients_wss_port: self.config.gateway_tasks.announce_wss_port, + entry: Some(nym_topology::EntryDetails { + ip_addresses, + clients_ws_port, + hostname: self.config.host.hostname.clone(), + clients_wss_port: self.config.gateway_tasks.announce_wss_port, + }), sphinx_key: *self.x25519_sphinx_key(), identity_key: *self.ed25519_identity_key(), - version: env!("CARGO_PKG_VERSION").into(), + supported_roles: nym_topology::SupportedRoles { + mixnode: false, + mixnet_entry: true, + mixnet_exit: true, + }, }) } diff --git a/nym-node/src/node/shared_topology.rs b/nym-node/src/node/shared_topology.rs index a4a96676155..b65f1e1a5bb 100644 --- a/nym-node/src/node/shared_topology.rs +++ b/nym-node/src/node/shared_topology.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use nym_gateway::node::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; -use nym_topology::{gateway, NymTopology, TopologyProvider}; +use nym_topology::node::RoutingNode; +use nym_topology::{NymTopology, Role, TopologyProvider}; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; @@ -20,7 +21,7 @@ pub struct NymNodeTopologyProvider { impl NymNodeTopologyProvider { pub fn new( - gateway_node: gateway::LegacyNode, + gateway_node: RoutingNode, cache_ttl: Duration, user_agent: UserAgent, nym_api_url: Vec, @@ -51,7 +52,7 @@ struct NymNodeTopologyProviderInner { cache_ttl: Duration, cached_at: OffsetDateTime, cached: Option, - gateway_node: gateway::LegacyNode, + gateway_node: RoutingNode, } impl NymNodeTopologyProviderInner { @@ -69,13 +70,14 @@ impl NymNodeTopologyProviderInner { let updated_cache = match self.inner.get_new_topology().await { None => None, Some(mut base) => { - if !base.gateway_exists(&self.gateway_node.identity_key) { + if !base.has_node_details(self.gateway_node.node_id) { debug!( "{} didn't exist in topology. inserting it.", self.gateway_node.identity_key ); - base.insert_gateway(self.gateway_node.clone()); + base.insert_node_details(self.gateway_node.clone()); } + base.force_set_active(self.gateway_node.node_id, Role::EntryGateway); Some(base) } }; diff --git a/service-providers/network-requester/src/reply.rs b/service-providers/network-requester/src/reply.rs index 6523383ad99..8bfd67e0708 100644 --- a/service-providers/network-requester/src/reply.rs +++ b/service-providers/network-requester/src/reply.rs @@ -191,7 +191,6 @@ impl MixnetAddress { recipient: *recipient, data: message, lane: TransmissionLane::ConnectionId(connection_id), - mix_hops: None, }), packet_type, }, From e672673a57aa687cdc0739431ff352949a0a9f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Dec 2024 13:35:54 +0000 Subject: [PATCH 6/9] updated rest of non-test code --- .../topology_control/geo_aware_provider.rs | 13 +++- common/topology/src/lib.rs | 73 +++++++++---------- 2 files changed, 47 insertions(+), 39 deletions(-) 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 d9a0519e0b1..ffdfcb821cb 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,6 @@ use log::{debug, error}; use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ - nym_topology_from_basic_info, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; @@ -90,6 +89,15 @@ impl GeoAwareTopologyProvider { } async fn get_topology(&self) -> Option { + let rewarded_set = self + .validator_client + .get_current_rewarded_set() + .await + .inspect_err(|err| error!("failed to get current rewarded set: {err}")) + .ok()?; + + let mut topology = NymTopology::new_empty(rewarded_set); + let mixnodes = match self .validator_client .get_all_basic_active_mixing_assigned_nodes() @@ -173,7 +181,8 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_basic_info(&mixnodes, &gateways); + topology.add_additional_nodes(mixnodes.iter()); + topology.add_additional_nodes(gateways.iter()); // TODO: return real error type check_layer_integrity(topology.clone()).ok()?; diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 0b6c9570bd2..8dda20f4158 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,7 +3,6 @@ use ::serde::{Deserialize, Serialize}; use log::{debug, warn}; -use nym_api_requests::nym_nodes::SkimmedNode; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::IteratorRandom; @@ -849,42 +848,42 @@ impl NymTopology { // } // } -pub fn nym_topology_from_basic_info( - basic_mixes: &[SkimmedNode], - basic_gateways: &[SkimmedNode], -) -> NymTopology { - todo!() - // let mut mixes = BTreeMap::new(); - // for mix in basic_mixes { - // let Some(layer) = mix.get_mix_layer() else { - // warn!("node {} doesn't have any assigned mix layer!", mix.node_id); - // continue; - // }; - // - // let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - // match mix.try_into() { - // Ok(mix) => layer_entry.push(mix), - // Err(err) => { - // warn!("node (mixnode) {} is malformed: {err}", mix.node_id); - // continue; - // } - // } - // } - // - // let mut gateways = Vec::with_capacity(basic_gateways.len()); - // for gateway in basic_gateways { - // match gateway.try_into() { - // Ok(gate) => gateways.push(gate), - // Err(err) => { - // warn!("node (gateway) {} is malformed: {err}", gateway.node_id); - // continue; - // } - // } - // } - // - // // NymTopology::new(mixes, gateways) - // todo!() -} +// pub fn nym_topology_from_basic_info( +// basic_mixes: &[SkimmedNode], +// basic_gateways: &[SkimmedNode], +// ) -> NymTopology { +// todo!() +// // let mut mixes = BTreeMap::new(); +// // for mix in basic_mixes { +// // let Some(layer) = mix.get_mix_layer() else { +// // warn!("node {} doesn't have any assigned mix layer!", mix.node_id); +// // continue; +// // }; +// // +// // let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); +// // match mix.try_into() { +// // Ok(mix) => layer_entry.push(mix), +// // Err(err) => { +// // warn!("node (mixnode) {} is malformed: {err}", mix.node_id); +// // continue; +// // } +// // } +// // } +// // +// // let mut gateways = Vec::with_capacity(basic_gateways.len()); +// // for gateway in basic_gateways { +// // match gateway.try_into() { +// // Ok(gate) => gateways.push(gate), +// // Err(err) => { +// // warn!("node (gateway) {} is malformed: {err}", gateway.node_id); +// // continue; +// // } +// // } +// // } +// // +// // // NymTopology::new(mixes, gateways) +// // todo!() +// } #[cfg(test)] mod converting_mixes_to_vec { From aa0c46b6a2bd263a74cc72f0530a8bd95261431a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Dec 2024 16:21:35 +0000 Subject: [PATCH 7/9] updated the rest of the codebase --- common/client-core/Cargo.toml | 2 +- .../client-core/src/client/base_client/mod.rs | 10 +- .../topology_control/geo_aware_provider.rs | 8 +- .../src/client/topology_control/mod.rs | 1 + .../topology_control/nym_api_provider.rs | 6 +- common/client-core/src/error.rs | 8 +- common/client-core/src/init/helpers.rs | 4 +- common/client-core/src/init/types.rs | 2 +- common/node-tester-utils/src/tester.rs | 3 +- common/nymsphinx/chunking/src/lib.rs | 4 +- common/nymsphinx/src/receiver.rs | 106 +--- common/topology/src/error.rs | 4 +- common/topology/src/gateway.rs | 179 ------ common/topology/src/lib.rs | 581 +++--------------- common/topology/src/mix.rs | 132 ---- common/topology/src/node.rs | 25 +- common/topology/src/wasm_helpers.rs | 37 +- common/wasm/client-core/src/config/mod.rs | 13 + .../wasm/client-core/src/config/override.rs | 17 + common/wasm/client-core/src/helpers.rs | 20 +- common/wasm/client-core/src/topology.rs | 41 +- nym-network-monitor/src/accounting.rs | 30 +- nym-network-monitor/src/main.rs | 23 +- .../examples/custom_topology_provider.rs | 14 +- .../nym-sdk/examples/geo_topology_provider.rs | 50 -- .../examples/manually_overwrite_topology.rs | 74 ++- sdk/rust/nym-sdk/src/lib.rs | 1 + sdk/rust/nym-sdk/src/mixnet.rs | 1 + wasm/client/Cargo.toml | 8 +- wasm/client/src/client.rs | 6 +- wasm/client/src/helpers.rs | 33 +- wasm/node-tester/src/tester.rs | 6 +- 32 files changed, 327 insertions(+), 1122 deletions(-) delete mode 100644 common/topology/src/gateway.rs delete mode 100644 common/topology/src/mix.rs delete mode 100644 sdk/rust/nym-sdk/examples/geo_topology_provider.rs diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 63b92f9b8b2..f6536b9762e 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -3,7 +3,7 @@ name = "nym-client-core" version = "1.1.15" authors = ["Dave Hrycyszyn "] edition = "2021" -rust-version = "1.70" +rust-version = "1.76" license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 5dd57155a50..3057128afdc 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -3,7 +3,6 @@ use super::received_buffer::ReceivedBufferMessage; use super::statistics_control::StatisticsControl; -use super::topology_control::geo_aware_provider::GeoAwareTopologyProvider; use crate::client::base_client::storage::helpers::store_client_keys; use crate::client::base_client::storage::MixnetClientStorage; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; @@ -464,8 +463,8 @@ where details_store .upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key) .await.map_err(|err| { - error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}"); - ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) } + error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}"); + ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) } })? } @@ -544,7 +543,10 @@ where user_agent, )), config::TopologyStructure::GeoAware(group_by) => { - Box::new(GeoAwareTopologyProvider::new(nym_api_urls, group_by)) + warn!("using deprecated 'GeoAware' topology provider - this option will be removed very soon"); + + #[allow(deprecated)] + Box::new(crate::client::topology_control::GeoAwareTopologyProvider::new(nym_api_urls, group_by)) } }) } 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 ffdfcb821cb..459209a977c 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 @@ -67,11 +67,13 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { Ok(()) } +#[deprecated(note = "use NymApiTopologyProvider instead as explorer API will soon be removed")] pub struct GeoAwareTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, filter_on: GroupBy, } +#[allow(deprecated)] impl GeoAwareTopologyProvider { pub fn new(mut nym_api_urls: Vec, filter_on: GroupBy) -> GeoAwareTopologyProvider { log::info!( @@ -181,8 +183,8 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - topology.add_additional_nodes(mixnodes.iter()); - topology.add_additional_nodes(gateways.iter()); + topology.add_skimmed_nodes(&mixnodes); + topology.add_skimmed_nodes(&gateways); // TODO: return real error type check_layer_integrity(topology.clone()).ok()?; @@ -191,6 +193,7 @@ impl GeoAwareTopologyProvider { } } +#[allow(deprecated)] #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for GeoAwareTopologyProvider { @@ -200,6 +203,7 @@ impl TopologyProvider for GeoAwareTopologyProvider { } } +#[allow(deprecated)] #[cfg(target_arch = "wasm32")] #[async_trait(?Send)] impl TopologyProvider for GeoAwareTopologyProvider { diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index 121e08e98ed..a19497e1976 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -19,6 +19,7 @@ mod accessor; pub mod geo_aware_provider; pub mod nym_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; 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 4f41d27671a..30d2461abd7 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 @@ -164,6 +164,10 @@ impl TopologyProvider for NymApiTopologyProvider { #[async_trait(?Send)] impl TopologyProvider for NymApiTopologyProvider { async fn get_new_topology(&mut self) -> Option { - self.get_current_compatible_topology().await + let Some(topology) = self.get_current_compatible_topology().await else { + self.use_next_nym_api(); + return None; + }; + Some(topology) } } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index b115acb82c4..5aaedc84ae7 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::mix_traffic::transceiver::ErasedGatewayError; -use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; use nym_topology::node::RoutingNodeError; @@ -78,7 +77,7 @@ pub enum ClientCoreError { #[error("the node is malformed: {source}")] MalformedGateway { #[from] - source: RoutingNodeError, + source: Box, }, #[error("failed to establish connection to gateway: {source}")] @@ -161,10 +160,7 @@ pub enum ClientCoreError { UnsupportedWssProtocol { gateway: String }, #[error("node {id} ({identity}) does not support mixnet entry mode")] - UnsupportedEntry { - id: NodeId, - identity: ed25519::PublicKey, - }, + UnsupportedEntry { id: NodeId, identity: String }, #[error( "failed to load custom topology using path '{}'. detailed message: {source}", file_path.display() diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 5c35be69262..e4cb9f42a74 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -145,7 +145,7 @@ where let Some(addr) = gateway.clients_address(false) else { return Err(ClientCoreError::UnsupportedEntry { id: gateway.node_id(), - identity: gateway.identity(), + identity: gateway.identity().to_string(), }); }; trace!( @@ -291,7 +291,7 @@ pub(super) fn get_specified_gateway( let Some(entry_details) = gateway.entry.as_ref() else { return Err(ClientCoreError::UnsupportedEntry { id: gateway.node_id, - identity: gateway.identity(), + identity: gateway.identity().to_string(), }); }; diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 2f9b9893b5a..35a5a5a1491 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -53,7 +53,7 @@ impl SelectedGateway { node.ws_entry_address(prefer_ipv6) .ok_or(ClientCoreError::UnsupportedEntry { id: node.node_id, - identity: node.identity_key, + identity: node.identity_key.to_base58_string(), })? }; diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index 97e17b67b21..211eb988dbd 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -9,7 +9,6 @@ use nym_sphinx::message::NymMessage; use nym_sphinx::params::PacketSize; use nym_sphinx::preparer::{FragmentPreparer, PreparedFragment}; use nym_sphinx_params::PacketType; -use nym_topology::node::LegacyMixLayer; use nym_topology::node::RoutingNode; use nym_topology::{NymRouteProvider, NymTopology, Role}; use rand::{CryptoRng, Rng}; @@ -17,6 +16,8 @@ use serde::Serialize; use std::sync::Arc; use std::time::Duration; +pub use nym_topology::node::LegacyMixLayer; + pub struct NodeTester { rng: R, diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 5500363594e..5a6e24633cd 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -85,8 +85,8 @@ pub struct FragmentMixParams { } impl FragmentMixParams { - pub fn destination(&self) -> &PublicKey { - &self.destination + pub fn destination(&self) -> PublicKey { + self.destination } } diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 89cc01a9b4e..f2f3feadd92 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -154,7 +154,7 @@ pub trait MessageReceiver { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct SphinxMessageReceiver { /// High level public structure used to buffer all received data [`Fragment`]s and eventually /// returning original messages that they encapsulate. @@ -183,107 +183,3 @@ impl MessageReceiver for SphinxMessageReceiver { &mut self.reconstructor } } - -impl Default for SphinxMessageReceiver { - fn default() -> Self { - SphinxMessageReceiver { - reconstructor: Default::default(), - } - } -} - -#[cfg(test)] -mod message_receiver { - - use super::*; - use nym_crypto::asymmetric::identity; - use nym_mixnet_contract_common::LegacyMixLayer; - use nym_topology::{gateway, mix, NymTopology}; - use std::collections::BTreeMap; - - // TODO: is it somehow maybe possible to move it to `topology` and have if conditionally - // available to other modules? - /// Returns a hardcoded, valid instance of [`NymTopology`] that is to be used in - /// tests requiring instance of topology. - #[allow(dead_code)] - fn topology_fixture() -> NymTopology { - let mut mixes = BTreeMap::new(); - mixes.insert( - 1, - vec![mix::LegacyNode { - mix_id: 123, - host: "10.20.30.40".parse().unwrap(), - mix_host: "10.20.30.40:1789".parse().unwrap(), - identity_key: identity::PublicKey::from_base58_string( - "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7", - ) - .unwrap(), - sphinx_key: encryption::PublicKey::from_base58_string( - "B3GzG62aXAZNg14RoMCp3BhELNBrySLr2JqrwyfYFzRc", - ) - .unwrap(), - layer: LegacyMixLayer::One, - version: "0.8.0-dev".into(), - }], - ); - - mixes.insert( - 2, - vec![mix::LegacyNode { - mix_id: 234, - host: "11.21.31.41".parse().unwrap(), - mix_host: "11.21.31.41:1789".parse().unwrap(), - identity_key: identity::PublicKey::from_base58_string( - "D6YaMzLSY7mANtSQRKXsmMZpqgqiVkeiagKM4V4oFPFr", - ) - .unwrap(), - sphinx_key: encryption::PublicKey::from_base58_string( - "5Z1VqYwM2xeKxd8H7fJpGWasNiDFijYBAee7MErkZ5QT", - ) - .unwrap(), - layer: LegacyMixLayer::Two, - version: "0.8.0-dev".into(), - }], - ); - - mixes.insert( - 3, - vec![mix::LegacyNode { - mix_id: 456, - host: "12.22.32.42".parse().unwrap(), - mix_host: "12.22.32.42:1789".parse().unwrap(), - identity_key: identity::PublicKey::from_base58_string( - "GkWDysw4AjESv1KiAiVn7JzzCMJeksxNSXVfr1PpX8wD", - ) - .unwrap(), - sphinx_key: encryption::PublicKey::from_base58_string( - "9EyjhCggr2QEA2nakR88YHmXgpy92DWxoe2draDRkYof", - ) - .unwrap(), - layer: LegacyMixLayer::Three, - version: "0.8.0-dev".into(), - }], - ); - - NymTopology::new( - // currently coco_nodes don't really exist so this is still to be determined - mixes, - vec![gateway::LegacyNode { - node_id: 789, - host: "1.2.3.4".parse().unwrap(), - mix_host: "1.2.3.4:1789".parse().unwrap(), - clients_ws_port: 9000, - clients_wss_port: None, - identity_key: identity::PublicKey::from_base58_string( - "FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML", - ) - .unwrap(), - sphinx_key: encryption::PublicKey::from_base58_string( - "EB42xvMFMD5rUCstE2CDazgQQJ22zLv8SPm1Luxni44c", - ) - .unwrap(), - version: "0.8.0-dev".into(), - }], - ) - } -} diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 02e900f47ca..bf7efc334fb 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -14,10 +14,10 @@ pub enum NymTopologyError { EmptyNetworkTopology, #[error("no node with identity {node_identity} is known")] - NonExistentNode { node_identity: NodeIdentity }, + NonExistentNode { node_identity: Box }, #[error("could not use node with identity {node_identity} as egress since it didn't get assigned valid role in the current epoch")] - InvalidEgressRole { node_identity: NodeIdentity }, + InvalidEgressRole { node_identity: Box }, #[error("one (or more) of mixing layers does not have any valid nodes available")] InsufficientMixingNodes, diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs deleted file mode 100644 index 1fd09f0aa7a..00000000000 --- a/common/topology/src/gateway.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; -use crate::{NetworkAddress, NodeVersion}; -use nym_api_requests::nym_nodes::SkimmedNode; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::NodeId; -use nym_sphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; -use nym_sphinx_types::Node as SphinxNode; -use rand::seq::SliceRandom; -use rand::thread_rng; -use std::fmt; -use std::fmt::Formatter; -use std::io; -use std::net::AddrParseError; -use std::net::SocketAddr; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum GatewayConversionError { - #[error("gateway identity key was malformed - {0}")] - InvalidIdentityKey(#[from] identity::Ed25519RecoveryError), - - #[error("gateway sphinx key was malformed - {0}")] - InvalidSphinxKey(#[from] encryption::KeyRecoveryError), - - #[error("'{value}' is not a valid gateway address - {source}")] - InvalidAddress { - value: String, - #[source] - source: io::Error, - }, - - #[error("'{gateway}' has not provided any valid ip addresses")] - NoIpAddressesProvided { gateway: String }, - - #[error("'{gateway}' has provided a malformed ip address: {err}")] - MalformedIpAddress { - gateway: String, - - #[source] - err: AddrParseError, - }, - - #[error("provided node is not an entry gateway in this epoch!")] - NotGateway, -} - -#[derive(Clone)] -pub struct LegacyNode { - pub node_id: NodeId, - - pub host: NetworkAddress, - // we're keeping this as separate resolved field since we do not want to be resolving the potential - // hostname every time we want to construct a path via this node - pub mix_host: SocketAddr, - - // #[serde(alias = "clients_port")] - pub clients_ws_port: u16, - - // #[serde(default)] - pub clients_wss_port: Option, - - pub identity_key: identity::PublicKey, - pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 - - // to be removed: - pub version: NodeVersion, -} - -impl From for RoutingNode { - fn from(gateway: LegacyNode) -> Self { - RoutingNode { - node_id: gateway.node_id, - mix_host: gateway.mix_host, - entry: Some(EntryDetails { - ip_addresses: vec![gateway.mix_host.ip()], - clients_ws_port: gateway.clients_ws_port, - hostname: gateway.host.as_hostname(), - clients_wss_port: gateway.clients_wss_port, - }), - identity_key: gateway.identity_key, - sphinx_key: gateway.sphinx_key, - supported_roles: SupportedRoles { - mixnode: false, - mixnet_entry: true, - mixnet_exit: false, - }, - } - } -} - -impl std::fmt::Debug for LegacyNode { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("gateway::Node") - .field("host", &self.host) - .field("mix_host", &self.mix_host) - .field("clients_ws_port", &self.clients_ws_port) - .field("clients_wss_port", &self.clients_wss_port) - .field("identity_key", &self.identity_key.to_base58_string()) - .field("sphinx_key", &self.sphinx_key.to_base58_string()) - .field("version", &self.version) - .finish() - } -} - -impl LegacyNode { - pub fn identity(&self) -> &NodeIdentity { - &self.identity_key - } - - pub fn clients_address(&self) -> String { - self.clients_address_tls() - .unwrap_or_else(|| self.clients_address_no_tls()) - } - - pub fn clients_address_no_tls(&self) -> String { - format!("ws://{}:{}", self.host, self.clients_ws_port) - } - - pub fn clients_address_tls(&self) -> Option { - self.clients_wss_port - .map(|p| format!("wss://{}:{p}", self.host)) - } -} - -impl fmt::Display for LegacyNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "legacy gateway {} @ {}", self.node_id, self.host) - } -} - -impl<'a> From<&'a LegacyNode> for SphinxNode { - fn from(node: &'a LegacyNode) -> Self { - let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) - .try_into() - .unwrap(); - - SphinxNode::new(node_address_bytes, (&node.sphinx_key).into()) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { - type Error = GatewayConversionError; - - fn try_from(value: &'a SkimmedNode) -> Result { - let Some(entry_details) = &value.entry else { - return Err(GatewayConversionError::NotGateway); - }; - - if value.ip_addresses.is_empty() { - return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.ed25519_identity_pubkey.to_base58_string(), - }); - } - - // safety: we just checked the slice is not empty - #[allow(clippy::unwrap_used)] - let ip = value.ip_addresses.choose(&mut thread_rng()).unwrap(); - - let host = if let Some(hostname) = &entry_details.hostname { - NetworkAddress::Hostname(hostname.to_string()) - } else { - NetworkAddress::IpAddr(*ip) - }; - - Ok(LegacyNode { - node_id: value.node_id, - host, - mix_host: SocketAddr::new(*ip, value.mix_port), - clients_ws_port: entry_details.ws_port, - clients_wss_port: entry_details.wss_port, - identity_key: value.ed25519_identity_pubkey, - sphinx_key: value.x25519_sphinx_pubkey, - version: NodeVersion::Unknown, - }) - } -} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 8dda20f4158..d942de49406 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,10 +3,12 @@ use ::serde::{Deserialize, Serialize}; use log::{debug, warn}; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::IteratorRandom; use rand::{CryptoRng, Rng}; +use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::net::IpAddr; @@ -18,19 +20,13 @@ pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId}; pub use rewarded_set::CachedEpochRewardedSet; pub mod error; - -// #[deprecated] -// pub mod gateway; -// -// #[deprecated] -// pub mod mix; pub mod node; pub mod rewarded_set; #[cfg(feature = "provider-trait")] pub mod provider_trait; #[cfg(feature = "wasm-serde-types")] -pub(crate) mod wasm_helpers; +pub mod wasm_helpers; #[cfg(feature = "provider-trait")] pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider}; @@ -107,7 +103,7 @@ pub struct NymTopology { node_details: HashMap, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct NymRouteProvider { pub topology: NymTopology, @@ -162,6 +158,10 @@ impl NymRouteProvider { .egress_by_identity(node_identity, self.ignore_egress_epoch_roles) } + pub fn node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> { + self.topology.find_node_by_identity(node_identity) + } + /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1, /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node pub fn random_route_to_egress( @@ -175,6 +175,18 @@ impl NymRouteProvider { self.topology .random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles) } + + pub fn random_path_to_egress( + &self, + rng: &mut R, + egress_identity: NodeIdentity, + ) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + self.topology + .random_path_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles) + } } impl NymTopology { @@ -201,6 +213,27 @@ impl NymTopology { serde_json::from_reader(file).map_err(Into::into) } + pub fn add_skimmed_nodes(&mut self, nodes: &[SkimmedNode]) { + self.add_additional_nodes(nodes.iter()) + } + + pub fn add_routing_nodes>( + &mut self, + nodes: impl IntoIterator, + ) { + for node_details in nodes { + let node_details = node_details.borrow(); + let node_id = node_details.node_id; + if self + .node_details + .insert(node_id, node_details.clone()) + .is_some() + { + debug!("overwriting node details for node {node_id}") + } + } + } + pub fn add_additional_nodes(&mut self, nodes: impl Iterator) where N: TryInto, @@ -229,6 +262,10 @@ impl NymTopology { self.node_details.insert(node_details.node_id, node_details); } + pub fn rewarded_set(&self) -> &CachedEpochRewardedSet { + &self.rewarded_set + } + pub fn force_set_active(&mut self, node_id: NodeId, role: Role) { match role { Role::EntryGateway => self.rewarded_set.entry_gateways.insert(node_id), @@ -277,15 +314,11 @@ impl NymTopology { Ok(()) } - fn get_sphinx_node(&self, node_id: NodeId) -> Option { - self.node_details.get(&node_id).map(Into::into) - } - fn find_valid_mix_hop( &self, rng: &mut R, id_choices: Vec, - ) -> Result + ) -> Result<&RoutingNode, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -295,7 +328,7 @@ impl NymTopology { // SAFETY: this is not run if the vector is empty let candidate_id = id_choices[index]; - match self.get_sphinx_node(candidate_id) { + match self.node_details.get(&candidate_id) { Some(node) => { return Ok(node); } @@ -314,7 +347,7 @@ impl NymTopology { &self, rng: &mut R, assigned_nodes: &HashSet, - ) -> Result + ) -> Result<&RoutingNode, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -325,7 +358,7 @@ impl NymTopology { return Err(NymTopologyError::NoMixnodesAvailable); }; - match self.get_sphinx_node(*candidate) { + match self.node_details.get(candidate) { Some(node) => Ok(node), None => { let remaining_choices = assigned_nodes @@ -354,17 +387,23 @@ impl NymTopology { ignore_epoch_roles: bool, ) -> Result<&RoutingNode, NymTopologyError> { let Some(node) = self.find_node_by_identity(node_identity) else { - return Err(NymTopologyError::NonExistentNode { node_identity }); + return Err(NymTopologyError::NonExistentNode { + node_identity: Box::new(node_identity), + }); }; // a 'valid' egress is one assigned to either entry role (i.e. entry for another client) // or exit role (as a service provider) if !ignore_epoch_roles { let Some(role) = self.rewarded_set.role(node.node_id) else { - return Err(NymTopologyError::InvalidEgressRole { node_identity }); + return Err(NymTopologyError::InvalidEgressRole { + node_identity: Box::new(node_identity), + }); }; if !matches!(role, Role::EntryGateway | Role::ExitGateway) { - return Err(NymTopologyError::InvalidEgressRole { node_identity }); + return Err(NymTopologyError::InvalidEgressRole { + node_identity: Box::new(node_identity), + }); } } Ok(node) @@ -379,7 +418,7 @@ impl NymTopology { .map(Into::into) } - pub fn random_mix_route(&self, rng: &mut R) -> Result, NymTopologyError> + fn random_mix_path_nodes(&self, rng: &mut R) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -397,6 +436,17 @@ impl NymTopology { Ok(mix_route) } + pub fn random_mix_route(&self, rng: &mut R) -> Result, NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + Ok(self + .random_mix_path_nodes(rng)? + .into_iter() + .map(Into::into) + .collect()) + } + /// Tries to create a route to the egress point, such that it goes through mixnode on layer 1, /// mixnode on layer2, .... mixnode on layer n and finally the target egress, which can be any known node pub fn random_route_to_egress( @@ -414,7 +464,21 @@ impl NymTopology { Ok(mix_route) } - pub fn nodes_with_role<'a>(&'a self, role: Role) -> impl Iterator + 'a { + pub fn random_path_to_egress( + &self, + rng: &mut R, + egress_identity: NodeIdentity, + ignore_epoch_roles: bool, + ) -> Result<(Vec<&RoutingNode>, &RoutingNode), NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + let egress = self.egress_by_identity(egress_identity, ignore_epoch_roles)?; + let mix_route = self.random_mix_path_nodes(rng)?; + Ok((mix_route, egress)) + } + + pub fn nodes_with_role(&self, role: Role) -> impl Iterator { self.node_details.values().filter(move |node| match role { Role::EntryGateway => self.rewarded_set.entry_gateways.contains(&node.node_id), Role::Layer1 => self.rewarded_set.layer1.contains(&node.node_id), @@ -468,478 +532,3 @@ impl NymTopology { .filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id)) } } - -// // the reason for those having `Legacy` prefix is that eventually they should be using -// // exactly the same types -// #[derive(Debug, Clone, Default)] -// pub struct NymTopologyOld { -// mixes: BTreeMap>, -// gateways: Vec, -// } -// -// impl NymTopologyOld { -// #[deprecated] -// pub async fn new_from_env() -> Result { -// let api_url = std::env::var(NYM_API)?; -// -// info!("Generating topology from {api_url}"); -// -// let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) -// .await? -// .json::>() -// .await? -// .nodes -// .iter() -// .map(mix::LegacyNode::try_from) -// .filter(Result::is_ok) -// .collect::, _>>()?; -// -// let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) -// .await? -// .json::>() -// .await? -// .nodes -// .iter() -// .map(gateway::LegacyNode::try_from) -// .filter(Result::is_ok) -// .collect::, _>>()?; -// let topology = Self::new_unordered(mixnodes, gateways); -// Ok(topology) -// } -// -// pub fn new( -// mixes: BTreeMap>, -// gateways: Vec, -// ) -> Self { -// NymTopologyOld { mixes, gateways } -// } -// -// #[deprecated] -// pub fn new_unordered( -// unordered_mixes: Vec, -// gateways: Vec, -// ) -> Self { -// let mut mixes = BTreeMap::new(); -// for node in unordered_mixes.into_iter() { -// let layer = node.layer as MixLayer; -// let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); -// layer_entry.push(node) -// } -// -// NymTopologyOld { mixes, gateways } -// } -// -// pub fn from_unordered(unordered_mixes: MI, unordered_gateways: GI) -> Self -// where -// MI: Iterator, -// GI: Iterator, -// G: TryInto, -// M: TryInto, -// >::Error: Display, -// >::Error: Display, -// { -// let mut mixes = BTreeMap::new(); -// let mut gateways = Vec::new(); -// -// for node in unordered_mixes.into_iter() { -// match node.try_into() { -// Ok(mixnode) => mixes -// .entry(mixnode.layer as MixLayer) -// .or_insert_with(Vec::new) -// .push(mixnode), -// Err(err) => debug!("malformed mixnode: {err}"), -// } -// } -// -// for node in unordered_gateways.into_iter() { -// match node.try_into() { -// Ok(gateway) => gateways.push(gateway), -// Err(err) => debug!("malformed gateway: {err}"), -// } -// } -// -// NymTopologyOld::new(mixes, gateways) -// } -// -// #[cfg(feature = "serde")] -// pub fn new_from_file>(path: P) -> std::io::Result { -// todo!() -// // let file = std::fs::File::open(path)?; -// // serde_json::from_reader(file).map_err(Into::into) -// } -// -// pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { -// todo!() -// // nym_topology_from_basic_info(basic_mixes, basic_gateways) -// } -// -// pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { -// for nodes in self.mixes.values() { -// for node in nodes { -// if node.mix_id == mix_id { -// return Some(node); -// } -// } -// } -// None -// } -// -// pub fn find_mix_by_identity( -// &self, -// mixnode_identity: IdentityKeyRef, -// ) -> Option<&mix::LegacyNode> { -// for nodes in self.mixes.values() { -// for node in nodes { -// if node.identity_key.to_base58_string() == mixnode_identity { -// return Some(node); -// } -// } -// } -// None -// } -// -// pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { -// self.gateways -// .iter() -// .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) -// } -// -// pub fn mixes(&self) -> &BTreeMap> { -// &self.mixes -// } -// -// pub fn num_mixnodes(&self) -> usize { -// self.mixes.values().map(|m| m.len()).sum() -// } -// -// pub fn mixes_as_vec(&self) -> Vec { -// let mut mixes: Vec = vec![]; -// -// for layer in self.mixes().values() { -// mixes.extend(layer.to_owned()) -// } -// mixes -// } -// -// pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { -// assert!([1, 2, 3].contains(&layer)); -// self.mixes.get(&layer).unwrap().to_owned() -// } -// -// pub fn gateways(&self) -> &[gateway::LegacyNode] { -// &self.gateways -// } -// -// pub fn get_gateways(&self) -> Vec { -// self.gateways.clone() -// } -// -// pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { -// self.gateways -// .iter() -// .find(|gateway| gateway.identity() == gateway_identity) -// } -// -// pub fn gateway_exists(&self, gateway_identity: &NodeIdentity) -> bool { -// 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 -// } -// -// pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> -// where -// R: Rng + CryptoRng, -// { -// self.gateways -// .choose(rng) -// .ok_or(NymTopologyError::NoGatewaysAvailable) -// } -// -// /// Returns a vec of size of `num_mix_hops` of mixnodes, such that each subsequent node is on -// /// next layer, starting from layer 1 -// pub fn random_mix_route( -// &self, -// rng: &mut R, -// num_mix_hops: u8, -// ) -> Result, NymTopologyError> -// where -// R: Rng + CryptoRng + ?Sized, -// { -// if self.mixes.len() < num_mix_hops as usize { -// return Err(NymTopologyError::InvalidNumberOfHopsError { -// available: self.mixes.len(), -// requested: num_mix_hops as usize, -// }); -// } -// let mut route = Vec::with_capacity(num_mix_hops as usize); -// -// // there is no "layer 0" -// for layer in 1..=num_mix_hops { -// // get all mixes on particular layer -// let layer_mixes = self -// .mixes -// .get(&layer) -// .ok_or(NymTopologyError::EmptyMixLayer { layer })?; -// -// // choose a random mix from the above list -// // this can return a 'None' only if slice is empty -// let random_mix = layer_mixes -// .choose(rng) -// .ok_or(NymTopologyError::EmptyMixLayer { layer })?; -// route.push(random_mix.clone()); -// } -// -// Ok(route) -// } -// -// pub fn random_path_to_egress( -// &self, -// rng: &mut R, -// num_mix_hops: u8, -// egress_identity: &NodeIdentity, -// ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> -// where -// R: Rng + CryptoRng + ?Sized, -// { -// let gateway = -// self.get_gateway(egress_identity) -// .ok_or(NymTopologyError::NonExistentGatewayError { -// identity_key: egress_identity.to_base58_string(), -// })?; -// -// let path = self.random_mix_route(rng, num_mix_hops)?; -// -// Ok((path, gateway.clone())) -// } -// -// /// Tries to create a route to the specified gateway, such that it goes through mixnode on layer 1, -// /// mixnode on layer2, .... mixnode on layer n and finally the target gateway -// pub fn random_route_to_egress( -// &self, -// rng: &mut R, -// num_mix_hops: u8, -// egress_identity: &NodeIdentity, -// ) -> Result, NymTopologyError> -// where -// R: Rng + CryptoRng + ?Sized, -// { -// let gateway = -// self.get_gateway(egress_identity) -// .ok_or(NymTopologyError::NonExistentGatewayError { -// identity_key: egress_identity.to_base58_string(), -// })?; -// -// Ok(self -// .random_mix_route(rng, num_mix_hops)? -// .into_iter() -// .map(|node| SphinxNode::from(&node)) -// .chain(std::iter::once(gateway.into())) -// .collect()) -// } -// -// /// Overwrites the existing nodes in the specified layer -// pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { -// self.mixes.insert(layer, mixes); -// } -// -// /// Checks if a mixnet path can be constructed using the specified number of hops -// pub fn ensure_can_construct_path_through( -// &self, -// num_mix_hops: u8, -// ) -> Result<(), NymTopologyError> { -// let mixnodes = self.mixes(); -// // 1. is it completely empty? -// if mixnodes.is_empty() && self.gateways().is_empty() { -// return Err(NymTopologyError::EmptyNetworkTopology); -// } -// -// // 2. does it have any mixnode at all? -// if mixnodes.is_empty() { -// return Err(NymTopologyError::NoMixnodesAvailable); -// } -// -// // 3. does it have any gateways at all? -// if self.gateways().is_empty() { -// return Err(NymTopologyError::NoGatewaysAvailable); -// } -// -// // 4. does it have a mixnode on each layer? -// for layer in 1..=num_mix_hops { -// match mixnodes.get(&layer) { -// None => return Err(NymTopologyError::EmptyMixLayer { layer }), -// Some(layer_nodes) => { -// if layer_nodes.is_empty() { -// return Err(NymTopologyError::EmptyMixLayer { layer }); -// } -// } -// } -// } -// -// Ok(()) -// } -// -// pub fn ensure_even_layer_distribution( -// &self, -// lower_threshold: f32, -// upper_threshold: f32, -// ) -> Result<(), NymTopologyError> { -// let mixnodes_count = self.num_mixnodes(); -// -// let layers = self -// .mixes -// .iter() -// .map(|(k, v)| (*k, v.len())) -// .collect::>(); -// -// if self.gateways.is_empty() { -// return Err(NymTopologyError::NoGatewaysAvailable); -// } -// -// if layers.is_empty() { -// return Err(NymTopologyError::NoMixnodesAvailable); -// } -// -// let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize; -// let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize; -// -// for (layer, nodes) in &layers { -// if nodes < &lower_bound || nodes > &upper_bound { -// return Err(NymTopologyError::UnevenLayerDistribution { -// layer: *layer, -// nodes: *nodes, -// lower_bound, -// upper_bound, -// total_nodes: mixnodes_count, -// layer_distribution: layers, -// }); -// } -// } -// -// Ok(()) -// } -// } -// -// #[cfg(feature = "serde")] -// impl Serialize for NymTopologyOld { -// fn serialize(&self, serializer: S) -> Result -// where -// S: Serializer, -// { -// todo!() -// // crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) -// } -// } -// -// #[cfg(feature = "serde")] -// impl<'de> Deserialize<'de> for NymTopologyOld { -// fn deserialize(deserializer: D) -> Result -// where -// D: Deserializer<'de>, -// { -// todo!() -// // let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; -// // serializable.try_into().map_err(::serde::de::Error::custom) -// } -// } - -// pub fn nym_topology_from_basic_info( -// basic_mixes: &[SkimmedNode], -// basic_gateways: &[SkimmedNode], -// ) -> NymTopology { -// todo!() -// // let mut mixes = BTreeMap::new(); -// // for mix in basic_mixes { -// // let Some(layer) = mix.get_mix_layer() else { -// // warn!("node {} doesn't have any assigned mix layer!", mix.node_id); -// // continue; -// // }; -// // -// // let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); -// // match mix.try_into() { -// // Ok(mix) => layer_entry.push(mix), -// // Err(err) => { -// // warn!("node (mixnode) {} is malformed: {err}", mix.node_id); -// // continue; -// // } -// // } -// // } -// // -// // let mut gateways = Vec::with_capacity(basic_gateways.len()); -// // for gateway in basic_gateways { -// // match gateway.try_into() { -// // Ok(gate) => gateways.push(gate), -// // Err(err) => { -// // warn!("node (gateway) {} is malformed: {err}", gateway.node_id); -// // continue; -// // } -// // } -// // } -// // -// // // NymTopology::new(mixes, gateways) -// // todo!() -// } - -#[cfg(test)] -mod converting_mixes_to_vec { - use super::*; - - #[cfg(test)] - mod when_nodes_exist { - use nym_crypto::asymmetric::{encryption, identity}; - - use super::*; - use nym_mixnet_contract_common::LegacyMixLayer; - - #[test] - fn returns_a_vec_with_hashmap_values() { - let node1 = mix::LegacyNode { - mix_id: 42, - host: "3.3.3.3".parse().unwrap(), - mix_host: "3.3.3.3:1789".parse().unwrap(), - identity_key: identity::PublicKey::from_base58_string( - "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7", - ) - .unwrap(), - sphinx_key: encryption::PublicKey::from_base58_string( - "C7cown6dYCLZpLiMFC1PaBmhvLvmJmLDJGeRTbPD45bX", - ) - .unwrap(), - layer: LegacyMixLayer::One, - version: "0.2.0".into(), - }; - - let node2 = mix::LegacyNode { ..node1.clone() }; - - let node3 = mix::LegacyNode { ..node1.clone() }; - - let mut mixes = BTreeMap::new(); - mixes.insert(1, vec![node1, node2]); - mixes.insert(2, vec![node3]); - - let topology = NymTopology::new(mixes, vec![]); - let mixvec = topology.mixes_as_vec(); - assert!(mixvec - .iter() - .any(|node| &node.identity_key.to_base58_string() - == "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7")); - } - } - - #[cfg(test)] - mod when_no_nodes_exist { - use super::*; - - #[test] - fn returns_an_empty_vec() { - let topology = NymTopology::new(BTreeMap::new(), vec![]); - let mixvec = topology.mixes_as_vec(); - assert!(mixvec.is_empty()); - } - } -} diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs deleted file mode 100644 index 1171e839b65..00000000000 --- a/common/topology/src/mix.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::node::{RoutingNode, SupportedRoles}; -use crate::{NetworkAddress, NodeVersion}; -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::NodeId; -use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; -use nym_sphinx_types::Node as SphinxNode; -use rand::seq::SliceRandom; -use rand::thread_rng; -use std::fmt::Formatter; -use std::io; -use std::net::SocketAddr; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum MixnodeConversionError { - #[error("mixnode identity key was malformed - {0}")] - InvalidIdentityKey(#[from] identity::Ed25519RecoveryError), - - #[error("mixnode sphinx key was malformed - {0}")] - InvalidSphinxKey(#[from] encryption::KeyRecoveryError), - - #[error("'{value}' is not a valid mixnode address - {source}")] - InvalidAddress { - value: String, - #[source] - source: io::Error, - }, - - #[error("invalid mix layer")] - InvalidLayer, - - #[error("'{mixnode}' has not provided any valid ip addresses")] - NoIpAddressesProvided { mixnode: String }, - - #[error("provided node is not a mixnode in this epoch!")] - NotMixnode, -} - -#[derive(Clone)] -pub struct LegacyNode { - pub mix_id: NodeId, - pub host: NetworkAddress, - // we're keeping this as separate resolved field since we do not want to be resolving the potential - // hostname every time we want to construct a path via this node - pub mix_host: SocketAddr, - pub identity_key: identity::PublicKey, - pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 - pub layer: LegacyMixLayer, - - // to be removed: - pub version: NodeVersion, -} - -impl From for RoutingNode { - fn from(node: LegacyNode) -> Self { - RoutingNode { - node_id: node.mix_id, - mix_host: node.mix_host, - entry: None, - identity_key: node.identity_key, - sphinx_key: node.sphinx_key, - supported_roles: SupportedRoles { - mixnode: true, - mixnet_entry: false, - mixnet_exit: false, - }, - } - } -} - -impl std::fmt::Debug for LegacyNode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("mix::Node") - .field("mix_id", &self.mix_id) - .field("host", &self.host) - .field("mix_host", &self.mix_host) - .field("identity_key", &self.identity_key.to_base58_string()) - .field("sphinx_key", &self.sphinx_key.to_base58_string()) - .field("layer", &self.layer) - .field("version", &self.version) - .finish() - } -} - -impl<'a> From<&'a LegacyNode> for SphinxNode { - fn from(node: &'a LegacyNode) -> Self { - let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) - .try_into() - .unwrap(); - - SphinxNode::new(node_address_bytes, (&node.sphinx_key).into()) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { - type Error = MixnodeConversionError; - - fn try_from(value: &'a SkimmedNode) -> Result { - if value.ip_addresses.is_empty() { - return Err(MixnodeConversionError::NoIpAddressesProvided { - mixnode: value.ed25519_identity_pubkey.to_base58_string(), - }); - } - - let layer = match value.role { - NodeRole::Mixnode { layer } => layer - .try_into() - .map_err(|_| MixnodeConversionError::InvalidLayer)?, - _ => return Err(MixnodeConversionError::NotMixnode), - }; - - // safety: we just checked the slice is not empty - #[allow(clippy::unwrap_used)] - let ip = value.ip_addresses.choose(&mut thread_rng()).unwrap(); - - let host = NetworkAddress::IpAddr(*ip); - - Ok(LegacyNode { - mix_id: value.node_id, - host, - mix_host: SocketAddr::new(*ip, value.mix_port), - identity_key: value.ed25519_identity_pubkey, - sphinx_key: value.x25519_sphinx_pubkey, - layer, - version: NodeVersion::Unknown, - }) - } -} diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index df749479a17..81ab236f765 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -15,14 +15,8 @@ pub use nym_mixnet_contract_common::LegacyMixLayer; #[derive(Error, Debug)] pub enum RoutingNodeError { - #[error("this node has no mixing information available")] - NoMixingInformationAvailable, - #[error("node {node_id} ('{identity}') has not provided any valid ip addresses")] - NoIpAddressesProvided { - node_id: NodeId, - identity: ed25519::PublicKey, - }, + NoIpAddressesProvided { node_id: NodeId, identity: String }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -126,19 +120,16 @@ impl<'a> TryFrom<&'a SkimmedNode> for RoutingNode { let Some(first_ip) = value.ip_addresses.first() else { return Err(RoutingNodeError::NoIpAddressesProvided { node_id: value.node_id, - identity: value.ed25519_identity_pubkey, + identity: value.ed25519_identity_pubkey.to_string(), }); }; - let entry = match &value.entry { - None => None, - Some(entry) => Some(EntryDetails { - ip_addresses: value.ip_addresses.clone(), - clients_ws_port: entry.ws_port, - hostname: entry.hostname.clone(), - clients_wss_port: entry.wss_port, - }), - }; + let entry = value.entry.as_ref().map(|entry| EntryDetails { + ip_addresses: value.ip_addresses.clone(), + clients_ws_port: entry.ws_port, + hostname: entry.hostname.clone(), + clients_wss_port: entry.wss_port, + }); Ok(RoutingNode { node_id: value.node_id, diff --git a/common/topology/src/wasm_helpers.rs b/common/topology/src/wasm_helpers.rs index 8c0d9476b16..ecb451e8beb 100644 --- a/common/topology/src/wasm_helpers.rs +++ b/common/topology/src/wasm_helpers.rs @@ -5,8 +5,7 @@ #![allow(clippy::empty_docs)] use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles}; -use crate::NymTopology; -use nym_mixnet_contract_common::EpochRewardedSet; +use crate::{CachedEpochRewardedSet, NymTopology}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; @@ -39,7 +38,7 @@ impl From for JsValue { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct WasmFriendlyNymTopology { - pub rewarded_set: EpochRewardedSet, + pub rewarded_set: CachedEpochRewardedSet, pub node_details: HashMap, } @@ -50,14 +49,27 @@ impl TryFrom for NymTopology { fn try_from(value: WasmFriendlyNymTopology) -> Result { let node_details = value .node_details - .into_iter() - .map(|(_, details)| details.try_into()) + .into_values() + .map(|details| details.try_into()) .collect::>()?; Ok(NymTopology::new(value.rewarded_set, node_details)) } } +impl From for WasmFriendlyNymTopology { + fn from(value: NymTopology) -> Self { + WasmFriendlyNymTopology { + rewarded_set: value.rewarded_set, + node_details: value + .node_details + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")] @@ -72,7 +84,6 @@ pub struct WasmFriendlyRoutingNode { pub sphinx_key: String, pub supported_roles: SupportedRoles, - pub performance: f64, } impl TryFrom for RoutingNode { @@ -94,7 +105,19 @@ impl TryFrom for RoutingNode { } })?, supported_roles: value.supported_roles, - performance: value.performance, }) } } + +impl From for WasmFriendlyRoutingNode { + fn from(node: RoutingNode) -> Self { + WasmFriendlyRoutingNode { + node_id: node.node_id, + mix_host: node.mix_host, + entry: node.entry, + identity_key: node.identity_key.to_string(), + sphinx_key: node.sphinx_key.to_string(), + supported_roles: node.supported_roles, + } + } +} diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 486e8e830c3..aab813f5daf 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -387,6 +387,15 @@ pub struct TopologyWasm { /// Specifies a minimum performance of a gateway that is used on route construction. /// This setting is only applicable when `NymApi` topology is used. pub minimum_gateway_performance: u8, + + /// Specifies whether this client should attempt to retrieve all available network nodes + /// as opposed to just active mixnodes/gateways. + /// Useless without `ignore_epoch_roles = true` + 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, } impl Default for TopologyWasm { @@ -409,6 +418,8 @@ impl From for ConfigTopology { topology_structure: Default::default(), minimum_mixnode_performance: topology.minimum_mixnode_performance, minimum_gateway_performance: topology.minimum_gateway_performance, + use_extended_topology: topology.use_extended_topology, + ignore_egress_epoch_role: topology.ignore_egress_epoch_role, } } } @@ -424,6 +435,8 @@ impl From for TopologyWasm { disable_refreshing: topology.disable_refreshing, minimum_mixnode_performance: topology.minimum_mixnode_performance, minimum_gateway_performance: topology.minimum_gateway_performance, + use_extended_topology: topology.use_extended_topology, + ignore_egress_epoch_role: topology.ignore_egress_epoch_role, } } } diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 9a8f6312154..f85ad9d241c 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -271,6 +271,17 @@ pub struct TopologyWasmOverride { /// This setting is only applicable when `NymApi` topology is used. #[tsify(optional)] pub minimum_gateway_performance: Option, + + /// Specifies whether this client should attempt to retrieve all available network nodes + /// as opposed to just active mixnodes/gateways. + /// Useless without `ignore_epoch_roles = true` + #[tsify(optional)] + pub use_extended_topology: Option, + + /// Specifies whether this client should ignore the current epoch role of the target egress node + /// when constructing the final hop packets. + #[tsify(optional)] + pub ignore_egress_epoch_role: Option, } impl From for TopologyWasm { @@ -294,6 +305,12 @@ impl From for TopologyWasm { minimum_gateway_performance: value .minimum_gateway_performance .unwrap_or(def.minimum_gateway_performance), + use_extended_topology: value + .use_extended_topology + .unwrap_or(def.use_extended_topology), + ignore_egress_epoch_role: value + .ignore_egress_epoch_role + .unwrap_or(def.ignore_egress_epoch_role), } } } diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 04d6960377f..eee589aead5 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -15,7 +15,8 @@ use nym_client_core::init::{ }; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use nym_topology::{gateway, NymTopology, SerializableNymTopology}; +use nym_topology::wasm_helpers::WasmFriendlyNymTopology; +use nym_topology::{NymTopology, RoutingNode}; use nym_validator_client::client::IdentityKey; use nym_validator_client::NymApiClient; use rand::thread_rng; @@ -55,7 +56,7 @@ pub fn parse_sender_tag(tag: &str) -> Result pub async fn current_network_topology_async( nym_api_url: String, -) -> Result { +) -> Result { let url: Url = match nym_api_url.parse() { Ok(url) => url, Err(source) => { @@ -67,12 +68,17 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); + let rewarded_set = api_client.get_current_rewarded_set().await?; let mixnodes = api_client .get_all_basic_active_mixing_assigned_nodes() .await?; let gateways = api_client.get_all_basic_entry_assigned_nodes().await?; - Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) + let mut topology = NymTopology::new_empty(rewarded_set); + topology.add_skimmed_nodes(&mixnodes); + topology.add_skimmed_nodes(&gateways); + + Ok(topology.into()) } #[wasm_bindgen(js_name = "currentNetworkTopology")] @@ -90,7 +96,7 @@ pub async fn setup_gateway_wasm( client_store: &ClientStorage, force_tls: bool, chosen_gateway: Option, - gateways: &[gateway::LegacyNode], + gateways: Vec, ) -> Result { // TODO: so much optimization and extra features could be added here, but that's for the future @@ -107,7 +113,7 @@ pub async fn setup_gateway_wasm( GatewaySetup::New { specification: selection_spec, - available_gateways: gateways.to_vec(), + available_gateways: gateways, } }; @@ -125,7 +131,7 @@ pub async fn setup_gateway_from_api( ) -> Result { let mut rng = thread_rng(); let gateways = current_gateways(&mut rng, nym_apis, None, minimum_performance).await?; - setup_gateway_wasm(client_store, force_tls, chosen_gateway, &gateways).await + setup_gateway_wasm(client_store, force_tls, chosen_gateway, gateways).await } pub async fn setup_from_topology( @@ -134,6 +140,6 @@ pub async fn setup_from_topology( topology: &NymTopology, client_store: &ClientStorage, ) -> Result { - let gateways = topology.gateways(); + let gateways = topology.entry_capable_nodes().cloned().collect::>(); setup_gateway_wasm(client_store, force_tls, explicit_gateway, gateways).await } diff --git a/common/wasm/client-core/src/topology.rs b/common/wasm/client-core/src/topology.rs index 25300c41a42..bd849164e31 100644 --- a/common/wasm/client-core/src/topology.rs +++ b/common/wasm/client-core/src/topology.rs @@ -1,51 +1,26 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_topology::SerializableTopologyError; +use nym_topology::wasm_helpers::SerializableTopologyError; use nym_validator_client::client::IdentityKeyRef; -use wasm_utils::console_log; -pub use nym_topology::{ - gateway, mix, SerializableGateway, SerializableMixNode, SerializableNymTopology, -}; +pub use nym_topology::wasm_helpers::{WasmFriendlyNymTopology, WasmFriendlyRoutingNode}; +pub use nym_topology::{Role, RoutingNode}; // redeclare this as a type alias for easy of use pub type WasmTopologyError = SerializableTopologyError; // helper trait to define extra functionality on the external type that we used to have here before pub trait SerializableTopologyExt { - fn print(&self); + // fn print(&self); fn ensure_contains_gateway_id(&self, gateway_id: IdentityKeyRef) -> bool; } -impl SerializableTopologyExt for SerializableNymTopology { - fn print(&self) { - if !self.mixnodes.is_empty() { - console_log!("mixnodes:"); - for (layer, nodes) in &self.mixnodes { - console_log!("\tlayer {layer}:"); - for node in nodes { - // console_log!("\t\t{} - {}", node.mix_id, node.identity_key) - console_log!("\t\t{} - {:?}", node.mix_id, node) - } - } - } else { - console_log!("NO MIXNODES") - } - - if !self.gateways.is_empty() { - console_log!("gateways:"); - for gateway in &self.gateways { - // console_log!("\t{}", gateway.identity_key) - console_log!("\t{:?}", gateway) - } - } else { - console_log!("NO GATEWAYS") - } - } - +impl SerializableTopologyExt for WasmFriendlyNymTopology { fn ensure_contains_gateway_id(&self, gateway_id: IdentityKeyRef) -> bool { - self.gateways.iter().any(|g| g.identity_key == gateway_id) + self.node_details + .values() + .any(|node| node.identity_key == gateway_id) } } diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index 139e5e8dccf..df049b4ab63 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -7,7 +7,7 @@ use anyhow::Result; use futures::{pin_mut, stream::FuturesUnordered, StreamExt}; use log::{debug, info}; use nym_sphinx::chunking::{monitoring, SentFragment}; -use nym_topology::{gateway, mix, NymTopology}; +use nym_topology::{NymRouteProvider, RoutingNode}; use nym_types::monitoring::{MonitorMessage, NodeResult}; use nym_validator_client::nym_api::routes::{API_VERSION, STATUS, SUBMIT_GATEWAY, SUBMIT_NODE}; use rand::SeedableRng; @@ -19,8 +19,8 @@ use utoipa::ToSchema; use crate::{NYM_API_URL, PRIVATE_KEY, TOPOLOGY}; struct HydratedRoute { - mix_nodes: Vec, - gateway_node: gateway::LegacyNode, + mix_nodes: Vec, + gateway_node: RoutingNode, } #[derive(Serialize, Deserialize, Debug, Default, ToSchema)] @@ -61,12 +61,12 @@ pub struct NetworkAccount { gateway_stats: HashMap, incomplete_routes: Vec>, #[serde(skip)] - topology: NymTopology, + topology: NymRouteProvider, tested_nodes: HashSet, #[serde(skip)] - mix_details: HashMap, + mix_details: HashMap, #[serde(skip)] - gateway_details: HashMap, + gateway_details: HashMap, } impl NetworkAccount { @@ -126,7 +126,7 @@ impl NetworkAccount { fn new() -> Self { let topology = TOPOLOGY.get().expect("Topology not set yet!").clone(); let mut account = NetworkAccount { - topology, + topology: NymRouteProvider::new(topology, true), ..Default::default() }; for fragment_set in monitoring::FRAGMENTS_SENT.iter() { @@ -162,14 +162,12 @@ impl NetworkAccount { fn hydrate_route(&self, fragment: SentFragment) -> anyhow::Result { let mut rng = ChaCha8Rng::seed_from_u64(fragment.seed() as u64); - let (nodes, gw) = self.topology.random_path_to_egress( - &mut rng, - fragment.mixnet_params().hops(), - fragment.mixnet_params().destination(), - )?; + let (nodes, gw) = self + .topology + .random_path_to_egress(&mut rng, fragment.mixnet_params().destination())?; Ok(HydratedRoute { - mix_nodes: nodes, - gateway_node: gw, + mix_nodes: nodes.into_iter().cloned().collect(), + gateway_node: gw.clone(), }) } @@ -181,11 +179,11 @@ impl NetworkAccount { let mix_ids = route .mix_nodes .iter() - .map(|n| n.mix_id) + .map(|n| n.node_id) .collect::>(); self.tested_nodes.extend(&mix_ids); self.mix_details - .extend(route.mix_nodes.iter().map(|n| (n.mix_id, n.clone()))); + .extend(route.mix_nodes.iter().map(|n| (n.node_id, n.clone()))); let gateway_stats_entry = self .gateway_stats .entry(route.gateway_node.identity_key.to_base58_string()) diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index 89fee6df6ff..26476813a4e 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -3,6 +3,7 @@ use accounting::submit_metrics; use anyhow::Result; use clap::Parser; use log::{error, info, warn}; +use nym_bin_common::bin_info; use nym_client_core::ForgetMe; use nym_crypto::asymmetric::ed25519::PrivateKey; use nym_network_defaults::setup_env; @@ -158,6 +159,26 @@ fn generate_key_pair() -> Result<()> { Ok(()) } +async fn nym_topology_from_env() -> anyhow::Result { + let api_url = std::env::var(NYM_API)?; + + info!("Generating topology from {api_url}"); + let client = nym_validator_client::client::NymApiClient::new_with_user_agent( + api_url.parse()?, + bin_info!(), + ); + + let rewarded_set = client.get_current_rewarded_set().await?; + + // just get all nodes to make our lives easier because it's just one query for the whole duration of the monitor (?) + let nodes = client.get_all_basic_nodes().await?; + + let mut topology = NymTopology::new_empty(rewarded_set); + topology.add_skimmed_nodes(&nodes); + + Ok(topology) +} + #[tokio::main] async fn main() -> Result<()> { nym_bin_common::logging::setup_logging(); @@ -187,7 +208,7 @@ async fn main() -> Result<()> { .set(if let Some(topology_file) = args.topology { NymTopology::new_from_file(topology_file)? } else { - NymTopology::new_from_env().await? + nym_topology_from_env().await? }) .ok(); diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index df5f4ef5a65..7cd5a6f50cd 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -4,7 +4,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, TopologyProvider}; -use nym_topology::{nym_topology_from_basic_info, NymTopology}; +use nym_topology::NymTopology; use url::Url; struct MyTopologyProvider { @@ -19,6 +19,14 @@ impl MyTopologyProvider { } async fn get_topology(&self) -> NymTopology { + let rewarded_set = self + .validator_client + .get_current_rewarded_set() + .await + .unwrap(); + + let mut base_topology = NymTopology::new_empty(rewarded_set); + let mixnodes = self .validator_client .get_all_basic_active_mixing_assigned_nodes() @@ -39,7 +47,9 @@ impl MyTopologyProvider { .await .unwrap(); - nym_topology_from_basic_info(&filtered_mixnodes, &gateways) + base_topology.add_skimmed_nodes(&filtered_mixnodes); + base_topology.add_skimmed_nodes(&gateways); + base_topology } } diff --git a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs b/sdk/rust/nym-sdk/examples/geo_topology_provider.rs deleted file mode 100644 index 3b327d7377d..00000000000 --- a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_sdk::mixnet; -use nym_sdk::mixnet::MixnetMessageSender; - -#[tokio::main] -async fn main() { - nym_bin_common::logging::setup_logging(); - - let nym_api = "https://validator.nymtech.net/api/".parse().unwrap(); - - // We can group on something which is to a first approximation a continent. - let group_by = mixnet::GroupBy::CountryGroup(mixnet::CountryGroup::Europe); - - // ... or on a nym-address. This means we use the geo location of the gateway that the - // nym-address is connected to. - //let group_by = GroupBy::NymAddress("id.enc@gateway".parse().unwrap()); - - let geo_topology_provider = mixnet::GeoAwareTopologyProvider::new( - vec![nym_api], - // We filter on the version of the mixnodes. Be prepared to manually update - // this to keep this example working, as we can't (currently) fetch to current - // latest version. - group_by, - ); - - // Passing no config makes the client fire up an ephemeral session and figure things out on its own - let mut client = mixnet::MixnetClientBuilder::new_ephemeral() - .custom_topology_provider(Box::new(geo_topology_provider)) - .build() - .unwrap() - .connect_to_mixnet() - .await - .unwrap(); - - let our_address = client.nym_address(); - println!("Our client nym address is: {our_address}"); - - // Send a message through the mixnet to ourselves - client - .send_plain_message(*our_address, "hello there") - .await - .unwrap(); - - println!("Waiting for message (ctrl-c to exit)"); - client - .on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message))) - .await; -} diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index e889c4d9bea..38c2b3d7f31 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,8 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::node::LegacyMixLayer; -use std::collections::BTreeMap; +use nym_topology::{NymTopology, RoutingNode, SupportedRoles}; #[tokio::main] async fn main() { @@ -12,63 +11,74 @@ async fn main() { // Passing no config makes the client fire up an ephemeral session and figure shit out on its own let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); - let starting_topology = client.read_current_route_provider().await.unwrap(); + let starting_topology = client.read_current_route_provider().await.unwrap().clone(); // but we don't like our default topology, we want to use only those very specific, hardcoded, nodes: - let mut mixnodes = BTreeMap::new(); - mixnodes.insert( - 1, - vec![mix::LegacyNode { - mix_id: 63, - host: "172.105.92.48".parse().unwrap(), + let nodes = vec![ + RoutingNode { + node_id: 63, mix_host: "172.105.92.48:1789".parse().unwrap(), + entry: None, identity_key: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK" .parse() .unwrap(), sphinx_key: "CBmYewWf43iarBq349KhbfYMc9ys2ebXWd4Vp4CLQ5Rq" .parse() .unwrap(), - layer: LegacyMixLayer::One, - version: "1.1.0".into(), - }], - ); - mixnodes.insert( - 2, - vec![mix::LegacyNode { - mix_id: 23, - host: "178.79.143.65".parse().unwrap(), + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: false, + mixnet_exit: false, + }, + }, + RoutingNode { + node_id: 23, mix_host: "178.79.143.65:1789".parse().unwrap(), + entry: None, identity_key: "4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz" .parse() .unwrap(), sphinx_key: "8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG" .parse() .unwrap(), - layer: LegacyMixLayer::Two, - version: "1.1.0".into(), - }], - ); - mixnodes.insert( - 3, - vec![mix::LegacyNode { - mix_id: 66, - host: "139.162.247.97".parse().unwrap(), + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: false, + mixnet_exit: false, + }, + }, + RoutingNode { + node_id: 66, mix_host: "139.162.247.97:1789".parse().unwrap(), + entry: None, identity_key: "66UngapebhJRni3Nj52EW1qcNsWYiuonjkWJzHFsmyYY" .parse() .unwrap(), sphinx_key: "7KyZh8Z8KxuVunqytAJ2eXFuZkCS7BLTZSzujHJZsGa2" .parse() .unwrap(), - layer: LegacyMixLayer::Three, - version: "1.1.0".into(), - }], - ); + supported_roles: SupportedRoles { + mixnode: true, + mixnet_entry: false, + mixnet_exit: false, + }, + }, + ]; + + // make sure our custom nodes are in the fake rewarded set (so they'd be used by default by the client) + let mut rewarded_set = starting_topology.topology.rewarded_set().clone(); + rewarded_set.layer1.insert(nodes[0].node_id); + rewarded_set.layer2.insert(nodes[1].node_id); + rewarded_set.layer3.insert(nodes[2].node_id); // but we like the available gateways, so keep using them! // (we like them because the author of this example is too lazy to use the same hardcoded gateway // during client initialisation to make sure we are able to send to ourselves : ) ) - let custom_topology = NymTopology::new(mixnodes, starting_topology.gateways().to_vec()); + let gateways = starting_topology.topology.entry_capable_nodes(); + + let mut custom_topology = NymTopology::new_empty(rewarded_set); + custom_topology.add_routing_nodes(nodes); + custom_topology.add_routing_nodes(gateways); client.manually_overwrite_topology(custom_topology).await; diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index 5b8afd246c3..22e954e926f 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -10,6 +10,7 @@ pub mod mixnet; pub mod tcp_proxy; pub use error::{Error, Result}; +#[allow(deprecated)] pub use nym_client_core::{ client::{ mix_traffic::transceiver::*, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index f8c226a7e7b..6fc8cae2dc0 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -42,6 +42,7 @@ pub use client::{DisconnectedMixnetClient, IncludedSurbs, MixnetClientBuilder}; pub use config::Config; pub use native_client::MixnetClient; pub use native_client::MixnetClientSender; +#[allow(deprecated)] pub use nym_client_core::{ client::{ base_client::storage::{ diff --git a/wasm/client/Cargo.toml b/wasm/client/Cargo.toml index 48589a20c2f..dbd1e4b0c39 100644 --- a/wasm/client/Cargo.toml +++ b/wasm/client/Cargo.toml @@ -3,11 +3,11 @@ name = "nym-client-wasm" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] version = "1.3.0-rc.0" edition = "2021" -keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] +keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy"] license = "Apache-2.0" repository = "https://github.com/nymtech/nym" description = "A webassembly client which can be used to interact with the the Nym privacy platform. Wasm is used for Sphinx packet generation." -rust-version = "1.56" +rust-version = "1.76" [lib] crate-type = ["cdylib", "rlib"] @@ -22,7 +22,7 @@ serde_json = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } -thiserror = { workspace = true } +thiserror = { workspace = true } tsify = { workspace = true, features = ["js"] } nym-bin-common = { path = "../../common/bin-common" } @@ -30,7 +30,7 @@ wasm-client-core = { path = "../../common/wasm/client-core" } wasm-utils = { path = "../../common/wasm/utils" } nym-node-tester-utils = { path = "../../common/node-tester-utils", optional = true } -nym-node-tester-wasm = { path = "../node-tester", optional = true} +nym-node-tester-wasm = { path = "../node-tester", optional = true } [dev-dependencies] wasm-bindgen-test = { workspace = true } diff --git a/wasm/client/src/client.rs b/wasm/client/src/client.rs index 4bb9f2c2f2e..80abcdca306 100644 --- a/wasm/client/src/client.rs +++ b/wasm/client/src/client.rs @@ -30,7 +30,7 @@ use wasm_client_core::nym_task::connections::TransmissionLane; use wasm_client_core::nym_task::TaskManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; use wasm_client_core::storage::ClientStorage; -use wasm_client_core::topology::{SerializableNymTopology, SerializableTopologyExt}; +use wasm_client_core::topology::{SerializableTopologyExt, WasmFriendlyNymTopology}; use wasm_client_core::{ HardcodedTopologyProvider, IdentityKey, NymTopology, PacketType, QueryReqwestRpcNyxdClient, TopologyProvider, @@ -103,7 +103,7 @@ impl NymClientBuilder { // NOTE: you most likely want to use `[NymNodeTester]` instead. #[cfg(feature = "node-tester")] pub fn new_tester( - topology: SerializableNymTopology, + topology: WasmFriendlyNymTopology, on_message: js_sys::Function, gateway: Option, ) -> Result { @@ -340,7 +340,7 @@ impl NymClient { .mix_test_request(test_id, mixnode_identity, num_test_packets) } - pub fn change_hardcoded_topology(&self, topology: SerializableNymTopology) -> Promise { + pub fn change_hardcoded_topology(&self, topology: WasmFriendlyNymTopology) -> Promise { self.client_state.change_hardcoded_topology(topology) } diff --git a/wasm/client/src/helpers.rs b/wasm/client/src/helpers.rs index e8570d02b90..b6cdba3d33c 100644 --- a/wasm/client/src/helpers.rs +++ b/wasm/client/src/helpers.rs @@ -8,7 +8,7 @@ use wasm_bindgen_futures::future_to_promise; use wasm_client_core::client::base_client::{ClientInput, ClientState}; use wasm_client_core::client::inbound_messages::InputMessage; use wasm_client_core::error::WasmCoreError; -use wasm_client_core::topology::SerializableNymTopology; +use wasm_client_core::topology::{Role, WasmFriendlyNymTopology}; use wasm_client_core::NymTopology; use wasm_utils::error::simple_js_error; use wasm_utils::{check_promise_result, console_log}; @@ -36,7 +36,7 @@ pub struct NymClientTestRequest { #[cfg(feature = "node-tester")] #[wasm_bindgen] impl NymClientTestRequest { - pub fn injectable_topology(&self) -> SerializableNymTopology { + pub fn injectable_topology(&self) -> WasmFriendlyNymTopology { self.testable_topology.clone().into() } } @@ -78,7 +78,7 @@ impl InputSender for Arc { pub(crate) trait WasmTopologyExt { /// Changes the current network topology to the provided value. - fn change_hardcoded_topology(&self, topology: SerializableNymTopology) -> Promise; + fn change_hardcoded_topology(&self, topology: WasmFriendlyNymTopology) -> Promise; /// Returns the current network topology. fn current_topology(&self) -> Promise; @@ -96,7 +96,7 @@ pub(crate) trait WasmTopologyTestExt { } impl WasmTopologyExt for Arc { - fn change_hardcoded_topology(&self, topology: SerializableNymTopology) -> Promise { + fn change_hardcoded_topology(&self, topology: WasmFriendlyNymTopology) -> Promise { let nym_topology: NymTopology = check_promise_result!(topology.try_into()); let this = Arc::clone(self); @@ -112,11 +112,11 @@ impl WasmTopologyExt for Arc { fn current_topology(&self) -> Promise { let this = Arc::clone(self); future_to_promise(async move { - match this.topology_accessor.current_topology().await { - Some(topology) => Ok(serde_wasm_bindgen::to_value(&SerializableNymTopology::from( - topology, - )) - .expect("SerializableNymTopology failed serialization")), + match this.topology_accessor.current_route_provider().await { + Some(route_provider) => Ok(serde_wasm_bindgen::to_value( + &WasmFriendlyNymTopology::from(route_provider.topology.clone()), + ) + .expect("WasmFriendlyNymTopology failed serialization")), None => Err(WasmCoreError::UnavailableNetworkTopology.into()), } }) @@ -135,21 +135,26 @@ impl WasmTopologyTestExt for Arc { let this = Arc::clone(self); future_to_promise(async move { - let Some(current_topology) = this.topology_accessor.current_topology().await else { + let Some(current_topology) = this.topology_accessor.current_route_provider().await + else { return Err(WasmCoreError::UnavailableNetworkTopology.into()); }; - let Some(mix) = current_topology.find_mix_by_identity(&mixnode_identity) else { + let Ok(node_identity) = mixnode_identity.parse() else { return Err(WasmCoreError::NonExistentMixnode { mixnode_identity }.into()); }; + let Some(mix) = current_topology.node_by_identity(node_identity) else { + return Err(WasmCoreError::NonExistentMixnode { mixnode_identity }.into()); + }; + + let mut updated = current_topology.topology.clone(); + updated.set_testable_node(Role::Layer2, mix.clone()); + let ext = WasmTestMessageExt::new(test_id); let test_msgs = NodeTestMessage::mix_plaintexts(mix, num_test_packets, ext) .map_err(crate::error::WasmClientError::from)?; - let mut updated = current_topology.clone(); - updated.set_mixes_in_layer(mix.layer.into(), vec![mix.to_owned()]); - Ok(JsValue::from(NymClientTestRequest { test_msgs, testable_topology: updated, diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 65991a29555..de0badf5ca2 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -13,6 +13,7 @@ use crate::types::{NodeTestResult, WasmTestMessageExt}; use futures::channel::mpsc; use js_sys::Promise; use nym_node_tester_utils::receiver::SimpleMessageReceiver; +use nym_node_tester_utils::tester::LegacyMixLayer; use nym_node_tester_utils::{NodeTester, PacketSize, PreparedFragment}; use nym_task::TaskManager; use rand::rngs::OsRng; @@ -32,7 +33,7 @@ use wasm_client_core::helpers::{ current_network_topology_async, setup_from_topology, EphemeralCredentialStorage, }; use wasm_client_core::storage::ClientStorage; -use wasm_client_core::topology::SerializableNymTopology; +use wasm_client_core::topology::WasmFriendlyNymTopology; use wasm_client_core::{ nym_task, BandwidthController, ClientKeys, ClientStatsSender, GatewayClient, GatewayClientConfig, GatewayConfig, IdentityKey, InitialisationResult, NodeIdentity, @@ -103,7 +104,7 @@ pub struct NymNodeTesterOpts { nym_api: Option, #[tsify(optional)] - topology: Option, + topology: Option, #[tsify(optional)] gateway: Option, @@ -332,6 +333,7 @@ impl NymNodeTester { tester_permit .existing_identity_mixnode_test_packets( mixnode_identity, + LegacyMixLayer::Two, test_ext, num_test_packets, None, From 65e8a678c68ed42beacfcee68a5c77093195d67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Dec 2024 17:41:01 +0000 Subject: [PATCH 8/9] additional tweaks --- Cargo.lock | 3 +-- .../validator-client/src/nym_api/mod.rs | 3 +++ common/topology/Cargo.toml | 3 +-- common/topology/src/error.rs | 3 --- common/topology/src/lib.rs | 25 +++++++++++++------ sdk/rust/nym-sdk/src/mixnet/client.rs | 12 +++++++++ .../mixnet-connectivity-check/src/main.rs | 2 ++ 7 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eddc68ddb0f..1db93bd23e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6559,7 +6559,6 @@ name = "nym-topology" version = "0.1.0" dependencies = [ "async-trait", - "log", "nym-api-requests", "nym-config", "nym-crypto", @@ -6569,10 +6568,10 @@ dependencies = [ "nym-sphinx-types", "rand", "reqwest 0.12.4", - "semver 1.0.23", "serde", "serde_json", "thiserror", + "tracing", "tsify", "wasm-bindgen", "wasm-utils", 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 bf630f3914f..730c70c8606 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -921,6 +921,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn force_refresh_describe_cache( &self, request: &NodeRefreshBody, @@ -933,6 +934,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_for( &self, expiration_date: Date, @@ -949,6 +951,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_challenge( &self, expiration_date: Date, diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index e96881ed111..be031ed095c 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -12,10 +12,9 @@ documentation = { workspace = true } [dependencies] async-trait = { workspace = true, optional = true } -log = { workspace = true } +tracing = { workspace = true } rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } -semver = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index bf7efc334fb..36896effa3f 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -22,9 +22,6 @@ pub enum NymTopologyError { #[error("one (or more) of mixing layers does not have any valid nodes available")] InsufficientMixingNodes, - // - // - // #[error("The provided network topology has no gateways available")] NoGatewaysAvailable, diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index d942de49406..c71ada3c2dc 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use ::serde::{Deserialize, Serialize}; -use log::{debug, warn}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; @@ -12,6 +11,7 @@ use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::net::IpAddr; +use tracing::{debug, warn}; pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; pub use error::NymTopologyError; @@ -287,13 +287,22 @@ impl NymTopology { } pub fn is_minimally_routable(&self) -> bool { - self.node_details_exists(&self.rewarded_set.layer1) - && self.node_details_exists(&self.rewarded_set.layer2) - && self.node_details_exists(&self.rewarded_set.layer3) - && (!self.rewarded_set.exit_gateways.is_empty() - || !self.rewarded_set.entry_gateways.is_empty()) - - // TODO: we should also include gateways in that check, but right now we're allowing ALL gateways, even inactive + let has_layer1 = self.node_details_exists(&self.rewarded_set.layer1); + let has_layer2 = self.node_details_exists(&self.rewarded_set.layer2); + let has_layer3 = self.node_details_exists(&self.rewarded_set.layer3); + let has_exit_gateways = !self.rewarded_set.exit_gateways.is_empty(); + let has_entry_gateways = !self.rewarded_set.entry_gateways.is_empty(); + + debug!( + has_layer1 = %has_layer1, + has_layer2 = %has_layer2, + has_layer3 = %has_layer3, + has_entry_gateways = %has_entry_gateways, + has_exit_gateways = %has_exit_gateways, + "network status" + ); + + has_layer1 && has_layer2 && has_layer3 && (has_exit_gateways || has_entry_gateways) } pub fn ensure_minimally_routable(&self) -> Result<(), NymTopologyError> { diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 598bbd4808e..3b72319cee8 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -178,6 +178,18 @@ where self } + #[must_use] + pub fn with_extended_topology(mut self, use_extended_topology: bool) -> Self { + self.config.debug_config.topology.use_extended_topology = use_extended_topology; + self + } + + #[must_use] + pub fn with_ignore_epoch_roles(mut self, ignore_epoch_roles: bool) -> Self { + self.config.debug_config.topology.ignore_egress_epoch_role = ignore_epoch_roles; + self + } + /// Use a specific network instead of the default (mainnet) one. #[must_use] pub fn network_details(mut self, network_details: NymNetworkDetails) -> Self { diff --git a/tools/internal/mixnet-connectivity-check/src/main.rs b/tools/internal/mixnet-connectivity-check/src/main.rs index be66b4f443c..4dd68a61ab1 100644 --- a/tools/internal/mixnet-connectivity-check/src/main.rs +++ b/tools/internal/mixnet-connectivity-check/src/main.rs @@ -88,6 +88,8 @@ async fn connectivity_test(args: ConnectivityArgs) -> anyhow::Result<()> { let mixnet_client = if let Some(gateway) = args.gateway { client_builder .request_gateway(gateway.to_string()) + .with_ignore_epoch_roles(true) + .with_extended_topology(true) .build()? } else { client_builder.build()? From 8b0629d604fdd1df7b002c4e3935360edfbc631a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 17 Dec 2024 09:18:01 +0000 Subject: [PATCH 9/9] linux clippy fixes + adding additional dummy ipr types for better linting on non-linux targets --- service-providers/ip-packet-router/src/lib.rs | 1 + .../ip-packet-router/src/mixnet_listener.rs | 48 ++++++------------- .../ip-packet-router/src/non_linux_dummy.rs | 43 +++++++++++++++++ 3 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 service-providers/ip-packet-router/src/non_linux_dummy.rs diff --git a/service-providers/ip-packet-router/src/lib.rs b/service-providers/ip-packet-router/src/lib.rs index 3d464b05c3b..cb4a84ec658 100644 --- a/service-providers/ip-packet-router/src/lib.rs +++ b/service-providers/ip-packet-router/src/lib.rs @@ -11,6 +11,7 @@ pub mod error; mod ip_packet_router; mod mixnet_client; mod mixnet_listener; +pub(crate) mod non_linux_dummy; pub mod request_filter; mod tun_listener; mod util; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index fe029c0fb78..6e6980ccd43 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -1,7 +1,3 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::sync::Arc; -use std::{collections::HashMap, net::SocketAddr}; - use bytes::{Bytes, BytesMut}; use futures::StreamExt; use nym_ip_packet_requests::v7::response::{ @@ -23,8 +19,10 @@ use nym_ip_packet_requests::{ use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::Arc; +use std::{collections::HashMap, net::SocketAddr}; use tap::TapFallible; -#[cfg(target_os = "linux")] use tokio::io::AsyncWriteExt; use tokio::sync::RwLock; use tokio_util::codec::Decoder; @@ -95,6 +93,7 @@ impl ConnectedClients { }) } + #[allow(dead_code)] fn lookup_client_from_nym_address(&self, nym_address: &Recipient) -> Option<&ConnectedClient> { self.clients_ipv4_mapping .iter() @@ -111,7 +110,6 @@ impl ConnectedClients { &mut self, ips: IpPair, nym_address: Recipient, - mix_hops: Option, forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, close_tx: tokio::sync::oneshot::Sender<()>, handle: tokio::task::JoinHandle<()>, @@ -121,7 +119,6 @@ impl ConnectedClients { let client = ConnectedClient { nym_address, ipv6: ips.ipv6, - mix_hops, last_activity: Arc::new(RwLock::new(std::time::Instant::now())), _close_tx: Arc::new(CloseTx { nym_address, @@ -236,9 +233,6 @@ pub(crate) struct ConnectedClient { // The assigned IPv6 address of this client pub(crate) ipv6: Ipv6Addr, - // Number of mix node hops that the client has requested to use - pub(crate) mix_hops: Option, - // Keep track of last activity so we can disconnect inactive clients pub(crate) last_activity: Arc>, @@ -390,7 +384,13 @@ impl Response { } } +#[cfg(not(target_os = "linux"))] +type TunDevice = crate::non_linux_dummy::DummyDevice; + #[cfg(target_os = "linux")] +type TunDevice = tokio_tun::Tun; + +// #[cfg(target_os = "linux")] pub(crate) struct MixnetListener { // The configuration for the mixnet listener pub(crate) _config: Config, @@ -399,7 +399,7 @@ pub(crate) struct MixnetListener { pub(crate) request_filter: request_filter::RequestFilter, // The TUN device that we use to send and receive packets from the internet - pub(crate) tun_writer: tokio::io::WriteHalf, + pub(crate) tun_writer: tokio::io::WriteHalf, // The mixnet client that we use to send and receive packets from the mixnet pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, @@ -412,7 +412,7 @@ pub(crate) struct MixnetListener { pub(crate) connected_clients: ConnectedClients, } -#[cfg(target_os = "linux")] +// #[cfg(target_os = "linux")] impl MixnetListener { // Receving a static connect request from a client with an IP provided that we assign to them, // if it's available. If it's not available, we send a failure response. @@ -429,7 +429,6 @@ impl MixnetListener { let request_id = connect_request.request_id; let requested_ips = connect_request.ips; let reply_to = connect_request.reply_to; - let reply_to_hops = connect_request.reply_to_hops; // TODO: add to connect request let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; // TODO: ignoring reply_to_avg_mix_delays for now @@ -556,21 +555,14 @@ impl MixnetListener { let (forward_from_tun_tx, close_tx, handle) = connected_client_handler::ConnectedClientHandler::start( reply_to, - reply_to_hops, buffer_timeout, client_version, self.mixnet_client.split_sender(), ); // Register the new client in the set of connected clients - self.connected_clients.connect( - new_ips, - reply_to, - reply_to_hops, - forward_from_tun_tx, - close_tx, - handle, - ); + self.connected_clients + .connect(new_ips, reply_to, forward_from_tun_tx, close_tx, handle); Ok(Some(Response::new_dynamic_connect_success( request_id, reply_to, @@ -754,17 +746,7 @@ impl MixnetListener { let response_packet = response.to_bytes()?; - // We could avoid this lookup if we check this when we create the response. - let mix_hops = if let Some(c) = self - .connected_clients - .lookup_client_from_nym_address(recipient) - { - c.mix_hops - } else { - None - }; - - let input_message = create_input_message(*recipient, response_packet, mix_hops); + let input_message = create_input_message(*recipient, response_packet); self.mixnet_client .send(input_message) .await diff --git a/service-providers/ip-packet-router/src/non_linux_dummy.rs b/service-providers/ip-packet-router/src/non_linux_dummy.rs new file mode 100644 index 00000000000..4369f726899 --- /dev/null +++ b/service-providers/ip-packet-router/src/non_linux_dummy.rs @@ -0,0 +1,43 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Error; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +pub(crate) struct DummyDevice; + +impl AsyncRead for DummyDevice { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + unimplemented!("tunnel devices are not supported by non-linux targets") + } +} + +impl AsyncWrite for DummyDevice { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &[u8], + ) -> Poll> { + unimplemented!("tunnel devices are not supported by non-linux targets") + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + unimplemented!("tunnel devices are not supported by non-linux targets") + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + unimplemented!("tunnel devices are not supported by non-linux targets") + } +}