diff --git a/Cargo.lock b/Cargo.lock index d84ab9916c1..1db93bd23e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6559,10 +6559,7 @@ name = "nym-topology" version = "0.1.0" dependencies = [ "async-trait", - "bs58", - "log", "nym-api-requests", - "nym-bin-common", "nym-config", "nym-crypto", "nym-mixnet-contract-common", @@ -6571,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-core/Cargo.toml b/common/client-core/Cargo.toml index b5191eef802..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 @@ -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 = ["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/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/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/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index bb0ffd5d03b..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; @@ -24,7 +23,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; @@ -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) } })? } @@ -539,15 +538,15 @@ 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, )), 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)) } }) } @@ -558,7 +557,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 +589,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 +739,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..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::{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, @@ -216,7 +202,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) => { @@ -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) @@ -424,10 +409,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 +421,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 +453,6 @@ where &self.config.ack_key, &recipient, packet_type, - mix_hops, )?; let real_message = RealMessage::new( @@ -478,8 +460,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 +477,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 +493,6 @@ where recipient, TransmissionLane::AdditionalReplySurbs, packet_type, - mix_hops, ) .await?; @@ -530,7 +509,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 +519,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,23 +533,18 @@ 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; 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, - mix_hops, - ) - .unwrap(); + let prepared_fragment = self.message_preparer.prepare_chunk_for_sending( + chunk, + topology, + &self.config.ack_key, + &recipient, + packet_type, + )?; Ok(prepared_fragment) } @@ -624,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/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 6b12d64562c..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,43 +59,31 @@ impl<'a> TopologyReadPermit<'a> { &'a self, ack_recipient: &Recipient, packet_recipient: Option<&Recipient>, - ) -> 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)?; - - // 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)?; - - // 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(), - }); - } + 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 _ = route_provider.egress_by_identity(ack_recipient.gateway())?; - // 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 _ = 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 } } } @@ -99,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, + ))), } } @@ -121,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) { @@ -140,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..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 @@ -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, }; @@ -15,8 +14,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,30 +60,20 @@ 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(()) } +#[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!( @@ -104,6 +91,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() @@ -187,7 +183,8 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_basic_info(&mixnodes, &gateways); + topology.add_skimmed_nodes(&mixnodes); + topology.add_skimmed_nodes(&gateways); // TODO: return real error type check_layer_integrity(topology.clone()).ok()?; @@ -196,6 +193,7 @@ impl GeoAwareTopologyProvider { } } +#[allow(deprecated)] #[cfg(not(target_arch = "wasm32"))] #[async_trait] impl TopologyProvider for GeoAwareTopologyProvider { @@ -205,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 4e60278a22f..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; @@ -27,7 +28,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 +97,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 +132,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 3b87086f59a..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 @@ -4,32 +4,39 @@ use async_trait::async_trait; use log::{debug, error, warn}; use nym_topology::provider_trait::TopologyProvider; -use nym_topology::{NymTopology, NymTopologyError}; +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_egress_epoch_role: bool, } -impl Default for Config { - fn default() -> Self { - // old values that decided on blacklist membership +impl From for Config { + fn from(value: nym_client_core_config_types::Topology) -> Self { Config { - min_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, - min_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, + 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 { + // 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) + } +} + pub struct NymApiTopologyProvider { config: Config, @@ -39,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 { @@ -52,7 +63,7 @@ impl NymApiTopologyProvider { }; NymApiTopologyProvider { - config, + config: config.into(), validator_client, nym_api_urls, currently_used_api: 0, @@ -70,70 +81,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 + 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 = NymTopology::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) } } @@ -142,7 +152,11 @@ impl NymApiTopologyProvider { #[async_trait] 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) } } @@ -150,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 0cc9e04d753..5aaedc84ae7 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -4,8 +4,8 @@ use crate::client::mix_traffic::transceiver::ErasedGatewayError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; -use nym_topology::gateway::GatewayConversionError; -use nym_topology::NymTopologyError; +use nym_topology::node::RoutingNodeError; +use nym_topology::{NodeId, NymTopologyError}; use nym_validator_client::ValidatorClientError; use std::error::Error; use std::path::PathBuf; @@ -74,10 +74,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: Box, }, #[error("failed to establish connection to gateway: {source}")] @@ -159,6 +159,9 @@ 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: 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 68b3b8d4570..e4cb9f42a74 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().to_string(), + }); + }; 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().to_string(), + }); + }; + + 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..4954efbb3d4 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -19,7 +19,7 @@ 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}; use serde::Serialize; @@ -50,7 +50,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..35a5a5a1491 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -13,7 +13,7 @@ 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; use serde::Serialize; @@ -38,16 +38,23 @@ pub enum SelectedGateway { impl SelectedGateway { pub fn from_topology_node( - node: gateway::LegacyNode, + 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.to_base58_string(), + })? }; let gateway_listener = @@ -200,7 +207,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/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..730c70c8606 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))] @@ -912,6 +921,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn force_refresh_describe_cache( &self, request: &NodeRefreshBody, @@ -924,6 +934,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_ticketbooks_for( &self, expiration_date: Date, @@ -940,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/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/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/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index eb5f972e4b5..d722f5af938 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 { @@ -69,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/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 dc125068b79..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, @@ -285,6 +286,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/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..211eb988dbd 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -2,21 +2,22 @@ // 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::RoutingNode; +use nym_topology::{NymRouteProvider, NymTopology, Role}; use rand::{CryptoRng, Rng}; use serde::Serialize; use std::sync::Arc; use std::time::Duration; +pub use nym_topology::node::LegacyMixLayer; + pub struct NodeTester { rng: R, @@ -38,10 +39,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 +67,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 +95,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 +113,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 +122,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 +135,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 +146,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, + }); + }; + + 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(), msg_ext, test_packets, custom_recipient) + 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 +177,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 +192,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 +225,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 +257,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/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index a9311be597b..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_gateway(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/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/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index f25871ba0be..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::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, 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_gateway(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(); @@ -110,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/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 802d2325a66..5a6e24633cd 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,16 +82,11 @@ 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 + pub fn destination(&self) -> PublicKey { + self.destination } } @@ -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 23c83c3ed11..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_gateway(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/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/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/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 8578b655b0a..e4e0e3e7bae 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -14,9 +14,9 @@ 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::{NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -54,14 +54,13 @@ 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; 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 +78,7 @@ pub trait FragmentPreparer { &mut self, recipient: &Recipient, fragment_id: FragmentIdentifier, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_type: PacketType, ) -> Result { @@ -109,7 +108,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, @@ -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(); @@ -190,12 +188,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 +201,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 +236,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_gateway(&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_gateway(&mut rng, hops, destination)? + topology.random_route_to_egress(&mut rng, destination)? }; let destination = packet_recipient.as_sphinx_destination(); @@ -335,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, } @@ -361,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; @@ -380,7 +365,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 +384,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 +405,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 +420,6 @@ where &sender, packet_recipient, packet_type, - mix_hops, ) } @@ -444,7 +427,7 @@ where pub fn generate_surb_ack( &mut self, fragment_id: FragmentIdentifier, - topology: &NymTopology, + topology: &NymRouteProvider, ack_key: &AckKey, packet_type: PacketType, ) -> Result { @@ -483,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..f2f3feadd92 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, @@ -156,28 +154,11 @@ 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. 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,112 +182,4 @@ 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, - } - } -} - -#[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/Cargo.toml b/common/topology/Cargo.toml index 055175fd36d..be031ed095c 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -12,15 +12,13 @@ documentation = { workspace = true } [dependencies] async-trait = { workspace = true, optional = true } -bs58 = { workspace = 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 } -# 'serializable' feature -serde = { workspace = true, features = ["derive"], optional = true } +# 'serde' feature serde_json = { workspace = true, optional = true } # 'wasm-serde-types' feature @@ -28,7 +26,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" } @@ -51,5 +48,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"] +persistence = ["serde_json"] outfox = [] diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 835ea37b1ff..36896effa3f 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -4,18 +4,28 @@ 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: 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: Box }, + + #[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, - #[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")] @@ -55,12 +65,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 deleted file mode 100644 index 545f47dd211..00000000000 --- a/common/topology/src/gateway.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -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 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 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 - } - - 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 4133a8ae0e3..c71ada3c2dc 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,581 +1,543 @@ // 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 ::serde::{Deserialize, Serialize}; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; -use rand::prelude::SliceRandom; +use rand::prelude::IteratorRandom; use rand::{CryptoRng, Rng}; -use std::collections::BTreeMap; -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::borrow::Borrow; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::net::IpAddr; +use tracing::{debug, warn}; -#[cfg(feature = "serializable")] -use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; +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}; +pub use rewarded_set::CachedEpochRewardedSet; pub mod error; -pub mod gateway; -pub mod mix; -pub mod random_route_provider; +pub mod node; +pub mod rewarded_set; #[cfg(feature = "provider-trait")] pub mod provider_trait; - -#[cfg(feature = "serializable")] -pub(crate) mod serde; - -#[cfg(feature = "serializable")] -pub use crate::serde::{ - SerializableGateway, SerializableMixNode, SerializableNymTopology, SerializableTopologyError, -}; +#[cfg(feature = "wasm-serde-types")] +pub mod wasm_helpers; #[cfg(feature = "provider-trait")] pub use provider_trait::{HardcodedTopologyProvider, TopologyProvider}; -#[derive(Debug, Default, Clone)] -pub enum NodeVersion { - Explicit(semver::Version), - - #[default] - Unknown, +#[deprecated] +#[derive(Debug, Clone)] +pub enum NetworkAddress { + IpAddr(IpAddr), + Hostname(String), } -// 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(), +#[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), + } } } -} -// 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 + 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()) + } + } } } -} -#[derive(Debug, Clone)] -pub enum NetworkAddress { - IpAddr(IpAddr), - Hostname(String), -} + impl FromStr for NetworkAddress { + type Err = Infallible; -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()) + 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 FromStr for NetworkAddress { - type Err = Infallible; +pub type MixLayer = u8; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct NymTopology { + // for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering + // so we use the same 'master' rewarded set information for that + // + // how do we solve the problem of "we have to go through a node that we want to filter out?" + // ¯\_(ツ)_/¯ that's a future problem + rewarded_set: CachedEpochRewardedSet, + + node_details: HashMap, +} - 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())) +#[derive(Clone, Debug, Default)] +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 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 NymRouteProvider { + pub fn new(topology: NymTopology, ignore_egress_epoch_roles: bool) -> Self { + NymRouteProvider { + topology, + ignore_egress_epoch_roles, } } -} -pub type MixLayer = u8; + 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) + } -// 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 { - mixes: BTreeMap>, - gateways: Vec, -} + pub fn update(&mut self, new_topology: NymTopology) { + self.topology = new_topology; + } -impl NymTopology { - 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 = NymTopology::new_unordered(mixnodes, gateways); - Ok(topology) + pub fn clear_topology(&mut self) { + self.topology = Default::default(); } - pub fn new( - mixes: BTreeMap>, - gateways: Vec, - ) -> Self { - NymTopology { mixes, gateways } + 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 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) - } + pub fn ignore_egress_epoch_roles(&mut self, ignore_egress_epoch_roles: bool) { + self.ignore_egress_epoch_roles = ignore_egress_epoch_roles; + } - NymTopology { mixes, gateways } + pub fn egress_by_identity( + &self, + node_identity: NodeIdentity, + ) -> Result<&RoutingNode, NymTopologyError> { + self.topology + .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) } - pub fn from_unordered(unordered_mixes: MI, unordered_gateways: GI) -> Self + /// 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 - MI: Iterator, - GI: Iterator, - G: TryInto, - M: TryInto, - >::Error: Display, - >::Error: Display, + R: Rng + CryptoRng + ?Sized, { - let mut mixes = BTreeMap::new(); - let mut gateways = Vec::new(); + self.topology + .random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles) + } - 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}"), - } - } + 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) + } +} - for node in unordered_gateways.into_iter() { - match node.try_into() { - Ok(gateway) => gateways.push(gateway), - Err(err) => debug!("malformed gateway: {err}"), - } +impl NymTopology { + pub fn new_empty(rewarded_set: impl Into) -> Self { + NymTopology { + rewarded_set: rewarded_set.into(), + node_details: Default::default(), } + } - NymTopology::new(mixes, gateways) + pub fn new( + rewarded_set: impl Into, + node_details: Vec, + ) -> Self { + NymTopology { + rewarded_set: rewarded_set.into(), + node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), + } } - #[cfg(feature = "serializable")] + #[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) } - pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { - nym_topology_from_basic_info(basic_mixes, basic_gateways) + pub fn add_skimmed_nodes(&mut self, nodes: &[SkimmedNode]) { + self.add_additional_nodes(nodes.iter()) } - 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 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}") } } - 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); + 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}") } } } - 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 has_node_details(&self, node_id: NodeId) -> bool { + self.node_details.contains_key(&node_id) } - pub fn mixes(&self) -> &BTreeMap> { - &self.mixes + pub fn insert_node_details(&mut self, node_details: RoutingNode) { + self.node_details.insert(node_details.node_id, node_details); } - pub fn num_mixnodes(&self) -> usize { - self.mixes.values().map(|m| m.len()).sum() + pub fn rewarded_set(&self) -> &CachedEpochRewardedSet { + &self.rewarded_set } - 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 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), + }; } - pub fn gateways(&self) -> &[gateway::LegacyNode] { - &self.gateways + fn node_details_exists(&self, ids: &HashSet) -> bool { + for id in ids { + if self.node_details.contains_key(id) { + return true; + } + } + false } - pub fn get_gateways(&self) -> Vec { - self.gateways.clone() - } + pub fn is_minimally_routable(&self) -> bool { + 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(); - pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { - self.gateways - .iter() - .find(|gateway| gateway.identity() == gateway_identity) - } + 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" + ); - pub fn gateway_exists(&self, gateway_identity: &NodeIdentity) -> bool { - self.get_gateway(gateway_identity).is_some() + has_layer1 && has_layer2 && has_layer3 && (has_exit_gateways || has_entry_gateways) } - pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) { - self.gateways.push(gateway) + pub fn ensure_minimally_routable(&self) -> Result<(), NymTopologyError> { + if !self.is_minimally_routable() { + return Err(NymTopologyError::InsufficientMixingNodes); + } + Ok(()) } - pub fn set_gateways(&mut self, gateways: Vec) { - self.gateways = gateways + pub fn is_empty(&self) -> bool { + self.rewarded_set.is_empty() || self.node_details.is_empty() } - pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> - where - R: Rng + CryptoRng, - { - self.gateways - .choose(rng) - .ok_or(NymTopologyError::NoGatewaysAvailable) + pub fn ensure_not_empty(&self) -> Result<(), NymTopologyError> { + if self.is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); + } + Ok(()) } - /// 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( + fn find_valid_mix_hop( &self, rng: &mut R, - num_mix_hops: u8, - ) -> Result, NymTopologyError> + id_choices: Vec, + ) -> Result<&RoutingNode, 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()); + 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.node_details.get(&candidate_id) { + Some(node) => { + return Ok(node); + } + // this will mess with VRF, but that's a future problem + None => { + id_choices.remove(index); + continue; + } + } } - Ok(route) + Err(NymTopologyError::NoMixnodesAvailable) } - pub fn random_path_to_gateway( + fn choose_mixing_node( &self, rng: &mut R, - num_mix_hops: u8, - gateway_identity: &NodeIdentity, - ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> + assigned_nodes: &HashSet, + ) -> Result<&RoutingNode, 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 path = self.random_mix_route(rng, num_mix_hops)?; + // try first choice without cloning the ids (because I reckon, more often than not, it will actually work) + // 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); + }; - Ok((path, gateway.clone())) + match self.node_details.get(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) + } + } } - /// 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( - &self, - rng: &mut R, - num_mix_hops: u8, - gateway_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(), - }, - )?; - - Ok(self - .random_mix_route(rng, num_mix_hops)? - .into_iter() - .map(|node| SphinxNode::from(&node)) - .chain(std::iter::once(gateway.into())) - .collect()) + pub fn find_node_by_identity(&self, node_identity: NodeIdentity) -> Option<&RoutingNode> { + self.node_details + .values() + .find(|n| n.identity_key == node_identity) } - /// 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); + pub fn find_node(&self, node_id: NodeId) -> Option<&RoutingNode> { + self.node_details.get(&node_id) } - /// Checks if a mixnet path can be constructed using the specified number of hops - pub fn ensure_can_construct_path_through( + pub fn egress_by_identity( &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); - } + node_identity: NodeIdentity, + ignore_epoch_roles: bool, + ) -> Result<&RoutingNode, NymTopologyError> { + let Some(node) = self.find_node_by_identity(node_identity) else { + return Err(NymTopologyError::NonExistentNode { + node_identity: Box::new(node_identity), + }); + }; - // 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 }); - } - } + // 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: Box::new(node_identity), + }); + }; + if !matches!(role, Role::EntryGateway | Role::ExitGateway) { + return Err(NymTopologyError::InvalidEgressRole { + node_identity: Box::new(node_identity), + }); } } - - Ok(()) + Ok(node) } - pub fn ensure_even_layer_distribution( + fn egress_node_by_identity( &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); - } + node_identity: NodeIdentity, + ignore_epoch_roles: bool, + ) -> Result { + self.egress_by_identity(node_identity, ignore_epoch_roles) + .map(Into::into) + } - if layers.is_empty() { - return Err(NymTopologyError::NoMixnodesAvailable); + fn random_mix_path_nodes(&self, rng: &mut R) -> Result, NymTopologyError> + where + R: Rng + CryptoRng + ?Sized, + { + if self.rewarded_set.is_empty() || self.node_details.is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); } - 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, - }); - } - } + // we reserve an additional item in the route because we'll have to push an egress + let mut mix_route = Vec::with_capacity(4); - Ok(()) + 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) } -} -#[cfg(feature = "serializable")] -impl Serialize for NymTopology { - fn serialize(&self, serializer: S) -> Result + pub fn random_mix_route(&self, rng: &mut R) -> Result, NymTopologyError> where - S: Serializer, + R: Rng + CryptoRng + ?Sized, { - crate::serde::SerializableNymTopology::from(self.clone()).serialize(serializer) + Ok(self + .random_mix_path_nodes(rng)? + .into_iter() + .map(Into::into) + .collect()) } -} -#[cfg(feature = "serializable")] -impl<'de> Deserialize<'de> for NymTopology { - fn deserialize(deserializer: D) -> Result + /// 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, + ignore_epoch_roles: bool, + ) -> Result, NymTopologyError> where - D: Deserializer<'de>, + R: Rng + CryptoRng + ?Sized, { - let serializable = crate::serde::SerializableNymTopology::deserialize(deserializer)?; - serializable.try_into().map_err(::serde::de::Error::custom) + 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) } -} - -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; - } + 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), + 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 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; + 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) } } } - NymTopology::new(mixes, gateways) -} - -#[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")); - } + pub fn entry_gateways(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| self.rewarded_set.entry_gateways.contains(&n.node_id)) } - #[cfg(test)] - mod when_no_nodes_exist { - use super::*; + // ideally this shouldn't exist... + pub fn entry_capable_nodes(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| n.supported_roles.mixnet_entry) + } - #[test] - fn returns_an_empty_vec() { - let topology = NymTopology::new(BTreeMap::new(), vec![]); - let mixvec = topology.mixes_as_vec(); - assert!(mixvec.is_empty()); - } + pub fn mixnodes(&self) -> impl Iterator { + self.node_details + .values() + .filter(|n| self.rewarded_set.is_active_mixnode(&n.node_id)) } } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs deleted file mode 100644 index 40c61cff4b4..00000000000 --- a/common/topology/src/mix.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -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; -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 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 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) - .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 new file mode 100644 index 00000000000..81ab236f765 --- /dev/null +++ b/common/topology/src/node.rs @@ -0,0 +1,143 @@ +// 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 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("node {node_id} ('{identity}') has not provided any valid ip addresses")] + NoIpAddressesProvided { node_id: NodeId, identity: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +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, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +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(Clone, Debug, Serialize, Deserialize)] +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, +} + +impl RoutingNode { + pub fn ws_entry_address_tls(&self) -> Option { + 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 { + 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 { + if let Some(tls) = self.ws_entry_address_tls() { + return Some(tls); + } + self.ws_entry_address_no_tls(prefer_ipv6) + } + + pub fn identity(&self) -> ed25519::PublicKey { + self.identity_key + } +} + +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 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, + identity: value.ed25519_identity_pubkey.to_string(), + }); + }; + + 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, + 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(), + }) + } +} diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/provider_trait.rs index 0dddecf2cb7..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 = "serializable")] + #[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/random_route_provider.rs b/common/topology/src/random_route_provider.rs deleted file mode 100644 index 1771c83eb71..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_gateway(&mut self.rng, hops, destination.gateway()) - } -} 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 601b78dfd34..00000000000 --- a/common/topology/src/serde.rs +++ /dev/null @@ -1,262 +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 { - 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(), - } - } -} - -#[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..ecb451e8beb --- /dev/null +++ b/common/topology/src/wasm_helpers.rs @@ -0,0 +1,123 @@ +// 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::{CachedEpochRewardedSet, NymTopology}; +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: CachedEpochRewardedSet, + + pub node_details: HashMap, +} + +impl TryFrom for NymTopology { + type Error = SerializableTopologyError; + + fn try_from(value: WasmFriendlyNymTopology) -> Result { + let node_details = value + .node_details + .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")] +#[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, +} + +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, + }) + } +} + +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/Cargo.toml b/common/wasm/client-core/Cargo.toml index 1b0b3df67f7..d63e31e50a2 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 = ["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/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-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 3b9c759620b..720553ccf3e 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, @@ -1355,6 +1361,36 @@ 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, + }, + } + } +} + +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 fcfd09c2be3..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, } @@ -134,7 +135,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")] diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index fce29eda8d4..ed9abf30ce1 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -6,7 +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::{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; @@ -93,11 +93,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/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 d259109f988..ba2c7d91154 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -3,130 +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, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, - RewardedSet, RewardingParams, + 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) 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: RewardedSet) -> 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(), - } - } -} - -impl From for RewardedSet { - 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(), - } - } -} - -impl From<&CachedRewardedSet> for RewardedSetResponse { - fn from(value: &CachedRewardedSet) -> Self { - RewardedSetResponse { - 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, @@ -150,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 1738901550e..b4c184d4b6d 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -11,9 +11,10 @@ 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, 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)] @@ -80,7 +79,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, @@ -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 6681ac669e7..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::{LegacyMixLayer, RewardedSet}; +use nym_mixnet_contract_common::{EpochRewardedSet, LegacyMixLayer}; 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/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/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-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/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index 2fcacee75a3..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_gateway( - &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/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 594fb5f4fb3..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, @@ -31,6 +32,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), @@ -49,7 +52,7 @@ struct NymNodeTopologyProviderInner { cache_ttl: Duration, cached_at: OffsetDateTime, cached: Option, - gateway_node: gateway::LegacyNode, + gateway_node: RoutingNode, } impl NymNodeTopologyProviderInner { @@ -67,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/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 67f4a773a77..38c2b3d7f31 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,9 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::mix::LegacyMixLayer; -use nym_topology::{mix, NymTopology}; -use std::collections::BTreeMap; +use nym_topology::{NymTopology, RoutingNode, SupportedRoles}; #[tokio::main] async fn main() { @@ -13,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_topology().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/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/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() 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/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 914cd03b36a..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 @@ -464,7 +463,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 +472,6 @@ impl MixnetListener { self.connected_clients.connect( requested_ips, reply_to, - reply_to_hops, forward_from_tun_tx, close_tx, handle, @@ -518,7 +515,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 @@ -559,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, @@ -757,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") + } +} 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) } 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, }, 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()? 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,