From fc79fe47380f09a4f46847964729a675ebc2d69e Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Thu, 28 May 2026 14:09:36 +0200 Subject: [PATCH] trait rework, removed Ts and NdId generic --- common/nym-lp-data/src/clients/driver.rs | 34 +-- common/nym-lp-data/src/clients/helpers.rs | 25 +- common/nym-lp-data/src/clients/mod.rs | 23 -- common/nym-lp-data/src/clients/traits.rs | 238 +++++++--------- common/nym-lp-data/src/clients/types.rs | 91 +++---- common/nym-lp-data/src/common/helpers.rs | 31 ++- common/nym-lp-data/src/common/traits.rs | 63 ++--- .../nym-lp-data/src/fragmentation/fragment.rs | 48 +--- .../src/fragmentation/reconstruction.rs | 254 +++++++++--------- common/nym-lp-data/src/lib.rs | 83 +++--- common/nym-lp-data/src/nymnodes/traits.rs | 25 +- .../tests/integration/common/mod.rs | 131 +++------ common/nym-lp-data/tests/integration/main.rs | 4 +- nym-mix-sim/src/client/mod.rs | 77 +++--- nym-mix-sim/src/client/nymnode.rs | 166 ++++-------- nym-mix-sim/src/client/simple.rs | 140 ++++------ nym-mix-sim/src/client/sphinx/mod.rs | 210 ++++++--------- .../client/sphinx/poisson_cover_traffic.rs | 69 +++-- nym-mix-sim/src/client/sphinx/surb_acks.rs | 41 ++- nym-mix-sim/src/driver/mod.rs | 133 +++------ nym-mix-sim/src/driver/nymnode.rs | 16 +- nym-mix-sim/src/driver/simple.rs | 9 +- nym-mix-sim/src/driver/sphinx.rs | 62 +---- nym-mix-sim/src/helpers.rs | 31 +++ nym-mix-sim/src/lib.rs | 1 + nym-mix-sim/src/main.rs | 10 +- nym-mix-sim/src/node/mod.rs | 94 ++----- nym-mix-sim/src/node/nymnode.rs | 17 +- nym-mix-sim/src/node/simple.rs | 86 +++--- nym-mix-sim/src/node/sphinx.rs | 105 +++++--- nym-mix-sim/src/packet/mod.rs | 2 +- nym-mix-sim/src/packet/simple.rs | 49 ++-- nym-mix-sim/src/packet/sphinx.rs | 130 +-------- nym-mix-sim/src/topology/directory.rs | 18 +- nym-node/src/node/lp/data/handler/mod.rs | 9 +- .../lp/data/handler/pipeline/mixing_node.rs | 41 ++- .../node/lp/data/handler/pipeline/nymnode.rs | 46 ++-- .../src/node/lp/data/handler/pipeline/wire.rs | 12 +- .../node/lp/data/handler/processing/outfox.rs | 9 +- .../node/lp/data/handler/processing/sphinx.rs | 10 +- nym-node/src/node/lp/data/shared.rs | 9 +- 41 files changed, 992 insertions(+), 1660 deletions(-) create mode 100644 nym-mix-sim/src/helpers.rs diff --git a/common/nym-lp-data/src/clients/driver.rs b/common/nym-lp-data/src/clients/driver.rs index 48a8b916d0..1cb51cbe8d 100644 --- a/common/nym-lp-data/src/clients/driver.rs +++ b/common/nym-lp-data/src/clients/driver.rs @@ -1,10 +1,11 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::net::SocketAddr; use std::sync::mpsc; +use std::time::Instant; use crate::AddressedTimedData; -use crate::clients::InputOptions; use crate::clients::traits::DynClientWrappingPipeline; /// Drives a [`DynClientWrappingPipeline`] tick-by-tick, feeding it raw application @@ -21,33 +22,24 @@ use crate::clients::traits::DynClientWrappingPipeline; /// 3. Packets whose `timestamp ≤ now` are extracted from the buffer and /// returned to the caller for sending. /// -/// `Ts` must implement `Clone + PartialOrd` so that timestamps can be compared -/// to decide which packets are due. +/// Timestamps are [`Instant`]s, compared with `≤` to decide which packets are due. /// -pub struct ClientWrappingPipelineDriver -where - Ts: Clone + PartialOrd, - Opts: InputOptions, -{ - pipeline: Box>, +pub struct ClientWrappingPipelineDriver { + pipeline: Box>, - packet_buffer: Vec>, + packet_buffer: Vec>, - input: mpsc::Receiver<(Vec, Opts)>, + input: mpsc::Receiver<(Vec, Opts, SocketAddr)>, // Keeping a ref so we don't have problem about it being dropped - input_sender: mpsc::SyncSender<(Vec, Opts)>, + input_sender: mpsc::SyncSender<(Vec, Opts, SocketAddr)>, } -impl ClientWrappingPipelineDriver -where - Ts: Clone + PartialOrd, - Opts: InputOptions, -{ +impl ClientWrappingPipelineDriver { /// Create a new driver wrapping `pipeline`. /// /// Internally allocates a zero-capacity `sync_channel` for input payloads. - pub fn new(pipeline: impl DynClientWrappingPipeline + 'static) -> Self { + pub fn new(pipeline: impl DynClientWrappingPipeline + 'static) -> Self { let (input_sender, input_receiver) = mpsc::sync_channel(0); Self { @@ -62,7 +54,7 @@ where /// /// Send raw application payloads here; they will be picked up on the next /// tick when the pipeline's internal buffers are empty. - pub fn input_sender(&self) -> mpsc::SyncSender<(Vec, Opts)> { + pub fn input_sender(&self) -> mpsc::SyncSender<(Vec, Opts, SocketAddr)> { self.input_sender.clone() } @@ -71,7 +63,7 @@ where /// Reads a pending input payload (if both the packet buffer and the /// obfuscation buffer are empty), runs it through the pipeline, then /// returns all packets whose `timestamp ≤ now`. - pub fn tick(&mut self, timestamp: Ts) -> Vec<(Pkt, NdId)> { + pub fn tick(&mut self, timestamp: Instant) -> Vec<(Pkt, SocketAddr)> { // We're reading a message only if our buffer is empty // Otherwise, we will have buffers adding latencies to data let next_message = if self.packet_buffer.is_empty() { @@ -83,7 +75,7 @@ where None }; self.packet_buffer - .extend(self.pipeline.process(next_message, timestamp.clone())); + .extend(self.pipeline.process(next_message, timestamp)); self.packet_buffer .extract_if(.., |p| p.data.timestamp <= timestamp) diff --git a/common/nym-lp-data/src/clients/helpers.rs b/common/nym-lp-data/src/clients/helpers.rs index 4e4bb32363..3d81dd90a7 100644 --- a/common/nym-lp-data/src/clients/helpers.rs +++ b/common/nym-lp-data/src/clients/helpers.rs @@ -1,6 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::Instant; + use crate::PipelinePayload; use crate::clients::traits::{Obfuscation, Reliability, RoutingSecurity}; @@ -10,16 +12,16 @@ use crate::clients::traits::{Obfuscation, Reliability, RoutingSecurity}; /// passes the payload through unchanged with zero byte overhead. pub trait NoOpReliability {} -impl Reliability for T +impl Reliability for T where T: NoOpReliability, { const OVERHEAD_SIZE: usize = 0; fn reliable_encode( &mut self, - input: Option>, - _: Ts, - ) -> Vec> { + input: Option>, + _: Instant, + ) -> Vec> { input.map(|payload| vec![payload]).unwrap_or_default() } } @@ -30,7 +32,7 @@ where /// passes the payload through unchanged with zero byte overhead and `nb_frames() == 1`. pub trait NoOpRoutingSecurity {} -impl RoutingSecurity for T +impl RoutingSecurity for T where T: NoOpRoutingSecurity, { @@ -40,10 +42,7 @@ where 1 } - fn encrypt( - &mut self, - input: PipelinePayload, - ) -> PipelinePayload { + fn encrypt(&mut self, input: PipelinePayload) -> PipelinePayload { input } } @@ -55,15 +54,15 @@ where /// buffering. pub trait NoOpObfuscation {} -impl Obfuscation for T +impl Obfuscation for T where T: NoOpObfuscation, { fn obfuscate( &mut self, - input: Option>, - _: Ts, - ) -> Vec> { + input: Option>, + _: Instant, + ) -> Vec> { input.map(|payload| vec![payload]).unwrap_or_default() } } diff --git a/common/nym-lp-data/src/clients/mod.rs b/common/nym-lp-data/src/clients/mod.rs index dac1e27c6c..12e72a5464 100644 --- a/common/nym-lp-data/src/clients/mod.rs +++ b/common/nym-lp-data/src/clients/mod.rs @@ -5,26 +5,3 @@ pub mod driver; pub mod helpers; pub mod traits; pub mod types; - -/// Per-message pipeline configuration carried alongside every payload. -/// -/// Each pipeline stage (reliability, routing security, obfuscation) is optional -/// and toggled per-message by the corresponding accessor. The next-hop -/// destination is also resolved from the options so that addressing is decided -/// before the payload reaches [`Framing`]. -/// -/// # Type Parameters -/// - `NdId`: addressing type used to identify the next-hop destination. -/// -/// [`Framing`]: crate::common::traits::Framing -pub trait InputOptions: Clone { - /// Whether reliability encoding (e.g. SURB ACKs) should be applied. - fn reliability(&self) -> bool; - /// Whether routing-security encryption (e.g. Sphinx) should be applied. - fn routing_security(&self) -> bool; - /// Whether obfuscation (e.g. cover traffic) should be applied. - fn obfuscation(&self) -> bool; - - /// Identifier of the next-hop node this message should be sent to. - fn next_hop(&self) -> NdId; -} diff --git a/common/nym-lp-data/src/clients/traits.rs b/common/nym-lp-data/src/clients/traits.rs index d7cd22499d..22c123791e 100644 --- a/common/nym-lp-data/src/clients/traits.rs +++ b/common/nym-lp-data/src/clients/traits.rs @@ -1,43 +1,38 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::net::SocketAddr; +use std::time::Instant; + use crate::PipelinePayload; -use crate::clients::InputOptions; use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline}; use crate::{AddressedTimedData, TimedPayload}; /// Trait for splitting an incoming payload into timestamped chunks. /// /// # Type Parameters -/// - `Ts`: Timestamp type associated with each produced [`PipelinePayload`]. -/// - `Opts`: Per-message pipeline options (must implement [`InputOptions`]). -/// - `NdId`: Addressing type for the next-hop destination. +/// - `Opts`: Opaque per-message metadata carried by each produced [`PipelinePayload`]. /// /// # Required Methods -/// - `chunked`: Split `input` into chunks of at most `chunk_size` bytes, tagging -/// each chunk with `timestamp` and `input_options`. Returns one -/// [`PipelinePayload`] per chunk, ready to be fed through the rest of the -/// pipeline. -pub trait Chunking -where - Opts: InputOptions, -{ +/// - `chunked`: Split `input` (a [`PipelinePayload`] carrying the raw bytes, +/// per-message options, and destination) into chunks of at most `chunk_size` +/// bytes. Each output [`PipelinePayload`] inherits the input's options and +/// destination and is stamped with `timestamp`, ready to be fed through the +/// rest of the pipeline. +pub trait Chunking { fn chunked( &mut self, - input: Vec, - input_options: Opts, + input: PipelinePayload, chunk_size: usize, - timestamp: Ts, - ) -> Vec>; + timestamp: Instant, + ) -> Vec>; } /// Trait for applying reliability encoding (e.g. SURB ACKs, retransmissions) to /// a timed payload. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the [`PipelinePayload`]. -/// - `Opts`: Per-message pipeline options. -/// - `NdId`: Addressing type for the next-hop destination. +/// - `Opts`: Opaque per-message metadata carried by the [`PipelinePayload`]. /// /// # Associated Constants /// - `OVERHEAD_SIZE`: Number of additional bytes added by the reliability scheme. @@ -46,13 +41,13 @@ where /// - `reliable_encode`: Encode `input` with the reliability mechanism. When /// `input` is `None`, the method is still called every tick so the layer can /// emit pending retransmissions or scheduled control packets. -pub trait Reliability { +pub trait Reliability { const OVERHEAD_SIZE: usize; fn reliable_encode( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec>; + input: Option>, + timestamp: Instant, + ) -> Vec>; } /// Trait for applying obfuscation (cover traffic, traffic shaping) to a timed payload. @@ -62,10 +57,8 @@ pub trait Reliability { /// schedule even when the application has nothing to send. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the [`PipelinePayload`]. -/// - `Opts`: Per-message pipeline options. -/// - `NdId`: Addressing type for the next-hop destination. -pub trait Obfuscation { +/// - `Opts`: Opaque per-message metadata carried by the [`PipelinePayload`]. +pub trait Obfuscation { /// Obfuscate `input` at the given `timestamp`. /// /// # Parameters @@ -78,17 +71,15 @@ pub trait Obfuscation { /// emitted at this tick. fn obfuscate( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec>; + input: Option>, + timestamp: Instant, + ) -> Vec>; } /// Trait for applying routing-security encryption (e.g. Sphinx) to a timed payload. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the [`PipelinePayload`]. -/// - `Opts`: Per-message pipeline options. -/// - `NdId`: Addressing type for the next-hop destination. +/// - `Opts`: Opaque per-message metadata carried by the [`PipelinePayload`]. /// /// # Associated Constants /// - `OVERHEAD_SIZE`: Number of additional bytes added by the encryption scheme. @@ -100,15 +91,10 @@ pub trait Obfuscation { /// - `nb_frames`: Number of transport frames that one encrypted payload expands /// into; defaults to `1`. Override when the encryption scheme (e.g. Sphinx) /// produces multiple frames per input chunk. -pub trait RoutingSecurity { +pub trait RoutingSecurity { const OVERHEAD_SIZE: usize; - fn nb_frames(&self) -> usize { - 1 - } - fn encrypt( - &mut self, - input: PipelinePayload, - ) -> PipelinePayload; + fn nb_frames(&self) -> usize; + fn encrypt(&mut self, input: PipelinePayload) -> PipelinePayload; } /// Full client-side outbound message pipeline. @@ -118,109 +104,77 @@ pub trait RoutingSecurity { /// (framing + transport) — into a single `process` call that takes a raw byte /// payload and returns a list of timestamped transport packets ready for sending. /// +/// Every stage runs unconditionally; a pipeline that does not want a given stage +/// composes a no-op implementation for it (see the `NoOp*` marker traits), whose +/// `OVERHEAD_SIZE` is `0`. +/// /// # Type Parameters -/// - `Ts`: Timestamp type carried through the pipeline. /// - `Pkt`: Final transport packet type produced by transport. -/// - `Opts`: Per-message pipeline options (must implement [`InputOptions`]). -/// - `NdId`: Addressing type for the next-hop destination. +/// - `Opts`: Opaque per-message metadata threaded through the pipeline. /// /// # Provided Methods /// - `chunk_size`: Derived from `frame_size` (via [`WireWrappingPipeline`]) minus /// routing-security and reliability overheads, accounting for `nb_frames` expansion. /// - `process`: Runs the full pipeline in order: /// chunk → reliability encode → obfuscate → encrypt → frame → transport. -pub trait ClientWrappingPipeline: - Chunking - + Reliability - + Obfuscation - + RoutingSecurity - + WireWrappingPipeline -where - Ts: Clone, - NdId: Clone, - Opts: InputOptions, +pub trait ClientWrappingPipeline: + Chunking + + Reliability + + Obfuscation + + RoutingSecurity + + WireWrappingPipeline { - fn chunk_size(&self, input_options: Opts) -> usize { + fn chunk_size(&self) -> usize { // Frame size comes from WireWrappingPipeline - let mut chunk_size = self.frame_size(); - - if input_options.routing_security() { - // SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway - #[allow(clippy::expect_used)] - let pre_security_chunk_size = (chunk_size * self.nb_frames()) - .checked_sub(>::OVERHEAD_SIZE) - .expect("not enough room in a packet for routing security overhead"); - chunk_size = pre_security_chunk_size; - } - - if input_options.reliability() { - // SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway - #[allow(clippy::expect_used)] - let pre_reliability_chunk_size = chunk_size - .checked_sub(>::OVERHEAD_SIZE) - .expect("not enough room in a packet for reliability overhead"); - chunk_size = pre_reliability_chunk_size; - } - - chunk_size + // SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway + #[allow(clippy::expect_used)] + (self.frame_size() * self.nb_frames()) + .checked_sub(>::OVERHEAD_SIZE) + .expect("not enough room in a packet for routing security overhead") + .checked_sub(>::OVERHEAD_SIZE) + .expect("not enough room in a packet for reliability overhead") } fn process( &mut self, - input: Option<(Vec, Opts)>, // Optional to be able to tick the pipeline without input - timestamp: Ts, - ) -> Vec> { - let mut chunks = if let Some((input_data, input_options)) = input { - self.chunked( - input_data, - input_options.clone(), - self.chunk_size(input_options.clone()), - timestamp.clone(), - ) + input: Option<(Vec, Opts, SocketAddr)>, // Optional to be able to tick the pipeline without input + timestamp: Instant, + ) -> Vec> { + let chunk_size = self.chunk_size(); + let mut chunks = if let Some((input_data, input_options, next_hop)) = input { + let input_payload = + PipelinePayload::new(timestamp, input_data, input_options, next_hop); + self.chunked(input_payload, chunk_size, timestamp) } else { Vec::new() }; - // Reliability stage with chunks that needs reliability + // Reliability stage + chunks = if chunks.is_empty() { + // Even if we had nothing go into the reliability stage, we need to catch potential retransmissions + self.reliable_encode(None, timestamp) + } else { + chunks + .into_iter() + .flat_map(|chunk| self.reliable_encode(Some(chunk), timestamp)) + .collect() + }; + + // Obfuscation stage + chunks = if chunks.is_empty() { + // Even if we had nothing go into the obfuscation stage, we need to catch potential cover traffic + self.obfuscate(None, timestamp) + } else { + chunks + .into_iter() + .flat_map(|chunk| self.obfuscate(Some(chunk), timestamp)) + .collect() + }; + + // Routing-security stage chunks = chunks .into_iter() - .flat_map(|chunk| { - if chunk.options.reliability() { - self.reliable_encode(Some(chunk), timestamp.clone()) - } else { - vec![chunk] - } - }) - .collect(); - - // Even if we had nothing go into the reliability stage, we need to catch potential retransmissions - // If we had, this should be a no-op, since it already has been called with the same timestamp - chunks.append(&mut self.reliable_encode(None, timestamp.clone())); - - chunks = chunks - .into_iter() - .flat_map(|chunk| { - if chunk.options.obfuscation() { - self.obfuscate(Some(chunk), timestamp.clone()) - } else { - vec![chunk] - } - }) - .collect(); - - // Even if we had nothing go into the obfuscation stage, we need to catch potential cover traffic - // If we had, this should be a no-op, since it already has been called with the same timestamp - chunks.append(&mut self.obfuscate(None, timestamp.clone())); - - chunks = chunks - .into_iter() - .map(|chunk| { - if chunk.options.routing_security() { - self.encrypt(chunk) - } else { - chunk - } - }) + .map(|chunk| self.encrypt(chunk)) .collect(); chunks @@ -233,30 +187,26 @@ where /// Dyn-compatible mirror of [`ClientWrappingPipeline`]. /// /// All associated constants from the sub-traits are exposed as methods so the -/// trait can be used as `dyn DynClientWrappingPipeline`, -/// erasing the concrete pipeline type while keeping `Ts`, `Pkt`, `Opts`, and -/// `NdId` visible. +/// trait can be used as `dyn DynClientWrappingPipeline`, erasing the +/// concrete pipeline type while keeping `Pkt` and `Opts` visible. /// /// Implement [`ClientWrappingPipeline`] on your concrete type; the blanket impl /// below provides `DynClientWrappingPipeline` for free. -pub trait DynClientWrappingPipeline { +pub trait DynClientWrappingPipeline { /// On-wire size of an output packet in bytes. fn packet_size(&self) -> usize; /// Run the full client wrapping pipeline; see [`ClientWrappingPipeline::process`]. fn process( &mut self, - input: Option<(Vec, Opts)>, - timestamp: Ts, - ) -> Vec>; + input: Option<(Vec, Opts, SocketAddr)>, + timestamp: Instant, + ) -> Vec>; } -impl DynClientWrappingPipeline for T +impl DynClientWrappingPipeline for T where - Ts: Clone, - NdId: Clone, - Opts: InputOptions, - T: ClientWrappingPipeline, + T: ClientWrappingPipeline, { fn packet_size(&self) -> usize { WireWrappingPipeline::packet_size(self) @@ -264,9 +214,9 @@ where fn process( &mut self, - input: Option<(Vec, Opts)>, - timestamp: Ts, - ) -> Vec> { + input: Option<(Vec, Opts, SocketAddr)>, + timestamp: Instant, + ) -> Vec> { ClientWrappingPipeline::process(self, input, timestamp) } } @@ -278,7 +228,6 @@ where /// fills in (routing-security decrypt, reliability decode, chunk reassembly, etc.). /// /// # Type Parameters -/// - `Ts`: Timestamp type. /// - `Pkt`: Transport packet type consumed as input. /// - `Mk`: Message-kind marker returned alongside reassembled payloads. /// @@ -290,13 +239,10 @@ where /// # Provided Methods /// - `unwrap`: Strips the wire layers via [`WireUnwrappingPipeline::wire_unwrap`], /// then delegates to `process_unwrapped`. -pub trait ClientUnwrappingPipeline: WireUnwrappingPipeline -where - Ts: Clone, -{ - fn process_unwrapped(&mut self, payload: TimedPayload, kind: Mk) -> Option>; +pub trait ClientUnwrappingPipeline: WireUnwrappingPipeline { + fn process_unwrapped(&mut self, payload: TimedPayload, kind: Mk) -> Option>; - fn unwrap(&mut self, input: Pkt, timestamp: Ts) -> Result>, Self::Error> { + fn unwrap(&mut self, input: Pkt, timestamp: Instant) -> Result>, Self::Error> { Ok(self .wire_unwrap(input, timestamp)? .and_then(|(payload, kind)| self.process_unwrapped(payload, kind))) diff --git a/common/nym-lp-data/src/clients/types.rs b/common/nym-lp-data/src/clients/types.rs index 879b0f8bcc..a8c8456a23 100644 --- a/common/nym-lp-data/src/clients/types.rs +++ b/common/nym-lp-data/src/clients/types.rs @@ -1,7 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::clients::InputOptions; +use std::time::Instant; + use crate::clients::traits::{ Chunking, ClientWrappingPipeline, Obfuscation, Reliability, RoutingSecurity, }; @@ -36,55 +37,51 @@ pub struct Pipeline { pub transport: T, } -impl Chunking for Pipeline +impl Chunking for Pipeline where - Opts: InputOptions, - C: Chunking, + C: Chunking, { fn chunked( &mut self, - input: Vec, - input_options: Opts, + input: PipelinePayload, chunk_size: usize, - timestamp: Ts, - ) -> Vec> { - self.chunking - .chunked(input, input_options, chunk_size, timestamp) + timestamp: Instant, + ) -> Vec> { + self.chunking.chunked(input, chunk_size, timestamp) } } -impl Reliability for Pipeline +impl Reliability for Pipeline where - R: Reliability, + R: Reliability, { const OVERHEAD_SIZE: usize = R::OVERHEAD_SIZE; fn reliable_encode( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec> { + input: Option>, + timestamp: Instant, + ) -> Vec> { self.reliability.reliable_encode(input, timestamp) } } -impl Obfuscation for Pipeline +impl Obfuscation for Pipeline where - O: Obfuscation, + O: Obfuscation, { fn obfuscate( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec> { + input: Option>, + timestamp: Instant, + ) -> Vec> { self.obfuscation.obfuscate(input, timestamp) } } -impl RoutingSecurity - for Pipeline +impl RoutingSecurity for Pipeline where - Rs: RoutingSecurity, + Rs: RoutingSecurity, { const OVERHEAD_SIZE: usize = Rs::OVERHEAD_SIZE; @@ -92,69 +89,59 @@ where self.security.nb_frames() } - fn encrypt( - &mut self, - input: PipelinePayload, - ) -> PipelinePayload { + fn encrypt(&mut self, input: PipelinePayload) -> PipelinePayload { self.security.encrypt(input) } } -impl Framing for Pipeline +impl Framing for Pipeline where - F: Framing, + F: Framing, { type Frame = F::Frame; const OVERHEAD_SIZE: usize = F::OVERHEAD_SIZE; fn to_frame( &mut self, - payload: PipelinePayload, + payload: PipelinePayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { self.framing.to_frame(payload, frame_size) } } -impl Transport for Pipeline +impl Transport for Pipeline where - T: Transport, + T: Transport, { type Frame = T::Frame; const OVERHEAD_SIZE: usize = T::OVERHEAD_SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { self.transport.to_transport_packet(frame) } } -impl WireWrappingPipeline - for Pipeline +impl WireWrappingPipeline for Pipeline where - Ts: Clone, - NdId: Clone, - F: Framing, - T: Transport, + F: Framing, + T: Transport, { fn packet_size(&self) -> usize { self.packet_size } } -impl ClientWrappingPipeline - for Pipeline +impl ClientWrappingPipeline for Pipeline where - Ts: Clone, - NdId: Clone, - Opts: InputOptions, - C: Chunking, - R: Reliability, - O: Obfuscation, - Rs: RoutingSecurity, - F: Framing, - T: Transport, + C: Chunking, + R: Reliability, + O: Obfuscation, + Rs: RoutingSecurity, + F: Framing, + T: Transport, { } diff --git a/common/nym-lp-data/src/common/helpers.rs b/common/nym-lp-data/src/common/helpers.rs index 7ff9bd85dc..ad60fdd0cf 100644 --- a/common/nym-lp-data/src/common/helpers.rs +++ b/common/nym-lp-data/src/common/helpers.rs @@ -1,6 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::Instant; + use crate::{ AddressedTimedData, AddressedTimedPayload, PipelinePayload, TimedData, TimedPayload, common::traits::{ @@ -17,7 +19,7 @@ pub trait NoOpWireWrapper { const PACKET_SIZE: usize = 1500; } -impl Framing for T +impl Framing for T where T: NoOpWireWrapper, { @@ -25,14 +27,14 @@ where const OVERHEAD_SIZE: usize = 0; fn to_frame( &mut self, - payload: PipelinePayload, + payload: PipelinePayload, _: usize, - ) -> Vec> { + ) -> Vec { vec![payload.into_addressed()] } } -impl Transport for T +impl Transport for T where T: NoOpWireWrapper, Pkt: From>, @@ -41,18 +43,16 @@ where const OVERHEAD_SIZE: usize = 0; fn to_transport_packet( &mut self, - frame: AddressedTimedPayload, - ) -> AddressedTimedData { + frame: AddressedTimedPayload, + ) -> AddressedTimedData { frame.data_transform(|data| data.into()) } } -impl WireWrappingPipeline for T +impl WireWrappingPipeline for T where T: NoOpWireWrapper, - Ts: Clone, Pkt: From>, - NdId: Clone, { fn packet_size(&self) -> usize { T::PACKET_SIZE @@ -65,18 +65,18 @@ where /// passes the payload through unchanged. pub trait NoOpWireUnwrapper {} -impl FramingUnwrap for T +impl FramingUnwrap for T where T: NoOpWireUnwrapper, Mk: Default, { type Frame = Vec; - fn frame_to_message(&mut self, frame: TimedPayload) -> Option<(TimedPayload, Mk)> { + fn frame_to_message(&mut self, frame: TimedPayload) -> Option<(TimedPayload, Mk)> { Some((frame, Default::default())) } } -impl TransportUnwrap for T +impl TransportUnwrap for T where T: NoOpWireUnwrapper, Pkt: Into>, @@ -86,8 +86,8 @@ where fn packet_to_frame( &mut self, packet: Pkt, - timestamp: Ts, - ) -> Result, Self::Error> { + timestamp: Instant, + ) -> Result { Ok(TimedData { timestamp, data: packet.into(), @@ -95,10 +95,9 @@ where } } -impl WireUnwrappingPipeline for T +impl WireUnwrappingPipeline for T where T: NoOpWireUnwrapper, - Ts: Clone, Pkt: Into>, Mk: Default, { diff --git a/common/nym-lp-data/src/common/traits.rs b/common/nym-lp-data/src/common/traits.rs index edfcf5896e..f45b0a8814 100644 --- a/common/nym-lp-data/src/common/traits.rs +++ b/common/nym-lp-data/src/common/traits.rs @@ -1,14 +1,14 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::Instant; + use crate::{AddressedTimedData, PipelinePayload, TimedData, TimedPayload}; /// Trait for applying framing to a timed payload. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the `PipelinePayload`. /// - `Opts` : Opts type carried by the `PipelinePayload` -/// - `NdId` : Addressing type /// /// # Associated Types /// - `Frame`: Frame type produced by the framing operation. @@ -17,21 +17,20 @@ use crate::{AddressedTimedData, PipelinePayload, TimedData, TimedPayload}; /// - `OVERHEAD_SIZE`: Number of additional bytes added by the framing scheme. /// /// # Required Methods -/// - `to_frame`: Splits the payload into a `Vec>` of frames of the given size. -pub trait Framing { +/// - `to_frame`: Splits the payload into a `Vec>` of frames of the given size. +pub trait Framing { type Frame; const OVERHEAD_SIZE: usize; fn to_frame( &mut self, - payload: PipelinePayload, + payload: PipelinePayload, frame_size: usize, - ) -> Vec>; + ) -> Vec>; } /// Trait for unwrapping framing from a frame back into a payload. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the `TimedPayload`. /// - `Mk`: Enum describing the kind of message that can be returned. /// /// # Associated Types @@ -40,18 +39,14 @@ pub trait Framing { /// # Required Methods /// - `frame_to_message`: Attempts to reassemble a payload from the given frame, returning /// `Some((payload, kind))` when a complete message is available, or `None` otherwise. -pub trait FramingUnwrap { +pub trait FramingUnwrap { type Frame; - fn frame_to_message( - &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, Mk)>; + fn frame_to_message(&mut self, frame: TimedData) -> Option<(TimedPayload, Mk)>; } /// Trait for applying a transport layer to a framed payload. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the `TimedPayload`. /// - `Pkt`: Transport packet type produced as output. /// /// # Associated Types @@ -62,19 +57,18 @@ pub trait FramingUnwrap { /// /// # Required Methods /// - `to_transport_packet`: Wraps a frame into a transport packet. -pub trait Transport { +pub trait Transport { type Frame; const OVERHEAD_SIZE: usize; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData; + frame: AddressedTimedData, + ) -> AddressedTimedData; } /// Trait for unwrapping a transport packet back into a frame. /// /// # Type Parameters -/// - `Ts`: Timestamp type carried by the `TimedPayload`. /// - `Pkt`: Transport packet type consumed as input. /// /// # Associated Types @@ -84,14 +78,14 @@ pub trait Transport { /// # Required Methods /// - `packet_to_frame`: Strips the transport layer from a packet, returning the inner frame /// tagged with the given timestamp. -pub trait TransportUnwrap { +pub trait TransportUnwrap { type Frame; type Error; fn packet_to_frame( &mut self, packet: Pkt, - timestamp: Ts, - ) -> Result, Self::Error>; + timestamp: Instant, + ) -> Result, Self::Error>; } /// Supertrait combining [`Framing`] and [`Transport`] into a reusable wire-wrapping layer. @@ -99,10 +93,8 @@ pub trait TransportUnwrap { /// Used as the bottom stage of any outbound pipeline (client or mixnode). /// /// # Type Parameters -/// - `Ts`: Timestamp type. /// - `Pkt`: Final transport packet type. /// - `Opts` : Option type -/// - `NdId` : Addressing type /// /// Both [`Framing`] and [`Transport`] declare their own `type Frame`; this /// supertrait cross-constrains them so `to_frame`'s output feeds directly into @@ -114,12 +106,8 @@ pub trait TransportUnwrap { /// # Provided Methods /// - `frame_size`: Derived from `packet_size` minus transport and framing overheads. /// - `wire_wrap`: Frames a payload and wraps each frame into a transport packet. -pub trait WireWrappingPipeline: - Transport - + Framing>::Frame> -where - Ts: Clone, - NdId: Clone, +pub trait WireWrappingPipeline: + Transport + Framing>::Frame> { // IMPORTANT NOTE : This fn can be not constant to allow e.g. flexible MTU // However, every possible value must be able to accommodate the different overhead. @@ -131,16 +119,12 @@ where #[allow(clippy::expect_used)] self.packet_size() .checked_sub( - >::OVERHEAD_SIZE - + >::OVERHEAD_SIZE, + >::OVERHEAD_SIZE + >::OVERHEAD_SIZE, ) .expect("packet_size smaller than transport + framing overhead") } - fn wire_wrap( - &mut self, - payload: PipelinePayload, - ) -> Vec> { + fn wire_wrap(&mut self, payload: PipelinePayload) -> Vec> { let frame_size = self.frame_size(); self.to_frame(payload, frame_size) .into_iter() @@ -155,7 +139,6 @@ where /// Used as the bottom stage of any inbound pipeline (client or mixnode). /// /// # Type Parameters -/// - `Ts`: Timestamp type. /// - `Pkt`: Transport packet type consumed as input. /// - `Mk`: Message-kind marker returned alongside the reassembled payload. /// @@ -166,16 +149,14 @@ where /// # Provided Methods /// - `wire_unwrap`: Strips the transport layer from a packet and attempts to reassemble /// a payload, returning `Some((payload, kind))` when a complete message is available. -pub trait WireUnwrappingPipeline: - TransportUnwrap + FramingUnwrap>::Frame> -where - Ts: Clone, +pub trait WireUnwrappingPipeline: + TransportUnwrap + FramingUnwrap>::Frame> { fn wire_unwrap( &mut self, input: Pkt, - timestamp: Ts, - ) -> Result, Mk)>, Self::Error> { + timestamp: Instant, + ) -> Result, Self::Error> { let frame = self.packet_to_frame(input, timestamp)?; Ok(self.frame_to_message(frame)) } diff --git a/common/nym-lp-data/src/fragmentation/fragment.rs b/common/nym-lp-data/src/fragmentation/fragment.rs index d798af2447..073ff33af6 100644 --- a/common/nym-lp-data/src/fragmentation/fragment.rs +++ b/common/nym-lp-data/src/fragmentation/fragment.rs @@ -9,16 +9,6 @@ use crate::{ }, }; -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -/// Key for reconstruction hashmap -pub struct FragmentHashKey(LpFrameKind, u64); - -impl From<(LpFrameKind, u64)> for FragmentHashKey { - fn from(value: (LpFrameKind, u64)) -> Self { - FragmentHashKey(value.0, value.1) - } -} - #[derive(PartialEq, Clone, Debug)] pub struct FragmentHeader { /// ID associated to this particular `Fragment`. @@ -31,26 +21,17 @@ pub struct FragmentHeader { /// Index of this fragment, in (0..total_fragments) current_fragment: u8, - /// The kind of the inner frame - next_frame_kind: LpFrameKind, // SW I can remove that - - reserved: [u8; 2], + reserved: [u8; 4], } impl FragmentHeader { // It's up to the caller to make sure values are valid - fn new( - id: u64, - total_fragments: u8, - current_fragment: u8, - next_frame_kind: LpFrameKind, - ) -> Self { + fn new(id: u64, total_fragments: u8, current_fragment: u8) -> Self { FragmentHeader { id, total_fragments, current_fragment, - next_frame_kind, - reserved: [0; 2], + reserved: [0; 4], } } } @@ -61,8 +42,7 @@ impl From for LpFrameAttributes { buf[0..8].copy_from_slice(&value.id.to_be_bytes()); buf[8] = value.total_fragments; buf[9] = value.current_fragment; - buf[10..12].copy_from_slice(&u16::to_be_bytes(value.next_frame_kind.into())); - buf[12..14].copy_from_slice(&value.reserved); + buf[10..14].copy_from_slice(&value.reserved); buf } } @@ -83,9 +63,7 @@ impl TryFrom for FragmentHeader { total_fragments, current_fragment, #[allow(clippy::unwrap_used)] - next_frame_kind: u16::from_be_bytes(value[10..12].try_into().unwrap()).into(), - #[allow(clippy::unwrap_used)] - reserved: value[12..14].try_into().unwrap(), + reserved: value[10..14].try_into().unwrap(), }) } } @@ -98,14 +76,8 @@ pub struct Fragment { impl Fragment { // It's up to the caller to make sure values are valid - fn new( - payload: &[u8], - id: u64, - total_fragments: u8, - current_fragment: u8, - next_frame_kind: LpFrameKind, - ) -> Self { - let header = FragmentHeader::new(id, total_fragments, current_fragment, next_frame_kind); + fn new(payload: &[u8], id: u64, total_fragments: u8, current_fragment: u8) -> Self { + let header = FragmentHeader::new(id, total_fragments, current_fragment); Fragment { header, payload: payload.to_vec(), @@ -132,10 +104,6 @@ impl Fragment { self.header.current_fragment } - pub fn hash_key(&self) -> FragmentHashKey { - (self.header.next_frame_kind, self.header.id).into() - } - /// Consumes `self` to obtain payload (i.e. part of original message) associated with this /// `Fragment`. pub(crate) fn extract_payload(self) -> Vec { @@ -165,7 +133,6 @@ pub fn fragment_lp_message( ) -> Vec { debug_assert!(message.len() <= u8::MAX as usize * fragment_payload_size); - let message_kind = message.kind(); let message_bytes = message.to_bytes(); let id = rng.r#gen(); @@ -182,7 +149,6 @@ pub fn fragment_lp_message( id, num_fragments, i as u8, - message_kind, )) } diff --git a/common/nym-lp-data/src/fragmentation/reconstruction.rs b/common/nym-lp-data/src/fragmentation/reconstruction.rs index cfc578ed24..fed99f740d 100644 --- a/common/nym-lp-data/src/fragmentation/reconstruction.rs +++ b/common/nym-lp-data/src/fragmentation/reconstruction.rs @@ -1,13 +1,11 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::fragmentation::fragment::{Fragment, FragmentHashKey}; +use crate::fragmentation::fragment::Fragment; use crate::packet::{LpFrame, MalformedLpPacketError}; use dashmap::DashMap; use dashmap::mapref::entry::Entry; -use std::fmt::Debug; -use std::ops::Add; use std::sync::Arc; use std::time::{Duration, Instant}; use tracing::{debug, trace, warn}; @@ -17,10 +15,7 @@ pub const DEFAULT_FRAGMENT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Per-message buffer that collects every `Fragment` of a fragmented message /// and reassembles the original payload once they are all in. #[derive(Debug, Clone)] -struct MessageBuffer -where - Ts: PartialOrd + Clone + Debug, -{ +struct MessageBuffer { /// Cached completion flag, set as soon as the last missing slot has been /// filled. Avoids re-scanning `fragments` on every read. is_complete: bool, @@ -33,16 +28,13 @@ where /// Timestamp of the most recently inserted fragment. Read by /// [`MessageReconstructor::cleanup_stale_buffers`] to evict messages whose /// remaining fragments never showed up. - last_fragment_timestamp: Ts, + last_fragment_timestamp: Instant, } -impl MessageBuffer -where - Ts: PartialOrd + Clone + Debug, -{ +impl MessageBuffer { /// Create an empty buffer sized for `total_fragments` slots. /// The `u8` argument bounds the allocation at `u8::MAX`. - fn new(total_fragments: u8, timestamp: Ts) -> Self { + fn new(total_fragments: u8, timestamp: Instant) -> Self { // `new` should never be called with size 0: `total_fragments` is taken // from the first received `Fragment` of the message, and decoding // rejects any header where `current_fragment >= total_fragments`, so @@ -91,7 +83,7 @@ where /// update `last_fragment_timestamp` and `is_complete` accordingly. /// /// Duplicate fragments are logged, then ignored - fn insert_fragment(&mut self, fragment: Fragment, timestamp: Ts) { + fn insert_fragment(&mut self, fragment: Fragment, timestamp: Instant) { self.last_fragment_timestamp = timestamp; // All fragments routed into a given buffer must share the same id — @@ -124,31 +116,21 @@ where } /// Public reassembly state for fragmented messages. Buffers in-flight -/// messages keyed on their [`FragmentHashKey`] and yields the original bytes +/// messages keyed on their fragment id and yields the original bytes /// once every fragment of a given message has been received. #[derive(Debug, Clone)] -pub struct MessageReconstructor -where - Ts: PartialOrd + Debug + Clone + Add, - To: Clone + Debug, -{ - /// In-flight messages keyed on `(id, frame_kind)`. The frame kind is - /// part of the key so that a random-id collision between two unrelated - /// kinds cannot accidentally route fragments into the same buffer. - in_flight_messages: Arc>>, +pub struct MessageReconstructor { + /// In-flight messages keyed on the random 64-bit fragment id. + in_flight_messages: Arc>, /// How long an incomplete message is allowed to sit before it is /// dropped on the next `cleanup_stale_buffers` pass. - incomplete_message_timeout: To, + incomplete_message_timeout: Duration, } -impl MessageReconstructor -where - Ts: PartialOrd + Debug + Clone + Add, - To: Clone + Debug, -{ +impl MessageReconstructor { /// Create an empty `MessageReconstructor`. - pub fn new(incomplete_message_timeout: To) -> Self { + pub fn new(incomplete_message_timeout: Duration) -> Self { Self { in_flight_messages: Default::default(), incomplete_message_timeout, @@ -162,22 +144,22 @@ where pub fn insert_new_fragment( &self, fragment: Fragment, - timestamp: Ts, + timestamp: Instant, ) -> Option> { - let key = fragment.hash_key(); + let frag_id = fragment.id(); let total_fragments = fragment.total_fragments(); - let maybe_message = match self.in_flight_messages.entry(key) { + let maybe_message = match self.in_flight_messages.entry(frag_id) { Entry::Occupied(mut entry) => { - entry.get_mut().insert_fragment(fragment, timestamp.clone()); + entry.get_mut().insert_fragment(fragment, timestamp); entry .get() .is_complete .then(|| LpFrame::decode(&entry.remove().into_message())) } Entry::Vacant(entry) => { - let mut buf = MessageBuffer::new(total_fragments, timestamp.clone()); - buf.insert_fragment(fragment, timestamp.clone()); + let mut buf = MessageBuffer::new(total_fragments, timestamp); + buf.insert_fragment(fragment, timestamp); if buf.is_complete { Some(LpFrame::decode(&buf.into_message())) } else { @@ -188,18 +170,16 @@ where }; // This might be a bit slow, keep an eye on it - self.cleanup_stale_buffers(timestamp.clone()); + self.cleanup_stale_buffers(timestamp); maybe_message } /// Drop incomplete messages whose `last_fragment_timestamp` is older /// than `incomplete_message_timeout` ago. - pub fn cleanup_stale_buffers(&self, timestamp: Ts) { + pub fn cleanup_stale_buffers(&self, timestamp: Instant) { trace!("Cleaning up stale buffers"); self.in_flight_messages.retain(|_, buf| { - let keep = buf.last_fragment_timestamp.clone() - + self.incomplete_message_timeout.clone() - > timestamp; + let keep = buf.last_fragment_timestamp + self.incomplete_message_timeout > timestamp; if !keep { debug!( "Removing stale buffer for message id {:?}", @@ -213,7 +193,7 @@ where } } -impl Default for MessageReconstructor { +impl Default for MessageReconstructor { fn default() -> Self { MessageReconstructor::new(DEFAULT_FRAGMENT_TIMEOUT_DURATION) } @@ -231,7 +211,6 @@ mod tests { use rand::rngs::StdRng; const SPHINX: LpFrameKind = LpFrameKind::SphinxPacket; - const OUTFOX: LpFrameKind = LpFrameKind::OutfoxPacket; /// Build a `Fragment` with explicit header values via the public /// `LpFrame` round-trip, so tests can craft duplicates, out-of-order @@ -257,68 +236,113 @@ mod tests { fragment_lp_message(&mut rng, message, fragment_size) } + /// Shared base instant for the test module. `Instant` cannot be constructed + /// from an absolute value, so we anchor on a single `now()` and express the + /// formerly-`u64` tick timestamps as offsets from it — only differences + /// matter for buffering/eviction logic, so determinism is preserved. + static BASE: std::sync::LazyLock = std::sync::LazyLock::new(Instant::now); + + /// A timestamp `ms` milliseconds after [`BASE`] (replaces the old `u64` ticks). + fn at(ms: u64) -> Instant { + *BASE + Duration::from_millis(ms) + } + + /// A timeout of `ms` milliseconds (replaces the old `u64` offsets). + fn timeout(ms: u64) -> Duration { + Duration::from_millis(ms) + } + + /// Build a deterministic, *decodable* set of `Fragment`s for a message of + /// `inner_kind` carrying `content`, tagged with `id` and split into exactly + /// `count` fragments. + /// + /// Unlike [`make_fragment`], which crafts a single fragment from a raw + /// payload, this encodes a real [`LpFrame`] first (header + content) and + /// slices the encoded bytes — matching what `fragment_lp_message` does in + /// production, so the reassembled bytes decode back into the original frame. + fn make_message_fragments( + id: u64, + inner_kind: LpFrameKind, + content: &[u8], + count: u8, + ) -> Vec { + let encoded = LpFrame::new(inner_kind, content.to_vec()).to_bytes(); + let frag_size = encoded.len().div_ceil(count as usize); + let frags: Vec = encoded + .chunks(frag_size) + .enumerate() + .map(|(i, chunk)| make_fragment(id, count, i as u8, inner_kind, chunk.to_vec())) + .collect(); + assert_eq!( + frags.len() as u8, + count, + "content/count combination did not split into exactly {count} fragments" + ); + frags + } + // ---------- MessageBuffer ---------- #[test] fn buffer_completes_on_single_fragment() { let f = make_fragment(1, 1, 0, SPHINX, b"hi".to_vec()); - let mut buf = MessageBuffer::::new(1, 0); + let mut buf = MessageBuffer::new(1, at(0)); assert!(!buf.is_complete); - buf.insert_fragment(f, 0); + buf.insert_fragment(f, at(0)); assert!(buf.is_complete); assert_eq!(buf.into_message(), b"hi"); } #[test] fn buffer_completes_only_after_last_fragment() { - let mut buf = MessageBuffer::::new(3, 0); - buf.insert_fragment(make_fragment(7, 3, 0, SPHINX, vec![0xaa]), 1); + let mut buf = MessageBuffer::new(3, at(0)); + buf.insert_fragment(make_fragment(7, 3, 0, SPHINX, vec![0xaa]), at(1)); assert!(!buf.is_complete); - buf.insert_fragment(make_fragment(7, 3, 1, SPHINX, vec![0xbb]), 2); + buf.insert_fragment(make_fragment(7, 3, 1, SPHINX, vec![0xbb]), at(2)); assert!(!buf.is_complete); - buf.insert_fragment(make_fragment(7, 3, 2, SPHINX, vec![0xcc]), 3); + buf.insert_fragment(make_fragment(7, 3, 2, SPHINX, vec![0xcc]), at(3)); assert!(buf.is_complete); assert_eq!(buf.into_message(), vec![0xaa, 0xbb, 0xcc]); } #[test] fn buffer_reassembles_in_order_regardless_of_insertion_order() { - let mut buf = MessageBuffer::::new(4, 0); - buf.insert_fragment(make_fragment(1, 4, 2, SPHINX, vec![3]), 0); - buf.insert_fragment(make_fragment(1, 4, 0, SPHINX, vec![1]), 0); - buf.insert_fragment(make_fragment(1, 4, 3, SPHINX, vec![4]), 0); - buf.insert_fragment(make_fragment(1, 4, 1, SPHINX, vec![2]), 0); + let mut buf = MessageBuffer::new(4, at(0)); + buf.insert_fragment(make_fragment(1, 4, 2, SPHINX, vec![3]), at(0)); + buf.insert_fragment(make_fragment(1, 4, 0, SPHINX, vec![1]), at(0)); + buf.insert_fragment(make_fragment(1, 4, 3, SPHINX, vec![4]), at(0)); + buf.insert_fragment(make_fragment(1, 4, 1, SPHINX, vec![2]), at(0)); assert!(buf.is_complete); assert_eq!(buf.into_message(), vec![1, 2, 3, 4]); } #[test] fn buffer_tracks_last_fragment_timestamp() { - let mut buf = MessageBuffer::::new(2, 100); - assert_eq!(buf.last_fragment_timestamp, 100); - buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0]), 250); - assert_eq!(buf.last_fragment_timestamp, 250); - buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![1]), 400); - assert_eq!(buf.last_fragment_timestamp, 400); + let mut buf = MessageBuffer::new(2, at(100)); + assert_eq!(buf.last_fragment_timestamp, at(100)); + buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0]), at(250)); + assert_eq!(buf.last_fragment_timestamp, at(250)); + buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![1]), at(400)); + assert_eq!(buf.last_fragment_timestamp, at(400)); } #[test] fn buffer_duplicate_fragment_does_not_break_completion() { - let mut buf = MessageBuffer::::new(2, 0); - buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0xaa]), 0); + let mut buf = MessageBuffer::new(2, at(0)); + buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0xaa]), at(0)); // Same slot twice - buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0xaa]), 0); + buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![0xaa]), at(0)); assert!(!buf.is_complete); - buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![0xbb]), 0); + buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![0xbb]), at(0)); assert!(buf.is_complete); assert_eq!(buf.into_message(), vec![0xaa, 0xbb]); } #[test] fn buffer_empty_payloads_reassemble_to_empty_message() { - let mut buf = MessageBuffer::::new(2, 0); - buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![]), 0); - buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![]), 0); + let mut buf = MessageBuffer::new(2, at(0)); + buf.insert_fragment(make_fragment(1, 2, 0, SPHINX, vec![]), at(0)); + buf.insert_fragment(make_fragment(1, 2, 1, SPHINX, vec![]), at(0)); assert!(buf.is_complete); assert!(buf.into_message().is_empty()); } @@ -331,8 +355,8 @@ mod tests { let mut fragments = split(message.clone(), 64); assert_eq!(fragments.len(), 1); - let rec = MessageReconstructor::::new(60); - let out = rec.insert_new_fragment(fragments.pop().unwrap(), 0); + let rec = MessageReconstructor::new(timeout(60)); + let out = rec.insert_new_fragment(fragments.pop().unwrap(), at(0)); let recovered_frame = out .expect("single fragment must complete the message") .unwrap(); @@ -345,11 +369,11 @@ mod tests { let fragments = split(message.clone(), 16); assert!(fragments.len() > 1); - let rec = MessageReconstructor::::new(60); + let rec = MessageReconstructor::new(timeout(60)); let total = fragments.len(); let mut out = None; for (i, f) in fragments.into_iter().enumerate() { - out = rec.insert_new_fragment(f, i as u64); + out = rec.insert_new_fragment(f, at(i as u64)); if i + 1 < total { assert!(out.is_none(), "premature completion at fragment {i}"); } @@ -367,10 +391,10 @@ mod tests { // Reverse arrival order. fragments.reverse(); - let rec = MessageReconstructor::::new(60); + let rec = MessageReconstructor::new(timeout(60)); let mut out = None; for (i, f) in fragments.into_iter().enumerate() { - out = rec.insert_new_fragment(f, i as u64); + out = rec.insert_new_fragment(f, at(i as u64)); } let recovered_frame = out .expect("last fragment must complete the message") @@ -381,51 +405,33 @@ mod tests { #[test] fn reconstructor_keeps_distinct_messages_separate() { // Two messages with different ids interleaved. - let mut a = vec![ - make_fragment(1, 2, 0, SPHINX, vec![0xa1]), - make_fragment(1, 2, 1, SPHINX, vec![0xa2]), - ]; - let mut b = vec![ - make_fragment(2, 2, 0, SPHINX, vec![0xb1]), - make_fragment(2, 2, 1, SPHINX, vec![0xb2]), - ]; + let mut a = make_message_fragments(1, SPHINX, &[0xa1, 0xa2], 2); + let mut b = make_message_fragments(2, SPHINX, &[0xb1, 0xb2], 2); - let rec = MessageReconstructor::::new(60); + let rec = MessageReconstructor::new(timeout(60)); // Interleave. - assert!(rec.insert_new_fragment(a.remove(0), 0).is_none()); - assert!(rec.insert_new_fragment(b.remove(0), 1).is_none()); - let msg_a = rec.insert_new_fragment(a.remove(0), 2).unwrap().unwrap(); - let msg_b = rec.insert_new_fragment(b.remove(0), 3).unwrap().unwrap(); + assert!(rec.insert_new_fragment(a.remove(0), at(0)).is_none()); + assert!(rec.insert_new_fragment(b.remove(0), at(1)).is_none()); + let msg_a = rec + .insert_new_fragment(a.remove(0), at(2)) + .unwrap() + .unwrap(); + let msg_b = rec + .insert_new_fragment(b.remove(0), at(3)) + .unwrap() + .unwrap(); assert_eq!(msg_a.content, vec![0xa1, 0xa2]); assert_eq!(msg_b.content, vec![0xb1, 0xb2]); } - #[test] - fn reconstructor_does_not_mix_same_id_across_frame_kinds() { - // Same id, different frame kinds — must not share a buffer. - let s1 = make_fragment(42, 2, 0, SPHINX, vec![0x10]); - let s2 = make_fragment(42, 2, 1, SPHINX, vec![0x11]); - let o1 = make_fragment(42, 2, 0, OUTFOX, vec![0x20]); - let o2 = make_fragment(42, 2, 1, OUTFOX, vec![0x21]); - - let rec = MessageReconstructor::::new(60); - assert!(rec.insert_new_fragment(s1, 0).is_none()); - assert!(rec.insert_new_fragment(o1, 1).is_none()); - let s_msg = rec.insert_new_fragment(s2, 2).unwrap().unwrap(); - let o_msg = rec.insert_new_fragment(o2, 3).unwrap().unwrap(); - - assert_eq!(s_msg.content, vec![0x10, 0x11]); - assert_eq!(s_msg.kind(), SPHINX); - assert_eq!(o_msg.content, vec![0x20, 0x21]); - assert_eq!(o_msg.kind(), OUTFOX); - } - #[test] fn reconstructor_clears_buffer_after_emitting_message() { - let f = make_fragment(99, 1, 0, SPHINX, vec![0xff]); - let rec = MessageReconstructor::::new(60); - let _ = rec.insert_new_fragment(f, 0).unwrap(); + let f = make_message_fragments(99, SPHINX, &[0xff], 1).remove(0); + let rec = MessageReconstructor::new(timeout(60)); + rec.insert_new_fragment(f, at(0)) + .expect("single fragment must complete the message") + .unwrap(); assert!( rec.in_flight_messages.is_empty(), "completed messages must not linger in the in-flight map" @@ -437,18 +443,18 @@ mod tests { #[test] fn cleanup_evicts_buffers_older_than_timeout() { let f = make_fragment(1, 2, 0, SPHINX, vec![0]); - let rec = MessageReconstructor::::new(10); + let rec = MessageReconstructor::new(timeout(10)); // First (and only) fragment received at t=0; the message stays // incomplete. - assert!(rec.insert_new_fragment(f, 0).is_none()); + assert!(rec.insert_new_fragment(f, at(0)).is_none()); assert_eq!(rec.in_flight_messages.len(), 1); // Within the timeout window — buffer must survive. - rec.cleanup_stale_buffers(5); + rec.cleanup_stale_buffers(at(5)); assert_eq!(rec.in_flight_messages.len(), 1); // Past the window — evicted. - rec.cleanup_stale_buffers(100); + rec.cleanup_stale_buffers(at(100)); assert!(rec.in_flight_messages.is_empty()); } @@ -457,14 +463,15 @@ mod tests { // Stale message at t=0, then a brand new message arrives well past // the timeout. The implicit cleanup inside `insert_new_fragment` // must drop the stale entry. - let stale = make_fragment(1, 2, 0, SPHINX, vec![0]); - let fresh = make_fragment(2, 1, 0, SPHINX, vec![0xff]); + // Only the first of the stale message's two fragments is ever delivered. + let stale = make_message_fragments(1, SPHINX, &[0x00, 0x01], 2).remove(0); + let fresh = make_message_fragments(2, SPHINX, &[0xff], 1).remove(0); - let rec = MessageReconstructor::::new(10); - assert!(rec.insert_new_fragment(stale, 0).is_none()); + let rec = MessageReconstructor::new(timeout(10)); + assert!(rec.insert_new_fragment(stale, at(0)).is_none()); assert_eq!(rec.in_flight_messages.len(), 1); - let msg = rec.insert_new_fragment(fresh, 1_000).unwrap().unwrap(); + let msg = rec.insert_new_fragment(fresh, at(1_000)).unwrap().unwrap(); assert_eq!(msg.content, vec![0xff]); // `fresh` was a single-fragment message and is removed on emission; // the stale buffer must also be gone. @@ -476,19 +483,20 @@ mod tests { // A buffer that keeps receiving fragments must not be evicted // even if the absolute time exceeds the timeout, as long as the // gap between fragments stays under it. - let rec = MessageReconstructor::::new(10); + let rec = MessageReconstructor::new(timeout(10)); + let mut frags = make_message_fragments(1, SPHINX, &[0xa, 0xb, 0xc], 3).into_iter(); assert!( - rec.insert_new_fragment(make_fragment(1, 3, 0, SPHINX, vec![0xa]), 0) + rec.insert_new_fragment(frags.next().unwrap(), at(0)) .is_none() ); assert!( - rec.insert_new_fragment(make_fragment(1, 3, 1, SPHINX, vec![0xb]), 8) + rec.insert_new_fragment(frags.next().unwrap(), at(8)) .is_none() ); // Absolute time is now 16 (> 10), but the gap from the previous // fragment (8) to now (16) is 8, still within the 10-tick timeout. - let out = rec.insert_new_fragment(make_fragment(1, 3, 2, SPHINX, vec![0xc]), 16); + let out = rec.insert_new_fragment(frags.next().unwrap(), at(16)); let msg = out.expect("buffer must still be alive").unwrap(); assert_eq!(msg.content, vec![0xa, 0xb, 0xc]); } diff --git a/common/nym-lp-data/src/lib.rs b/common/nym-lp-data/src/lib.rs index a1b30e4da3..84aa5c85aa 100644 --- a/common/nym-lp-data/src/lib.rs +++ b/common/nym-lp-data/src/lib.rs @@ -10,15 +10,16 @@ //! |--------|---------| //! | [`clients`] | Client-side pipeline traits and types: chunking, reliability, obfuscation, routing security, framing, transport | //! | [`common`] | Shared framing and transport traits used by both clients and mixnodes | -//! | [`mixnodes`] | Mixnode-side pipeline traits: unwrap incoming packets, re-wrap and forward them | +//! | [`nymnodes`] | Mixnode-side pipeline traits: unwrap incoming packets, re-wrap and forward them | //! //! ## Core types //! -//! [`TimedData`] is the foundational wrapper that pairs any piece of data with a -//! timestamp, threading timing information through every stage of the pipeline. -//! [`TimedPayload`] is a convenience alias for `TimedData>`. +//! [`TimedData`] is the foundational wrapper that pairs any piece of data with an +//! [`Instant`] timestamp, threading timing information through every stage of the +//! pipeline. [`TimedPayload`] is a convenience alias for `TimedData>`. -use std::fmt::Debug; +use std::net::SocketAddr; +use std::time::Instant; pub mod clients; pub mod common; @@ -27,32 +28,32 @@ pub mod nymnodes; pub mod packet; /// Convenience alias for [`TimedData`] when the payload is a raw byte buffer. -pub type TimedPayload = TimedData>; +pub type TimedPayload = TimedData>; /// Convenience alias for [`AddressedTimedData`] when the payload is a raw byte buffer. -pub type AddressedTimedPayload = AddressedTimedData, NdId>; +pub type AddressedTimedPayload = AddressedTimedData>; /// Convenience alias for [`PipelineData`] when the payload is a raw byte buffer. -pub type PipelinePayload = PipelineData, Opts, NdId>; +pub type PipelinePayload = PipelineData, Opts, NdId>; -/// A value of type `D` tagged with a timestamp of type `Ts`. +/// A value of type `D` tagged with an [`Instant`] timestamp. /// /// `TimedData` threads timing information through every stage of the LP /// pipeline. It is produced by [`clients::traits::Chunking`] and propagated -/// unchanged (or with the timestamp transformed) through every subsequent -/// pipeline stage until the packet is sent on the wire. +/// unchanged (or with its timestamp replaced via [`TimedData::with_timestamp`]) +/// through every subsequent pipeline stage until the packet is sent on the wire. #[derive(Clone, Debug)] -pub struct TimedData { - pub timestamp: Ts, +pub struct TimedData { + pub timestamp: Instant, pub data: D, } -impl TimedData { - pub fn new(timestamp: Ts, data: D) -> Self { +impl TimedData { + pub fn new(timestamp: Instant, data: D) -> Self { TimedData { timestamp, data } } /// Apply `op` to the data component, leaving the timestamp unchanged. /// /// `Nd` can differ from `D`, so this also acts as a type transform. - pub fn data_transform(self, mut op: F) -> TimedData + pub fn data_transform(self, mut op: F) -> TimedData where F: FnMut(D) -> Nd, { @@ -62,14 +63,11 @@ impl TimedData { } } - /// Apply `op` to the timestamp component, leaving the data unchanged. - pub fn ts_transform(self, mut op: F) -> Self - where - F: FnMut(Ts) -> Ts, - { + /// Set a new timestamp + pub fn with_timestamp(self, new_timestamp: Instant) -> Self { TimedData { data: self.data, - timestamp: op(self.timestamp), + timestamp: new_timestamp, } } } @@ -81,10 +79,9 @@ impl TimedData { /// [`Transport`]). It carries: /// /// - `data`: a [`TimedData`] pairing the payload with its scheduled timestamp, -/// - `options`: per-message configuration consumed by the pipeline (typically an -/// [`InputOptions`] implementor on the client side; `()` once the message is -/// reduced to an addressed payload), -/// - `dst`: the next-hop destination identifier the wire layer should send to. +/// - `options`: opaque per-message metadata threaded through the pipeline (`()` +/// once the message is reduced to an addressed payload), +/// - `dst`: the next-hop socket address the wire layer should send to. /// /// [`Chunking`]: crate::clients::traits::Chunking /// [`Reliability`]: crate::clients::traits::Reliability @@ -92,17 +89,16 @@ impl TimedData { /// [`RoutingSecurity`]: crate::clients::traits::RoutingSecurity /// [`Framing`]: crate::common::traits::Framing /// [`Transport`]: crate::common::traits::Transport -/// [`InputOptions`]: crate::clients::InputOptions #[derive(Clone, Debug)] -pub struct PipelineData { - pub data: TimedData, +pub struct PipelineData { + pub data: TimedData, pub options: Opts, pub dst: NdId, } -impl PipelineData { +impl PipelineData { /// Construct a new [`PipelineData`] from its parts. - pub fn new(timestamp: Ts, data: D, options: Opts, dst: NdId) -> Self { + pub fn new(timestamp: Instant, data: D, options: Opts, dst: NdId) -> Self { PipelineData { data: TimedData::new(timestamp, data), options, @@ -114,7 +110,7 @@ impl PipelineData { /// destination unchanged. /// /// `Nd` can differ from `D`, so this also acts as a type transform. - pub fn data_transform(self, op: F) -> PipelineData + pub fn data_transform(self, op: F) -> PipelineData where F: FnMut(D) -> Nd, { @@ -125,13 +121,10 @@ impl PipelineData { } } - /// Apply `op` to the timestamp component, leaving the data unchanged. - pub fn ts_transform(self, op: F) -> Self - where - F: FnMut(Ts) -> Ts, - { + /// Set a new timestamp + pub fn with_timestamp(self, new_timestamp: Instant) -> Self { PipelineData { - data: self.data.ts_transform(op), + data: self.data.with_timestamp(new_timestamp), options: self.options, dst: self.dst, } @@ -141,7 +134,7 @@ impl PipelineData { /// destination unchanged. /// /// `No` can differ from `O`, so this also acts as a type transform. - pub fn options_transform(self, mut op: F) -> PipelineData + pub fn options_transform(self, mut op: F) -> PipelineData where F: FnMut(Opts) -> No, { @@ -153,7 +146,7 @@ impl PipelineData { } /// Set a new destination - pub fn with_dst(self, new_dst: NewNdId) -> PipelineData { + pub fn with_dst(self, new_dst: NewNdId) -> PipelineData { PipelineData { data: self.data, options: self.options, @@ -162,7 +155,7 @@ impl PipelineData { } /// Drop the pipeline options, producing a plain addressed payload. - pub fn into_addressed(self) -> AddressedTimedData { + pub fn into_addressed(self) -> AddressedTimedData { AddressedTimedData { data: self.data, options: (), @@ -173,11 +166,11 @@ impl PipelineData { /// Convenience alias for [`PipelineData`] when no per-message pipeline options /// are needed. Avoids duplicating the pipeline data structure. -pub type AddressedTimedData = PipelineData; +pub type AddressedTimedData = PipelineData; -impl AddressedTimedData { +impl AddressedTimedData { /// Construct a new [`AddressedTimedData`] with unit `options`. - pub fn new_addressed(timestamp: Ts, data: D, dst: NdId) -> Self { + pub fn new_addressed(timestamp: Instant, data: D, dst: NdId) -> Self { AddressedTimedData { data: TimedData::new(timestamp, data), options: (), @@ -186,7 +179,7 @@ impl AddressedTimedData { } /// Convert a [`AddressedTimedData`] into a [`PipelineData`] with the provided options. - pub fn with_options(self, opts: Opts) -> PipelineData { + pub fn with_options(self, opts: Opts) -> PipelineData { PipelineData { data: self.data, options: opts, diff --git a/common/nym-lp-data/src/nymnodes/traits.rs b/common/nym-lp-data/src/nymnodes/traits.rs index 4bb11ebe2f..0af22aef12 100644 --- a/common/nym-lp-data/src/nymnodes/traits.rs +++ b/common/nym-lp-data/src/nymnodes/traits.rs @@ -1,6 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::Instant; + use crate::{AddressedTimedData, PipelinePayload, TimedPayload}; use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline}; @@ -11,9 +13,7 @@ use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline}; /// step that the implementor fills in (decrypt, route, re-encrypt, cover traffic, etc.). /// /// # Type Parameters -/// - `Ts`: Timestamp / tick-context type. /// - `Pkt`: Transport packet type; the same type is consumed and produced. -/// - `NdId`: Identifier type for the next-hop destination. /// /// # Associated Types /// - `Options`: Per-message pipeline options carried into the re-wrapping side. @@ -36,12 +36,9 @@ use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline}; /// [`WireWrappingPipeline::wire_wrap`]. /// /// [`mix`]: NymNodeProcessingPipeline::mix -pub trait NymNodeProcessingPipeline: - WireUnwrappingPipeline>::MessageKind> - + WireWrappingPipeline>::Options, NdId> -where - Ts: Clone, - NdId: Clone, +pub trait NymNodeProcessingPipeline: + WireUnwrappingPipeline>::MessageKind> + + WireWrappingPipeline>::Options> { type Options; type MessageKind; @@ -49,16 +46,16 @@ where fn mix( &mut self, message_kind: Self::MessageKind, - payload: TimedPayload, - timestamp: Ts, - ) -> Vec>; + payload: TimedPayload, + timestamp: Instant, + ) -> Vec>; fn process( &mut self, input: Pkt, - timestamp: Ts, - ) -> Result>, Self::Error> { - let Some((payload, kind)) = self.wire_unwrap(input, timestamp.clone())? else { + timestamp: Instant, + ) -> Result>, Self::Error> { + let Some((payload, kind)) = self.wire_unwrap(input, timestamp)? else { return Ok(Vec::new()); }; let mixed = self.mix(kind, payload, timestamp); diff --git a/common/nym-lp-data/tests/integration/common/mod.rs b/common/nym-lp-data/tests/integration/common/mod.rs index a174520e6f..88560103bd 100644 --- a/common/nym-lp-data/tests/integration/common/mod.rs +++ b/common/nym-lp-data/tests/integration/common/mod.rs @@ -1,6 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::{Duration, Instant}; + use nym_lp_data::packet::{ LpFrame, LpHeader, LpPacket, frame::{LpFrameHeader, LpFrameKind}, @@ -8,63 +10,25 @@ use nym_lp_data::packet::{ use nym_lp_data::{ AddressedTimedData, PipelinePayload, - clients::{ - InputOptions, - traits::{Chunking, Obfuscation, Reliability, RoutingSecurity}, - }, + clients::traits::{Chunking, Obfuscation, Reliability, RoutingSecurity}, common::traits::{Framing, Transport}, }; -#[derive(Clone, Copy)] -pub struct BasicOptions { - pub reliability: bool, - pub security: bool, - pub obfuscation: bool, - pub next_hop: u8, -} - -impl InputOptions for BasicOptions { - fn reliability(&self) -> bool { - self.reliability - } - - fn routing_security(&self) -> bool { - self.security - } - - fn obfuscation(&self) -> bool { - self.obfuscation - } - - fn next_hop(&self) -> u8 { - self.next_hop - } -} - -pub type BasicPipelinePayload = PipelinePayload; +pub type BasicPipelinePayload = PipelinePayload<()>; pub struct MockChunking; -impl Chunking for MockChunking -where - Ts: Clone, -{ +impl Chunking<()> for MockChunking { fn chunked( &mut self, - input: Vec, - input_options: BasicOptions, + input: BasicPipelinePayload, chunk_size: usize, - timestamp: Ts, - ) -> Vec> { + timestamp: Instant, + ) -> Vec { input + .data + .data .chunks(chunk_size) - .map(|chunk| { - BasicPipelinePayload::new( - timestamp.clone(), - chunk.to_vec(), - input_options, - input_options.next_hop(), - ) - }) + .map(|chunk| BasicPipelinePayload::new(timestamp, chunk.to_vec(), (), input.dst)) .collect() } } @@ -75,13 +39,13 @@ impl MockReliability { const HEADER: &[u8; 5] = b"0KCP0"; } -impl Reliability for MockReliability { +impl Reliability<()> for MockReliability { const OVERHEAD_SIZE: usize = Self::HEADER.len(); fn reliable_encode( &mut self, - input: Option>, - _: Ts, - ) -> Vec> { + input: Option, + _: Instant, + ) -> Vec { input .map(|data| { vec![data.data_transform(|data| { @@ -102,14 +66,14 @@ impl MockSphinxSecurity { const HEADER: &[u8; 8] = b"0SPHINX0"; } -impl RoutingSecurity for MockSphinxSecurity { +impl RoutingSecurity<()> for MockSphinxSecurity { const OVERHEAD_SIZE: usize = Self::HEADER.len(); fn nb_frames(&self) -> usize { self.nb_frames } - fn encrypt(&mut self, input: BasicPipelinePayload) -> BasicPipelinePayload { + fn encrypt(&mut self, input: BasicPipelinePayload) -> BasicPipelinePayload { input.data_transform(|data| { let mut packet = Self::HEADER.to_vec(); packet.extend(data); @@ -120,47 +84,15 @@ impl RoutingSecurity for MockSphinxSecurity { pub struct KekwObfuscation; -impl Obfuscation for KekwObfuscation { +impl Obfuscation<()> for KekwObfuscation { fn obfuscate( &mut self, - input: Option>, - _timestamp: u32, - ) -> Vec> { + input: Option, + _timestamp: Instant, + ) -> Vec { if let Some(input) = input { - vec![input.ts_transform(|ts| ts + 1)] - } else { - Vec::new() - } - } -} - -#[allow(dead_code)] -pub struct ReallyOddObfuscation { - next_ts: u32, -} - -impl ReallyOddObfuscation { - #[allow(dead_code)] - pub fn new(start_ts: u32) -> Self { - let next_ts = if !start_ts.is_multiple_of(2) { - start_ts - } else { - start_ts + 1 - }; - Self { next_ts } - } -} - -impl Obfuscation for ReallyOddObfuscation { - fn obfuscate( - &mut self, - input: Option>, - _timestamp: u32, - ) -> Vec> { - if let Some(input) = input { - let pkt = input.ts_transform(|_| self.next_ts); - self.next_ts += 2; - vec![pkt] + let new_timestamp = input.data.timestamp + Duration::from_millis(1); + vec![input.with_timestamp(new_timestamp)] } else { Vec::new() } @@ -173,17 +105,14 @@ impl MockLpFraming { const FRAME_ATTRIBUTES: &[u8; 14] = b"0LpFrameAttrs0"; } -impl Framing for MockLpFraming -where - Ts: Clone, -{ +impl Framing<()> for MockLpFraming { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE; fn to_frame( &mut self, - input: PipelinePayload, + input: BasicPipelinePayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { input .data .data @@ -192,7 +121,7 @@ where let header = LpFrameHeader::new(LpFrameKind::Opaque, *Self::FRAME_ATTRIBUTES); AddressedTimedData::new_addressed( - input.data.timestamp.clone(), + input.data.timestamp, LpFrame { header, content: frame_payload.to_vec().into(), @@ -206,13 +135,13 @@ where pub struct MockLpTransport; -impl Transport for MockLpTransport { +impl Transport for MockLpTransport { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpHeader::SIZE; fn to_transport_packet( &mut self, - input: AddressedTimedData, - ) -> AddressedTimedData { + input: AddressedTimedData, + ) -> AddressedTimedData { AddressedTimedData::new_addressed( input.data.timestamp, LpPacket::new(LpHeader::new(7, 7, 7), input.data.data), diff --git a/common/nym-lp-data/tests/integration/main.rs b/common/nym-lp-data/tests/integration/main.rs index 97c4f8d0fd..a82133ccca 100644 --- a/common/nym-lp-data/tests/integration/main.rs +++ b/common/nym-lp-data/tests/integration/main.rs @@ -1,6 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::time::Instant; + use nym_lp_data::clients::{traits::ClientWrappingPipeline, types::Pipeline}; use crate::common::{ @@ -27,7 +29,7 @@ fn empty_input_yields_empty_output() { packet_size, }; - let output = mock_pipeline.process(None, 1); + let output = mock_pipeline.process(None, Instant::now()); assert!(output.is_empty()); } diff --git a/nym-mix-sim/src/client/mod.rs b/nym-mix-sim/src/client/mod.rs index 8b2f537486..a199dc4613 100644 --- a/nym-mix-sim/src/client/mod.rs +++ b/nym-mix-sim/src/client/mod.rs @@ -5,12 +5,12 @@ use std::{ fmt::Debug, io::ErrorKind, net::{SocketAddr, UdpSocket}, - sync::Arc, + time::Instant, }; use nym_lp_data::AddressedTimedData; -use crate::{node::NodeId, packet::WirePacketFormat, topology::directory::Directory}; +use crate::{node::NodeId, packet::WirePacketFormat}; pub mod nymnode; pub mod simple; @@ -25,8 +25,8 @@ pub type ClientId = NodeId; /// Implemented by [`simple::SimpleClient`] and any other concrete client types. /// /// [`MixSimDriver`]: crate::driver::MixSimDriver -pub trait MixSimClient: Send { - fn tick(&mut self, timestamp: Ts); +pub trait MixSimClient: Send { + fn tick(&mut self, timestamp: Instant); } /// Pipeline interface used by [`BaseClient`] to convert raw app payloads into @@ -39,21 +39,21 @@ pub trait MixSimClient: Send { /// /// [`SimplePacket`]: crate::packet::simple::SimplePacket /// [`SimMixPacket`]: crate::packet::sphinx::SimMixPacket -pub trait ProcessingClient: Send { +pub trait ProcessingClient: Send { /// Wrap `input` into one or more outbound packets addressed toward `dst`. fn process( &mut self, input: Vec, dst: ClientId, - timestamp: Ts, - ) -> Vec>; + timestamp: Instant, + ) -> Vec>; /// Unwrap an inbound packet received from the mix network. /// /// Returns `Ok(Some(plaintext))` for a real message, `Ok(None)` when the /// packet is cover traffic or an incomplete fragment, and `Err` when /// decryption or deserialisation fails. - fn unwrap(&mut self, input: RcvPkt, timestamp: Ts) -> anyhow::Result>>; + fn unwrap(&mut self, input: RcvPkt, timestamp: Instant) -> anyhow::Result>>; } /// Shared UDP transport layer for simulated clients. @@ -62,7 +62,7 @@ pub trait ProcessingClient: Send { /// multiple concrete client types can reuse `send_to_node`, `recv_from_mix`, /// and `recv_from_app` without duplicating that logic. Packet types are /// method-level generics so `BaseClient` itself has no type parameters. -pub struct BaseClient { +pub struct BaseClient { /// Identifier of this client within the topology. id: ClientId, /// Socket bound to the mix-network address; sends to first-hop nodes and @@ -71,12 +71,10 @@ pub struct BaseClient { /// Socket bound to the app address; receives application payloads from /// external CLIs (e.g. `mix-client`). app_socket: UdpSocket, - /// Shared routing table used to resolve next-hop [`NodeId`]s to addresses. - directory: Arc, /// Packets that have been processed and are waiting to be forwarded to their /// first-hop node, sorted (loosely) by scheduled send timestamp. - outgoing_queue: Vec>, + outgoing_queue: Vec>, /// Concrete client-processing implementation invoked from each tick phase. processing_client: Pc, @@ -85,13 +83,12 @@ pub struct BaseClient { _marker: std::marker::PhantomData, } -impl BaseClient { +impl BaseClient { /// Bind both UDP sockets to the given addresses. pub(crate) fn with_pipeline( client_id: ClientId, mixnet_address: SocketAddr, app_address: SocketAddr, - directory: Arc, processing_client: Pc, ) -> anyhow::Result { let mix_socket = UdpSocket::bind(mixnet_address)?; @@ -104,7 +101,6 @@ impl BaseClient { id: client_id, mix_socket, app_socket, - directory, outgoing_queue: Vec::new(), processing_client, _marker: std::marker::PhantomData, @@ -112,29 +108,24 @@ impl BaseClient { } } -impl BaseClient +impl BaseClient where SndPkt: WirePacketFormat, RcvPkt: WirePacketFormat, { /// Send `packet` to the mix node identified by `node_id` via `mix_socket`. /// - /// Resolves `node_id` against the shared [`Directory`], serialises via + /// Resolves `node_id` against the shared [`crate::topology::directory::Directory`], serialises via /// [`WirePacketFormat::to_bytes`], and dispatches with a single `sendto`. /// Errors are logged but not propagated. - pub fn send_to_node(&self, node_id: NodeId, packet: SndPkt) { - if let Some(node) = self.directory.node(node_id) { - if let Err(e) = self.mix_socket.send_to(&packet.to_bytes(), node.addr) { - tracing::error!("[Client {}] Failed to send to node {node_id}: {e}", self.id); - } else { - tracing::debug!( - "[Client {}] Sent packet to node {node_id} @ {}", - self.id, - node.addr - ); - } + pub fn send_to_node(&self, node_address: SocketAddr, packet: SndPkt) { + if let Err(e) = self.mix_socket.send_to(&packet.to_bytes(), node_address) { + tracing::error!( + "[Client {}] Failed to send to node @ {node_address}: {e}", + self.id + ); } else { - tracing::error!("[Client {}] Node {node_id} not found in directory", self.id); + tracing::debug!("[Client {}] Sent packet to node @ {node_address}", self.id); } } @@ -175,30 +166,28 @@ where } } -impl MixSimClient for BaseClient +impl MixSimClient for BaseClient where - Ts: Clone + PartialOrd + Debug + Send, SndPkt: WirePacketFormat + Debug + Send, RcvPkt: WirePacketFormat + Debug + Send, - Pc: ProcessingClient, + Pc: ProcessingClient, { - fn tick(&mut self, timestamp: Ts) { - self.tick_app_incoming(timestamp.clone()); - self.tick_outgoing(timestamp.clone()); + fn tick(&mut self, timestamp: Instant) { + self.tick_app_incoming(timestamp); + self.tick_outgoing(timestamp); self.tick_mix_incoming(timestamp); } } -impl BaseClient +impl BaseClient where - Ts: Clone + PartialOrd + Debug + Send, SndPkt: WirePacketFormat + Debug + Send, RcvPkt: WirePacketFormat + Debug + Send, - Pc: ProcessingClient, + Pc: ProcessingClient, { /// **Phase 1 — app incoming**: drain the app socket, run each payload /// through the processing pipeline, and enqueue the resulting packets. - fn tick_app_incoming(&mut self, timestamp: Ts) { + fn tick_app_incoming(&mut self, timestamp: Instant) { // Collect (dst, payload) pairs from the app socket. let mut inputs = Vec::new(); while let Some(result) = self.recv_from_app() { @@ -237,16 +226,14 @@ where } for (dst, payload) in inputs { - let packets = self - .processing_client - .process(payload, dst, timestamp.clone()); + let packets = self.processing_client.process(payload, dst, timestamp); self.outgoing_queue.extend(packets); } } /// **Phase 2 — outgoing**: send all queued packets whose scheduled /// timestamp is ≤ `timestamp` to their first-hop node. - fn tick_outgoing(&mut self, timestamp: Ts) { + fn tick_outgoing(&mut self, timestamp: Instant) { let to_send = self .outgoing_queue .extract_if(.., |pkt| pkt.data.timestamp <= timestamp) @@ -258,10 +245,10 @@ where /// **Phase 3 — mix incoming**: drain the mix socket and pass each packet /// through the unwrapping pipeline. - fn tick_mix_incoming(&mut self, timestamp: Ts) { + fn tick_mix_incoming(&mut self, timestamp: Instant) { while let Some(result) = self.recv_from_mix() { match result { - Ok(pkt) => match self.processing_client.unwrap(pkt, timestamp.clone()) { + Ok(pkt) => match self.processing_client.unwrap(pkt, timestamp) { Ok(Some(content)) => { tracing::info!( "[Client {}] Received: {:?}", diff --git a/nym-mix-sim/src/client/nymnode.rs b/nym-mix-sim/src/client/nymnode.rs index f24b458a9e..c8d48b65c8 100644 --- a/nym-mix-sim/src/client/nymnode.rs +++ b/nym-mix-sim/src/client/nymnode.rs @@ -1,24 +1,19 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -//! [`NymNodeSimClient`] — simulated client that produces sphinx-in-LP packets -//! consumed by the [`NymNodeSimNode`](crate::node::nymnode::NymNodeSimNode). +//! [`SimNymClient`] — simulated client that produces sphinx-in-LP packets +//! consumed by the [`SimNymNode`](crate::node::nymnode::SimNymNode). //! //! The wrapping pipeline applies sphinx-style chunking, full Sphinx encryption //! over a 3-hop route, and LP framing/transport. Reliability and obfuscation //! are no-ops to keep the wire trace easy to follow. -use std::{ - net::SocketAddr, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{sync::Arc, time::Instant}; use nym_crypto::asymmetric::x25519; use nym_lp_data::{ AddressedTimedData, PipelinePayload, TimedData, TimedPayload, clients::{ - InputOptions, helpers::{NoOpObfuscation, NoOpReliability}, traits::{Chunking, ClientUnwrappingPipeline, ClientWrappingPipeline, RoutingSecurity}, }, @@ -46,14 +41,15 @@ use rand::Rng; use crate::{ client::{BaseClient, ClientId, ProcessingClient}, - node::NodeId, - packet::sphinx::{GenerateDelay, SphinxMessage}, + helpers, topology::{ TopologyClient, directory::{Directory, DirectoryClient}, }, }; +// SW To be replaced with actual client implementation + /// A simulated client that produces sphinx-in-LP packets. /// /// `Ts` is fixed to [`Instant`] because the real [`NymNodeDataPipeline`] only @@ -63,7 +59,7 @@ use crate::{ /// struct adds the outgoing queue and the wrapping/unwrapping pipelines. /// /// [`NymNodeDataPipeline`]: nym_node::node::lp::data::handler::pipeline::NymNodeDataPipeline -pub type SimNymClient = BaseClient, EncryptedLpPacket>; +pub type SimNymClient = BaseClient, EncryptedLpPacket>; impl SimNymClient { /// Bind both UDP sockets and return a new client. @@ -91,36 +87,18 @@ impl SimNymClient { topology_client.client_id, topology_client.mixnet_address, topology_client.app_address, - directory, processing_client, ) } } -/// [`InputOptions`] for the nymnode pipeline. /// /// `dst` is the final destination [`ClientId`] embedded in the sphinx packet's -/// destination address. `first_hop` is the [`SocketAddr`] of the first mix node; -/// the client sends the LP packet there. +/// destination address. `first_hop` is the [`std::net::SocketAddr`] of the +/// first mix node; the client sends the LP packet there. #[derive(Clone, Copy)] pub struct SimNymClientInputOptions { pub dst: DirectoryClient, - pub first_hop: SocketAddr, -} - -impl InputOptions for SimNymClientInputOptions { - fn reliability(&self) -> bool { - false - } - fn routing_security(&self) -> bool { - true - } - fn obfuscation(&self) -> bool { - false - } - fn next_hop(&self) -> SocketAddr { - self.first_hop - } } pub struct SimNymProcesssingClient { @@ -128,13 +106,13 @@ pub struct SimNymProcesssingClient { unwrapper: NymNodeUnwrappingPipeline, } -impl ProcessingClient for SimNymProcesssingClient { +impl ProcessingClient for SimNymProcesssingClient { fn process( &mut self, input: Vec, dst: ClientId, timestamp: Instant, - ) -> Vec> { + ) -> Vec> { if input.is_empty() { return Vec::new(); } @@ -143,32 +121,17 @@ impl ProcessingClient for SimNymProce return Vec::new(); }; - // SAFETY : sampling from the directory, we know the node exists - #[allow(clippy::unwrap_used)] - let first_hop = *self + let first_hop = self .wrapper .directory - .node( - self.wrapper - .directory - .random_next_hop(&mut self.wrapper.rng), - ) - .unwrap(); + .random_next_hop(&mut self.wrapper.rng); let input_options = SimNymClientInputOptions { dst: destination_client, - first_hop: first_hop.addr, }; - let packets = self - .wrapper - .process(Some((input, input_options)), timestamp); - - // SW THIS IS TEMPORARY WHILE I FIGURE OUT HOW TO CHANGE BASE CLIENTS - packets - .into_iter() - .map(|p| p.with_dst(first_hop.id)) - .collect() + self.wrapper + .process(Some((input, input_options, first_hop.addr)), timestamp) } fn unwrap( @@ -183,7 +146,7 @@ impl ProcessingClient for SimNymProce // ───────────────────────────────────────────────────────────────────────────── // Wrapping pipeline -/// Full wrapping pipeline for [`NymNodeSimClient`]. +/// Full wrapping pipeline for [`SimNymClient`]. /// /// Applies, in order: sphinx-style chunking, Sphinx onion encryption over a /// random 3-hop route, LP framing (with fragmentation when the encrypted packet @@ -195,35 +158,24 @@ pub struct SimNymClientWrappingPipeline { rng: R, } -pub(crate) type NymNodePipelinePayload = - PipelinePayload; - -impl Chunking - for SimNymClientWrappingPipeline -{ +impl Chunking for SimNymClientWrappingPipeline { /// Split `input` into sphinx-sized chunks using the standard sphinx /// fragmentation. Every chunk is addressed to the configured first hop so /// the LP packet reaches the network entry node. fn chunked( &mut self, - input: Vec, - options: SimNymClientInputOptions, + input: PipelinePayload, chunk_size: usize, timestamp: Instant, - ) -> Vec { - let fragments = NymMessage::new_plain(input) + ) -> Vec> { + let fragments = NymMessage::new_plain(input.data.data) .pad_to_full_packet_lengths(chunk_size) .split_into_fragments(&mut self.rng, chunk_size); fragments .into_iter() .map(|fragment| { - NymNodePipelinePayload::new( - timestamp, - fragment.into_bytes(), - options, - options.first_hop, - ) + PipelinePayload::new(timestamp, fragment.into_bytes(), input.options, input.dst) }) .collect() } @@ -232,9 +184,7 @@ impl Chunking impl NoOpReliability for SimNymClientWrappingPipeline {} impl NoOpObfuscation for SimNymClientWrappingPipeline {} -impl RoutingSecurity - for SimNymClientWrappingPipeline -{ +impl RoutingSecurity for SimNymClientWrappingPipeline { // We are wrapping the sphinx packet in an LpFrame, hence the extra header overhead const OVERHEAD_SIZE: usize = nym_sphinx::HEADER_SIZE + nym_sphinx::PAYLOAD_OVERHEAD_SIZE + LpFrameHeader::SIZE; @@ -248,8 +198,11 @@ impl RoutingSecurity /// The route is built by taking `options.first_hop` as the first hop and /// choosing two additional hops at random. The final destination address /// is derived from `options.dst`. Per-hop delays come from - /// [`GenerateDelay::generate_mix_delay`]. - fn encrypt(&mut self, input: NymNodePipelinePayload) -> NymNodePipelinePayload { + /// [`crate::helpers::generate_mix_delay`]. + fn encrypt( + &mut self, + input: PipelinePayload, + ) -> PipelinePayload { let route = self.directory.random_route(3, &mut self.rng); let first_mix_hop = route[0].id; @@ -261,19 +214,17 @@ impl RoutingSecurity .collect::>(); let delays = (0..sphinx_route.len()) - .map(|_| Delay::new_from_millis(Instant::generate_mix_delay(&mut self.rng))) + .map(|_| Delay::new_from_millis(helpers::generate_mix_delay(&mut self.rng))) .collect::>(); let plaintext_size = (>::packet_size(self) - - >::OVERHEAD_SIZE - - >::OVERHEAD_SIZE) + - >::OVERHEAD_SIZE + - >::OVERHEAD_SIZE) * self.nb_frames() - - >::OVERHEAD_SIZE; + - >::OVERHEAD_SIZE; let packet_builder = SphinxPacketBuilder::new() .with_payload_size(plaintext_size + nym_sphinx::PAYLOAD_OVERHEAD_SIZE); @@ -300,26 +251,24 @@ impl RoutingSecurity packet.to_bytes(), ); - NymNodePipelinePayload::new( + PipelinePayload::new( input.data.timestamp, framed_packet.to_bytes(), input.options, - input.options.first_hop, + input.dst, ) } } -impl Framing - for SimNymClientWrappingPipeline -{ +impl Framing for SimNymClientWrappingPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE; fn to_frame( &mut self, - payload: NymNodePipelinePayload, + payload: PipelinePayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { // SAFETY : we know the inupt is long enough #[allow(clippy::unwrap_used)] let input_frame = LpFrame::decode(&payload.data.data).unwrap(); @@ -332,19 +281,19 @@ impl Framing } } -impl Transport for SimNymClientWrappingPipeline { +impl Transport for SimNymClientWrappingPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpHeader::SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { frame.data_transform(|f| LpPacket::new(LpHeader::new(0, 0, version::CURRENT), f).encode()) } } -impl WireWrappingPipeline +impl WireWrappingPipeline for SimNymClientWrappingPipeline { fn packet_size(&self) -> usize { @@ -352,8 +301,7 @@ impl WireWrappingPipeline - ClientWrappingPipeline +impl ClientWrappingPipeline for SimNymClientWrappingPipeline { } @@ -366,14 +314,14 @@ impl // for completeness — it decodes LP packets and would surface reassembled // payloads if delivery were ever enabled. -/// Unwrapping pipeline for [`NymNodeSimClient`]. +/// Unwrapping pipeline for [`SimNymClient`]. pub struct NymNodeUnwrappingPipeline { - message_reconstructor: MessageReconstructor, + message_reconstructor: MessageReconstructor, sphinx_message_reconstructor: SphinxMessageReconstructor, sphinx_secret_key: x25519::PrivateKey, } -impl TransportUnwrap for NymNodeUnwrappingPipeline { +impl TransportUnwrap for NymNodeUnwrappingPipeline { type Frame = LpFrame; type Error = MalformedLpPacketError; @@ -381,19 +329,16 @@ impl TransportUnwrap for NymNodeUnwrappingPipeline { &mut self, packet: EncryptedLpPacket, timestamp: Instant, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let lp = LpPacket::decode(packet)?; Ok(TimedData::new(timestamp, lp.into_frame())) } } -impl FramingUnwrap for NymNodeUnwrappingPipeline { +impl FramingUnwrap<()> for NymNodeUnwrappingPipeline { type Frame = LpFrame; - fn frame_to_message( - &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, SphinxMessage)> { + fn frame_to_message(&mut self, frame: TimedData) -> Option<(TimedPayload, ())> { let recovered_message = match frame.data.kind() { LpFrameKind::FragmentedData => { let fragment = frame.data.try_into().ok()?; // This should never fail @@ -410,24 +355,15 @@ impl FramingUnwrap for NymNodeUnwrappingPipeline { }; Some(( TimedPayload::new(frame.timestamp, recovered_message.content.to_vec()), - SphinxMessage, // SW take that from the attributes + (), )) } } -impl WireUnwrappingPipeline - for NymNodeUnwrappingPipeline -{ -} +impl WireUnwrappingPipeline for NymNodeUnwrappingPipeline {} -impl ClientUnwrappingPipeline - for NymNodeUnwrappingPipeline -{ - fn process_unwrapped( - &mut self, - timed_plaintext: TimedPayload, - _kind: SphinxMessage, - ) -> Option> { +impl ClientUnwrappingPipeline for NymNodeUnwrappingPipeline { + fn process_unwrapped(&mut self, timed_plaintext: TimedPayload, _: ()) -> Option> { let sphinx_packet = SphinxPacket::from_bytes(&timed_plaintext.data) .inspect_err(|e| tracing::warn!("Impossible to recover sphinx packet : {e}")) .ok()?; diff --git a/nym-mix-sim/src/client/simple.rs b/nym-mix-sim/src/client/simple.rs index 3daad69ad0..33b9b00491 100644 --- a/nym-mix-sim/src/client/simple.rs +++ b/nym-mix-sim/src/client/simple.rs @@ -22,12 +22,11 @@ //! └────────────────────────┴─────────────────────┘ //! ``` -use std::sync::Arc; +use std::{net::SocketAddr, sync::Arc, time::Instant}; use nym_lp_data::{ - AddressedTimedData, PipelinePayload, TimedData, TimedPayload, + AddressedTimedData, AddressedTimedPayload, TimedData, TimedPayload, clients::{ - InputOptions, helpers::{NoOpObfuscation, NoOpReliability, NoOpRoutingSecurity}, traits::{Chunking, ClientUnwrappingPipeline, ClientWrappingPipeline}, }, @@ -39,10 +38,7 @@ use nym_lp_data::{ use crate::{ client::{BaseClient, ClientId, ProcessingClient}, - node::NodeId, - packet::simple::{ - SimpleFrame, SimpleMessage, SimplePacket, SimpleWireUnwrapper, SimpleWireWrapper, - }, + packet::simple::{SimpleFrame, SimplePacket, SimpleWireUnwrapper, SimpleWireWrapper}, topology::{TopologyClient, directory::Directory}, }; @@ -53,16 +49,20 @@ use crate::{ /// /// UDP transport and routing are handled by the embedded [`BaseClient`]; this /// struct adds the outgoing queue and the wrapping/unwrapping pipelines. -pub type SimpleClient = BaseClient; +pub type SimpleClient = BaseClient; -impl SimpleClient { +impl SimpleClient { /// Bind both UDP sockets and return a new client. /// /// # Errors /// /// Returns an error if either socket fails to bind or set non-blocking. pub fn new(topology_client: TopologyClient, directory: Arc) -> anyhow::Result { + // SAFETY : node 0 always exists, otherwise we don't have any nodes + #[allow(clippy::unwrap_used)] + let first_hop_address = directory.node(0).unwrap().addr; let processing_client = SimpleProcessingClient { + first_hop: first_hop_address, wrapper: SimpleClientWrappingPipeline::default(), unwrapper: SimpleClientUnwrapping::default(), }; @@ -70,53 +70,34 @@ impl SimpleClient { topology_client.client_id, topology_client.mixnet_address, topology_client.app_address, - directory, processing_client, ) } } -/// [`InputOptions`] for the simple pipeline — all optional features disabled, -/// next hop is always node 0, really simple routing -#[derive(Clone, Copy)] -pub struct SimpleInputOptions; - -impl InputOptions for SimpleInputOptions { - fn reliability(&self) -> bool { - false - } - - fn routing_security(&self) -> bool { - false - } - - fn obfuscation(&self) -> bool { - false - } - - fn next_hop(&self) -> NodeId { - 0 - } -} - /// Bridges [`BaseClient`] to the simple wrapping and unwrapping pipelines. pub struct SimpleProcessingClient { + first_hop: SocketAddr, wrapper: SimpleClientWrappingPipeline, unwrapper: SimpleClientUnwrapping, } -impl ProcessingClient for SimpleProcessingClient { +impl ProcessingClient for SimpleProcessingClient { fn process( &mut self, input: Vec, _: ClientId, - timestamp: Ts, - ) -> Vec> { + timestamp: Instant, + ) -> Vec> { self.wrapper - .process(Some((input, SimpleInputOptions)), timestamp) + .process(Some((input, (), self.first_hop)), timestamp) } - fn unwrap(&mut self, input: SimplePacket, timestamp: Ts) -> anyhow::Result>> { + fn unwrap( + &mut self, + input: SimplePacket, + timestamp: Instant, + ) -> anyhow::Result>> { self.unwrapper.unwrap(input, timestamp) } } @@ -134,15 +115,13 @@ impl ProcessingClient for SimpleProcessingClient { /// impl in `nym_lp_data`. pub struct SimpleClientWrappingPipeline(SimpleWireWrapper); -pub(crate) type SimplePipelinePayload = PipelinePayload; - impl Default for SimpleClientWrappingPipeline { fn default() -> Self { Self(SimpleWireWrapper) } } -impl Chunking for SimpleClientWrappingPipeline { +impl Chunking<()> for SimpleClientWrappingPipeline { /// Split `input` into chunks of `chunk_size` bytes, padding the last chunk /// with zero bytes if necessary. /// @@ -150,27 +129,20 @@ impl Chunking for SimpleClientWrappin /// strip trailing zeros. fn chunked( &mut self, - mut input: Vec, - options: SimpleInputOptions, + input: AddressedTimedPayload, chunk_size: usize, - timestamp: Ts, - ) -> Vec> { - input.push(1); - if !input.len().is_multiple_of(chunk_size) { - let padding = vec![0; chunk_size - input.len() % chunk_size]; - input.extend_from_slice(&padding); + timestamp: Instant, + ) -> Vec { + let mut input_data = input.data.data; + input_data.push(1); + if !input_data.len().is_multiple_of(chunk_size) { + let padding = vec![0; chunk_size - input_data.len() % chunk_size]; + input_data.extend_from_slice(&padding); } - input + input_data .chunks(chunk_size) - .map(|chunk| { - SimplePipelinePayload::new( - timestamp.clone(), - chunk.to_vec(), - options, - options.next_hop(), - ) - }) + .map(|chunk| AddressedTimedPayload::new_addressed(timestamp, chunk.to_vec(), input.dst)) .collect() } } @@ -180,43 +152,38 @@ impl NoOpObfuscation for SimpleClientWrappingPipeline {} impl NoOpRoutingSecurity for SimpleClientWrappingPipeline {} // Delegation to SimpleWireWrapper -impl Framing for SimpleClientWrappingPipeline { +impl Framing<()> for SimpleClientWrappingPipeline { type Frame = SimpleFrame; - const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; + const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; fn to_frame( &mut self, - payload: PipelinePayload, + payload: AddressedTimedPayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { self.0.to_frame(payload, frame_size) } } // Delegation to SimpleWireWrapper -impl Transport for SimpleClientWrappingPipeline { +impl Transport for SimpleClientWrappingPipeline { type Frame = SimpleFrame; - const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; + const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { self.0.to_transport_packet(frame) } } // Delegation to SimpleWireWrapper -impl WireWrappingPipeline - for SimpleClientWrappingPipeline -{ +impl WireWrappingPipeline for SimpleClientWrappingPipeline { fn packet_size(&self) -> usize { - >::packet_size(&self.0) + >::packet_size(&self.0) } } -impl ClientWrappingPipeline - for SimpleClientWrappingPipeline -{ -} +impl ClientWrappingPipeline for SimpleClientWrappingPipeline {} // ───────────────────────────────────────────────────────────────────────────── /// Unwrapping pipeline for [`SimpleClient`]: strips the frame header and @@ -230,39 +197,30 @@ impl Default for SimpleClientUnwrapping { } // Delegation to SimpleWireUnwrapper -impl FramingUnwrap for SimpleClientUnwrapping { +impl FramingUnwrap<()> for SimpleClientUnwrapping { type Frame = SimpleFrame; - fn frame_to_message( - &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, SimpleMessage)> { + fn frame_to_message(&mut self, frame: TimedData) -> Option<(TimedPayload, ())> { self.0.frame_to_message(frame) } } // Delegation to SimpleWireUnwrapper -impl TransportUnwrap for SimpleClientUnwrapping { +impl TransportUnwrap for SimpleClientUnwrapping { type Frame = SimpleFrame; type Error = anyhow::Error; fn packet_to_frame( &mut self, packet: SimplePacket, - timestamp: Ts, - ) -> anyhow::Result> { + timestamp: Instant, + ) -> anyhow::Result> { self.0.packet_to_frame(packet, timestamp) } } -impl WireUnwrappingPipeline for SimpleClientUnwrapping {} +impl WireUnwrappingPipeline for SimpleClientUnwrapping {} -impl ClientUnwrappingPipeline - for SimpleClientUnwrapping -{ - fn process_unwrapped( - &mut self, - payload: TimedPayload, - _kind: SimpleMessage, - ) -> Option> { +impl ClientUnwrappingPipeline for SimpleClientUnwrapping { + fn process_unwrapped(&mut self, payload: TimedPayload, _: ()) -> Option> { let mut data = payload.data; if let Some(pos) = data.iter().rposition(|&b| b == 1) { data.truncate(pos); diff --git a/nym-mix-sim/src/client/sphinx/mod.rs b/nym-mix-sim/src/client/sphinx/mod.rs index 2d020ae694..7ca2d54d76 100644 --- a/nym-mix-sim/src/client/sphinx/mod.rs +++ b/nym-mix-sim/src/client/sphinx/mod.rs @@ -7,16 +7,13 @@ //! and Poisson cover traffic obfuscation. The unwrapping pipeline reconstructs //! fragmented messages and filters out cover traffic. -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; use nym_lp_data::{ AddressedTimedData, PipelinePayload, TimedPayload, - clients::{ - InputOptions, - traits::{ - Chunking, ClientUnwrappingPipeline, ClientWrappingPipeline, Obfuscation, Reliability, - RoutingSecurity, - }, + clients::traits::{ + Chunking, ClientUnwrappingPipeline, ClientWrappingPipeline, Obfuscation, Reliability, + RoutingSecurity, }, common::{ helpers::{NoOpWireUnwrapper, NoOpWireWrapper}, @@ -24,7 +21,7 @@ use nym_lp_data::{ }, }; use nym_sphinx::{ - Delay, Destination, DestinationAddressBytes, SphinxPacketBuilder, + Delay, SphinxPacketBuilder, chunking::{fragment::Fragment, reconstruction::MessageReconstructor}, message::{NymMessage, PaddedMessage}, }; @@ -35,9 +32,12 @@ use crate::{ BaseClient, ClientId, ProcessingClient, sphinx::{poisson_cover_traffic::PoissonCoverTraffic, surb_acks::SurbAcksReliability}, }, - node::NodeId, - packet::sphinx::{GenerateDelay, SimMixPacket, SphinxMessage, SurbAck}, - topology::{TopologyClient, directory::Directory}, + helpers, + packet::sphinx::{SimMixPacket, SurbAck}, + topology::{ + TopologyClient, + directory::{Directory, DirectoryClient, DirectoryNode}, + }, }; mod poisson_cover_traffic; @@ -50,9 +50,9 @@ mod surb_acks; /// /// UDP transport and routing are handled by the embedded [`BaseClient`]; this /// struct adds the outgoing queue and the wrapping/unwrapping pipelines. -pub type SphinxClient = BaseClient, SimMixPacket, Vec>; +pub type SphinxClient = BaseClient, SimMixPacket, Vec>; -impl SphinxClient { +impl SphinxClient { /// Bind both UDP sockets and return a new client. /// /// # Errors @@ -61,23 +61,23 @@ impl Sphin pub fn new( topology_client: TopologyClient, directory: Arc, - current_timestamp: Ts, + current_timestamp: Instant, rng: R, ) -> anyhow::Result { let processing_client = SphinxProcessingClient { wrapper: SphinxClientWrappingPipeline { cover_traffic: PoissonCoverTraffic::new( - topology_client.client_id, + (&topology_client).into(), directory.clone(), current_timestamp, rng.clone(), ), reliability: SurbAcksReliability::new( rng.clone(), - topology_client.client_id, + (&topology_client).into(), directory.clone(), ), - directory: directory.clone(), + directory, rng, }, unwrapper: SphinxClientUnwrapping::default(), @@ -86,69 +86,50 @@ impl Sphin topology_client.client_id, topology_client.mixnet_address, topology_client.app_address, - directory, processing_client, ) } } -/// [`InputOptions`] for the Sphinx pipeline — reliability, routing security, -/// and obfuscation are all enabled. #[derive(Clone, Copy)] pub struct SphinxInputOptions { - /// Destination client ID, embedded in the Sphinx destination address. - dst: ClientId, - /// First-hop node ID. In a real Nym network this would be the client's - /// gateway; here it is chosen at random from the topology because there is - /// no gateway concept in the simulation. - next_hop: NodeId, -} - -impl InputOptions for SphinxInputOptions { - fn reliability(&self) -> bool { - true - } - - fn routing_security(&self) -> bool { - true - } - - fn obfuscation(&self) -> bool { - true - } - - fn next_hop(&self) -> NodeId { - self.next_hop - } + /// Destination client + dst: DirectoryClient, + first_hop: DirectoryNode, } /// Bridges [`BaseClient`] to the Sphinx wrapping and unwrapping pipelines. -pub struct SphinxProcessingClient { - wrapper: SphinxClientWrappingPipeline, +pub struct SphinxProcessingClient { + wrapper: SphinxClientWrappingPipeline, unwrapper: SphinxClientUnwrapping, } -impl - ProcessingClient> for SphinxProcessingClient -{ +impl ProcessingClient> for SphinxProcessingClient { fn process( &mut self, input: Vec, dst: ClientId, - timestamp: Ts, - ) -> Vec> { + timestamp: Instant, + ) -> Vec> { + let first_hop = self + .wrapper + .directory + .random_next_hop(&mut self.wrapper.rng); + + let Some(&destination_client) = self.wrapper.directory.client(dst) else { + tracing::error!("Destination {dst} does not exist in the topology"); + return Vec::new(); + }; + let input_options = SphinxInputOptions { - dst, - next_hop: self - .wrapper - .directory - .random_next_hop(&mut self.wrapper.rng), // This substitutes for a real gateway selection — in the simulation every node is equally eligible as a first hop + dst: destination_client, + first_hop, }; self.wrapper - .process(Some((input, input_options)), timestamp) + .process(Some((input, input_options, first_hop.addr)), timestamp) } - fn unwrap(&mut self, input: Vec, timestamp: Ts) -> anyhow::Result>> { + fn unwrap(&mut self, input: Vec, timestamp: Instant) -> anyhow::Result>> { Ok(self.unwrapper.unwrap(input, timestamp)?) } } @@ -162,9 +143,9 @@ impl /// reliability prefix, Poisson cover traffic obfuscation, Sphinx onion /// encryption, and a no-op wire wrapper (a Sphinx packet is already its own /// wire unit). -pub struct SphinxClientWrappingPipeline { +pub struct SphinxClientWrappingPipeline { /// Poisson cover traffic generator providing the [`Obfuscation`] stage. - cover_traffic: PoissonCoverTraffic, + cover_traffic: PoissonCoverTraffic, /// SURB-ACK reliability layer providing the [`Reliability`] stage. reliability: SurbAcksReliability, /// Shared routing table; used to sample the 3-hop Sphinx route in `encrypt`. @@ -173,70 +154,55 @@ pub struct SphinxClientWrappingPipeline = PipelinePayload; - -impl Chunking - for SphinxClientWrappingPipeline -{ +impl Chunking for SphinxClientWrappingPipeline { fn chunked( &mut self, - input: Vec, - options: SphinxInputOptions, + input: PipelinePayload, chunk_size: usize, - timestamp: Ts, - ) -> Vec> { - if input.is_empty() { + timestamp: Instant, + ) -> Vec> { + let input_data = input.data.data; + if input_data.is_empty() { return Vec::new(); } // This is using standard sphinx chunking. Proper LP should use a different one - let fragments = NymMessage::new_plain(input) + let fragments = NymMessage::new_plain(input_data) .pad_to_full_packet_lengths(chunk_size) .split_into_fragments(&mut self.rng, chunk_size); fragments .into_iter() .map(|fragment| { - SphinxPipelinePayload::new( - timestamp.clone(), - fragment.into_bytes(), - options, - options.dst, - ) + PipelinePayload::new(timestamp, fragment.into_bytes(), input.options, input.dst) }) .collect() } } -impl Reliability - for SphinxClientWrappingPipeline -{ +impl Reliability for SphinxClientWrappingPipeline { const OVERHEAD_SIZE: usize = - as Reliability>::OVERHEAD_SIZE; + as Reliability>::OVERHEAD_SIZE; fn reliable_encode( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec> { + input: Option>, + timestamp: Instant, + ) -> Vec> { self.reliability.reliable_encode(input, timestamp) } } -impl Obfuscation - for SphinxClientWrappingPipeline -{ +impl Obfuscation for SphinxClientWrappingPipeline { fn obfuscate( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec> { + input: Option>, + timestamp: Instant, + ) -> Vec> { self.cover_traffic.obfuscate(input, timestamp) } } -impl RoutingSecurity - for SphinxClientWrappingPipeline -{ +impl RoutingSecurity for SphinxClientWrappingPipeline { const OVERHEAD_SIZE: usize = nym_sphinx::HEADER_SIZE + nym_sphinx::PAYLOAD_OVERHEAD_SIZE; fn nb_frames(&self) -> usize { 1 @@ -247,44 +213,34 @@ impl RoutingSecurity) -> SphinxPipelinePayload { - // SAFETY: IDs were sampled from the directory, so they are guaranteed to exist. - #[allow(clippy::unwrap_used)] - let first_hop = self - .directory - .node(input.options.next_hop) - .unwrap() - .as_sphinx_node_id(); + /// [`crate::helpers::generate_mix_delay`]. + fn encrypt( + &mut self, + input: PipelinePayload, + ) -> PipelinePayload { + let first_hop = input.options.first_hop.as_sphinx_node_socket(); let route = std::iter::once(first_hop) .chain( self.directory .random_route(2, &mut self.rng) .iter() - .map(|n| n.as_sphinx_node_id()), + .map(|n| n.as_sphinx_node_socket()), ) .collect::>(); - let destination = Destination::new( - DestinationAddressBytes::from_bytes([input.options.dst; 32]), - [input.options.dst; 16], - ); + let destination = input.options.dst.as_sphinx_destination(); let delays = (0..route.len()) - .map(|_| Delay::new_from_millis(Ts::generate_mix_delay(&mut self.rng))) + .map(|_| Delay::new_from_millis(helpers::generate_mix_delay(&mut self.rng))) .collect::>(); // Useful payload size is packet size - transport overhead - framing overhead - routing overhead - let plaintext_size = >::packet_size(self) - - >::OVERHEAD_SIZE - - >::OVERHEAD_SIZE - - >::OVERHEAD_SIZE; + let plaintext_size = + >::packet_size(self) + - >::OVERHEAD_SIZE + - >::OVERHEAD_SIZE + - >::OVERHEAD_SIZE; // Packet builder's size includes the payload overhead so we have to add it let packet_builder = SphinxPacketBuilder::new() @@ -297,7 +253,7 @@ impl RoutingSecurity RoutingSecurity NoOpWireWrapper - for SphinxClientWrappingPipeline -{ -} +impl NoOpWireWrapper for SphinxClientWrappingPipeline {} -impl - ClientWrappingPipeline - for SphinxClientWrappingPipeline +impl ClientWrappingPipeline + for SphinxClientWrappingPipeline { } // ───────────────────────────────────────────────────────────────────────────── @@ -330,12 +282,8 @@ pub struct SphinxClientUnwrapping { impl NoOpWireUnwrapper for SphinxClientUnwrapping {} -impl ClientUnwrappingPipeline, SphinxMessage> for SphinxClientUnwrapping { - fn process_unwrapped( - &mut self, - timed_plaintext: TimedPayload, - _kind: SphinxMessage, - ) -> Option> { +impl ClientUnwrappingPipeline, ()> for SphinxClientUnwrapping { + fn process_unwrapped(&mut self, timed_plaintext: TimedPayload, _: ()) -> Option> { let plaintext = timed_plaintext.data; // Ditch cover traffic diff --git a/nym-mix-sim/src/client/sphinx/poisson_cover_traffic.rs b/nym-mix-sim/src/client/sphinx/poisson_cover_traffic.rs index 0825d07bcf..3f4436826b 100644 --- a/nym-mix-sim/src/client/sphinx/poisson_cover_traffic.rs +++ b/nym-mix-sim/src/client/sphinx/poisson_cover_traffic.rs @@ -19,20 +19,16 @@ //! //! [`SphinxClient`]: super::SphinxClient -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; -use nym_lp_data::clients::traits::Obfuscation; +use nym_lp_data::{PipelinePayload, clients::traits::Obfuscation}; use nym_sphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; use rand::Rng; use crate::{ - client::{ - ClientId, - sphinx::{SphinxInputOptions, SphinxPipelinePayload}, - }, - node::NodeId, - packet::sphinx::GenerateDelay, - topology::directory::Directory, + client::sphinx::SphinxInputOptions, + helpers, + topology::directory::{Directory, DirectoryClient}, }; /// Two-loop Poisson cover traffic generator. @@ -40,24 +36,22 @@ use crate::{ /// Maintains two independent next-fire timestamps — one for the main sending /// loop and one for the secondary cover loop — and advances them by independent /// exponential delays on each firing. -pub struct PoissonCoverTraffic +pub struct PoissonCoverTraffic where - Ts: Clone + GenerateDelay + PartialOrd, R: Rng, { - address: ClientId, + cover_dst: DirectoryClient, directory: Arc, /// Timestamp at which the main loop next fires (real or cover packet). - main_loop_next_timestamp: Ts, + main_loop_next_timestamp: Instant, /// Timestamp at which the secondary cover loop next fires. - secondary_loop_next_timestamp: Ts, + secondary_loop_next_timestamp: Instant, /// Random number generator used for exponential delay sampling. rng: R, } -impl PoissonCoverTraffic +impl PoissonCoverTraffic where - Ts: Clone + GenerateDelay + PartialOrd, R: Rng, { /// Construct a new cover traffic generator. @@ -65,15 +59,15 @@ where /// Both loops are initialised to fire immediately at `current_timestamp` so /// that cover traffic begins on the very first tick. pub fn new( - address: ClientId, + cover_dst: DirectoryClient, directory: Arc, - current_timestamp: Ts, + current_timestamp: Instant, rng: R, ) -> Self { Self { - address, + cover_dst, directory, - main_loop_next_timestamp: current_timestamp.clone(), + main_loop_next_timestamp: current_timestamp, secondary_loop_next_timestamp: current_timestamp, rng, } @@ -85,15 +79,14 @@ where /// chosen at random from the directory, matching the real-message behaviour. pub fn cover_traffic_options(&mut self) -> SphinxInputOptions { SphinxInputOptions { - dst: self.address, - next_hop: self.directory.random_next_hop(&mut self.rng), + dst: self.cover_dst, + first_hop: self.directory.random_next_hop(&mut self.rng), } } } -impl Obfuscation for PoissonCoverTraffic +impl Obfuscation for PoissonCoverTraffic where - Ts: Clone + GenerateDelay + PartialOrd, R: Rng, { /// Produce the set of payloads to send at `timestamp`. @@ -103,23 +96,23 @@ where /// whether a real message is available. fn obfuscate( &mut self, - input: Option>, - timestamp: Ts, - ) -> Vec> { + input: Option>, + timestamp: Instant, + ) -> Vec> { let mut output = Vec::new(); // Secondary cover traffic loop // We should not schedule those in advance, because backpressure can't tell if it has real or cover traffic. if timestamp >= self.secondary_loop_next_timestamp { let cover_options = self.cover_traffic_options(); - output.push(SphinxPipelinePayload::new( - timestamp.clone(), + output.push(PipelinePayload::new( + timestamp, LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), cover_options, - cover_options.next_hop, + cover_options.first_hop.addr, )); - self.secondary_loop_next_timestamp = self.secondary_loop_next_timestamp.clone() - + Ts::generate_cover_traffic_delay(&mut self.rng); + self.secondary_loop_next_timestamp += + helpers::generate_cover_traffic_delay(&mut self.rng); } // Main cover traffic loop @@ -127,21 +120,19 @@ where match input { // If we have a message, schedule it for the next timestamp, prepare the following one Some(real_message) => { - output.push(real_message.ts_transform(|_| self.main_loop_next_timestamp.clone())); - self.main_loop_next_timestamp = self.main_loop_next_timestamp.clone() - + Ts::generate_sending_delay(&mut self.rng); + output.push(real_message.with_timestamp(self.main_loop_next_timestamp)); + self.main_loop_next_timestamp += helpers::generate_sending_delay(&mut self.rng); } // No message, but we need to send something => Send cover traffic right away, prepare next timestamp None if timestamp >= self.main_loop_next_timestamp => { let cover_options = self.cover_traffic_options(); - output.push(SphinxPipelinePayload::new( + output.push(PipelinePayload::new( timestamp, LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), cover_options, - cover_options.next_hop, + cover_options.first_hop.addr, )); - self.main_loop_next_timestamp = self.main_loop_next_timestamp.clone() - + Ts::generate_sending_delay(&mut self.rng); + self.main_loop_next_timestamp += helpers::generate_sending_delay(&mut self.rng); } // No message, not the time to send anything, nothing to do None => {} diff --git a/nym-mix-sim/src/client/sphinx/surb_acks.rs b/nym-mix-sim/src/client/sphinx/surb_acks.rs index 23e0790c7c..131a88ee8b 100644 --- a/nym-mix-sim/src/client/sphinx/surb_acks.rs +++ b/nym-mix-sim/src/client/sphinx/surb_acks.rs @@ -10,19 +10,15 @@ //! //! [`SphinxClient`]: super::SphinxClient -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; use crate::{ - client::{ - ClientId, - sphinx::{SphinxInputOptions, SphinxPipelinePayload}, - }, - node::NodeId, - packet::sphinx::{GenerateDelay, SurbAck}, - topology::directory::Directory, + client::sphinx::SphinxInputOptions, + packet::sphinx::SurbAck, + topology::directory::{Directory, DirectoryClient}, }; -use nym_lp_data::clients::traits::Reliability; +use nym_lp_data::{PipelinePayload, clients::traits::Reliability}; use rand::Rng; @@ -36,7 +32,7 @@ pub struct SurbAcksReliability where R: Rng, { - address: ClientId, + ack_dst: DirectoryClient, directory: Arc, rng: R, } @@ -49,19 +45,18 @@ where /// /// `address` is used as the SURB reply destination so that ACKs are routed /// back to this client. `directory` is used to sample the 3-hop SURB route. - pub fn new(rng: R, address: ClientId, directory: Arc) -> Self { + pub fn new(rng: R, ack_dst: DirectoryClient, directory: Arc) -> Self { Self { - address, + ack_dst, directory, rng, } } } -impl Reliability for SurbAcksReliability +impl Reliability for SurbAcksReliability where R: Rng, - Ts: GenerateDelay, { const OVERHEAD_SIZE: usize = SurbAck::len(); @@ -71,20 +66,16 @@ where /// in-flight packet carries a unique acknowledgement path. fn reliable_encode( &mut self, - input: Option>, - _timestamp: Ts, - ) -> Vec> { + input: Option>, + _: Instant, + ) -> Vec> { if let Some(packet) = input { let random_id = self.rng.next_u64(); tracing::debug!("Generating SURB Ack with ID {random_id}"); - let surb_ack = SurbAck::construct::( - &mut self.rng, - self.address, - random_id, - &self.directory, - ) - .prepare_for_sending() - .1; + let surb_ack = + SurbAck::construct::(&mut self.rng, self.ack_dst, random_id, &self.directory) + .prepare_for_sending() + .1; let reliable_packet = packet.data_transform(|payload| { surb_ack.iter().copied().chain(payload).collect::>() }); diff --git a/nym-mix-sim/src/driver/mod.rs b/nym-mix-sim/src/driver/mod.rs index cdb01384ab..740c00efb8 100644 --- a/nym-mix-sim/src/driver/mod.rs +++ b/nym-mix-sim/src/driver/mod.rs @@ -36,39 +36,43 @@ mod sphinx; pub use nymnode::NymNodeMixDriver; pub use simple::SimpleMixDriver; -pub use sphinx::{DiscreteSphinxMixDriver, SphinxMixDriver}; +pub use sphinx::SphinxMixDriver; /// Top-level orchestrator for the mix-network simulation. /// /// Holds ordered lists of type-erased [`MixSimNode`]s and [`MixSimClient`]s. /// Only the timestamp type `Ts` is visible at this level; packet format, frame /// type, and message marker are encapsulated inside each concrete node/client. -pub struct MixSimDriver -where - Ts: Clone + PartialOrd + Debug + Send, -{ - nodes: Vec + Send>>, - clients: Vec + Send>>, +pub struct MixSimDriver { + nodes: Vec>, + clients: Vec>, + clock_base: Instant, } -impl MixSimDriver -where - Ts: Clone + PartialOrd + Debug + Send, -{ +impl MixSimDriver { /// Construct the driver from pre-built nodes and clients. /// /// Topology parsing and socket binding are the caller's responsibility. pub fn new( - nodes: Vec + Send>>, - clients: Vec + Send>>, + nodes: Vec>, + clients: Vec>, ) -> Self { - Self { nodes, clients } + Self { + nodes, + clients, + clock_base: Instant::now(), + } + } + + pub fn display_tick(&self, tick: Instant) -> u128 { + tick.duration_since(self.clock_base).as_millis() } /// Pretty-print the current state of every node at `tick`. - pub fn display_state(&self, tick: Ts) { + pub fn display_state(&self, tick: Instant) { println!( - "┌─── Tick {tick:─<3?}────────────────────────────────────────────────────────────┐" + "┌─── Tick {:─<3} ms─────────────────────────────────────────────────────────┐", + self.display_tick(tick) ); for node in &self.nodes { node.display_state(); @@ -87,9 +91,9 @@ where /// 4. **Processing** — every node mixes buffered packets. /// 5. *(optional state display)* /// 6. **Outgoing** — nodes forward due packets; - pub fn tick(&mut self, timestamp: Ts, display_state: bool) { + pub fn tick(&mut self, timestamp: Instant, display_state: bool) { for client in &mut self.clients { - client.tick(timestamp.clone()); + client.tick(timestamp); } // Phase 1 — incoming for node in &mut self.nodes { @@ -97,85 +101,24 @@ where } if display_state { - self.display_state(timestamp.clone()); + self.display_state(timestamp); } // Phase 2 — processing for node in &mut self.nodes { - node.tick_processing(timestamp.clone()); + node.tick_processing(timestamp); } if display_state { - self.display_state(timestamp.clone()); + self.display_state(timestamp); } // Phase 3 — outgoing for node in &mut self.nodes { - node.tick_outgoing(timestamp.clone()); - } - } -} - -/// Driving logic for the concrete `Ts = u32` timestamp flavour. -/// -/// The timestamp is a monotonically increasing tick counter starting at zero. -/// If a richer timestamp type is needed in the future, a new impl block should -/// be added. -impl MixSimDriver { - /// Start the simulation in either manual or automatic mode. - pub async fn run( - self, - manual_mode: bool, - display_state: bool, - start_tick: u32, - tick_duration_ms: u64, - ) -> anyhow::Result<()> { - if manual_mode { - self.run_manual(start_tick, display_state) - } else { - self.run_automatic(start_tick, tick_duration_ms).await + node.tick_outgoing(timestamp); } } - /// Run the simulation automatically, advancing one tick every - /// `tick_duration_ms` milliseconds until Ctrl-C is received. - pub async fn run_automatic( - mut self, - start_tick: u32, - tick_duration_ms: u64, - ) -> anyhow::Result<()> { - info!("Automatic mode: tick duration : {tick_duration_ms} ms"); - let tick_duration = Duration::from_millis(tick_duration_ms); - let handle = tokio::spawn(async move { - let mut current_tick = start_tick; - loop { - self.tick(current_tick, false); - current_tick += 1; - tokio::time::sleep(tick_duration).await; - } - }); - tokio::signal::ctrl_c().await?; - handle.abort(); - Ok(()) - } - - /// Run the simulation interactively: one tick per ENTER key press. - pub fn run_manual(mut self, start_tick: u32, display_state: bool) -> anyhow::Result<()> { - info!("Manual mode: press ENTER to advance a tick, Ctrl-C to quit"); - let mut current_tick = start_tick; - let mut line = String::new(); - loop { - line.clear(); - std::io::stdin().read_line(&mut line)?; - info!("Tick {current_tick}"); - self.tick(current_tick, display_state); - current_tick += 1; - } - } -} - -/// Driving logic for the concrete `Ts = Instant` timestamp flavour. -impl MixSimDriver { /// Start the simulation in either manual or automatic mode. pub async fn run( self, @@ -212,16 +155,12 @@ impl MixSimDriver { info!("Manual mode: press ENTER to advance a tick, Ctrl-C to quit"); info!("One tick represent {tick_duration_ms}ms"); let tick_duration = Duration::from_millis(tick_duration_ms); - let start_tick = Instant::now(); - let mut current_tick = start_tick; + let mut current_tick = self.clock_base; let mut line = String::new(); loop { line.clear(); std::io::stdin().read_line(&mut line)?; - info!( - "Tick {}ms", - current_tick.duration_since(start_tick).as_millis() - ); + info!("Tick {}ms", self.display_tick(current_tick)); self.tick(current_tick, display_state); current_tick += tick_duration; } @@ -232,17 +171,14 @@ impl MixSimDriver { #[derive(Clone, Debug, Default, strum::Display, strum::EnumString)] #[strum(serialize_all = "kebab-case")] pub enum SimDriver { - /// Simple pass-through packets, discrete tick counter. + /// Simple pass-through packets. Simple, - /// Full Sphinx encryption, wall-clock timestamps, automatic mode only. + /// Full Sphinx encryption with SURBACKs and cover traffic Sphinx, - /// Full Sphinx encryption, discrete tick counter, supports manual mode. - #[default] - DiscreteSphinx, - /// Real [`NymNodeDataPipeline`] processing sphinx-in-LP packets, wall-clock - /// timestamps, automatic mode only. + /// Real [`NymNodeDataPipeline`] processing sphinx-in-LP packets. /// /// [`NymNodeDataPipeline`]: nym_node::node::lp::data::handler::pipeline::NymNodeDataPipeline + #[default] NymNode, } @@ -266,11 +202,6 @@ impl SimDriver { .run(manual, display_state, tick_duration_ms) .await } - SimDriver::DiscreteSphinx => { - DiscreteSphinxMixDriver::new(topology)? - .run(manual, display_state, tick_duration_ms) - .await - } SimDriver::NymNode => { NymNodeMixDriver::new(topology)? .run(manual, display_state, tick_duration_ms) diff --git a/nym-mix-sim/src/driver/nymnode.rs b/nym-mix-sim/src/driver/nymnode.rs index b805466850..d365a59dfd 100644 --- a/nym-mix-sim/src/driver/nymnode.rs +++ b/nym-mix-sim/src/driver/nymnode.rs @@ -9,7 +9,7 @@ //! //! [`NymNodeDataPipeline`]: nym_node::node::lp::data::handler::pipeline::NymNodeDataPipeline -use std::{sync::Arc, time::Instant}; +use std::sync::Arc; use anyhow::Context; use rand::rngs::OsRng; @@ -26,12 +26,15 @@ use crate::{ /// from every client. /// /// [`NymNodeDataPipeline`]: nym_node::node::lp::data::handler::pipeline::NymNodeDataPipeline -pub struct NymNodeMixDriver(MixSimDriver); +pub struct NymNodeMixDriver(MixSimDriver); impl NymNodeMixDriver { /// Load a topology JSON file and initialise the driver with one - /// [`NymNodeSimNode`] per topology node and one [`NymNodeSimClient`] per + /// [`SimNymNode`] per topology node and one [`SimNymClient`] per /// topology client. + /// + /// [`SimNymNode`]: crate::node::nymnode::SimNymNode + /// [`SimNymClient`]: crate::client::nymnode::SimNymClient pub fn new(topology: String) -> anyhow::Result { let topology_data = std::fs::read_to_string(&topology).context("Failed to read topology file")?; @@ -40,14 +43,13 @@ impl NymNodeMixDriver { let directory: Arc = Arc::new((&topology).into()); - let mut nodes: Vec + Send>> = - Vec::with_capacity(topology.nodes.len()); + let mut nodes: Vec> = Vec::with_capacity(topology.nodes.len()); for top_node in topology.nodes { let node = SimNymNode::new(top_node, directory.clone(), OsRng)?; nodes.push(Box::new(node)); } - let mut clients: Vec + Send>> = + let mut clients: Vec> = Vec::with_capacity(topology.clients.len()); for top_client in topology.clients { let client = SimNymClient::new(top_client, directory.clone(), OsRng)?; @@ -59,8 +61,6 @@ impl NymNodeMixDriver { /// Run the simulation; delegates to [`MixSimDriver::run`]. /// - /// `manual_mode` is ignored: [`Instant`]-based drivers cannot be stepped - /// manually because wall-clock time cannot be advanced by keypress. pub async fn run( self, manual_mode: bool, diff --git a/nym-mix-sim/src/driver/simple.rs b/nym-mix-sim/src/driver/simple.rs index e8739eea5d..092ca071c4 100644 --- a/nym-mix-sim/src/driver/simple.rs +++ b/nym-mix-sim/src/driver/simple.rs @@ -25,7 +25,7 @@ use crate::{ /// /// [`SimpleProcessingNode`]: crate::node::simple::SimpleProcessingNode /// [`SimpleClientWrappingPipeline`]: crate::client::simple::SimpleClientWrappingPipeline -pub struct SimpleMixDriver(MixSimDriver); +pub struct SimpleMixDriver(MixSimDriver); impl SimpleMixDriver { /// Load a topology JSON file and initialise the driver with simple pipelines. @@ -37,14 +37,13 @@ impl SimpleMixDriver { let directory: Arc = Arc::new((&topology).into()); - let mut nodes: Vec + Send>> = - Vec::with_capacity(topology.nodes.len()); + let mut nodes: Vec> = Vec::with_capacity(topology.nodes.len()); for top_node in topology.nodes { let node = SimpleNode::new(top_node, directory.clone())?; nodes.push(Box::new(node)); } - let mut clients: Vec + Send>> = + let mut clients: Vec> = Vec::with_capacity(topology.clients.len()); for top_client in topology.clients { let client = SimpleClient::new(top_client, directory.clone())?; @@ -62,7 +61,7 @@ impl SimpleMixDriver { tick_duration_ms: u64, ) -> anyhow::Result<()> { self.0 - .run(manual_mode, display_state, 0, tick_duration_ms) + .run(manual_mode, display_state, tick_duration_ms) .await } } diff --git a/nym-mix-sim/src/driver/sphinx.rs b/nym-mix-sim/src/driver/sphinx.rs index c384712080..b4e4cc71f4 100644 --- a/nym-mix-sim/src/driver/sphinx.rs +++ b/nym-mix-sim/src/driver/sphinx.rs @@ -22,7 +22,7 @@ use crate::{ }; /// Concrete [`MixSimDriver`] instantiation that uses [`SphinxPacket`](nym_sphinx::SphinxPacket)s. -pub struct SphinxMixDriver(MixSimDriver); +pub struct SphinxMixDriver(MixSimDriver); impl SphinxMixDriver { /// Load a topology JSON file and initialise the driver with Sphinx pipelines. @@ -34,14 +34,13 @@ impl SphinxMixDriver { let directory: Arc = Arc::new((&topology).into()); - let mut nodes: Vec + Send>> = - Vec::with_capacity(topology.nodes.len()); + let mut nodes: Vec> = Vec::with_capacity(topology.nodes.len()); for top_node in topology.nodes { let node = SphinxNode::new(top_node, directory.clone())?; nodes.push(Box::new(node)); } - let mut clients: Vec + Send>> = + let mut clients: Vec> = Vec::with_capacity(topology.clients.len()); for top_client in topology.clients { let client = SphinxClient::new(top_client, directory.clone(), Instant::now(), OsRng)?; @@ -66,58 +65,3 @@ impl SphinxMixDriver { .await } } - -/// Concrete [`MixSimDriver`] instantiation that uses full Sphinx encryption with a -/// discrete tick counter. -/// -/// Each tick corresponds to 1 ms of simulated time, enabling deterministic -/// stepping and delay arithmetic without requiring wall-clock time. This is -/// the default driver and the only Sphinx variant that supports manual mode. -pub struct DiscreteSphinxMixDriver(MixSimDriver); - -impl DiscreteSphinxMixDriver { - const START_TICK: u32 = 0; - - /// Load a topology JSON file and initialise the driver with Sphinx pipelines. - pub fn new(topology: String) -> anyhow::Result { - let topology_data = - std::fs::read_to_string(&topology).context("Failed to read topology file")?; - let topology: Topology = - serde_json::from_str(&topology_data).context("Topology file malformed")?; - - let directory: Arc = Arc::new((&topology).into()); - - let mut nodes: Vec + Send>> = - Vec::with_capacity(topology.nodes.len()); - for top_node in topology.nodes { - let node = SphinxNode::new(top_node, directory.clone())?; - nodes.push(Box::new(node)); - } - - let mut clients: Vec + Send>> = - Vec::with_capacity(topology.clients.len()); - for top_client in topology.clients { - let client = SphinxClient::new(top_client, directory.clone(), Self::START_TICK, OsRng)?; - clients.push(Box::new(client)); - } - - Ok(DiscreteSphinxMixDriver(MixSimDriver::new(nodes, clients))) - } - - /// Run the simulation; delegates to [`MixSimDriver::run`]. - pub async fn run( - self, - manual_mode: bool, - display_state: bool, - tick_duration_ms: u64, - ) -> anyhow::Result<()> { - self.0 - .run( - manual_mode, - display_state, - Self::START_TICK, - tick_duration_ms, - ) - .await - } -} diff --git a/nym-mix-sim/src/helpers.rs b/nym-mix-sim/src/helpers.rs new file mode 100644 index 0000000000..31a673e5a0 --- /dev/null +++ b/nym-mix-sim/src/helpers.rs @@ -0,0 +1,31 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +use rand::Rng; +use rand_distr::{Distribution, Exp}; + +/// Exponential with mean 50 ms. +pub fn generate_mix_delay(rng: &mut impl Rng) -> u64 { + // SAFETY : hardcoded > 0 value + #[allow(clippy::unwrap_used)] + let exp: Exp = Exp::new(1.0 / 50.0).unwrap(); + exp.sample(rng).round() as u64 +} + +/// Exponential with mean 20 ms. +pub fn generate_sending_delay(rng: &mut impl Rng) -> Duration { + // SAFETY : hardcoded > 0 value + #[allow(clippy::unwrap_used)] + let exp: Exp = Exp::new(1.0 / 20.0).unwrap(); + Duration::from_millis(exp.sample(rng).round() as u64) +} + +/// Exponential with mean 200 ms. +pub fn generate_cover_traffic_delay(rng: &mut impl Rng) -> Duration { + // SAFETY : hardcoded > 0 value + #[allow(clippy::unwrap_used)] + let exp: Exp = Exp::new(1.0 / 200.0).unwrap(); + Duration::from_millis(exp.sample(rng).round() as u64) +} diff --git a/nym-mix-sim/src/lib.rs b/nym-mix-sim/src/lib.rs index c4aa3884d7..72ebb46af3 100644 --- a/nym-mix-sim/src/lib.rs +++ b/nym-mix-sim/src/lib.rs @@ -38,6 +38,7 @@ pub mod client; pub mod driver; +pub mod helpers; pub mod node; pub mod packet; pub mod topology; diff --git a/nym-mix-sim/src/main.rs b/nym-mix-sim/src/main.rs index 41814a3925..05f8ff084d 100644 --- a/nym-mix-sim/src/main.rs +++ b/nym-mix-sim/src/main.rs @@ -37,7 +37,7 @@ enum Commands { /// /// Each node receives an auto-assigned ID (0..N-1) and a sequential /// localhost address starting at `127.0.0.1:9000`. - #[arg(short, long, default_value_t = 6)] + #[arg(short, long, default_value_t = 3)] nodes: u8, /// Number of clients to generate. @@ -67,12 +67,12 @@ enum Commands { #[arg(long)] no_display_state: bool, - /// Tick duration in milliseconds (automatic mode only). - #[arg(short = 'd', long, default_value = "1")] + /// Tick duration in milliseconds. + #[arg(short = 'd', long, default_value = "10")] tick_duration_ms: u64, - /// Simulation driver to use: simple | sphinx | discrete-sphinx (default). - #[arg(long, default_value_t = SimDriver::DiscreteSphinx)] + /// Simulation driver to use: simple | sphinx | nym-node (default). + #[arg(long, default_value_t = SimDriver::NymNode)] driver: SimDriver, }, } diff --git a/nym-mix-sim/src/node/mod.rs b/nym-mix-sim/src/node/mod.rs index fae2334da2..bc69d52e89 100644 --- a/nym-mix-sim/src/node/mod.rs +++ b/nym-mix-sim/src/node/mod.rs @@ -5,12 +5,12 @@ use std::{ fmt::Debug, io::ErrorKind, net::{SocketAddr, UdpSocket}, - sync::Arc, + time::Instant, }; use nym_lp_data::{AddressedTimedData, nymnodes::traits::NymNodeProcessingPipeline}; -use crate::{packet::WirePacketFormat, topology::directory::Directory}; +use crate::packet::WirePacketFormat; pub mod nymnode; pub mod simple; @@ -22,30 +22,6 @@ pub mod sphinx; /// realistic simulated topology. pub type NodeId = u8; -/// Trait that abstracts how a destination value resolves to a [`SocketAddr`]. -/// -/// Implemented for [`NodeId`] (looks the address up in the [`Directory`]) and for -/// [`SocketAddr`] (already an address; returned as-is). Allows [`BaseNode`] to -/// support both forwarding-by-id and forwarding-by-address pipelines. -pub trait DestinationAddress: Copy + Debug + Send { - fn resolve(self, directory: &Directory) -> Option; -} - -impl DestinationAddress for NodeId { - fn resolve(self, directory: &Directory) -> Option { - directory - .node(self) - .map(|n| n.addr) - .or_else(|| directory.client(self).map(|c| c.addr)) - } -} - -impl DestinationAddress for SocketAddr { - fn resolve(self, _: &Directory) -> Option { - Some(self) - } -} - /// Driver-facing interface for a mix node. /// /// Erases `Pkt` and `Pn` so that [`MixSimDriver`] only needs `Ts`. @@ -53,17 +29,17 @@ impl DestinationAddress for SocketAddr { /// `Pn`. /// /// [`MixSimDriver`]: crate::driver::MixSimDriver -pub trait MixSimNode: Send { +pub trait MixSimNode: Send { /// **Phase 1** — drain the UDP socket into the inbound buffer fn tick_incoming(&mut self); /// **Phase 2** — pass every buffered packet through the mix pipeline and /// move the results into the outbound queue. - fn tick_processing(&mut self, timestamp: Ts); + fn tick_processing(&mut self, timestamp: Instant); /// **Phase 3** — forward all outbound packets whose scheduled timestamp is /// ≤ `timestamp` to their next-hop address. - fn tick_outgoing(&mut self, timestamp: Ts); + fn tick_outgoing(&mut self, timestamp: Instant); /// Pretty-print the node's current buffer state to stdout (used in manual mode). fn display_state(&self); @@ -72,9 +48,8 @@ pub trait MixSimNode: Send { /// Full mix-node state: UDP transport, routing directory, packet buffers, and /// processing pipeline. /// -/// `Ts` is the timestamp / tick-context type. `Pkt` is the wire packet type -/// (e.g. [`SimplePacket`] or [`SimMixPacket`]). `Pn` is any type that -/// implements [`ProcessingNode`]. +/// `Pkt` is the wire packet type (e.g. [`SimplePacket`] or [`SimMixPacket`]). +/// `Pn` is any type that implements [`NymNodeProcessingPipeline`]. /// /// Concrete node variants (`SimpleNode`, `SphinxNode`, …) are type aliases /// over this struct and only need to supply a `new()` constructor that wires @@ -82,7 +57,7 @@ pub trait MixSimNode: Send { /// /// [`SimplePacket`]: crate::packet::simple::SimplePacket /// [`SimMixPacket`]: crate::packet::sphinx::SimMixPacket -pub struct BaseNode { +pub struct BaseNode { /// Identifier of this node within the topology. pub(crate) id: NodeId, /// Notional reliability percentage; not yet used by the simulator but kept @@ -92,28 +67,25 @@ pub struct BaseNode { pub(crate) socket_address: SocketAddr, /// Non-blocking UDP socket used for both receive and send. socket: UdpSocket, - /// Shared routing table used to resolve next-hop [`NodeId`]s to addresses. - directory: Arc, /// Inbound buffer: raw packets drained from the socket in `tick_incoming`, /// ready to be fed through the mix pipeline in `tick_processing`. packets_to_process: Vec, /// Outbound buffer: packets produced by the mix pipeline, each tagged with /// the timestamp at which it should be released by `tick_outgoing`. - processed_packets: Vec>, + processed_packets: Vec>, /// Concrete mix-processing implementation invoked by `tick_processing`. processing_node: Pn, } -impl BaseNode { +impl BaseNode { /// Bind a non-blocking UDP socket to `socket_address` and initialise the /// node with the given `pipeline`. pub(crate) fn with_pipeline( id: NodeId, reliability: u8, socket_address: SocketAddr, - directory: Arc, processing_node: Pn, ) -> anyhow::Result { let socket = UdpSocket::bind(socket_address)?; @@ -123,40 +95,24 @@ impl BaseNode { _reliability: reliability, socket_address, socket, - directory, packets_to_process: Vec::new(), processed_packets: Vec::new(), processing_node, }) } - /// Send `packet` to the destination identified by `dst`. + /// Send `packet` to the destination identified by `address`. /// - /// Resolves `dst` to a [`SocketAddr`] via [`DestinationAddress::resolve`], - /// serialises via [`WirePacketFormat::to_bytes`], and dispatches with a + /// Serialises via [`WirePacketFormat::to_bytes`], and dispatches with a /// single `sendto`. Errors are logged but not propagated. - pub fn send_to(&self, dst: Dst, packet: Pkt) + pub fn send_to(&self, address: SocketAddr, packet: Pkt) where Pkt: WirePacketFormat, - Dst: DestinationAddress, { - let Some(addr) = dst.resolve(&self.directory) else { - tracing::error!( - "[Node {}] Cannot resolve destination {dst:?} to a socket address", - self.id - ); - return; - }; - if let Err(e) = self.socket.send_to(&packet.to_bytes(), addr) { - tracing::error!( - "[Node {}] Failed to send data to {dst:?} ({addr}) : {e}", - self.id - ); + if let Err(e) = self.socket.send_to(&packet.to_bytes(), address) { + tracing::error!("[Node {}] Failed to send data to {address} : {e}", self.id); } else { - tracing::debug!( - "[Node {}] Successfully sent a packet to {dst:?} ({addr})", - self.id - ); + tracing::debug!("[Node {}] Successfully sent a packet to {address}", self.id); } } @@ -184,13 +140,11 @@ impl BaseNode { } } -impl MixSimNode for BaseNode +impl MixSimNode for BaseNode where - Ts: Clone + PartialOrd + Debug + Send, Pkt: WirePacketFormat + Debug + Send, - Pn: NymNodeProcessingPipeline + Send, - >::Error: Debug, - Dst: DestinationAddress + Send, + Pn: NymNodeProcessingPipeline + Send, + >::Error: Debug, { fn tick_incoming(&mut self) { while let Some(maybe_packet) = self.recv_packet() { @@ -201,9 +155,9 @@ where } } - fn tick_processing(&mut self, timestamp: Ts) { + fn tick_processing(&mut self, timestamp: Instant) { while let Some(packet) = self.packets_to_process.pop() { - match self.processing_node.process(packet, timestamp.clone()) { + match self.processing_node.process(packet, timestamp) { Ok(processed_packets) => self.processed_packets.extend(processed_packets), Err(e) => { tracing::error!("[Node {}] Failed to process packet : {e:?}", self.id) @@ -212,7 +166,7 @@ where } } - fn tick_outgoing(&mut self, timestamp: Ts) { + fn tick_outgoing(&mut self, timestamp: Instant) { let to_send = self .processed_packets .extract_if(.., |pkt| pkt.data.timestamp <= timestamp) @@ -232,7 +186,7 @@ where self.packets_to_process.len() ); for (i, pkt) in self.packets_to_process.iter().enumerate() { - println!("│ [{i}] {pkt:?}"); + println!("│ [{i}] {pkt:#?}"); } } @@ -244,7 +198,7 @@ where self.processed_packets.len() ); for (i, pkt) in self.processed_packets.iter().enumerate() { - println!("│ [{i}] {pkt:?}"); + println!("│ [{i}] {pkt:#?}"); } } } diff --git a/nym-mix-sim/src/node/nymnode.rs b/nym-mix-sim/src/node/nymnode.rs index 217e267a86..c207563126 100644 --- a/nym-mix-sim/src/node/nymnode.rs +++ b/nym-mix-sim/src/node/nymnode.rs @@ -1,9 +1,9 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -//! [`NymNodeSimNode`] — mix node that runs the real [`NymNodeDataPipeline`]. +//! [`SimNymNode`] — mix node that runs the real [`NymNodeDataPipeline`]. -use std::{net::SocketAddr, sync::Arc, time::Instant}; +use std::sync::Arc; use nym_lp_data::packet::EncryptedLpPacket; use nym_node::node::lp::data::{ @@ -19,16 +19,16 @@ use crate::{ /// A simulated mix node driven by the real [`NymNodeDataPipeline`]. /// -/// This is a type alias for [`BaseNode`] specialised to [`NymNodeSimPacket`] and -/// [`NymNodeProcessingAdapter`]. All tick logic lives in the generic -/// [`MixSimNode`] impl on `BaseNode`; routing produces [`SocketAddr`]s directly -/// (no Directory lookup is needed). +/// This is a type alias for [`BaseNode`] specialised to [`EncryptedLpPacket`] +/// and [`NymNodeDataPipeline`]. All tick logic lives in the generic +/// [`MixSimNode`] impl on `BaseNode`; routing produces [`std::net::SocketAddr`]s +/// directly (no Directory lookup is needed). /// /// [`MixSimNode`]: crate::node::MixSimNode -pub type SimNymNode = BaseNode, SocketAddr>; +pub type SimNymNode = BaseNode>; impl SimNymNode { - /// Create a [`NymNodeSimNode`] from a [`TopologyNode`] description by binding + /// Create a [`SimNymNode`] from a [`TopologyNode`] description by binding /// a non-blocking UDP socket to `node.socket_address` and constructing a /// simulation-ready [`NymNodeDataPipeline`] with the node's sphinx key. /// @@ -52,7 +52,6 @@ impl SimNymNode { topology_node.node_id, topology_node.reliability, topology_node.socket_address, - directory, pipeline, ) } diff --git a/nym-mix-sim/src/node/simple.rs b/nym-mix-sim/src/node/simple.rs index 9340a670b6..6b4535d68b 100644 --- a/nym-mix-sim/src/node/simple.rs +++ b/nym-mix-sim/src/node/simple.rs @@ -3,10 +3,10 @@ //! [`SimpleNode`] — mix node using the simple (non-Sphinx) packet pipeline. -use std::sync::Arc; +use std::{net::SocketAddr, sync::Arc, time::Instant}; use nym_lp_data::{ - AddressedTimedData, PipelinePayload, TimedData, TimedPayload, + AddressedTimedData, AddressedTimedPayload, TimedData, TimedPayload, common::traits::{ Framing, FramingUnwrap, Transport, TransportUnwrap, WireUnwrappingPipeline, WireWrappingPipeline, @@ -15,11 +15,8 @@ use nym_lp_data::{ }; use crate::{ - client::simple::SimpleInputOptions, node::{BaseNode, NodeId}, - packet::simple::{ - SimpleFrame, SimpleMessage, SimplePacket, SimpleWireUnwrapper, SimpleWireWrapper, - }, + packet::simple::{SimpleFrame, SimplePacket, SimpleWireUnwrapper, SimpleWireWrapper}, topology::{TopologyNode, directory::Directory}, }; @@ -30,9 +27,9 @@ use crate::{ /// [`MixSimNode`] impl on `BaseNode`. /// /// [`MixSimNode`]: crate::node::MixSimNode -pub type SimpleNode = BaseNode; +pub type SimpleNode = BaseNode; -impl SimpleNode { +impl SimpleNode { /// Create a [`SimpleNode`] from a [`TopologyNode`] description by binding a /// non-blocking UDP socket to `node.socket_address`. /// @@ -40,12 +37,11 @@ impl SimpleNode { /// /// Returns an error if the UDP socket cannot be bound or set non-blocking. pub fn new(topology_node: TopologyNode, directory: Arc) -> anyhow::Result { - let pipeline = SimpleProcessingNode::new(topology_node.node_id); + let pipeline = SimpleProcessingNode::new(topology_node.node_id, directory); BaseNode::with_pipeline( topology_node.node_id, topology_node.reliability, topology_node.socket_address, - directory, pipeline, ) } @@ -60,97 +56,97 @@ impl SimpleNode { /// [`NymNodeProcessingPipeline::mix`] (forwards to `self.id + 1`), then /// re-wraps the outgoing payload (payload → frame → transport) before sending. pub struct SimpleProcessingNode { - id: NodeId, + next_hop: SocketAddr, wrapper: SimpleWireWrapper, unwrapper: SimpleWireUnwrapper, } impl SimpleProcessingNode { /// Construct a pipeline for the node identified by `id`. - pub fn new(id: NodeId) -> Self { + pub fn new(id: NodeId, directory: Arc) -> Self { + // SAFETY : clients have the highest ID so there will be something ad id+1 + #[allow(clippy::unwrap_used)] + let next_hop = directory + .node(id + 1) + .map(|n| n.addr) + .or(directory.client(id + 1).map(|c| c.addr)) + .unwrap(); Self { - id, + next_hop, wrapper: SimpleWireWrapper, unwrapper: SimpleWireUnwrapper, } } } -impl NymNodeProcessingPipeline for SimpleProcessingNode { - type Options = SimpleInputOptions; - type MessageKind = SimpleMessage; +impl NymNodeProcessingPipeline for SimpleProcessingNode { + type Options = (); + type MessageKind = (); /// Route the payload to the next node in the chain (`self.id + 1`). /// /// This is a trivial fixed routing rule used for simulation testing. - /// Real mix nodes would perform cryptographic route unwrapping here. fn mix( &mut self, - _: SimpleMessage, - payload: TimedPayload, - _timestamp: Ts, - ) -> Vec> { - vec![PipelinePayload::new( + _: (), + payload: TimedPayload, + _timestamp: Instant, + ) -> Vec { + vec![AddressedTimedPayload::new_addressed( payload.timestamp, payload.data, - SimpleInputOptions, - self.id + 1, + self.next_hop, )] } } // Delegation of subtraits -impl Framing for SimpleProcessingNode { +impl Framing<()> for SimpleProcessingNode { type Frame = SimpleFrame; - const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; + const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; fn to_frame( &mut self, - payload: PipelinePayload, + payload: AddressedTimedPayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { self.wrapper.to_frame(payload, frame_size) } } -impl Transport for SimpleProcessingNode { +impl Transport for SimpleProcessingNode { type Frame = SimpleFrame; - const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; + const OVERHEAD_SIZE: usize = >::OVERHEAD_SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { self.wrapper.to_transport_packet(frame) } } -impl WireWrappingPipeline - for SimpleProcessingNode -{ +impl WireWrappingPipeline for SimpleProcessingNode { fn packet_size(&self) -> usize { - >::packet_size(&self.wrapper) + >::packet_size(&self.wrapper) } } -impl FramingUnwrap for SimpleProcessingNode { +impl FramingUnwrap<()> for SimpleProcessingNode { type Frame = SimpleFrame; - fn frame_to_message( - &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, SimpleMessage)> { + fn frame_to_message(&mut self, frame: TimedData) -> Option<(TimedPayload, ())> { self.unwrapper.frame_to_message(frame) } } -impl TransportUnwrap for SimpleProcessingNode { +impl TransportUnwrap for SimpleProcessingNode { type Frame = SimpleFrame; type Error = anyhow::Error; fn packet_to_frame( &mut self, packet: SimplePacket, - timestamp: Ts, - ) -> anyhow::Result> { + timestamp: Instant, + ) -> anyhow::Result> { self.unwrapper.packet_to_frame(packet, timestamp) } } -impl WireUnwrappingPipeline for SimpleProcessingNode {} +impl WireUnwrappingPipeline for SimpleProcessingNode {} diff --git a/nym-mix-sim/src/node/sphinx.rs b/nym-mix-sim/src/node/sphinx.rs index a6b3579034..8e2b676dc0 100644 --- a/nym-mix-sim/src/node/sphinx.rs +++ b/nym-mix-sim/src/node/sphinx.rs @@ -3,7 +3,7 @@ //! [`SphinxNode`] — mix node using the full Sphinx packet pipeline. -use std::sync::Arc; +use std::{sync::Arc, time::Instant}; use nym_crypto::asymmetric::x25519; use nym_lp_data::{ @@ -12,12 +12,13 @@ use nym_lp_data::{ nymnodes::traits::NymNodeProcessingPipeline, }; use nym_sphinx::SphinxPacket; +use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use crate::{ node::{BaseNode, NodeId}, packet::{ WirePacketFormat, - sphinx::{AddDelay, SimMixPacket, SphinxMessage, SurbAck}, + sphinx::{SimMixPacket, SurbAck}, }, topology::{TopologyNode, directory::Directory}, }; @@ -29,9 +30,9 @@ use crate::{ /// [`MixSimNode`] impl on `BaseNode`. /// /// [`MixSimNode`]: crate::node::MixSimNode -pub type SphinxNode = BaseNode; +pub type SphinxNode = BaseNode; -impl SphinxNode { +impl SphinxNode { /// Create a [`SphinxNode`] from a [`TopologyNode`] description by binding a /// non-blocking UDP socket to `node.socket_address`. /// @@ -39,13 +40,15 @@ impl SphinxNode { /// /// Returns an error if the UDP socket cannot be bound or set non-blocking. pub fn new(topology_node: TopologyNode, directory: Arc) -> anyhow::Result { - let pipeline = - SphinxProcessingNode::new(topology_node.node_id, topology_node.sphinx_private_key); + let pipeline = SphinxProcessingNode::new( + topology_node.node_id, + topology_node.sphinx_private_key, + directory, + ); BaseNode::with_pipeline( topology_node.node_id, topology_node.reliability, topology_node.socket_address, - directory, pipeline, ) } @@ -63,24 +66,27 @@ impl SphinxNode { pub struct SphinxProcessingNode { id: NodeId, sphinx_secret: x25519::PrivateKey, + directory: Arc, } impl SphinxProcessingNode { /// Construct a pipeline for the node identified by `node_id`, using /// `sphinx_secret` to decrypt incoming Sphinx packets. - pub fn new(node_id: NodeId, sphinx_secret: x25519::PrivateKey) -> Self { + pub fn new( + node_id: NodeId, + sphinx_secret: x25519::PrivateKey, + directory: Arc, + ) -> Self { Self { id: node_id, sphinx_secret, + directory, } } } -impl NymNodeProcessingPipeline for SphinxProcessingNode -where - Ts: AddDelay + Clone, -{ - type MessageKind = SphinxMessage; +impl NymNodeProcessingPipeline for SphinxProcessingNode { + type MessageKind = (); type Options = (); /// Peel one Sphinx layer and forward or deliver the result. @@ -92,10 +98,10 @@ where /// client (identified by byte 0 of the destination address). fn mix( &mut self, - _: SphinxMessage, - payload: TimedPayload, - timestamp: Ts, - ) -> Vec> { + _: (), + payload: TimedPayload, + timestamp: Instant, + ) -> Vec { // SAFETY: Given the no-op unwrapper used here, payload.data is always a // valid serialised SphinxPacket at this point. #[allow(clippy::unwrap_used)] @@ -108,10 +114,22 @@ where next_hop_address, delay, } => { + let Ok(routing_address) = next_hop_address.try_into() else { + tracing::warn!("[Node {}] Cannot recover routing address", self.id); + return Vec::new(); + }; + + let NymNodeRoutingAddress::Node(next_hop_address) = routing_address else { + tracing::warn!( + "[Node {}] Received a sphinx packet with a Client routing address", + self.id + ); + return Vec::new(); + }; let timed_sphinx = AddressedTimedData::new_addressed( - timestamp.add_delay(delay), + timestamp + delay.to_duration(), next_hop_packet.to_bytes(), - next_hop_address.as_bytes()[0], + next_hop_address, ); vec![timed_sphinx] } @@ -120,32 +138,55 @@ where identifier: _, payload, } => { + let mut packets_to_forward = Vec::new(); + if let Ok(plaintext) = payload .recover_plaintext() .inspect_err(|e| tracing::warn!("Impossible to recover plaintext : {e}")) { let (surb_ack_bytes, message) = SurbAck::extract_ack_and_message(plaintext); - let mut packets_to_forward = vec![AddressedTimedData::new_addressed( - timestamp.clone(), - message, - destination.as_bytes()[0], - )]; + + // Client packet handling + if let Some(client_socket_address) = self + .directory + .client(destination.as_bytes()[0]) + .map(|n| n.addr) + { + packets_to_forward.push(AddressedTimedData::new_addressed( + timestamp, + message, + client_socket_address, + )); + } else { + tracing::warn!( + "[Node {}] Client {} not found in the directory", + self.id, + destination.as_bytes()[0] + ); + }; + + // SURB_ACK handling if !surb_ack_bytes.is_empty() && let Ok((next_hop, surb_ack)) = SurbAck::try_recover_first_hop_packet( &surb_ack_bytes, ) .inspect_err(|e| tracing::warn!("Fail to deserialize SURB Ack : {e}")) { - packets_to_forward.push(AddressedTimedData::new_addressed( - timestamp, - surb_ack.to_bytes(), - next_hop, - )); + if let Some(next_hop_socket_address) = + self.directory.node(next_hop).map(|n| n.addr) + { + packets_to_forward.push(AddressedTimedData::new_addressed( + timestamp, + surb_ack.to_bytes(), + next_hop_socket_address, + )); + } else { + tracing::warn!("Node {next_hop} not found in the directory",); + } } - packets_to_forward - } else { - Vec::new() } + + packets_to_forward } }, Err(e) => { diff --git a/nym-mix-sim/src/packet/mod.rs b/nym-mix-sim/src/packet/mod.rs index d11f1a24b3..838f4521a5 100644 --- a/nym-mix-sim/src/packet/mod.rs +++ b/nym-mix-sim/src/packet/mod.rs @@ -6,7 +6,7 @@ //! The central abstraction is [`WirePacketFormat`]: a trait that any packet //! type must implement to participate in a simulation. It covers only //! wire serialisation; mix logic is handled separately by -//! [`nym_lp_data::mixnodes::traits::NymNodeProcessingPipeline`]. +//! [`nym_lp_data::nymnodes::traits::NymNodeProcessingPipeline`]. //! pub mod nymnode; diff --git a/nym-mix-sim/src/packet/simple.rs b/nym-mix-sim/src/packet/simple.rs index c70aa8ef62..c9d99dbe25 100644 --- a/nym-mix-sim/src/packet/simple.rs +++ b/nym-mix-sim/src/packet/simple.rs @@ -1,11 +1,11 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::Debug; +use std::{fmt::Debug, time::Instant}; use nym_common::debug::format_debug_bytes; use nym_lp_data::{ - AddressedTimedData, PipelinePayload, TimedData, TimedPayload, + AddressedTimedData, AddressedTimedPayload, TimedData, TimedPayload, common::traits::{ Framing, FramingUnwrap, Transport, TransportUnwrap, WireUnwrappingPipeline, WireWrappingPipeline, @@ -14,7 +14,7 @@ use nym_lp_data::{ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{client::simple::SimpleInputOptions, node::NodeId, packet::WirePacketFormat}; +use crate::packet::WirePacketFormat; /// A minimal, fixed-size packet used by the simulation. /// @@ -173,14 +173,6 @@ impl SimpleFrame { } } -/// Marker type identifying a fully-unwrapped simple payload. -/// -/// Passed through the pipeline's [`FramingUnwrap`] stage; because the simple -/// pipeline has only one message kind this is a zero-sized unit struct. -/// -/// [`FramingUnwrap`]: nym_lp_data::common::traits::FramingUnwrap -pub struct SimpleMessage; - // ───────────────────────────────────────────────────────────────────────────── // Building blocks @@ -191,21 +183,21 @@ pub struct SimpleMessage; /// pipeline that needs wire-wrapping by delegating to `SimpleWireWrapper`. pub struct SimpleWireWrapper; -impl Framing for SimpleWireWrapper { +impl Framing<()> for SimpleWireWrapper { type Frame = SimpleFrame; const OVERHEAD_SIZE: usize = SimpleFrame::HEADER.len(); fn to_frame( &mut self, - payload: PipelinePayload, + payload: AddressedTimedPayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { payload .data .data .chunks(frame_size) .map(|chunk| { AddressedTimedData::new_addressed( - payload.data.timestamp.clone(), + payload.data.timestamp, SimpleFrame { data: chunk.to_vec(), }, @@ -218,22 +210,20 @@ impl Framing for SimpleWireWrapper { /// Transport wraps a [`SimpleFrame`] into a [`SimplePacket`]. /// Overhead = 16 bytes (UUID), so effective payload = 48 bytes. -impl Transport for SimpleWireWrapper { +impl Transport for SimpleWireWrapper { type Frame = SimpleFrame; const OVERHEAD_SIZE: usize = 16; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { // SAFETY: If the pipeline is implemented properly, frames perfectly fit in a packet #[allow(clippy::unwrap_used)] frame.data_transform(|inner| SimplePacket::new(inner.to_bytes().try_into().unwrap())) } } -impl WireWrappingPipeline - for SimpleWireWrapper -{ +impl WireWrappingPipeline for SimpleWireWrapper { fn packet_size(&self) -> usize { SimplePacket::SIZE } @@ -247,30 +237,27 @@ impl WireWrappingPipeline FramingUnwrap for SimpleWireUnwrapper { +impl FramingUnwrap<()> for SimpleWireUnwrapper { type Frame = SimpleFrame; - fn frame_to_message( - &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, SimpleMessage)> { + fn frame_to_message(&mut self, frame: TimedData) -> Option<(TimedPayload, ())> { Some(( TimedPayload { data: frame.data.data, timestamp: frame.timestamp, }, - SimpleMessage, + (), )) } } -impl TransportUnwrap for SimpleWireUnwrapper { +impl TransportUnwrap for SimpleWireUnwrapper { type Frame = SimpleFrame; type Error = anyhow::Error; fn packet_to_frame( &mut self, packet: SimplePacket, - timestamp: Ts, - ) -> anyhow::Result> { + timestamp: Instant, + ) -> anyhow::Result> { // packet.data holds the framed bytes (HEADER + payload) Ok(TimedData::new( timestamp, @@ -279,4 +266,4 @@ impl TransportUnwrap for SimpleWireUnwrapper { } } -impl WireUnwrappingPipeline for SimpleWireUnwrapper {} +impl WireUnwrappingPipeline for SimpleWireUnwrapper {} diff --git a/nym-mix-sim/src/packet/sphinx.rs b/nym-mix-sim/src/packet/sphinx.rs index 8577b945d9..466d6f2db6 100644 --- a/nym-mix-sim/src/packet/sphinx.rs +++ b/nym-mix-sim/src/packet/sphinx.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use nym_common::debug::format_debug_bytes; -use nym_sphinx::{Delay, Destination, DestinationAddressBytes, SphinxPacketBuilder}; +use nym_sphinx::{Delay, SphinxPacketBuilder}; use rand::Rng; -use rand_distr::{Distribution, Exp}; -use std::{fmt::Debug, ops::Add, time::Duration}; + +use std::fmt::Debug; use crate::{ - client::ClientId, node::NodeId, packet::WirePacketFormat, topology::directory::Directory, + helpers, + node::NodeId, + packet::WirePacketFormat, + topology::directory::{Directory, DirectoryClient}, }; /// On-wire packet exchanged between mix nodes in the Sphinx pipeline. @@ -82,9 +85,9 @@ impl SurbAck { /// Samples a 3-hop route from `directory`, draws per-hop Sphinx delays using /// `Ts::generate_mix_delay`, and constructs a Sphinx packet whose payload is /// `MARKER || packet_id.to_le_bytes()`. - pub fn construct( + pub fn construct( rng: &mut R, - recipient: ClientId, + recipient: DirectoryClient, packet_id: u64, directory: &Directory, ) -> Self @@ -100,16 +103,13 @@ impl SurbAck { let first_hop_id = route.first().unwrap().id; let sphinx_route = route .into_iter() - .map(|n| n.as_sphinx_node_id()) + .map(|n| n.as_sphinx_node_socket()) .collect::>(); - let destination = Destination::new( - DestinationAddressBytes::from_bytes([recipient; 32]), - [recipient; 16], - ); + let destination = recipient.as_sphinx_destination(); let delays = (0..sphinx_route.len()) - .map(|_| Delay::new_from_millis(Ts::generate_mix_delay(rng))) + .map(|_| Delay::new_from_millis(helpers::generate_mix_delay(rng))) .collect::>(); let ack_payload = Self::MARKER @@ -204,109 +204,3 @@ impl SurbAck { data[..Self::MARKER.len()] == *Self::MARKER } } - -/// Marker type identifying a fully-unwrapped Sphinx payload. -/// -/// Passed through the pipeline's [`FramingUnwrap`] stage so that -/// [`ClientUnwrappingPipeline::process_unwrapped`] can dispatch on the message -/// kind. In the simulation there is only one kind, so this is a zero-sized -/// unit struct. -/// -/// [`FramingUnwrap`]: nym_lp_data::common::traits::FramingUnwrap -/// [`ClientUnwrappingPipeline::process_unwrapped`]: nym_lp_data::clients::traits::ClientUnwrappingPipeline::process_unwrapped -#[derive(Default)] -pub struct SphinxMessage; - -/// Abstracts adding a Sphinx [`Delay`] to a timestamp type. -/// -/// Implemented for `u32` (1 tick = 1 ms) and [`Instant`](std::time::Instant) -/// so the same mix logic can run against either timestamp flavour. -pub trait AddDelay: Sized { - /// Return a new timestamp shifted forward by `delay`. - fn add_delay(self, delay: nym_sphinx::Delay) -> Self; -} - -impl AddDelay for u32 { - /// One tick = 1 ms. - fn add_delay(self, delay: nym_sphinx::Delay) -> Self { - self + (delay.to_nanos() / 1_000_000) as u32 - } -} - -impl AddDelay for std::time::Instant { - fn add_delay(self, delay: nym_sphinx::Delay) -> Self { - self + delay.to_duration() - } -} - -/// Timestamp types that can generate Sphinx delays and be advanced by them. -/// -/// Implemented for `u32` (discrete ticks, 1 tick = 1 ms) and -/// [`Instant`](std::time::Instant) (wall-clock time). -pub trait GenerateDelay: Sized + Add { - /// The delay unit that can be added to `Self` (e.g. `u32` ticks or - /// [`Duration`]). - type Delay; - - /// Draw a per-hop mix delay in milliseconds for inclusion in a Sphinx packet header. - fn generate_mix_delay(rng: &mut impl Rng) -> u64; - - /// Draw an inter-packet sending delay for the main Poisson loop. - fn generate_sending_delay(rng: &mut impl Rng) -> Self::Delay; - - /// Draw an inter-packet sending delay for the secondary cover traffic loop. - fn generate_cover_traffic_delay(rng: &mut impl Rng) -> Self::Delay; -} - -impl GenerateDelay for u32 { - type Delay = u32; - - /// Uniform in `[0, 10]` ms. - fn generate_mix_delay(rng: &mut impl Rng) -> u64 { - rng.gen_range(0..=10) - } - - /// Exponential with mean 10 ticks (ms). - fn generate_sending_delay(rng: &mut impl Rng) -> u32 { - // SAFETY : hardcoded > 0 value - #[allow(clippy::unwrap_used)] - let exp: Exp = Exp::new(1.0 / 10.0).unwrap(); - exp.sample(rng).round() as u32 - } - - /// Exponential with mean 100 ticks (ms). - fn generate_cover_traffic_delay(rng: &mut impl Rng) -> u32 { - // SAFETY : hardcoded > 0 value - #[allow(clippy::unwrap_used)] - let exp: Exp = Exp::new(1.0 / 100.0).unwrap(); - exp.sample(rng).round() as u32 - } -} - -impl GenerateDelay for std::time::Instant { - type Delay = Duration; - - /// Exponential with mean 50 ms. - fn generate_mix_delay(rng: &mut impl Rng) -> u64 { - // SAFETY : hardcoded > 0 value - #[allow(clippy::unwrap_used)] - let exp: Exp = Exp::new(1.0 / 50.0).unwrap(); - exp.sample(rng).round() as u64 - } - - /// Exponential with mean 20 ms. - fn generate_sending_delay(rng: &mut impl Rng) -> Duration { - // SAFETY : hardcoded > 0 value - #[allow(clippy::unwrap_used)] - let exp: Exp = Exp::new(1.0 / 20.0).unwrap(); - Duration::from_millis(exp.sample(rng).round() as u64) - } - - /// Exponential with mean 200 ms. - fn generate_cover_traffic_delay(rng: &mut impl Rng) -> Duration { - // SAFETY : hardcoded > 0 value - #[allow(clippy::unwrap_used)] - let exp: Exp = Exp::new(1.0 / 200.0).unwrap(); - Duration::from_millis(exp.sample(rng).round() as u64) - } -} diff --git a/nym-mix-sim/src/topology/directory.rs b/nym-mix-sim/src/topology/directory.rs index f54dc0802b..5698ad8f0e 100644 --- a/nym-mix-sim/src/topology/directory.rs +++ b/nym-mix-sim/src/topology/directory.rs @@ -12,10 +12,10 @@ use std::{collections::HashMap, net::SocketAddr}; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_sphinx::{Destination, DestinationAddressBytes, Node as SphinxNode, NodeAddressBytes}; +use nym_sphinx::{Destination, DestinationAddressBytes, Node as SphinxNode}; use nym_sphinx_addressing::{ClientAddress, nodes::NymNodeRoutingAddress}; use nym_topology::{NymTopology, RoutingNode, SupportedRoles}; -use rand::{SeedableRng, prelude::SliceRandom, rngs::StdRng, seq::IteratorRandom}; +use rand::{SeedableRng, rngs::StdRng, seq::IteratorRandom}; use crate::{ client::ClientId, @@ -54,19 +54,14 @@ impl Directory { self.clients.get(&id) } - /// Return the [`NodeId`] of every node currently in the directory. - pub fn node_ids(&self) -> Vec { - self.nodes.keys().copied().collect() - } - /// Pick a random node from the directory and return its [`NodeId`]. /// /// Used by Sphinx clients to choose a first-hop node when the simulation has /// no explicit gateway concept. - pub fn random_next_hop(&self, rng: &mut impl rand::Rng) -> NodeId { + pub fn random_next_hop(&self, rng: &mut impl rand::Rng) -> DirectoryNode { // SAFETY: The directory always contains at least one node in a valid simulation. #[allow(clippy::unwrap_used)] - *self.node_ids().choose(rng).unwrap() + *self.nodes.values().choose(rng).unwrap() } /// Sample `length` random hops (with replacement) for a Sphinx route. @@ -143,11 +138,6 @@ impl From<&TopologyNode> for DirectoryNode { } impl DirectoryNode { - pub fn as_sphinx_node_id(&self) -> SphinxNode { - let address = NodeAddressBytes::from_bytes([self.id; 32]); - SphinxNode::new(address, *self.sphinx_public_key) - } - pub fn as_sphinx_node_socket(&self) -> SphinxNode { let address = NymNodeRoutingAddress::Node(self.addr); // SAFETY : our addressing scheme can fit in a sphinx packet diff --git a/nym-node/src/node/lp/data/handler/mod.rs b/nym-node/src/node/lp/data/handler/mod.rs index 1356da5504..9ebf8a259d 100644 --- a/nym-node/src/node/lp/data/handler/mod.rs +++ b/nym-node/src/node/lp/data/handler/mod.rs @@ -43,8 +43,7 @@ const PIPELINE_TICKING_DURATION: Duration = Duration::from_millis(1); /// bursty load and provides drop-based backpressure. const WORKER_QUEUE_DEPTH: usize = 128; -type WorkerOutput = - Result>, MalformedLpPacketError>; +type WorkerOutput = Result>, MalformedLpPacketError>; /// A single packet processing job dispatched to a worker thread. struct WorkerInput { @@ -72,7 +71,7 @@ pub struct LpDataHandler { /// Aggregated processed packets returned by the workers. worker_output_rx: mpsc::Receiver, - outgoing_pkt_buffer: Vec>, + outgoing_pkt_buffer: Vec>, /// Shutdown token shutdown: nym_task::ShutdownToken, @@ -244,8 +243,8 @@ impl LpDataHandler { input_rx: mpsc::Receiver, output_tx: mpsc::SyncSender, ) where - P: NymNodeProcessingPipeline - + TransportUnwrap, // This is needed to specify the error type + P: NymNodeProcessingPipeline + + TransportUnwrap, // This is needed to specify the error type { while let Ok(input) = input_rx.recv() { // Blocking is fine, we don't want to unclog ourself and process a new packet that will be dropped anyway diff --git a/nym-node/src/node/lp/data/handler/pipeline/mixing_node.rs b/nym-node/src/node/lp/data/handler/pipeline/mixing_node.rs index 79850bfc14..fc1c413d79 100644 --- a/nym-node/src/node/lp/data/handler/pipeline/mixing_node.rs +++ b/nym-node/src/node/lp/data/handler/pipeline/mixing_node.rs @@ -4,7 +4,7 @@ //! Pipeline for nodes operating purely as mixnodes (no client forwarding). //! -use std::{net::SocketAddr, sync::Arc, time::Instant}; +use std::{sync::Arc, time::Instant}; use nym_lp_data::{ AddressedTimedData, PipelinePayload, TimedData, TimedPayload, @@ -45,18 +45,16 @@ impl MixingNodeDataPipeline { } // Processing logic - stubbed; to be implemented. -impl NymNodeProcessingPipeline - for MixingNodeDataPipeline -{ +impl NymNodeProcessingPipeline for MixingNodeDataPipeline { type Options = MixMessage; type MessageKind = MixMessage; fn mix( &mut self, message_kind: MixMessage, - payload: TimedPayload, + payload: TimedPayload, _: Instant, - ) -> Vec> { + ) -> Vec> { // Everything specific to a given packet type should happen here let processing_result = NymNodeDataPipeline::::process_mix_packet(&self.state, message_kind, payload); @@ -101,15 +99,15 @@ impl NymNodeProcessingPipeline // ============== Wire wrap/unwrap: pure delegation to WirePipeline ============== -impl Framing for MixingNodeDataPipeline { +impl Framing for MixingNodeDataPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE; fn to_frame( &mut self, - payload: PipelinePayload, + payload: PipelinePayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { let frame = LpFrame { header: payload.options.into(), content: payload.data.data.into(), @@ -119,27 +117,25 @@ impl Framing for MixingNodeDataPipeline } } -impl Transport for MixingNodeDataPipeline { +impl Transport for MixingNodeDataPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpHeader::SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { self.wire.frame_to_packet(frame) } } -impl WireWrappingPipeline - for MixingNodeDataPipeline -{ +impl WireWrappingPipeline for MixingNodeDataPipeline { fn packet_size(&self) -> usize { nym_lp_data::packet::MTU } } -impl TransportUnwrap for MixingNodeDataPipeline { +impl TransportUnwrap for MixingNodeDataPipeline { type Frame = LpFrame; type Error = MalformedLpPacketError; @@ -147,18 +143,18 @@ impl TransportUnwrap for MixingNodeDataPipel &mut self, packet: EncryptedLpPacket, timestamp: Instant, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { self.wire.packet_to_frame(packet, timestamp) } } -impl FramingUnwrap for MixingNodeDataPipeline { +impl FramingUnwrap for MixingNodeDataPipeline { type Frame = LpFrame; fn frame_to_message( &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, MixMessage)> { + frame: TimedData, + ) -> Option<(TimedPayload, MixMessage)> { let reassembled = self.wire.frame_to_maybe_message(frame)?; let message_kind = MixMessage::from_frame_header(reassembled.data.header) .inspect_err(|e| warn!("{e}")) @@ -172,7 +168,4 @@ impl FramingUnwrap for MixingNodeDataPipeline { } } -impl WireUnwrappingPipeline - for MixingNodeDataPipeline -{ -} +impl WireUnwrappingPipeline for MixingNodeDataPipeline {} diff --git a/nym-node/src/node/lp/data/handler/pipeline/nymnode.rs b/nym-node/src/node/lp/data/handler/pipeline/nymnode.rs index ac17e841b9..eae7fcc3b2 100644 --- a/nym-node/src/node/lp/data/handler/pipeline/nymnode.rs +++ b/nym-node/src/node/lp/data/handler/pipeline/nymnode.rs @@ -1,7 +1,7 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::{net::SocketAddr, sync::Arc, time::Instant}; +use std::{sync::Arc, time::Instant}; use nym_lp_data::{ AddressedTimedData, PipelinePayload, TimedData, TimedPayload, @@ -52,9 +52,8 @@ impl NymNodeDataPipeline { pub fn process_mix_packet( shared_state: &SharedLpDataState, message_kind: MixMessage, - payload: TimedPayload, - ) -> Result, LpDataHandlerError> - { + payload: TimedPayload, + ) -> Result, LpDataHandlerError> { match message_kind { MixMessage::Sphinx(metadata) => { processing::sphinx::process(shared_state, payload, metadata) @@ -67,18 +66,16 @@ impl NymNodeDataPipeline { } // Processing logic -impl NymNodeProcessingPipeline - for NymNodeDataPipeline -{ +impl NymNodeProcessingPipeline for NymNodeDataPipeline { type Options = NymNodeMessage; type MessageKind = NymNodeMessage; fn mix( &mut self, message_kind: NymNodeMessage, - payload: TimedPayload, + payload: TimedPayload, _: Instant, - ) -> Vec> { + ) -> Vec> { // Everything specific to a given packet type should happen here let processing_result = match message_kind { NymNodeMessage::Mix(msg) => Self::process_mix_packet(&self.state, msg, payload) @@ -150,15 +147,15 @@ impl NymNodeProcessingPipeline // ============== Wire wrap/unwrap: pure delegation to WirePipeline ============== -impl Framing for NymNodeDataPipeline { +impl Framing for NymNodeDataPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE; fn to_frame( &mut self, - payload: PipelinePayload, + payload: PipelinePayload, frame_size: usize, - ) -> Vec> { + ) -> Vec> { let frame = LpFrame { header: payload.options.into(), content: payload.data.data.into(), @@ -168,27 +165,25 @@ impl Framing for NymNodeDataPipelin } } -impl Transport for NymNodeDataPipeline { +impl Transport for NymNodeDataPipeline { type Frame = LpFrame; const OVERHEAD_SIZE: usize = LpHeader::SIZE; fn to_transport_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { self.wire.frame_to_packet(frame) } } -impl WireWrappingPipeline - for NymNodeDataPipeline -{ +impl WireWrappingPipeline for NymNodeDataPipeline { fn packet_size(&self) -> usize { nym_lp_data::packet::MTU } } -impl TransportUnwrap for NymNodeDataPipeline { +impl TransportUnwrap for NymNodeDataPipeline { type Frame = LpFrame; type Error = MalformedLpPacketError; @@ -196,18 +191,18 @@ impl TransportUnwrap for NymNodeDataPipeline &mut self, packet: EncryptedLpPacket, timestamp: Instant, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { self.wire.packet_to_frame(packet, timestamp) } } -impl FramingUnwrap for NymNodeDataPipeline { +impl FramingUnwrap for NymNodeDataPipeline { type Frame = LpFrame; fn frame_to_message( &mut self, - frame: TimedData, - ) -> Option<(TimedPayload, NymNodeMessage)> { + frame: TimedData, + ) -> Option<(TimedPayload, NymNodeMessage)> { let reassembled = self.wire.frame_to_maybe_message(frame)?; let message_kind = NymNodeMessage::from_frame_header(reassembled.data.header) .inspect_err(|e| warn!("{e}")) @@ -221,10 +216,7 @@ impl FramingUnwrap for NymNodeDataPipeline { } } -impl WireUnwrappingPipeline - for NymNodeDataPipeline -{ -} +impl WireUnwrappingPipeline for NymNodeDataPipeline {} // ================================================================================================================================================ diff --git a/nym-node/src/node/lp/data/handler/pipeline/wire.rs b/nym-node/src/node/lp/data/handler/pipeline/wire.rs index 4c54285510..d144201a42 100644 --- a/nym-node/src/node/lp/data/handler/pipeline/wire.rs +++ b/nym-node/src/node/lp/data/handler/pipeline/wire.rs @@ -42,8 +42,8 @@ impl WirePipeline { /// Wrap an [`LpFrame`] into an [`EncryptedLpPacket`] for the wire. pub fn frame_to_packet( &mut self, - frame: AddressedTimedData, - ) -> AddressedTimedData { + frame: AddressedTimedData, + ) -> AddressedTimedData { // Here be LP encryption. For now, just wrap into an EncryptedLpPacket; we don't care at reception anyway frame.data_transform(|f| LpPacket::new(LpHeader::new(0, 0, version::CURRENT), f).encode()) } @@ -53,7 +53,7 @@ impl WirePipeline { &mut self, packet: EncryptedLpPacket, timestamp: Instant, - ) -> Result, MalformedLpPacketError> { + ) -> Result, MalformedLpPacketError> { // Here be LP decryption. For now we do as is, it's not encrypted let lp_packet = LpPacket::decode(packet).inspect_err(|_| { self.state.malformed_packet(); @@ -72,7 +72,7 @@ impl WirePipeline { frame: LpFrame, dst: SocketAddr, frame_size: usize, - ) -> Vec> { + ) -> Vec> { let output_frames = if frame.len() > frame_size { fragment_lp_message(&mut self.rng, frame, frame_size) .into_iter() @@ -94,8 +94,8 @@ impl WirePipeline { /// fragment. pub fn frame_to_maybe_message( &mut self, - frame: TimedData, - ) -> Option> { + frame: TimedData, + ) -> Option> { let reassembled = if frame.data.kind() == LpFrameKind::FragmentedData { let fragment = frame .data diff --git a/nym-node/src/node/lp/data/handler/processing/outfox.rs b/nym-node/src/node/lp/data/handler/processing/outfox.rs index c1ad2fc4a8..4f7309f7bb 100644 --- a/nym-node/src/node/lp/data/handler/processing/outfox.rs +++ b/nym-node/src/node/lp/data/handler/processing/outfox.rs @@ -1,6 +1,5 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::time::Instant; use nym_lp_data::{PipelinePayload, TimedPayload}; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; @@ -17,9 +16,9 @@ use crate::node::lp::data::{ pub(crate) fn process( shared_state: &SharedLpDataState, - outfox_packet: TimedPayload, + outfox_packet: TimedPayload, metadata: OutfoxMixMessage, -) -> Result, LpDataHandlerError> { +) -> Result, LpDataHandlerError> { let TimedPayload { data: outfox_bytes, timestamp: arrival_timestamp, @@ -45,9 +44,9 @@ pub(crate) fn process( pub(crate) fn process_forward( shared_gateway_state: &SharedGatewayLpDataState, - sphinx_packet: TimedPayload, + sphinx_packet: TimedPayload, metadata: ForwardOutfoxMessage, -) -> Result, LpDataHandlerError> { +) -> Result, LpDataHandlerError> { // SW TODO add bandwidth check here let node = shared_gateway_state diff --git a/nym-node/src/node/lp/data/handler/processing/sphinx.rs b/nym-node/src/node/lp/data/handler/processing/sphinx.rs index af732b116c..1bbe7eb17e 100644 --- a/nym-node/src/node/lp/data/handler/processing/sphinx.rs +++ b/nym-node/src/node/lp/data/handler/processing/sphinx.rs @@ -1,8 +1,6 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::time::Instant; - use nym_lp_data::{PipelinePayload, TimedPayload}; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_framing::processing::PacketProcessingError; @@ -19,9 +17,9 @@ use crate::node::lp::data::{ pub(crate) fn process( shared_state: &SharedLpDataState, - sphinx_packet: TimedPayload, + sphinx_packet: TimedPayload, metadata: SphinxMixMessage, -) -> Result, LpDataHandlerError> { +) -> Result, LpDataHandlerError> { let TimedPayload { data: sphinx_bytes, timestamp: arrival_timestamp, @@ -91,9 +89,9 @@ pub(crate) fn process( pub(crate) fn process_forward( shared_gateway_state: &SharedGatewayLpDataState, - sphinx_packet: TimedPayload, + sphinx_packet: TimedPayload, metadata: ForwardSphinxMessage, -) -> Result, LpDataHandlerError> { +) -> Result, LpDataHandlerError> { // SW TODO add bandwidth check here let node = shared_gateway_state diff --git a/nym-node/src/node/lp/data/shared.rs b/nym-node/src/node/lp/data/shared.rs index 6669755c24..9223f5ab9a 100644 --- a/nym-node/src/node/lp/data/shared.rs +++ b/nym-node/src/node/lp/data/shared.rs @@ -20,10 +20,7 @@ use nym_sphinx_params::SphinxKeyRotation; use nym_task::ShutdownToken; #[cfg(feature = "mix-sim")] use std::collections::HashMap; -use std::{ - net::SocketAddr, - time::{Duration, Instant}, -}; +use std::{net::SocketAddr, time::Duration}; use tracing::{Span, warn}; #[derive(Clone, Copy)] @@ -52,7 +49,7 @@ pub struct SharedLpDataState { pub(crate) replay_protection_filter: ReplayProtectionBloomfilters, - pub(crate) message_reconstructor: MessageReconstructor, + pub(crate) message_reconstructor: MessageReconstructor, pub(crate) routing_filter: NetworkRoutingFilter, @@ -168,7 +165,7 @@ impl SharedLpDataState { pub(super) fn update_processing_metrics( &self, processing_result: &Result< - PipelinePayload, NymNodeRoutingAddress>, + PipelinePayload, NymNodeRoutingAddress>, LpDataHandlerError, >, ) {