feature: nym topology revamp (#5271)

* revamped NymTopology

* wip

* working e2e client

* updated nym-api

* updated nym-node

* updated rest of non-test code

* updated the rest of the codebase

* additional tweaks

* linux clippy fixes + adding additional dummy ipr types for better linting on non-linux targets
This commit is contained in:
Jędrzej Stuczyński
2024-12-19 10:44:34 +00:00
committed by GitHub
parent ae346bb75b
commit a2322d6cdf
102 changed files with 1985 additions and 2609 deletions
Generated
+1 -4
View File
@@ -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",
+2 -2
View File
@@ -3,7 +3,7 @@ name = "nym-client-core"
version = "1.1.15"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
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" }
@@ -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,
}
}
}
@@ -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::<Vec<_>>();
if available_gateways.is_empty() {
@@ -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(
@@ -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<dyn TopologyProvider + Send + Sync>,
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
@@ -163,6 +163,7 @@ impl LoopCoverTrafficStream<OsRng> {
// 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),
@@ -28,7 +28,6 @@ pub enum InputMessage {
recipient: Recipient,
data: Vec<u8>,
lane: TransmissionLane,
mix_hops: Option<u8>,
},
/// Creates a message used for a duplex anonymous communication where the recipient
@@ -44,7 +43,6 @@ pub enum InputMessage {
data: Vec<u8>,
reply_surbs: u32,
lane: TransmissionLane,
mix_hops: Option<u8>,
},
/// 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<u8>` 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<u8>,
lane: TransmissionLane,
packet_type: Option<PacketType>,
mix_hops: Option<u8>,
) -> 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<PacketType>,
mix_hops: Option<u8>,
) -> 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)
@@ -73,11 +73,10 @@ where
content: Vec<u8>,
lane: TransmissionLane,
packet_type: PacketType,
mix_hops: Option<u8>,
) {
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<u8>,
) {
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,
@@ -70,7 +70,6 @@ pub(crate) struct PendingAcknowledgement {
message_chunk: Fragment,
delay: SphinxDelay,
destination: PacketDestination,
mix_hops: Option<u8>,
retransmissions: u32,
}
@@ -80,13 +79,11 @@ impl PendingAcknowledgement {
message_chunk: Fragment,
delay: SphinxDelay,
recipient: Recipient,
mix_hops: Option<u8>,
) -> 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,
}
}
@@ -52,18 +52,12 @@ where
packet_recipient: Recipient,
chunk_data: Fragment,
packet_type: PacketType,
mix_hops: Option<u8>,
) -> Result<PreparedFragment, PreparationError> {
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
}
@@ -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<u8>,
lane: TransmissionLane,
packet_type: PacketType,
mix_hops: Option<u8>,
) -> 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<u8>,
) -> 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<u8>,
) -> 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<u8>,
) -> 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<u8>,
) -> Result<PreparedFragment, PreparationError> {
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)
}
@@ -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),
@@ -516,7 +516,6 @@ where
recipient,
to_send,
nym_sphinx::params::PacketType::Mix,
self.config.reply_surbs.surb_mix_hops,
)
.await
{
@@ -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<Option<NymTopology>>,
topology: RwLock<NymRouteProvider>,
}
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<NymTopology>) {
*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<NymTopology>>,
permit: RwLockReadGuard<'a, NymRouteProvider>,
}
impl Deref for TopologyReadPermit<'_> {
type Target = Option<NymTopology>;
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)?;
topology.ensure_not_empty()?;
// 2. does it have any mixnode at all?
// 3. does it have any gateways at all?
// 4. does it have a mixnode on each layer?
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
// 2. does the topology have a node on each mixing layer?
topology.ensure_minimally_routable()?;
// 5. does it contain OUR gateway (so that we could create an ack packet)?
if !topology.gateway_exists(ack_recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: ack_recipient.gateway().to_base58_string(),
});
}
// 3. does it contain OUR gateway (so that we could create an ack packet)?
let _ = 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<RwLockReadGuard<'a, Option<NymTopology>>> for TopologyReadPermit<'a> {
fn from(read_permit: RwLockReadGuard<'a, Option<NymTopology>>) -> Self {
TopologyReadPermit {
permit: read_permit,
}
impl<'a> From<RwLockReadGuard<'a, NymRouteProvider>> 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<NymTopology> {
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<RwLockReadGuard<NymRouteProvider>> {
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()
}
}
@@ -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<ExplorerClient> {
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<CountryGroup, Vec<NodeId>>) {
}
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<Url>, filter_on: GroupBy) -> GeoAwareTopologyProvider {
log::info!(
@@ -104,6 +91,15 @@ impl GeoAwareTopologyProvider {
}
async fn get_topology(&self) -> Option<NymTopology> {
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::<Vec<_>>();
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 {
@@ -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...");
@@ -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<nym_client_core_config_types::Topology> 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<Url>, user_agent: Option<UserAgent>) -> Self {
pub fn new(
config: impl Into<Config>,
mut nym_api_urls: Vec<Url>,
user_agent: Option<UserAgent>,
) -> 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<NymTopology> {
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,
};
.inspect_err(|err| error!("failed to get current rewarded set: {err}"))
.ok()?;
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,
};
let mut topology = NymTopology::new_empty(rewarded_set);
debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(),
gateways.len()
);
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()?;
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
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 {
Some(topology)
// 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
}));
};
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<NymTopology> {
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<NymTopology> {
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)
}
}
+7 -4
View File
@@ -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<RoutingNodeError>,
},
#[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()
)]
+39 -19
View File
@@ -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<String>;
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 clients_address(&self) -> String {
self.clients_address()
fn identity(&self) -> identity::PublicKey {
self.identity_key
}
fn clients_address(&self, prefer_ipv6: bool) -> Option<String> {
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<R: Rng>(
nym_apis: &[Url],
user_agent: Option<UserAgent>,
minimum_performance: u8,
) -> Result<Vec<gateway::LegacyNode>, ClientCoreError> {
) -> Result<Vec<RoutingNode>, ClientCoreError> {
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
@@ -104,7 +112,7 @@ pub async fn current_gateways<R: Rng>(
.iter()
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
.filter_map(|gateway| gateway.try_into().ok())
.collect::<Vec<gateway::LegacyNode>>();
.collect::<Vec<_>>();
log::debug!("After checking validity: {}", valid_gateways.len());
log::trace!("Valid gateways: {:#?}", valid_gateways);
@@ -134,7 +142,12 @@ async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, 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<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<G: ConnectableGateway>(
pub(super) fn uniformly_random_gateway<R: Rng>(
rng: &mut R,
gateways: &[gateway::LegacyNode],
gateways: &[RoutingNode],
must_use_tls: bool,
) -> Result<gateway::LegacyNode, ClientCoreError> {
) -> Result<RoutingNode, ClientCoreError> {
filter_by_tls(gateways, must_use_tls)?
.choose(rng)
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
@@ -263,9 +276,9 @@ pub(super) fn uniformly_random_gateway<R: Rng>(
pub(super) fn get_specified_gateway(
gateway_identity: IdentityKeyRef,
gateways: &[gateway::LegacyNode],
gateways: &[RoutingNode],
must_use_tls: bool,
) -> Result<gateway::LegacyNode, ClientCoreError> {
) -> Result<RoutingNode, ClientCoreError> {
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(),
});
+2 -2
View File
@@ -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<K, D>(
key_store: &K,
details_store: &D,
selection_specification: GatewaySelectionSpecification,
available_gateways: Vec<gateway::LegacyNode>,
available_gateways: Vec<RoutingNode>,
) -> Result<InitialisationResult, ClientCoreError>
where
K: KeyStore,
+12 -5
View File
@@ -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<Self, ClientCoreError> {
// 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<gateway::LegacyNode>,
available_gateways: Vec<RoutingNode>,
},
ReuseConnection {
+1 -2
View File
@@ -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")]
@@ -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<EpochRewardedSet, ValidatorClientError> {
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(
@@ -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<RewardedSetResponse, NymAPIError> {
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,
@@ -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<T> 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<RewardedSet, NyxdError> {
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError> {
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,
},
})
}
}
@@ -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<LegacyMixLayer> for Role {
fn from(layer: LegacyMixLayer) -> Self {
match layer {
LegacyMixLayer::One => Role::Layer1,
LegacyMixLayer::Two => Role::Layer2,
LegacyMixLayer::Three => Role::Layer3,
}
}
}
impl From<LegacyMixLayer> for String {
fn from(layer: LegacyMixLayer) -> Self {
(layer as u8).to_string()
@@ -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<Role> {
// 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]
@@ -63,6 +63,7 @@ impl From<v6::request::StaticConnectRequest> for v7::request::StaticConnectReque
}
}
#[allow(deprecated)]
impl From<v6::request::DynamicConnectRequest> for v7::request::DynamicConnectRequest {
fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self {
Self {
@@ -51,6 +51,7 @@ impl IpPacketRequest {
)
}
#[allow(deprecated)]
pub fn new_dynamic_connect_request(
reply_to: Recipient,
reply_to_hops: Option<u8>,
@@ -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<u8>,
// The average delay at each mix node, in milliseconds. Currently this is not supported by the
+33 -31
View File
@@ -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<T = Empty> {
}
impl<T> TestMessage<T> {
pub fn new<N: Into<TestableNode>>(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<N>(
node: N,
msg_id: u32,
total_msgs: u32,
ext: T,
) -> Result<Vec<u8>, NetworkTestingError>
where
N: Into<TestableNode>,
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<N>(
node: &N,
pub fn new_plaintexts(
node: TestableNode,
total_msgs: u32,
ext: T,
) -> Result<Vec<Vec<u8>>, NetworkTestingError>
where
for<'a> &'a N: Into<TestableNode>,
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<Vec<Vec<u8>>, 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<Vec<Vec<u8>>, 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<String, NetworkTestingError>
+9 -33
View File
@@ -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!(
+39 -93
View File
@@ -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<R> {
rng: R,
@@ -38,10 +39,6 @@ pub struct NodeTester<R> {
/// 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<AckKey>,
@@ -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<Vec<PreparedFragment>, NetworkTestingError> {
self.mixnode_test_packets(mix, Empty, test_packets, None)
}
pub fn mixnode_test_packets<T>(
&mut self,
mix: &mix::LegacyNode,
mix: &RoutingNode,
legacy_mix_layer: LegacyMixLayer,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
@@ -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<T>(
&mut self,
nodes: &[mix::LegacyNode],
nodes: &[(LegacyMixLayer, RoutingNode)],
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
@@ -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<T>(
&mut self,
mix_id: NodeId,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, 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<T>(
&mut self,
encoded_mix_identity: String,
layer: LegacyMixLayer,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
@@ -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,
});
};
self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient)
let Some(node) = self.base_topology.find_node_by_identity(identity) else {
return Err(NetworkTestingError::NonExistentMixnodeIdentity {
mix_identity: encoded_mix_identity,
});
};
self.mixnode_test_packets(
&node.clone(),
layer,
msg_ext,
test_packets,
custom_recipient,
)
}
pub fn legacy_gateway_test_packets<T>(
&mut self,
gateway: &gateway::LegacyNode,
node_id: NodeId,
gateway: &RoutingNode,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
@@ -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<T>(
&mut self,
node_id: NodeId,
encoded_gateway_identity: String,
msg_ext: T,
test_packets: u32,
custom_recipient: Option<Recipient>,
) -> Result<Vec<PreparedFragment>, 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<u8>,
topology: &NymTopology,
topology: &NymRouteProvider,
custom_recipient: Option<Recipient>,
) -> Result<PreparedFragment, NetworkTestingError> {
let message = NymMessage::new_plain(plaintext);
@@ -274,14 +225,13 @@ where
&address,
&address,
PacketType::Mix,
None,
)?)
}
pub fn create_test_packet<T>(
&mut self,
message: &TestMessage<T>,
topology: &NymTopology,
topology: &NymRouteProvider,
custom_recipient: Option<Recipient>,
) -> Result<PreparedFragment, NetworkTestingError>
where
@@ -307,10 +257,6 @@ impl<R: CryptoRng + Rng> FragmentPreparer for NodeTester<R> {
1
}
fn num_mix_hops(&self) -> u8 {
self.num_mix_hops
}
fn average_packet_delay(&self) -> Duration {
self.average_packet_delay
}
@@ -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<Self, NymTopologyError>
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();
+2 -2
View File
@@ -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 {
@@ -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<Self, NymTopologyError>
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 {
@@ -169,10 +169,7 @@ impl RepliableMessage {
.collect()
}
pub fn try_from_bytes(
bytes: &[u8],
num_mix_hops: u8,
) -> Result<Self, InvalidReplyRequestError> {
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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<ReplySurb>, usize), InvalidReplyRequestError> {
fn recover_reply_surbs(bytes: &[u8]) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
let mut consumed = mem::size_of::<u32>();
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<Self, InvalidReplyRequestError> {
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::<u32>();
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::<u32>();
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::<u32>();
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<ReplySurb> {
pub(super) fn reply_surbs(rng: &mut ChaCha20Rng, n: usize) -> Vec<ReplySurb> {
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());
}
}
+6 -17
View File
@@ -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,
+5 -8
View File
@@ -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<R>(
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<R>(
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();
-4
View File
@@ -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
+1 -33
View File
@@ -1,19 +1,10 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<Vec<Node>, 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<Node> {
type Error = InvalidNumberOfHops;
fn sphinx_route(
&mut self,
hops: u8,
_destination: &Recipient,
) -> Result<Vec<Node>, 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<Delay> {
if average_packet_delay.is_zero() {
vec![nym_sphinx_types::Delay::new_from_millis(0); num_hops]
+11 -11
View File
@@ -149,7 +149,7 @@ impl NymMessage {
.collect()
}
fn try_from_bytes(bytes: &[u8], num_mix_hops: u8) -> Result<Self, NymMessageError> {
fn try_from_bytes(bytes: &[u8]) -> Result<Self, NymMessageError> {
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<NymMessage, NymMessageError> {
pub fn remove_padding(self) -> Result<NymMessage, NymMessageError> {
// 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());
}
}
+16 -37
View File
@@ -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<Vec<ReplySurb>, 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<SurbAck, NymTopologyError> {
@@ -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<u8>,
) -> Result<PreparedFragment, NymTopologyError> {
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<R> {
/// 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<Vec<ReplySurb>, 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<u8>,
) -> Result<PreparedFragment, NymTopologyError> {
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<SurbAck, NymTopologyError> {
@@ -483,10 +466,6 @@ impl<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<R> {
self.nonce
}
fn num_mix_hops(&self) -> u8 {
self.num_mix_hops
}
fn average_packet_delay(&self) -> Duration {
self.average_packet_delay
}
+2 -129
View File
@@ -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<C>(
&self,
@@ -143,7 +141,7 @@ pub trait MessageReceiver {
fragment: Fragment,
) -> Result<Option<(NymMessage, Vec<i32>)>, 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(),
}],
)
}
}
+4 -7
View File
@@ -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 = []
+12 -8
View File
@@ -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<NodeIdentity> },
#[error("could not use node with identity {node_identity} as egress since it didn't get assigned valid role in the current epoch")]
InvalidEgressRole { node_identity: Box<NodeIdentity> },
#[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),
}
-174
View File
@@ -1,174 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<u16>,
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<NetworkAddress, GatewayConversionError> {
// 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<SocketAddr, GatewayConversionError> {
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<String> {
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<Self, Self::Error> {
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,
})
}
}
+440 -478
View File
File diff suppressed because it is too large Load Diff
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<NetworkAddress, MixnodeConversionError> {
// 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<SocketAddr, MixnodeConversionError> {
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<Self, Self::Error> {
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,
})
}
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<IpAddr>,
pub clients_ws_port: u16,
pub hostname: Option<String>,
pub clients_wss_port: Option<u16>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SupportedRoles {
pub mixnode: bool,
pub mixnet_entry: bool,
pub mixnet_exit: bool,
}
impl From<DeclaredRoles> 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<EntryDetails>,
pub identity_key: ed25519::PublicKey,
pub sphinx_key: x25519::PublicKey,
pub supported_roles: SupportedRoles,
}
impl RoutingNode {
pub fn ws_entry_address_tls(&self) -> Option<String> {
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<String> {
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<String> {
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<Self, Self::Error> {
// 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(),
})
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ pub struct HardcodedTopologyProvider {
}
impl HardcodedTopologyProvider {
#[cfg(feature = "serializable")]
#[cfg(feature = "persistence")]
pub fn new_from_file<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
NymTopology::new_from_file(path).map(Self::new)
}
@@ -1,30 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<R> {
rng: R,
inner: NymTopology,
}
impl<R> SphinxRouteMaker for NymTopologyRouteProvider<R>
where
R: Rng + CryptoRng,
{
type Error = NymTopologyError;
fn sphinx_route(
&mut self,
hops: u8,
destination: &Recipient,
) -> Result<Vec<Node>, NymTopologyError> {
self.inner
.random_route_to_gateway(&mut self.rng, hops, destination.gateway())
}
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<NodeId>,
pub exit_gateways: HashSet<NodeId>,
pub layer1: HashSet<NodeId>,
pub layer2: HashSet<NodeId>,
pub layer3: HashSet<NodeId>,
pub standby: HashSet<NodeId>,
}
impl From<EpochRewardedSet> 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<CachedEpochRewardedSet> 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<Role> {
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<u8> {
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<NodeId> {
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<NodeId> {
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
}
}
-262
View File
@@ -1,262 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<SerializableTopologyError> 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<MixLayer, Vec<SerializableMixNode>>,
pub gateways: Vec<SerializableGateway>,
}
impl TryFrom<SerializableNymTopology> for NymTopology {
type Error = SerializableTopologyError;
fn try_from(value: SerializableNymTopology) -> Result<Self, Self::Error> {
let mut converted_mixes = BTreeMap::new();
for (layer, nodes) in value.mixnodes {
let layer_nodes = nodes
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
converted_mixes.insert(layer, layer_nodes);
}
let gateways = value
.gateways
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(NymTopology::new(converted_mixes, gateways))
}
}
impl From<NymTopology> 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<u16>,
#[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<String>,
}
impl TryFrom<SerializableMixNode> for mix::LegacyNode {
type Error = SerializableTopologyError;
fn try_from(value: SerializableMixNode) -> Result<Self, Self::Error> {
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<IpAddr>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "mix_port")]
pub mix_port: Option<u16>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "clients_port")]
#[serde(alias = "clients_ws_port")]
pub clients_ws_port: Option<u16>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "clients_wss_port")]
pub clients_wss_port: Option<u16>,
#[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<String>,
}
impl TryFrom<SerializableGateway> for gateway::LegacyNode {
type Error = SerializableTopologyError;
fn try_from(value: SerializableGateway) -> Result<Self, Self::Error> {
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()),
}
}
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<SerializableTopologyError> 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<u32, WasmFriendlyRoutingNode>,
}
impl TryFrom<WasmFriendlyNymTopology> for NymTopology {
type Error = SerializableTopologyError;
fn try_from(value: WasmFriendlyNymTopology) -> Result<Self, Self::Error> {
let node_details = value
.node_details
.into_values()
.map(|details| details.try_into())
.collect::<Result<_, _>>()?;
Ok(NymTopology::new(value.rewarded_set, node_details))
}
}
impl From<NymTopology> 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<EntryDetails>,
pub identity_key: String,
pub sphinx_key: String,
pub supported_roles: SupportedRoles,
}
impl TryFrom<WasmFriendlyRoutingNode> for RoutingNode {
type Error = SerializableTopologyError;
fn try_from(value: WasmFriendlyRoutingNode) -> Result<Self, Self::Error> {
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<RoutingNode> 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,
}
}
}
+2 -2
View File
@@ -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" }
+13
View File
@@ -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<TopologyWasm> 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<ConfigTopology> 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,
}
}
}
@@ -271,6 +271,17 @@ pub struct TopologyWasmOverride {
/// This setting is only applicable when `NymApi` topology is used.
#[tsify(optional)]
pub minimum_gateway_performance: Option<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`
#[tsify(optional)]
pub use_extended_topology: Option<bool>,
/// 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<bool>,
}
impl From<TopologyWasmOverride> for TopologyWasm {
@@ -294,6 +305,12 @@ impl From<TopologyWasmOverride> 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),
}
}
}
+13 -7
View File
@@ -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<AnonymousSenderTag, WasmCoreError>
pub async fn current_network_topology_async(
nym_api_url: String,
) -> Result<SerializableNymTopology, WasmCoreError> {
) -> Result<WasmFriendlyNymTopology, WasmCoreError> {
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<IdentityKey>,
gateways: &[gateway::LegacyNode],
gateways: Vec<RoutingNode>,
) -> Result<InitialisationResult, WasmCoreError> {
// 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<InitialisationResult, WasmCoreError> {
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<InitialisationResult, WasmCoreError> {
let gateways = topology.gateways();
let gateways = topology.entry_capable_nodes().cloned().collect::<Vec<_>>();
setup_gateway_wasm(client_store, force_tls, explicit_gateway, gateways).await
}
+8 -33
View File
@@ -1,51 +1,26 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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)
}
}
+37 -1
View File
@@ -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<NodeId>,
pub exit_gateways: Vec<NodeId>,
@@ -1355,6 +1361,36 @@ pub struct RewardedSetResponse {
pub standby: Vec<NodeId>,
}
impl From<RewardedSetResponse> 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<nym_mixnet_contract_common::EpochRewardedSet> 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;
+2 -2
View File
@@ -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<String>)]
pub ip_addresses: Vec<IpAddr>,
// TODO: to be deprecated in favour of well-known hardcoded port for everyone
pub mix_port: u16,
#[serde(with = "bs58_x25519_pubkey")]
+3 -2
View File
@@ -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<RewardedNodeWithParams> {
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();
+2 -2
View File
@@ -181,8 +181,8 @@ impl<R: MessageReceiver + Send + Sync> Monitor<R> {
}
fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet<NodeId>) {
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);
}
+93 -114
View File
@@ -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<ThreadRng> {
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<mix::LegacyNode, String> {
fn parse_bond(
bond: &LegacyMixNodeBondWithLayer,
) -> Result<mix::LegacyNode, MixnodeConversionError> {
let host = mix::LegacyNode::parse_host(&bond.mix_node.host)?;
) -> Result<RoutingNode, String> {
fn parse_bond(bond: &LegacyMixNodeBondWithLayer) -> Option<RoutingNode> {
let (ips, _) = legacy_host_to_ips_and_hostname(&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(),
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<gateway::LegacyNode, String> {
fn parse_bond(
bond: &LegacyGatewayBondWithId,
) -> Result<gateway::LegacyNode, GatewayConversionError> {
let host = gateway::LegacyNode::parse_host(&bond.gateway.host)?;
) -> Result<RoutingNode, String> {
fn parse_bond(bond: &LegacyGatewayBondWithId) -> Option<RoutingNode> {
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<R: Rng>(&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<NodeId, NodeAnnotation>,
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a,
) -> HashMap<LegacyMixLayer, Vec<(mix::LegacyNode, f64)>> {
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
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<NodeId, NodeAnnotation>,
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + '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<Vec<TestRoute>> {
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<LegacyMixNodeBondWithLayer>,
) -> (Vec<mix::LegacyNode>, Vec<InvalidNode>) {
) -> (Vec<RoutingNode>, Vec<InvalidNode>) {
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<LegacyGatewayBondWithId>,
) -> (Vec<(gateway::LegacyNode, NodeId)>, Vec<InvalidNode>) {
) -> (Vec<RoutingNode>, Vec<InvalidNode>) {
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<R: Rng>(
&self,
rng: &mut R,
rewarded_set: &CachedRewardedSet,
mixing_nym_node: &NymNodeDescription,
) -> Option<mix::LegacyNode> {
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::LegacyNode> {
gateway_capable_node.try_to_topology_gateway().ok()
fn nym_node_to_routing_node(&self, description: &NymNodeDescription) -> Option<RoutingNode> {
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::<Vec<_>>();
let mut gateways_under_test = gateways_to_test_details
.iter()
.map(|node| node.into())
.map(|node| TestableNode::new_routing(node, NodeType::Gateway))
.collect::<Vec<_>>();
// 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::<Vec<_>>();
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),
+19 -12
View File
@@ -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<String>,
/// 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<String>,
pub_key: ed25519::PublicKey,
packets: Vec<MixPacket>,
) -> 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<GatewayConfig> {
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<String>, pub_key: ed25519::PublicKey) -> Self {
GatewayPackets {
clients_address,
pub_key,
@@ -356,17 +358,22 @@ impl PacketSender {
fresh_gateway_client_data: Arc<FreshGatewayClientData>,
max_sending_rate: usize,
) -> Option<GatewayClientHandle> {
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
+2 -2
View File
@@ -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<NymApiTestMessageExt>;
@@ -24,7 +24,7 @@ impl NymApiTestMessageExt {
pub fn mix_plaintexts(
&self,
node: &mix::LegacyNode,
node: &RoutingNode,
test_packets: u32,
) -> Result<Vec<Vec<u8>>, NetworkTestingError> {
NodeTestMessage::mix_plaintexts(node, test_packets, *self)
+48 -23
View File
@@ -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<String> {
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)
}
+7 -83
View File
@@ -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<mix::LegacyNode, MixnodeConversionError>;
fn try_to_topology_gateway(&self) -> Result<gateway::LegacyNode, GatewayConversionError>;
fn try_to_topology_node(&self) -> Result<RoutingNode, RoutingNodeError>;
}
impl NodeDescriptionTopologyExt for NymNodeDescription {
// TODO: this might have to be moved around
fn try_to_topology_mix_node(
&self,
layer: LegacyMixLayer,
) -> Result<mix::LegacyNode, MixnodeConversionError> {
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<gateway::LegacyNode, GatewayConversionError> {
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<RoutingNode, RoutingNodeError> {
// 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()
}
}
+17 -33
View File
@@ -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<LegacyMixNodeDetailsWithLayer>,
interval_reward_params: RewardingParams,
current_interval: Interval,
rewarded_set: &CachedRewardedSet,
rewarded_set: &CachedEpochRewardedSet,
blacklist: &HashSet<NodeId>,
) -> HashMap<NodeId, MixNodeBondAnnotated> {
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::<Vec<_>>())
.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::<Vec<_>>())
.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<NodeId, NodeAnnotation> {
+4 -118
View File
@@ -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<NodeId>,
pub(crate) exit_gateways: HashSet<NodeId>,
pub(crate) layer1: HashSet<NodeId>,
pub(crate) layer2: HashSet<NodeId>,
pub(crate) layer3: HashSet<NodeId>,
pub(crate) standby: HashSet<NodeId>,
}
impl From<RewardedSet> 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<CachedRewardedSet> 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<Role> {
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<u8> {
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<NodeId> {
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<NodeId> {
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<Vec<LegacyMixNodeDetailsWithLayer>>,
pub(crate) legacy_gateways: Cache<Vec<LegacyGatewayBondWithId>>,
pub(crate) nym_nodes: Cache<Vec<NymNodeDetails>>,
pub(crate) rewarded_set: Cache<CachedRewardedSet>,
pub(crate) rewarded_set: Cache<CachedEpochRewardedSet>,
// 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.
+6 -7
View File
@@ -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<LegacyMixNodeDetailsWithLayer>,
gateways: Vec<LegacyGatewayBondWithId>,
nym_nodes: Vec<NymNodeDetails>,
rewarded_set: RewardedSet,
rewarded_set: EpochRewardedSet,
config_score_params: ConfigScoreParams,
nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>,
rewarding_params: RewardingParams,
@@ -264,11 +263,11 @@ impl NymContractCache {
.into_inner()
}
pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<Cache<CachedRewardedSet>>> {
pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<Cache<CachedEpochRewardedSet>>> {
self.get(|cache| &cache.rewarded_set).await
}
pub async fn rewarded_set_owned(&self) -> Cache<CachedRewardedSet> {
pub async fn rewarded_set_owned(&self) -> Cache<CachedEpochRewardedSet> {
self.get_owned(|cache| cache.rewarded_set.clone_cache())
.await
.unwrap_or_default()
+17 -5
View File
@@ -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::<HashSet<_>>();
let layer2 = rewarded_set.layer2.iter().collect::<HashSet<_>>();
let layer3 = rewarded_set.layer3.iter().collect::<HashSet<_>>();
let layer1 = rewarded_set
.assignment
.layer1
.iter()
.collect::<HashSet<_>>();
let layer2 = rewarded_set
.assignment
.layer2
.iter()
.collect::<HashSet<_>>();
let layer3 = rewarded_set
.assignment
.layer3
.iter()
.collect::<HashSet<_>>();
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
+6 -3
View File
@@ -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};
@@ -62,9 +61,13 @@ async fn rewarded_set(State(state): State<AppState>) -> AxumResult<Json<Rewarded
.nym_contract_cache()
.rewarded_set()
.await
.ok_or(UninitialisedCache)?;
.map(|cache| cache.clone_cache())
.ok_or(UninitialisedCache)?
.into_inner();
Ok(Json(cached_rewarded_set.deref().deref().into()))
Ok(Json(
nym_mixnet_contract_common::EpochRewardedSet::from(cached_rewarded_set).into(),
))
}
#[utoipa::path(
@@ -3,7 +3,6 @@
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::nym_contract_cache::cache::CachedRewardedSet;
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation};
use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole};
use crate::support::caching::Cache;
@@ -15,6 +14,7 @@ use nym_api_requests::nym_nodes::{
CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode,
};
use nym_mixnet_contract_common::NodeId;
use nym_topology::CachedEpochRewardedSet;
use std::collections::HashMap;
use std::future::Future;
use tokio::sync::RwLockReadGuard;
@@ -24,7 +24,7 @@ pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<Sk
/// Given all relevant caches, build part of response for JUST Nym Nodes
fn build_nym_nodes_response<'a, NI>(
rewarded_set: &CachedRewardedSet,
rewarded_set: &CachedEpochRewardedSet,
nym_nodes_subset: NI,
annotations: &HashMap<NodeId, NodeAnnotation>,
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<LN>(
nodes: &mut Vec<SkimmedNode>,
rewarded_set: &CachedRewardedSet,
rewarded_set: &CachedEpochRewardedSet,
describe_cache: &DescribedNodes,
annotated_legacy_nodes: &HashMap<NodeId, LN>,
active_only: bool,
+3 -2
View File
@@ -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<RwLockReadGuard<Cache<CachedRewardedSet>>, AxumErrorResponse> {
) -> Result<RwLockReadGuard<Cache<CachedEpochRewardedSet>>, AxumErrorResponse> {
self.nym_contract_cache()
.rewarded_set()
.await
+19
View File
@@ -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<IpAddr>, Option<String>)> {
if let Ok(ip) = IpAddr::from_str(legacy) {
return Some((vec![ip], None));
}
let resolved = (legacy, 1789u16)
.to_socket_addrs()
.ok()?
.collect::<Vec<_>>();
Some((
resolved.into_iter().map(|s| s.ip()).collect(),
Some(legacy.to_string()),
))
}
pub(crate) fn to_legacy_mixnode(
nym_node: &NymNodeDetails,
+4 -3
View File
@@ -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<RewardedSet, NyxdError> {
pub(crate) async fn get_rewarded_set_nodes(&self) -> Result<EpochRewardedSet, NyxdError> {
nyxd_query!(self, get_rewarded_set().await)
}
+3 -3
View File
@@ -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}"),
+14 -16
View File
@@ -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<mix::LegacyNode>,
gateway_node: gateway::LegacyNode,
mix_nodes: Vec<RoutingNode>,
gateway_node: RoutingNode,
}
#[derive(Serialize, Deserialize, Debug, Default, ToSchema)]
@@ -61,12 +61,12 @@ pub struct NetworkAccount {
gateway_stats: HashMap<String, GatewayStats>,
incomplete_routes: Vec<Vec<u32>>,
#[serde(skip)]
topology: NymTopology,
topology: NymRouteProvider,
tested_nodes: HashSet<u32>,
#[serde(skip)]
mix_details: HashMap<u32, mix::LegacyNode>,
mix_details: HashMap<u32, RoutingNode>,
#[serde(skip)]
gateway_details: HashMap<String, gateway::LegacyNode>,
gateway_details: HashMap<String, RoutingNode>,
}
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<HydratedRoute> {
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::<Vec<u32>>();
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())
+22 -1
View File
@@ -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<NymTopology> {
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();
+16 -8
View File
@@ -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<nym_topology::gateway::LegacyNode, NymNodeError> {
let Some(ip) = self.config.host.public_ips.first() else {
fn as_gateway_topology_node(&self) -> Result<nym_topology::RoutingNode, NymNodeError> {
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,
},
})
}
+9 -5
View File
@@ -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<Url>,
@@ -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<NymTopology>,
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)
}
};
@@ -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
}
}
@@ -1,50 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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;
}
@@ -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;
+1
View File
@@ -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::*,
+1
View File
@@ -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::{
+12
View File
@@ -180,6 +180,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 {
+7 -3
View File
@@ -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<NymTopology> {
self.client_state.topology_accessor.current_topology().await
pub async fn read_current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
self.client_state
.topology_accessor
.current_route_provider()
.await
}
/// Restore default topology refreshing behaviour of this client.
@@ -74,11 +74,6 @@ impl Socks5MixnetClient {
.await
}
/// Gets the value of the currently used network topology.
pub async fn read_current_topology(&self) -> Option<NymTopology> {
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()
@@ -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<u8>,
// Channel to receive packets from the tun_listener
forward_from_tun_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
@@ -47,7 +44,6 @@ pub(crate) struct ConnectedClientHandler {
impl ConnectedClientHandler {
pub(crate) fn start(
reply_to: Recipient,
reply_to_hops: Option<u8>,
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)
@@ -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;
@@ -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<u8>,
forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
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<u8>,
// Keep track of last activity so we can disconnect inactive clients
pub(crate) last_activity: Arc<RwLock<std::time::Instant>>,
@@ -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<tokio_tun::Tun>,
pub(crate) tun_writer: tokio::io::WriteHalf<TunDevice>,
// 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
@@ -0,0 +1,43 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<std::io::Result<()>> {
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<std::result::Result<usize, Error>> {
unimplemented!("tunnel devices are not supported by non-linux targets")
}
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Error>> {
unimplemented!("tunnel devices are not supported by non-linux targets")
}
fn poll_shutdown(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Error>> {
unimplemented!("tunnel devices are not supported by non-linux targets")
}
}
@@ -4,15 +4,8 @@ use nym_task::connections::TransmissionLane;
pub(crate) fn create_input_message(
nym_address: Recipient,
response_packet: Vec<u8>,
mix_hops: Option<u8>,
) -> 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)
}
@@ -191,7 +191,6 @@ impl MixnetAddress {
recipient: *recipient,
data: message,
lane: TransmissionLane::ConnectionId(connection_id),
mix_hops: None,
}),
packet_type,
},
@@ -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()?
+4 -4
View File
@@ -3,11 +3,11 @@ name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
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 }
+3 -3
View File
@@ -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<IdentityKey>,
) -> Result<NymClientBuilder, WasmClientError> {
@@ -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)
}

Some files were not shown because too many files have changed in this diff Show More