trait rework, removed Ts and NdId generic

This commit is contained in:
Simon Wicky
2026-05-28 14:09:36 +02:00
parent 187c6a51fd
commit fc79fe4738
41 changed files with 992 additions and 1660 deletions
+13 -21
View File
@@ -1,10 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Ts, Pkt, Opts, NdId>
where
Ts: Clone + PartialOrd,
Opts: InputOptions<NdId>,
{
pipeline: Box<dyn DynClientWrappingPipeline<Ts, Pkt, Opts, NdId>>,
pub struct ClientWrappingPipelineDriver<Pkt, Opts> {
pipeline: Box<dyn DynClientWrappingPipeline<Pkt, Opts>>,
packet_buffer: Vec<AddressedTimedData<Ts, Pkt, NdId>>,
packet_buffer: Vec<AddressedTimedData<Pkt>>,
input: mpsc::Receiver<(Vec<u8>, Opts)>,
input: mpsc::Receiver<(Vec<u8>, Opts, SocketAddr)>,
// Keeping a ref so we don't have problem about it being dropped
input_sender: mpsc::SyncSender<(Vec<u8>, Opts)>,
input_sender: mpsc::SyncSender<(Vec<u8>, Opts, SocketAddr)>,
}
impl<Ts, Pkt, Opts, NdId> ClientWrappingPipelineDriver<Ts, Pkt, Opts, NdId>
where
Ts: Clone + PartialOrd,
Opts: InputOptions<NdId>,
{
impl<Pkt, Opts> ClientWrappingPipelineDriver<Pkt, Opts> {
/// Create a new driver wrapping `pipeline`.
///
/// Internally allocates a zero-capacity `sync_channel` for input payloads.
pub fn new(pipeline: impl DynClientWrappingPipeline<Ts, Pkt, Opts, NdId> + 'static) -> Self {
pub fn new(pipeline: impl DynClientWrappingPipeline<Pkt, Opts> + '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<u8>, Opts)> {
pub fn input_sender(&self) -> mpsc::SyncSender<(Vec<u8>, 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)
+12 -13
View File
@@ -1,6 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<T, Ts, Opts, NdId> Reliability<Ts, Opts, NdId> for T
impl<T, Opts> Reliability<Opts> for T
where
T: NoOpReliability,
{
const OVERHEAD_SIZE: usize = 0;
fn reliable_encode(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
_: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
input: Option<PipelinePayload<Opts>>,
_: Instant,
) -> Vec<PipelinePayload<Opts>> {
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<T, Ts, Opts, NdId> RoutingSecurity<Ts, Opts, NdId> for T
impl<T, Opts> RoutingSecurity<Opts> for T
where
T: NoOpRoutingSecurity,
{
@@ -40,10 +42,7 @@ where
1
}
fn encrypt(
&mut self,
input: PipelinePayload<Ts, Opts, NdId>,
) -> PipelinePayload<Ts, Opts, NdId> {
fn encrypt(&mut self, input: PipelinePayload<Opts>) -> PipelinePayload<Opts> {
input
}
}
@@ -55,15 +54,15 @@ where
/// buffering.
pub trait NoOpObfuscation {}
impl<T, Ts, Opts, NdId> Obfuscation<Ts, Opts, NdId> for T
impl<T, Opts> Obfuscation<Opts> for T
where
T: NoOpObfuscation,
{
fn obfuscate(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
_: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
input: Option<PipelinePayload<Opts>>,
_: Instant,
) -> Vec<PipelinePayload<Opts>> {
input.map(|payload| vec![payload]).unwrap_or_default()
}
}
-23
View File
@@ -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<NdId>: 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;
}
+92 -146
View File
@@ -1,43 +1,38 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Ts, Opts, NdId>
where
Opts: InputOptions<NdId>,
{
/// - `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<Opts> {
fn chunked(
&mut self,
input: Vec<u8>,
input_options: Opts,
input: PipelinePayload<Opts>,
chunk_size: usize,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>>;
}
/// 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<Ts, Opts, NdId> {
pub trait Reliability<Opts> {
const OVERHEAD_SIZE: usize;
fn reliable_encode(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
input: Option<PipelinePayload<Opts>>,
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>>;
}
/// Trait for applying obfuscation (cover traffic, traffic shaping) to a timed payload.
@@ -62,10 +57,8 @@ pub trait Reliability<Ts, Opts, NdId> {
/// 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<Ts, Opts, NdId> {
/// - `Opts`: Opaque per-message metadata carried by the [`PipelinePayload`].
pub trait Obfuscation<Opts> {
/// Obfuscate `input` at the given `timestamp`.
///
/// # Parameters
@@ -78,17 +71,15 @@ pub trait Obfuscation<Ts, Opts, NdId> {
/// emitted at this tick.
fn obfuscate(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
input: Option<PipelinePayload<Opts>>,
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>>;
}
/// 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<Ts, Opts, NdId> {
/// - `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<Ts, Opts, NdId> {
pub trait RoutingSecurity<Opts> {
const OVERHEAD_SIZE: usize;
fn nb_frames(&self) -> usize {
1
}
fn encrypt(
&mut self,
input: PipelinePayload<Ts, Opts, NdId>,
) -> PipelinePayload<Ts, Opts, NdId>;
fn nb_frames(&self) -> usize;
fn encrypt(&mut self, input: PipelinePayload<Opts>) -> PipelinePayload<Opts>;
}
/// Full client-side outbound message pipeline.
@@ -118,109 +104,77 @@ pub trait RoutingSecurity<Ts, Opts, NdId> {
/// (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<Ts, Pkt, Opts, NdId>:
Chunking<Ts, Opts, NdId>
+ Reliability<Ts, Opts, NdId>
+ Obfuscation<Ts, Opts, NdId>
+ RoutingSecurity<Ts, Opts, NdId>
+ WireWrappingPipeline<Ts, Pkt, Opts, NdId>
where
Ts: Clone,
NdId: Clone,
Opts: InputOptions<NdId>,
pub trait ClientWrappingPipeline<Pkt, Opts>:
Chunking<Opts>
+ Reliability<Opts>
+ Obfuscation<Opts>
+ RoutingSecurity<Opts>
+ WireWrappingPipeline<Pkt, Opts>
{
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(<Self as RoutingSecurity<_, _, _>>::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(<Self as Reliability<_, _, _>>::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(<Self as RoutingSecurity<_>>::OVERHEAD_SIZE)
.expect("not enough room in a packet for routing security overhead")
.checked_sub(<Self as Reliability<_>>::OVERHEAD_SIZE)
.expect("not enough room in a packet for reliability overhead")
}
fn process(
&mut self,
input: Option<(Vec<u8>, Opts)>, // Optional to be able to tick the pipeline without input
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
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<u8>, Opts, SocketAddr)>, // Optional to be able to tick the pipeline without input
timestamp: Instant,
) -> Vec<AddressedTimedData<Pkt>> {
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<Ts, Pkt, Opts, NdId>`,
/// erasing the concrete pipeline type while keeping `Ts`, `Pkt`, `Opts`, and
/// `NdId` visible.
/// trait can be used as `dyn DynClientWrappingPipeline<Pkt, Opts>`, 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<Ts, Pkt, Opts, NdId> {
pub trait DynClientWrappingPipeline<Pkt, Opts> {
/// 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<u8>, Opts)>,
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>>;
input: Option<(Vec<u8>, Opts, SocketAddr)>,
timestamp: Instant,
) -> Vec<AddressedTimedData<Pkt>>;
}
impl<T, Ts, Pkt, Opts, NdId> DynClientWrappingPipeline<Ts, Pkt, Opts, NdId> for T
impl<T, Pkt, Opts> DynClientWrappingPipeline<Pkt, Opts> for T
where
Ts: Clone,
NdId: Clone,
Opts: InputOptions<NdId>,
T: ClientWrappingPipeline<Ts, Pkt, Opts, NdId>,
T: ClientWrappingPipeline<Pkt, Opts>,
{
fn packet_size(&self) -> usize {
WireWrappingPipeline::packet_size(self)
@@ -264,9 +214,9 @@ where
fn process(
&mut self,
input: Option<(Vec<u8>, Opts)>,
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
input: Option<(Vec<u8>, Opts, SocketAddr)>,
timestamp: Instant,
) -> Vec<AddressedTimedData<Pkt>> {
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<Ts, Pkt, Mk>: WireUnwrappingPipeline<Ts, Pkt, Mk>
where
Ts: Clone,
{
fn process_unwrapped(&mut self, payload: TimedPayload<Ts>, kind: Mk) -> Option<Vec<u8>>;
pub trait ClientUnwrappingPipeline<Pkt, Mk>: WireUnwrappingPipeline<Pkt, Mk> {
fn process_unwrapped(&mut self, payload: TimedPayload, kind: Mk) -> Option<Vec<u8>>;
fn unwrap(&mut self, input: Pkt, timestamp: Ts) -> Result<Option<Vec<u8>>, Self::Error> {
fn unwrap(&mut self, input: Pkt, timestamp: Instant) -> Result<Option<Vec<u8>>, Self::Error> {
Ok(self
.wire_unwrap(input, timestamp)?
.and_then(|(payload, kind)| self.process_unwrapped(payload, kind)))
+39 -52
View File
@@ -1,7 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<C, R, O, Rs, F, T> {
pub transport: T,
}
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Chunking<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
impl<Opts, C, R, O, Rs, F, T> Chunking<Opts> for Pipeline<C, R, O, Rs, F, T>
where
Opts: InputOptions<NdId>,
C: Chunking<Ts, Opts, NdId>,
C: Chunking<Opts>,
{
fn chunked(
&mut self,
input: Vec<u8>,
input_options: Opts,
input: PipelinePayload<Opts>,
chunk_size: usize,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
self.chunking
.chunked(input, input_options, chunk_size, timestamp)
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>> {
self.chunking.chunked(input, chunk_size, timestamp)
}
}
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Reliability<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
impl<Opts, C, R, O, Rs, F, T> Reliability<Opts> for Pipeline<C, R, O, Rs, F, T>
where
R: Reliability<Ts, Opts, NdId>,
R: Reliability<Opts>,
{
const OVERHEAD_SIZE: usize = R::OVERHEAD_SIZE;
fn reliable_encode(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
input: Option<PipelinePayload<Opts>>,
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>> {
self.reliability.reliable_encode(input, timestamp)
}
}
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Obfuscation<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
impl<Opts, C, R, O, Rs, F, T> Obfuscation<Opts> for Pipeline<C, R, O, Rs, F, T>
where
O: Obfuscation<Ts, Opts, NdId>,
O: Obfuscation<Opts>,
{
fn obfuscate(
&mut self,
input: Option<PipelinePayload<Ts, Opts, NdId>>,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
input: Option<PipelinePayload<Opts>>,
timestamp: Instant,
) -> Vec<PipelinePayload<Opts>> {
self.obfuscation.obfuscate(input, timestamp)
}
}
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> RoutingSecurity<Ts, Opts, NdId>
for Pipeline<C, R, O, Rs, F, T>
impl<Opts, C, R, O, Rs, F, T> RoutingSecurity<Opts> for Pipeline<C, R, O, Rs, F, T>
where
Rs: RoutingSecurity<Ts, Opts, NdId>,
Rs: RoutingSecurity<Opts>,
{
const OVERHEAD_SIZE: usize = Rs::OVERHEAD_SIZE;
@@ -92,69 +89,59 @@ where
self.security.nb_frames()
}
fn encrypt(
&mut self,
input: PipelinePayload<Ts, Opts, NdId>,
) -> PipelinePayload<Ts, Opts, NdId> {
fn encrypt(&mut self, input: PipelinePayload<Opts>) -> PipelinePayload<Opts> {
self.security.encrypt(input)
}
}
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Framing<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
impl<Opts, C, R, O, Rs, F, T> Framing<Opts> for Pipeline<C, R, O, Rs, F, T>
where
F: Framing<Ts, Opts, NdId>,
F: Framing<Opts>,
{
type Frame = F::Frame;
const OVERHEAD_SIZE: usize = F::OVERHEAD_SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, Opts, NdId>,
payload: PipelinePayload<Opts>,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, F::Frame, NdId>> {
) -> Vec<AddressedTimedData<F::Frame>> {
self.framing.to_frame(payload, frame_size)
}
}
impl<Ts, Pkt, NdId, C, R, O, Rs, F, T> Transport<Ts, Pkt, NdId> for Pipeline<C, R, O, Rs, F, T>
impl<Pkt, C, R, O, Rs, F, T> Transport<Pkt> for Pipeline<C, R, O, Rs, F, T>
where
T: Transport<Ts, Pkt, NdId>,
T: Transport<Pkt>,
{
type Frame = T::Frame;
const OVERHEAD_SIZE: usize = T::OVERHEAD_SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Ts, T::Frame, NdId>,
) -> AddressedTimedData<Ts, Pkt, NdId> {
frame: AddressedTimedData<T::Frame>,
) -> AddressedTimedData<Pkt> {
self.transport.to_transport_packet(frame)
}
}
impl<Ts, Pkt, Opts, NdId, C, R, O, Rs, F, T> WireWrappingPipeline<Ts, Pkt, Opts, NdId>
for Pipeline<C, R, O, Rs, F, T>
impl<Pkt, Opts, C, R, O, Rs, F, T> WireWrappingPipeline<Pkt, Opts> for Pipeline<C, R, O, Rs, F, T>
where
Ts: Clone,
NdId: Clone,
F: Framing<Ts, Opts, NdId>,
T: Transport<Ts, Pkt, NdId, Frame = F::Frame>,
F: Framing<Opts>,
T: Transport<Pkt, Frame = F::Frame>,
{
fn packet_size(&self) -> usize {
self.packet_size
}
}
impl<Ts, Pkt, Opts, NdId, C, R, O, Rs, F, T> ClientWrappingPipeline<Ts, Pkt, Opts, NdId>
for Pipeline<C, R, O, Rs, F, T>
impl<Pkt, Opts, C, R, O, Rs, F, T> ClientWrappingPipeline<Pkt, Opts> for Pipeline<C, R, O, Rs, F, T>
where
Ts: Clone,
NdId: Clone,
Opts: InputOptions<NdId>,
C: Chunking<Ts, Opts, NdId>,
R: Reliability<Ts, Opts, NdId>,
O: Obfuscation<Ts, Opts, NdId>,
Rs: RoutingSecurity<Ts, Opts, NdId>,
F: Framing<Ts, Opts, NdId>,
T: Transport<Ts, Pkt, NdId, Frame = F::Frame>,
C: Chunking<Opts>,
R: Reliability<Opts>,
O: Obfuscation<Opts>,
Rs: RoutingSecurity<Opts>,
F: Framing<Opts>,
T: Transport<Pkt, Frame = F::Frame>,
{
}
+15 -16
View File
@@ -1,6 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<T, Ts, Opts, NdId> Framing<Ts, Opts, NdId> for T
impl<T, Opts> Framing<Opts> for T
where
T: NoOpWireWrapper,
{
@@ -25,14 +27,14 @@ where
const OVERHEAD_SIZE: usize = 0;
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, Opts, NdId>,
payload: PipelinePayload<Opts>,
_: usize,
) -> Vec<AddressedTimedPayload<Ts, NdId>> {
) -> Vec<AddressedTimedPayload> {
vec![payload.into_addressed()]
}
}
impl<T, Ts, Pkt, NdId> Transport<Ts, Pkt, NdId> for T
impl<T, Pkt> Transport<Pkt> for T
where
T: NoOpWireWrapper,
Pkt: From<Vec<u8>>,
@@ -41,18 +43,16 @@ where
const OVERHEAD_SIZE: usize = 0;
fn to_transport_packet(
&mut self,
frame: AddressedTimedPayload<Ts, NdId>,
) -> AddressedTimedData<Ts, Pkt, NdId> {
frame: AddressedTimedPayload,
) -> AddressedTimedData<Pkt> {
frame.data_transform(|data| data.into())
}
}
impl<T, Ts, Pkt, Opts, NdId> WireWrappingPipeline<Ts, Pkt, Opts, NdId> for T
impl<T, Pkt, Opts> WireWrappingPipeline<Pkt, Opts> for T
where
T: NoOpWireWrapper,
Ts: Clone,
Pkt: From<Vec<u8>>,
NdId: Clone,
{
fn packet_size(&self) -> usize {
T::PACKET_SIZE
@@ -65,18 +65,18 @@ where
/// passes the payload through unchanged.
pub trait NoOpWireUnwrapper {}
impl<T, Ts, Mk> FramingUnwrap<Ts, Mk> for T
impl<T, Mk> FramingUnwrap<Mk> for T
where
T: NoOpWireUnwrapper,
Mk: Default,
{
type Frame = Vec<u8>;
fn frame_to_message(&mut self, frame: TimedPayload<Ts>) -> Option<(TimedPayload<Ts>, Mk)> {
fn frame_to_message(&mut self, frame: TimedPayload) -> Option<(TimedPayload, Mk)> {
Some((frame, Default::default()))
}
}
impl<T, Ts, Pkt> TransportUnwrap<Ts, Pkt> for T
impl<T, Pkt> TransportUnwrap<Pkt> for T
where
T: NoOpWireUnwrapper,
Pkt: Into<Vec<u8>>,
@@ -86,8 +86,8 @@ where
fn packet_to_frame(
&mut self,
packet: Pkt,
timestamp: Ts,
) -> Result<TimedPayload<Ts>, Self::Error> {
timestamp: Instant,
) -> Result<TimedPayload, Self::Error> {
Ok(TimedData {
timestamp,
data: packet.into(),
@@ -95,10 +95,9 @@ where
}
}
impl<T, Ts, Pkt, Mk> WireUnwrappingPipeline<Ts, Pkt, Mk> for T
impl<T, Pkt, Mk> WireUnwrappingPipeline<Pkt, Mk> for T
where
T: NoOpWireUnwrapper,
Ts: Clone,
Pkt: Into<Vec<u8>>,
Mk: Default,
{
+22 -41
View File
@@ -1,14 +1,14 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<TimedData<Ts, Self::Frame>>` of frames of the given size.
pub trait Framing<Ts, Opts, NdId> {
/// - `to_frame`: Splits the payload into a `Vec<AddressedTimedData<Self::Frame>>` of frames of the given size.
pub trait Framing<Opts> {
type Frame;
const OVERHEAD_SIZE: usize;
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, Opts, NdId>,
payload: PipelinePayload<Opts>,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, Self::Frame, NdId>>;
) -> Vec<AddressedTimedData<Self::Frame>>;
}
/// 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<Ts, Opts, NdId> {
/// # 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<Ts, Mk> {
pub trait FramingUnwrap<Mk> {
type Frame;
fn frame_to_message(
&mut self,
frame: TimedData<Ts, Self::Frame>,
) -> Option<(TimedPayload<Ts>, Mk)>;
fn frame_to_message(&mut self, frame: TimedData<Self::Frame>) -> 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<Ts, Mk> {
///
/// # Required Methods
/// - `to_transport_packet`: Wraps a frame into a transport packet.
pub trait Transport<Ts, Pkt, NdId> {
pub trait Transport<Pkt> {
type Frame;
const OVERHEAD_SIZE: usize;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Ts, Self::Frame, NdId>,
) -> AddressedTimedData<Ts, Pkt, NdId>;
frame: AddressedTimedData<Self::Frame>,
) -> AddressedTimedData<Pkt>;
}
/// 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<Ts, Pkt, NdId> {
/// # Required Methods
/// - `packet_to_frame`: Strips the transport layer from a packet, returning the inner frame
/// tagged with the given timestamp.
pub trait TransportUnwrap<Ts, Pkt> {
pub trait TransportUnwrap<Pkt> {
type Frame;
type Error;
fn packet_to_frame(
&mut self,
packet: Pkt,
timestamp: Ts,
) -> Result<TimedData<Ts, Self::Frame>, Self::Error>;
timestamp: Instant,
) -> Result<TimedData<Self::Frame>, Self::Error>;
}
/// Supertrait combining [`Framing`] and [`Transport`] into a reusable wire-wrapping layer.
@@ -99,10 +93,8 @@ pub trait TransportUnwrap<Ts, Pkt> {
/// 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<Ts, Pkt> {
/// # 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<Ts, Pkt, Opts, NdId>:
Transport<Ts, Pkt, NdId>
+ Framing<Ts, Opts, NdId, Frame = <Self as Transport<Ts, Pkt, NdId>>::Frame>
where
Ts: Clone,
NdId: Clone,
pub trait WireWrappingPipeline<Pkt, Opts>:
Transport<Pkt> + Framing<Opts, Frame = <Self as Transport<Pkt>>::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(
<Self as Transport<Ts, Pkt, NdId>>::OVERHEAD_SIZE
+ <Self as Framing<Ts, Opts, NdId>>::OVERHEAD_SIZE,
<Self as Transport<Pkt>>::OVERHEAD_SIZE + <Self as Framing<Opts>>::OVERHEAD_SIZE,
)
.expect("packet_size smaller than transport + framing overhead")
}
fn wire_wrap(
&mut self,
payload: PipelinePayload<Ts, Opts, NdId>,
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
fn wire_wrap(&mut self, payload: PipelinePayload<Opts>) -> Vec<AddressedTimedData<Pkt>> {
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<Ts, Pkt, Mk>:
TransportUnwrap<Ts, Pkt> + FramingUnwrap<Ts, Mk, Frame = <Self as TransportUnwrap<Ts, Pkt>>::Frame>
where
Ts: Clone,
pub trait WireUnwrappingPipeline<Pkt, Mk>:
TransportUnwrap<Pkt> + FramingUnwrap<Mk, Frame = <Self as TransportUnwrap<Pkt>>::Frame>
{
fn wire_unwrap(
&mut self,
input: Pkt,
timestamp: Ts,
) -> Result<Option<(TimedPayload<Ts>, Mk)>, Self::Error> {
timestamp: Instant,
) -> Result<Option<(TimedPayload, Mk)>, Self::Error> {
let frame = self.packet_to_frame(input, timestamp)?;
Ok(self.frame_to_message(frame))
}
@@ -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<FragmentHeader> 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<LpFrameAttributes> 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<u8> {
@@ -165,7 +133,6 @@ pub fn fragment_lp_message<R: rand::Rng>(
) -> Vec<Fragment> {
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<R: rand::Rng>(
id,
num_fragments,
i as u8,
message_kind,
))
}
@@ -1,13 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Ts>
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<Ts> MessageBuffer<Ts>
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<Ts, To>
where
Ts: PartialOrd + Debug + Clone + Add<To, Output = Ts>,
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<DashMap<FragmentHashKey, MessageBuffer<Ts>>>,
pub struct MessageReconstructor {
/// In-flight messages keyed on the random 64-bit fragment id.
in_flight_messages: Arc<DashMap<u64, MessageBuffer>>,
/// 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<Ts, To> MessageReconstructor<Ts, To>
where
Ts: PartialOrd + Debug + Clone + Add<To, Output = Ts>,
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<Result<LpFrame, MalformedLpPacketError>> {
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<Instant, Duration> {
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<Instant> = 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<Fragment> {
let encoded = LpFrame::new(inner_kind, content.to_vec()).to_bytes();
let frag_size = encoded.len().div_ceil(count as usize);
let frags: Vec<Fragment> = 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::<u64>::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::<u64>::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::<u64>::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::<u64>::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::<u64>::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::<u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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::<u64, u64>::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]);
}
+38 -45
View File
@@ -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<Ts, Vec<u8>>`.
//! [`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<Vec<u8>>`.
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<Ts> = TimedData<Ts, Vec<u8>>;
pub type TimedPayload = TimedData<Vec<u8>>;
/// Convenience alias for [`AddressedTimedData`] when the payload is a raw byte buffer.
pub type AddressedTimedPayload<Ts, NdId> = AddressedTimedData<Ts, Vec<u8>, NdId>;
pub type AddressedTimedPayload = AddressedTimedData<Vec<u8>>;
/// Convenience alias for [`PipelineData`] when the payload is a raw byte buffer.
pub type PipelinePayload<Ts, Opts, NdId> = PipelineData<Ts, Vec<u8>, Opts, NdId>;
pub type PipelinePayload<Opts, NdId = SocketAddr> = PipelineData<Vec<u8>, 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<Ts, D> {
pub timestamp: Ts,
pub struct TimedData<D> {
pub timestamp: Instant,
pub data: D,
}
impl<Ts, D> TimedData<Ts, D> {
pub fn new(timestamp: Ts, data: D) -> Self {
impl<D> TimedData<D> {
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<F, Nd>(self, mut op: F) -> TimedData<Ts, Nd>
pub fn data_transform<F, Nd>(self, mut op: F) -> TimedData<Nd>
where
F: FnMut(D) -> Nd,
{
@@ -62,14 +63,11 @@ impl<Ts, D> TimedData<Ts, D> {
}
}
/// Apply `op` to the timestamp component, leaving the data unchanged.
pub fn ts_transform<F>(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<Ts, D> TimedData<Ts, D> {
/// [`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<Ts, D> TimedData<Ts, D> {
/// [`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<Ts, D, Opts, NdId> {
pub data: TimedData<Ts, D>,
pub struct PipelineData<D, Opts, NdId = SocketAddr> {
pub data: TimedData<D>,
pub options: Opts,
pub dst: NdId,
}
impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
impl<D, Opts, NdId> PipelineData<D, Opts, NdId> {
/// 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<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
/// destination unchanged.
///
/// `Nd` can differ from `D`, so this also acts as a type transform.
pub fn data_transform<F, Nd>(self, op: F) -> PipelineData<Ts, Nd, Opts, NdId>
pub fn data_transform<F, Nd>(self, op: F) -> PipelineData<Nd, Opts, NdId>
where
F: FnMut(D) -> Nd,
{
@@ -125,13 +121,10 @@ impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
}
}
/// Apply `op` to the timestamp component, leaving the data unchanged.
pub fn ts_transform<F>(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<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
/// destination unchanged.
///
/// `No` can differ from `O`, so this also acts as a type transform.
pub fn options_transform<F, No>(self, mut op: F) -> PipelineData<Ts, D, No, NdId>
pub fn options_transform<F, No>(self, mut op: F) -> PipelineData<D, No, NdId>
where
F: FnMut(Opts) -> No,
{
@@ -153,7 +146,7 @@ impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
}
/// Set a new destination
pub fn with_dst<NewNdId>(self, new_dst: NewNdId) -> PipelineData<Ts, D, Opts, NewNdId> {
pub fn with_dst<NewNdId>(self, new_dst: NewNdId) -> PipelineData<D, Opts, NewNdId> {
PipelineData {
data: self.data,
options: self.options,
@@ -162,7 +155,7 @@ impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
}
/// Drop the pipeline options, producing a plain addressed payload.
pub fn into_addressed(self) -> AddressedTimedData<Ts, D, NdId> {
pub fn into_addressed(self) -> AddressedTimedData<D, NdId> {
AddressedTimedData {
data: self.data,
options: (),
@@ -173,11 +166,11 @@ impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
/// Convenience alias for [`PipelineData`] when no per-message pipeline options
/// are needed. Avoids duplicating the pipeline data structure.
pub type AddressedTimedData<Ts, D, NdId> = PipelineData<Ts, D, (), NdId>;
pub type AddressedTimedData<D, NdId = SocketAddr> = PipelineData<D, (), NdId>;
impl<Ts, D, NdId> AddressedTimedData<Ts, D, NdId> {
impl<D, NdId> AddressedTimedData<D, NdId> {
/// 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<Ts, D, NdId> AddressedTimedData<Ts, D, NdId> {
}
/// Convert a [`AddressedTimedData`] into a [`PipelineData`] with the provided options.
pub fn with_options<Opts>(self, opts: Opts) -> PipelineData<Ts, D, Opts, NdId> {
pub fn with_options<Opts>(self, opts: Opts) -> PipelineData<D, Opts, NdId> {
PipelineData {
data: self.data,
options: opts,
+11 -14
View File
@@ -1,6 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Ts, Pkt, NdId>:
WireUnwrappingPipeline<Ts, Pkt, <Self as NymNodeProcessingPipeline<Ts, Pkt, NdId>>::MessageKind>
+ WireWrappingPipeline<Ts, Pkt, <Self as NymNodeProcessingPipeline<Ts, Pkt, NdId>>::Options, NdId>
where
Ts: Clone,
NdId: Clone,
pub trait NymNodeProcessingPipeline<Pkt>:
WireUnwrappingPipeline<Pkt, <Self as NymNodeProcessingPipeline<Pkt>>::MessageKind>
+ WireWrappingPipeline<Pkt, <Self as NymNodeProcessingPipeline<Pkt>>::Options>
{
type Options;
type MessageKind;
@@ -49,16 +46,16 @@ where
fn mix(
&mut self,
message_kind: Self::MessageKind,
payload: TimedPayload<Ts>,
timestamp: Ts,
) -> Vec<PipelinePayload<Ts, Self::Options, NdId>>;
payload: TimedPayload,
timestamp: Instant,
) -> Vec<PipelinePayload<Self::Options>>;
fn process(
&mut self,
input: Pkt,
timestamp: Ts,
) -> Result<Vec<AddressedTimedData<Ts, Pkt, NdId>>, Self::Error> {
let Some((payload, kind)) = self.wire_unwrap(input, timestamp.clone())? else {
timestamp: Instant,
) -> Result<Vec<AddressedTimedData<Pkt>>, Self::Error> {
let Some((payload, kind)) = self.wire_unwrap(input, timestamp)? else {
return Ok(Vec::new());
};
let mixed = self.mix(kind, payload, timestamp);
@@ -1,6 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<u8> 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<Ts> = PipelinePayload<Ts, BasicOptions, u8>;
pub type BasicPipelinePayload = PipelinePayload<()>;
pub struct MockChunking;
impl<Ts> Chunking<Ts, BasicOptions, u8> for MockChunking
where
Ts: Clone,
{
impl Chunking<()> for MockChunking {
fn chunked(
&mut self,
input: Vec<u8>,
input_options: BasicOptions,
input: BasicPipelinePayload,
chunk_size: usize,
timestamp: Ts,
) -> Vec<BasicPipelinePayload<Ts>> {
timestamp: Instant,
) -> Vec<BasicPipelinePayload> {
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<Ts> Reliability<Ts, BasicOptions, u8> for MockReliability {
impl Reliability<()> for MockReliability {
const OVERHEAD_SIZE: usize = Self::HEADER.len();
fn reliable_encode(
&mut self,
input: Option<BasicPipelinePayload<Ts>>,
_: Ts,
) -> Vec<BasicPipelinePayload<Ts>> {
input: Option<BasicPipelinePayload>,
_: Instant,
) -> Vec<BasicPipelinePayload> {
input
.map(|data| {
vec![data.data_transform(|data| {
@@ -102,14 +66,14 @@ impl MockSphinxSecurity {
const HEADER: &[u8; 8] = b"0SPHINX0";
}
impl<Ts> RoutingSecurity<Ts, BasicOptions, u8> 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<Ts>) -> BasicPipelinePayload<Ts> {
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<Ts> RoutingSecurity<Ts, BasicOptions, u8> for MockSphinxSecurity {
pub struct KekwObfuscation;
impl Obfuscation<u32, BasicOptions, u8> for KekwObfuscation {
impl Obfuscation<()> for KekwObfuscation {
fn obfuscate(
&mut self,
input: Option<BasicPipelinePayload<u32>>,
_timestamp: u32,
) -> Vec<BasicPipelinePayload<u32>> {
input: Option<BasicPipelinePayload>,
_timestamp: Instant,
) -> Vec<BasicPipelinePayload> {
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<u32, BasicOptions, u8> for ReallyOddObfuscation {
fn obfuscate(
&mut self,
input: Option<BasicPipelinePayload<u32>>,
_timestamp: u32,
) -> Vec<BasicPipelinePayload<u32>> {
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<Ts> Framing<Ts, BasicOptions, u8> 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<Ts, BasicOptions, u8>,
input: BasicPipelinePayload,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, LpFrame, u8>> {
) -> Vec<AddressedTimedData<LpFrame>> {
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<Ts> Transport<Ts, LpPacket, u8> for MockLpTransport {
impl Transport<LpPacket> for MockLpTransport {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
fn to_transport_packet(
&mut self,
input: AddressedTimedData<Ts, Self::Frame, u8>,
) -> AddressedTimedData<Ts, LpPacket, u8> {
input: AddressedTimedData<Self::Frame>,
) -> AddressedTimedData<LpPacket> {
AddressedTimedData::new_addressed(
input.data.timestamp,
LpPacket::new(LpHeader::new(7, 7, 7), input.data.data),
+3 -1
View File
@@ -1,6 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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());
}
+32 -45
View File
@@ -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<Ts: Clone + PartialOrd + Debug + Send>: 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<Ts: Clone + PartialOrd + Debug + Send>: Send {
///
/// [`SimplePacket`]: crate::packet::simple::SimplePacket
/// [`SimMixPacket`]: crate::packet::sphinx::SimMixPacket
pub trait ProcessingClient<Ts, SndPkt, RcvPkt = SndPkt>: Send {
pub trait ProcessingClient<SndPkt, RcvPkt = SndPkt>: Send {
/// Wrap `input` into one or more outbound packets addressed toward `dst`.
fn process(
&mut self,
input: Vec<u8>,
dst: ClientId,
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, SndPkt, NodeId>>;
timestamp: Instant,
) -> Vec<AddressedTimedData<SndPkt>>;
/// 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<Option<Vec<u8>>>;
fn unwrap(&mut self, input: RcvPkt, timestamp: Instant) -> anyhow::Result<Option<Vec<u8>>>;
}
/// Shared UDP transport layer for simulated clients.
@@ -62,7 +62,7 @@ pub trait ProcessingClient<Ts, SndPkt, RcvPkt = SndPkt>: 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<Ts, Pc, SndPkt, RcvPkt = SndPkt> {
pub struct BaseClient<Pc, SndPkt, RcvPkt = SndPkt> {
/// 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<Ts, Pc, SndPkt, RcvPkt = SndPkt> {
/// 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<Directory>,
/// 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<AddressedTimedData<Ts, SndPkt, NodeId>>,
outgoing_queue: Vec<AddressedTimedData<SndPkt>>,
/// Concrete client-processing implementation invoked from each tick phase.
processing_client: Pc,
@@ -85,13 +83,12 @@ pub struct BaseClient<Ts, Pc, SndPkt, RcvPkt = SndPkt> {
_marker: std::marker::PhantomData<RcvPkt>,
}
impl<Ts, Pc, SndPkt, RcvPkt> BaseClient<Ts, Pc, SndPkt, RcvPkt> {
impl<Pc, SndPkt, RcvPkt> BaseClient<Pc, SndPkt, RcvPkt> {
/// Bind both UDP sockets to the given addresses.
pub(crate) fn with_pipeline(
client_id: ClientId,
mixnet_address: SocketAddr,
app_address: SocketAddr,
directory: Arc<Directory>,
processing_client: Pc,
) -> anyhow::Result<Self> {
let mix_socket = UdpSocket::bind(mixnet_address)?;
@@ -104,7 +101,6 @@ impl<Ts, Pc, SndPkt, RcvPkt> BaseClient<Ts, Pc, SndPkt, RcvPkt> {
id: client_id,
mix_socket,
app_socket,
directory,
outgoing_queue: Vec::new(),
processing_client,
_marker: std::marker::PhantomData,
@@ -112,29 +108,24 @@ impl<Ts, Pc, SndPkt, RcvPkt> BaseClient<Ts, Pc, SndPkt, RcvPkt> {
}
}
impl<Ts, Pc, SndPkt, RcvPkt> BaseClient<Ts, Pc, SndPkt, RcvPkt>
impl<Pc, SndPkt, RcvPkt> BaseClient<Pc, SndPkt, RcvPkt>
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<Ts, Pc, SndPkt, RcvPkt> MixSimClient<Ts> for BaseClient<Ts, Pc, SndPkt, RcvPkt>
impl<Pc, SndPkt, RcvPkt> MixSimClient for BaseClient<Pc, SndPkt, RcvPkt>
where
Ts: Clone + PartialOrd + Debug + Send,
SndPkt: WirePacketFormat + Debug + Send,
RcvPkt: WirePacketFormat + Debug + Send,
Pc: ProcessingClient<Ts, SndPkt, RcvPkt>,
Pc: ProcessingClient<SndPkt, RcvPkt>,
{
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<Ts, Pc, SndPkt, RcvPkt> BaseClient<Ts, Pc, SndPkt, RcvPkt>
impl<Pc, SndPkt, RcvPkt> BaseClient<Pc, SndPkt, RcvPkt>
where
Ts: Clone + PartialOrd + Debug + Send,
SndPkt: WirePacketFormat + Debug + Send,
RcvPkt: WirePacketFormat + Debug + Send,
Pc: ProcessingClient<Ts, SndPkt, RcvPkt>,
Pc: ProcessingClient<SndPkt, RcvPkt>,
{
/// **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: {:?}",
+51 -115
View File
@@ -1,24 +1,19 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<R> = BaseClient<Instant, SimNymProcesssingClient<R>, EncryptedLpPacket>;
pub type SimNymClient<R> = BaseClient<SimNymProcesssingClient<R>, EncryptedLpPacket>;
impl<R: Rng + Send> SimNymClient<R> {
/// Bind both UDP sockets and return a new client.
@@ -91,36 +87,18 @@ impl<R: Rng + Send> SimNymClient<R> {
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<SocketAddr> 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<R: Rng> {
@@ -128,13 +106,13 @@ pub struct SimNymProcesssingClient<R: Rng> {
unwrapper: NymNodeUnwrappingPipeline,
}
impl<R: Rng + Send> ProcessingClient<Instant, EncryptedLpPacket> for SimNymProcesssingClient<R> {
impl<R: Rng + Send> ProcessingClient<EncryptedLpPacket> for SimNymProcesssingClient<R> {
fn process(
&mut self,
input: Vec<u8>,
dst: ClientId,
timestamp: Instant,
) -> Vec<AddressedTimedData<Instant, EncryptedLpPacket, NodeId>> {
) -> Vec<AddressedTimedData<EncryptedLpPacket>> {
if input.is_empty() {
return Vec::new();
}
@@ -143,32 +121,17 @@ impl<R: Rng + Send> ProcessingClient<Instant, EncryptedLpPacket> 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<R: Rng + Send> ProcessingClient<Instant, EncryptedLpPacket> 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<R: Rng> {
rng: R,
}
pub(crate) type NymNodePipelinePayload =
PipelinePayload<Instant, SimNymClientInputOptions, SocketAddr>;
impl<R: Rng> Chunking<Instant, SimNymClientInputOptions, SocketAddr>
for SimNymClientWrappingPipeline<R>
{
impl<R: Rng> Chunking<SimNymClientInputOptions> for SimNymClientWrappingPipeline<R> {
/// 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<u8>,
options: SimNymClientInputOptions,
input: PipelinePayload<SimNymClientInputOptions>,
chunk_size: usize,
timestamp: Instant,
) -> Vec<NymNodePipelinePayload> {
let fragments = NymMessage::new_plain(input)
) -> Vec<PipelinePayload<SimNymClientInputOptions>> {
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<R: Rng> Chunking<Instant, SimNymClientInputOptions, SocketAddr>
impl<R: Rng> NoOpReliability for SimNymClientWrappingPipeline<R> {}
impl<R: Rng> NoOpObfuscation for SimNymClientWrappingPipeline<R> {}
impl<R: Rng> RoutingSecurity<Instant, SimNymClientInputOptions, SocketAddr>
for SimNymClientWrappingPipeline<R>
{
impl<R: Rng> RoutingSecurity<SimNymClientInputOptions> for SimNymClientWrappingPipeline<R> {
// 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<R: Rng> RoutingSecurity<Instant, SimNymClientInputOptions, SocketAddr>
/// 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<SimNymClientInputOptions>,
) -> PipelinePayload<SimNymClientInputOptions> {
let route = self.directory.random_route(3, &mut self.rng);
let first_mix_hop = route[0].id;
@@ -261,19 +214,17 @@ impl<R: Rng> RoutingSecurity<Instant, SimNymClientInputOptions, SocketAddr>
.collect::<Vec<_>>();
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::<Vec<_>>();
let plaintext_size = (<Self as WireWrappingPipeline<
Instant,
EncryptedLpPacket,
SimNymClientInputOptions,
SocketAddr,
>>::packet_size(self)
- <Self as Framing<Instant, SimNymClientInputOptions, SocketAddr>>::OVERHEAD_SIZE
- <Self as Transport<Instant, EncryptedLpPacket, SocketAddr>>::OVERHEAD_SIZE)
- <Self as Framing<SimNymClientInputOptions>>::OVERHEAD_SIZE
- <Self as Transport<EncryptedLpPacket>>::OVERHEAD_SIZE)
* self.nb_frames()
- <Self as RoutingSecurity<Instant, _, _>>::OVERHEAD_SIZE;
- <Self as RoutingSecurity<_>>::OVERHEAD_SIZE;
let packet_builder = SphinxPacketBuilder::new()
.with_payload_size(plaintext_size + nym_sphinx::PAYLOAD_OVERHEAD_SIZE);
@@ -300,26 +251,24 @@ impl<R: Rng> RoutingSecurity<Instant, SimNymClientInputOptions, SocketAddr>
packet.to_bytes(),
);
NymNodePipelinePayload::new(
PipelinePayload::new(
input.data.timestamp,
framed_packet.to_bytes(),
input.options,
input.options.first_hop,
input.dst,
)
}
}
impl<R: Rng> Framing<Instant, SimNymClientInputOptions, SocketAddr>
for SimNymClientWrappingPipeline<R>
{
impl<R: Rng> Framing<SimNymClientInputOptions> for SimNymClientWrappingPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
fn to_frame(
&mut self,
payload: NymNodePipelinePayload,
payload: PipelinePayload<SimNymClientInputOptions>,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, Self::Frame, SocketAddr>> {
) -> Vec<AddressedTimedData<Self::Frame>> {
// 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<R: Rng> Framing<Instant, SimNymClientInputOptions, SocketAddr>
}
}
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for SimNymClientWrappingPipeline<R> {
impl<R: Rng> Transport<EncryptedLpPacket> for SimNymClientWrappingPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Instant, Self::Frame, SocketAddr>,
) -> AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr> {
frame: AddressedTimedData<Self::Frame>,
) -> AddressedTimedData<EncryptedLpPacket> {
frame.data_transform(|f| LpPacket::new(LpHeader::new(0, 0, version::CURRENT), f).encode())
}
}
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, SimNymClientInputOptions, SocketAddr>
impl<R: Rng> WireWrappingPipeline<EncryptedLpPacket, SimNymClientInputOptions>
for SimNymClientWrappingPipeline<R>
{
fn packet_size(&self) -> usize {
@@ -352,8 +301,7 @@ impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, SimNymClientInputO
}
}
impl<R: Rng>
ClientWrappingPipeline<Instant, EncryptedLpPacket, SimNymClientInputOptions, SocketAddr>
impl<R: Rng> ClientWrappingPipeline<EncryptedLpPacket, SimNymClientInputOptions>
for SimNymClientWrappingPipeline<R>
{
}
@@ -366,14 +314,14 @@ impl<R: Rng>
// 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<Instant, Duration>,
message_reconstructor: MessageReconstructor,
sphinx_message_reconstructor: SphinxMessageReconstructor,
sphinx_secret_key: x25519::PrivateKey,
}
impl TransportUnwrap<Instant, EncryptedLpPacket> for NymNodeUnwrappingPipeline {
impl TransportUnwrap<EncryptedLpPacket> for NymNodeUnwrappingPipeline {
type Frame = LpFrame;
type Error = MalformedLpPacketError;
@@ -381,19 +329,16 @@ impl TransportUnwrap<Instant, EncryptedLpPacket> for NymNodeUnwrappingPipeline {
&mut self,
packet: EncryptedLpPacket,
timestamp: Instant,
) -> Result<TimedData<Instant, Self::Frame>, Self::Error> {
) -> Result<TimedData<Self::Frame>, Self::Error> {
let lp = LpPacket::decode(packet)?;
Ok(TimedData::new(timestamp, lp.into_frame()))
}
}
impl FramingUnwrap<Instant, SphinxMessage> for NymNodeUnwrappingPipeline {
impl FramingUnwrap<()> for NymNodeUnwrappingPipeline {
type Frame = LpFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Instant, Self::Frame>,
) -> Option<(TimedPayload<Instant>, SphinxMessage)> {
fn frame_to_message(&mut self, frame: TimedData<Self::Frame>) -> 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<Instant, SphinxMessage> for NymNodeUnwrappingPipeline {
};
Some((
TimedPayload::new(frame.timestamp, recovered_message.content.to_vec()),
SphinxMessage, // SW take that from the attributes
(),
))
}
}
impl WireUnwrappingPipeline<Instant, EncryptedLpPacket, SphinxMessage>
for NymNodeUnwrappingPipeline
{
}
impl WireUnwrappingPipeline<EncryptedLpPacket, ()> for NymNodeUnwrappingPipeline {}
impl ClientUnwrappingPipeline<Instant, EncryptedLpPacket, SphinxMessage>
for NymNodeUnwrappingPipeline
{
fn process_unwrapped(
&mut self,
timed_plaintext: TimedPayload<Instant>,
_kind: SphinxMessage,
) -> Option<Vec<u8>> {
impl ClientUnwrappingPipeline<EncryptedLpPacket, ()> for NymNodeUnwrappingPipeline {
fn process_unwrapped(&mut self, timed_plaintext: TimedPayload, _: ()) -> Option<Vec<u8>> {
let sphinx_packet = SphinxPacket::from_bytes(&timed_plaintext.data)
.inspect_err(|e| tracing::warn!("Impossible to recover sphinx packet : {e}"))
.ok()?;
+49 -91
View File
@@ -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<Ts> = BaseClient<Ts, SimpleProcessingClient, SimplePacket>;
pub type SimpleClient = BaseClient<SimpleProcessingClient, SimplePacket>;
impl<Ts> SimpleClient<Ts> {
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<Directory>) -> anyhow::Result<Self> {
// 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<Ts> SimpleClient<Ts> {
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<NodeId> 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<Ts: Clone> ProcessingClient<Ts, SimplePacket> for SimpleProcessingClient {
impl ProcessingClient<SimplePacket> for SimpleProcessingClient {
fn process(
&mut self,
input: Vec<u8>,
_: ClientId,
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, SimplePacket, NodeId>> {
timestamp: Instant,
) -> Vec<AddressedTimedData<SimplePacket>> {
self.wrapper
.process(Some((input, SimpleInputOptions)), timestamp)
.process(Some((input, (), self.first_hop)), timestamp)
}
fn unwrap(&mut self, input: SimplePacket, timestamp: Ts) -> anyhow::Result<Option<Vec<u8>>> {
fn unwrap(
&mut self,
input: SimplePacket,
timestamp: Instant,
) -> anyhow::Result<Option<Vec<u8>>> {
self.unwrapper.unwrap(input, timestamp)
}
}
@@ -134,15 +115,13 @@ impl<Ts: Clone> ProcessingClient<Ts, SimplePacket> for SimpleProcessingClient {
/// impl in `nym_lp_data`.
pub struct SimpleClientWrappingPipeline(SimpleWireWrapper);
pub(crate) type SimplePipelinePayload<Ts> = PipelinePayload<Ts, SimpleInputOptions, NodeId>;
impl Default for SimpleClientWrappingPipeline {
fn default() -> Self {
Self(SimpleWireWrapper)
}
}
impl<Ts: Clone> Chunking<Ts, SimpleInputOptions, NodeId> 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<Ts: Clone> Chunking<Ts, SimpleInputOptions, NodeId> for SimpleClientWrappin
/// strip trailing zeros.
fn chunked(
&mut self,
mut input: Vec<u8>,
options: SimpleInputOptions,
input: AddressedTimedPayload,
chunk_size: usize,
timestamp: Ts,
) -> Vec<SimplePipelinePayload<Ts>> {
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<AddressedTimedPayload> {
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<Ts: Clone> Framing<Ts, SimpleInputOptions, NodeId> for SimpleClientWrappingPipeline {
impl Framing<()> for SimpleClientWrappingPipeline {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Framing<Ts, _, _>>::OVERHEAD_SIZE;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Framing<_>>::OVERHEAD_SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, SimpleInputOptions, NodeId>,
payload: AddressedTimedPayload,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, SimpleFrame, NodeId>> {
) -> Vec<AddressedTimedData<SimpleFrame>> {
self.0.to_frame(payload, frame_size)
}
}
// Delegation to SimpleWireWrapper
impl<Ts: Clone> Transport<Ts, SimplePacket, NodeId> for SimpleClientWrappingPipeline {
impl Transport<SimplePacket> for SimpleClientWrappingPipeline {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Transport<Ts, _, _>>::OVERHEAD_SIZE;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Transport<_>>::OVERHEAD_SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Ts, SimpleFrame, NodeId>,
) -> AddressedTimedData<Ts, SimplePacket, NodeId> {
frame: AddressedTimedData<SimpleFrame>,
) -> AddressedTimedData<SimplePacket> {
self.0.to_transport_packet(frame)
}
}
// Delegation to SimpleWireWrapper
impl<Ts: Clone> WireWrappingPipeline<Ts, SimplePacket, SimpleInputOptions, NodeId>
for SimpleClientWrappingPipeline
{
impl WireWrappingPipeline<SimplePacket, ()> for SimpleClientWrappingPipeline {
fn packet_size(&self) -> usize {
<SimpleWireWrapper as WireWrappingPipeline<Ts, _, _, _>>::packet_size(&self.0)
<SimpleWireWrapper as WireWrappingPipeline<_, _>>::packet_size(&self.0)
}
}
impl<Ts: Clone> ClientWrappingPipeline<Ts, SimplePacket, SimpleInputOptions, NodeId>
for SimpleClientWrappingPipeline
{
}
impl ClientWrappingPipeline<SimplePacket, ()> for SimpleClientWrappingPipeline {}
// ─────────────────────────────────────────────────────────────────────────────
/// Unwrapping pipeline for [`SimpleClient`]: strips the frame header and
@@ -230,39 +197,30 @@ impl Default for SimpleClientUnwrapping {
}
// Delegation to SimpleWireUnwrapper
impl<Ts> FramingUnwrap<Ts, SimpleMessage> for SimpleClientUnwrapping {
impl FramingUnwrap<()> for SimpleClientUnwrapping {
type Frame = SimpleFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Ts, SimpleFrame>,
) -> Option<(TimedPayload<Ts>, SimpleMessage)> {
fn frame_to_message(&mut self, frame: TimedData<SimpleFrame>) -> Option<(TimedPayload, ())> {
self.0.frame_to_message(frame)
}
}
// Delegation to SimpleWireUnwrapper
impl<Ts: Clone> TransportUnwrap<Ts, SimplePacket> for SimpleClientUnwrapping {
impl TransportUnwrap<SimplePacket> for SimpleClientUnwrapping {
type Frame = SimpleFrame;
type Error = anyhow::Error;
fn packet_to_frame(
&mut self,
packet: SimplePacket,
timestamp: Ts,
) -> anyhow::Result<TimedData<Ts, SimpleFrame>> {
timestamp: Instant,
) -> anyhow::Result<TimedData<SimpleFrame>> {
self.0.packet_to_frame(packet, timestamp)
}
}
impl<Ts: Clone> WireUnwrappingPipeline<Ts, SimplePacket, SimpleMessage> for SimpleClientUnwrapping {}
impl WireUnwrappingPipeline<SimplePacket, ()> for SimpleClientUnwrapping {}
impl<Ts: Clone> ClientUnwrappingPipeline<Ts, SimplePacket, SimpleMessage>
for SimpleClientUnwrapping
{
fn process_unwrapped(
&mut self,
payload: TimedPayload<Ts>,
_kind: SimpleMessage,
) -> Option<Vec<u8>> {
impl ClientUnwrappingPipeline<SimplePacket, ()> for SimpleClientUnwrapping {
fn process_unwrapped(&mut self, payload: TimedPayload, _: ()) -> Option<Vec<u8>> {
let mut data = payload.data;
if let Some(pos) = data.iter().rposition(|&b| b == 1) {
data.truncate(pos);
+79 -131
View File
@@ -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<Ts, R> = BaseClient<Ts, SphinxProcessingClient<Ts, R>, SimMixPacket, Vec<u8>>;
pub type SphinxClient<R> = BaseClient<SphinxProcessingClient<R>, SimMixPacket, Vec<u8>>;
impl<Ts: Clone + GenerateDelay + PartialOrd + Send, R: Rng + Clone + Send> SphinxClient<Ts, R> {
impl<R: Rng + Clone + Send> SphinxClient<R> {
/// Bind both UDP sockets and return a new client.
///
/// # Errors
@@ -61,23 +61,23 @@ impl<Ts: Clone + GenerateDelay + PartialOrd + Send, R: Rng + Clone + Send> Sphin
pub fn new(
topology_client: TopologyClient,
directory: Arc<Directory>,
current_timestamp: Ts,
current_timestamp: Instant,
rng: R,
) -> anyhow::Result<Self> {
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<Ts: Clone + GenerateDelay + PartialOrd + Send, R: Rng + Clone + Send> 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<NodeId> 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<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> {
wrapper: SphinxClientWrappingPipeline<Ts, R>,
pub struct SphinxProcessingClient<R: Rng> {
wrapper: SphinxClientWrappingPipeline<R>,
unwrapper: SphinxClientUnwrapping,
}
impl<Ts: Clone + GenerateDelay + PartialOrd + Send, R: Rng + Send>
ProcessingClient<Ts, SimMixPacket, Vec<u8>> for SphinxProcessingClient<Ts, R>
{
impl<R: Rng + Send> ProcessingClient<SimMixPacket, Vec<u8>> for SphinxProcessingClient<R> {
fn process(
&mut self,
input: Vec<u8>,
dst: ClientId,
timestamp: Ts,
) -> Vec<AddressedTimedData<Ts, SimMixPacket, NodeId>> {
timestamp: Instant,
) -> Vec<AddressedTimedData<SimMixPacket>> {
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<u8>, timestamp: Ts) -> anyhow::Result<Option<Vec<u8>>> {
fn unwrap(&mut self, input: Vec<u8>, timestamp: Instant) -> anyhow::Result<Option<Vec<u8>>> {
Ok(self.unwrapper.unwrap(input, timestamp)?)
}
}
@@ -162,9 +143,9 @@ impl<Ts: Clone + GenerateDelay + PartialOrd + Send, R: Rng + Send>
/// 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<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> {
pub struct SphinxClientWrappingPipeline<R: Rng> {
/// Poisson cover traffic generator providing the [`Obfuscation`] stage.
cover_traffic: PoissonCoverTraffic<Ts, R>,
cover_traffic: PoissonCoverTraffic<R>,
/// SURB-ACK reliability layer providing the [`Reliability`] stage.
reliability: SurbAcksReliability<R>,
/// Shared routing table; used to sample the 3-hop Sphinx route in `encrypt`.
@@ -173,70 +154,55 @@ pub struct SphinxClientWrappingPipeline<Ts: Clone + GenerateDelay + PartialOrd,
rng: R,
}
pub(crate) type SphinxPipelinePayload<Ts> = PipelinePayload<Ts, SphinxInputOptions, NodeId>;
impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> Chunking<Ts, SphinxInputOptions, NodeId>
for SphinxClientWrappingPipeline<Ts, R>
{
impl<R: Rng> Chunking<SphinxInputOptions> for SphinxClientWrappingPipeline<R> {
fn chunked(
&mut self,
input: Vec<u8>,
options: SphinxInputOptions,
input: PipelinePayload<SphinxInputOptions>,
chunk_size: usize,
timestamp: Ts,
) -> Vec<SphinxPipelinePayload<Ts>> {
if input.is_empty() {
timestamp: Instant,
) -> Vec<PipelinePayload<SphinxInputOptions>> {
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<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> Reliability<Ts, SphinxInputOptions, NodeId>
for SphinxClientWrappingPipeline<Ts, R>
{
impl<R: Rng> Reliability<SphinxInputOptions> for SphinxClientWrappingPipeline<R> {
const OVERHEAD_SIZE: usize =
<SurbAcksReliability<R> as Reliability<Ts, SphinxInputOptions, _>>::OVERHEAD_SIZE;
<SurbAcksReliability<R> as Reliability<SphinxInputOptions>>::OVERHEAD_SIZE;
fn reliable_encode(
&mut self,
input: Option<SphinxPipelinePayload<Ts>>,
timestamp: Ts,
) -> Vec<SphinxPipelinePayload<Ts>> {
input: Option<PipelinePayload<SphinxInputOptions>>,
timestamp: Instant,
) -> Vec<PipelinePayload<SphinxInputOptions>> {
self.reliability.reliable_encode(input, timestamp)
}
}
impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> Obfuscation<Ts, SphinxInputOptions, NodeId>
for SphinxClientWrappingPipeline<Ts, R>
{
impl<R: Rng> Obfuscation<SphinxInputOptions> for SphinxClientWrappingPipeline<R> {
fn obfuscate(
&mut self,
input: Option<SphinxPipelinePayload<Ts>>,
timestamp: Ts,
) -> Vec<SphinxPipelinePayload<Ts>> {
input: Option<PipelinePayload<SphinxInputOptions>>,
timestamp: Instant,
) -> Vec<PipelinePayload<SphinxInputOptions>> {
self.cover_traffic.obfuscate(input, timestamp)
}
}
impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> RoutingSecurity<Ts, SphinxInputOptions, NodeId>
for SphinxClientWrappingPipeline<Ts, R>
{
impl<R: Rng> RoutingSecurity<SphinxInputOptions> for SphinxClientWrappingPipeline<R> {
const OVERHEAD_SIZE: usize = nym_sphinx::HEADER_SIZE + nym_sphinx::PAYLOAD_OVERHEAD_SIZE;
fn nb_frames(&self) -> usize {
1
@@ -247,44 +213,34 @@ impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> RoutingSecurity<Ts, SphinxI
/// and choosing two additional hops at random from the directory (repeats are
/// allowed). The final destination is the client identified by
/// `input_options.dst`. Per-hop delays are drawn from
/// [`GenerateDelay::generate_mix_delay`].
fn encrypt(&mut self, input: SphinxPipelinePayload<Ts>) -> SphinxPipelinePayload<Ts> {
// 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<SphinxInputOptions>,
) -> PipelinePayload<SphinxInputOptions> {
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::<Vec<_>>();
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::<Vec<_>>();
// Useful payload size is packet size - transport overhead - framing overhead - routing overhead
let plaintext_size = <Self as WireWrappingPipeline<
Ts,
SimMixPacket,
SphinxInputOptions,
NodeId,
>>::packet_size(self)
- <Self as Framing<Ts, SphinxInputOptions, NodeId>>::OVERHEAD_SIZE
- <Self as Transport<Ts, SimMixPacket, NodeId>>::OVERHEAD_SIZE
- <Self as RoutingSecurity<Ts, _, _>>::OVERHEAD_SIZE;
let plaintext_size =
<Self as WireWrappingPipeline<SimMixPacket, SphinxInputOptions>>::packet_size(self)
- <Self as Framing<SphinxInputOptions>>::OVERHEAD_SIZE
- <Self as Transport<SimMixPacket>>::OVERHEAD_SIZE
- <Self as RoutingSecurity<_>>::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<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> RoutingSecurity<Ts, SphinxI
.build_packet(input.data.data, &route, &destination, &delays)
.unwrap();
SphinxPipelinePayload::new(
PipelinePayload::new(
input.data.timestamp,
packet.to_bytes(),
input.options,
@@ -306,14 +262,10 @@ impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> RoutingSecurity<Ts, SphinxI
}
}
impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng> NoOpWireWrapper
for SphinxClientWrappingPipeline<Ts, R>
{
}
impl<R: Rng> NoOpWireWrapper for SphinxClientWrappingPipeline<R> {}
impl<Ts: Clone + GenerateDelay + PartialOrd, R: Rng>
ClientWrappingPipeline<Ts, SimMixPacket, SphinxInputOptions, NodeId>
for SphinxClientWrappingPipeline<Ts, R>
impl<R: Rng> ClientWrappingPipeline<SimMixPacket, SphinxInputOptions>
for SphinxClientWrappingPipeline<R>
{
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -330,12 +282,8 @@ pub struct SphinxClientUnwrapping {
impl NoOpWireUnwrapper for SphinxClientUnwrapping {}
impl<Ts: Clone> ClientUnwrappingPipeline<Ts, Vec<u8>, SphinxMessage> for SphinxClientUnwrapping {
fn process_unwrapped(
&mut self,
timed_plaintext: TimedPayload<Ts>,
_kind: SphinxMessage,
) -> Option<Vec<u8>> {
impl ClientUnwrappingPipeline<Vec<u8>, ()> for SphinxClientUnwrapping {
fn process_unwrapped(&mut self, timed_plaintext: TimedPayload, _: ()) -> Option<Vec<u8>> {
let plaintext = timed_plaintext.data;
// Ditch cover traffic
@@ -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<Ts, R>
pub struct PoissonCoverTraffic<R>
where
Ts: Clone + GenerateDelay + PartialOrd,
R: Rng,
{
address: ClientId,
cover_dst: DirectoryClient,
directory: Arc<Directory>,
/// 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<Ts, R> PoissonCoverTraffic<Ts, R>
impl<R> PoissonCoverTraffic<R>
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<Directory>,
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<Ts, R> Obfuscation<Ts, SphinxInputOptions, NodeId> for PoissonCoverTraffic<Ts, R>
impl<R> Obfuscation<SphinxInputOptions> for PoissonCoverTraffic<R>
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<SphinxPipelinePayload<Ts>>,
timestamp: Ts,
) -> Vec<SphinxPipelinePayload<Ts>> {
input: Option<PipelinePayload<SphinxInputOptions>>,
timestamp: Instant,
) -> Vec<PipelinePayload<SphinxInputOptions>> {
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 => {}
+16 -25
View File
@@ -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<R>
where
R: Rng,
{
address: ClientId,
ack_dst: DirectoryClient,
directory: Arc<Directory>,
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<Directory>) -> Self {
pub fn new(rng: R, ack_dst: DirectoryClient, directory: Arc<Directory>) -> Self {
Self {
address,
ack_dst,
directory,
rng,
}
}
}
impl<Ts, R> Reliability<Ts, SphinxInputOptions, NodeId> for SurbAcksReliability<R>
impl<R> Reliability<SphinxInputOptions> for SurbAcksReliability<R>
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<SphinxPipelinePayload<Ts>>,
_timestamp: Ts,
) -> Vec<SphinxPipelinePayload<Ts>> {
input: Option<PipelinePayload<SphinxInputOptions>>,
_: Instant,
) -> Vec<PipelinePayload<SphinxInputOptions>> {
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::<Ts, R>(
&mut self.rng,
self.address,
random_id,
&self.directory,
)
.prepare_for_sending()
.1;
let surb_ack =
SurbAck::construct::<R>(&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::<Vec<_>>()
});
+32 -101
View File
@@ -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<Ts>
where
Ts: Clone + PartialOrd + Debug + Send,
{
nodes: Vec<Box<dyn MixSimNode<Ts> + Send>>,
clients: Vec<Box<dyn MixSimClient<Ts> + Send>>,
pub struct MixSimDriver {
nodes: Vec<Box<dyn MixSimNode + Send>>,
clients: Vec<Box<dyn MixSimClient + Send>>,
clock_base: Instant,
}
impl<Ts> MixSimDriver<Ts>
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<Box<dyn MixSimNode<Ts> + Send>>,
clients: Vec<Box<dyn MixSimClient<Ts> + Send>>,
nodes: Vec<Box<dyn MixSimNode + Send>>,
clients: Vec<Box<dyn MixSimClient + Send>>,
) -> 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<u32> {
/// 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<Instant> {
/// Start the simulation in either manual or automatic mode.
pub async fn run(
self,
@@ -212,16 +155,12 @@ impl MixSimDriver<Instant> {
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<Instant> {
#[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)
+8 -8
View File
@@ -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<Instant>);
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<Self> {
let topology_data =
std::fs::read_to_string(&topology).context("Failed to read topology file")?;
@@ -40,14 +43,13 @@ impl NymNodeMixDriver {
let directory: Arc<Directory> = Arc::new((&topology).into());
let mut nodes: Vec<Box<dyn MixSimNode<Instant> + Send>> =
Vec::with_capacity(topology.nodes.len());
let mut nodes: Vec<Box<dyn MixSimNode + Send>> = 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<Box<dyn MixSimClient<Instant> + Send>> =
let mut clients: Vec<Box<dyn MixSimClient + Send>> =
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,
+4 -5
View File
@@ -25,7 +25,7 @@ use crate::{
///
/// [`SimpleProcessingNode`]: crate::node::simple::SimpleProcessingNode
/// [`SimpleClientWrappingPipeline`]: crate::client::simple::SimpleClientWrappingPipeline
pub struct SimpleMixDriver(MixSimDriver<u32>);
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<Directory> = Arc::new((&topology).into());
let mut nodes: Vec<Box<dyn MixSimNode<u32> + Send>> =
Vec::with_capacity(topology.nodes.len());
let mut nodes: Vec<Box<dyn MixSimNode + Send>> = 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<Box<dyn MixSimClient<u32> + Send>> =
let mut clients: Vec<Box<dyn MixSimClient + Send>> =
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
}
}
+3 -59
View File
@@ -22,7 +22,7 @@ use crate::{
};
/// Concrete [`MixSimDriver`] instantiation that uses [`SphinxPacket`](nym_sphinx::SphinxPacket)s.
pub struct SphinxMixDriver(MixSimDriver<Instant>);
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<Directory> = Arc::new((&topology).into());
let mut nodes: Vec<Box<dyn MixSimNode<Instant> + Send>> =
Vec::with_capacity(topology.nodes.len());
let mut nodes: Vec<Box<dyn MixSimNode + 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<Box<dyn MixSimClient<Instant> + Send>> =
let mut clients: Vec<Box<dyn MixSimClient + Send>> =
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<u32>);
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<Self> {
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<Directory> = Arc::new((&topology).into());
let mut nodes: Vec<Box<dyn MixSimNode<u32> + 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<Box<dyn MixSimClient<u32> + 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
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<f64> = 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<f64> = 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<f64> = Exp::new(1.0 / 200.0).unwrap();
Duration::from_millis(exp.sample(rng).round() as u64)
}
+1
View File
@@ -38,6 +38,7 @@
pub mod client;
pub mod driver;
pub mod helpers;
pub mod node;
pub mod packet;
pub mod topology;
+5 -5
View File
@@ -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,
},
}
+24 -70
View File
@@ -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<SocketAddr>;
}
impl DestinationAddress for NodeId {
fn resolve(self, directory: &Directory) -> Option<SocketAddr> {
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<SocketAddr> {
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<Ts: Clone + PartialOrd + Debug + Send>: 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<Ts: Clone + PartialOrd + Debug + Send>: 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<Ts, Pkt>`].
/// `Pkt` is the wire packet type (e.g. [`SimplePacket`] or [`SimMixPacket`]).
/// `Pn` is any type that implements [`NymNodeProcessingPipeline<Pkt>`].
///
/// 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<Ts: Clone + PartialOrd + Debug + Send>: Send {
///
/// [`SimplePacket`]: crate::packet::simple::SimplePacket
/// [`SimMixPacket`]: crate::packet::sphinx::SimMixPacket
pub struct BaseNode<Ts, Pkt, Pn, Dst = NodeId> {
pub struct BaseNode<Pkt, Pn> {
/// 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<Ts, Pkt, Pn, Dst = NodeId> {
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<Directory>,
/// 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<Pkt>,
/// 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<AddressedTimedData<Ts, Pkt, Dst>>,
processed_packets: Vec<AddressedTimedData<Pkt>>,
/// Concrete mix-processing implementation invoked by `tick_processing`.
processing_node: Pn,
}
impl<Ts, Pkt, Pn, Dst> BaseNode<Ts, Pkt, Pn, Dst> {
impl<Pkt, Pn> BaseNode<Pkt, Pn> {
/// 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<Directory>,
processing_node: Pn,
) -> anyhow::Result<Self> {
let socket = UdpSocket::bind(socket_address)?;
@@ -123,40 +95,24 @@ impl<Ts, Pkt, Pn, Dst> BaseNode<Ts, Pkt, Pn, Dst> {
_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<Ts, Pkt, Pn, Dst> BaseNode<Ts, Pkt, Pn, Dst> {
}
}
impl<Ts, Pkt, Pn, Dst> MixSimNode<Ts> for BaseNode<Ts, Pkt, Pn, Dst>
impl<Pkt, Pn> MixSimNode for BaseNode<Pkt, Pn>
where
Ts: Clone + PartialOrd + Debug + Send,
Pkt: WirePacketFormat + Debug + Send,
Pn: NymNodeProcessingPipeline<Ts, Pkt, Dst> + Send,
<Pn as nym_lp_data::common::traits::TransportUnwrap<Ts, Pkt>>::Error: Debug,
Dst: DestinationAddress + Send,
Pn: NymNodeProcessingPipeline<Pkt> + Send,
<Pn as nym_lp_data::common::traits::TransportUnwrap<Pkt>>::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:#?}");
}
}
}
+8 -9
View File
@@ -1,9 +1,9 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<R> = BaseNode<Instant, EncryptedLpPacket, NymNodeDataPipeline<R>, SocketAddr>;
pub type SimNymNode<R> = BaseNode<EncryptedLpPacket, NymNodeDataPipeline<R>>;
impl<R: Rng + Send> SimNymNode<R> {
/// 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<R: Rng + Send> SimNymNode<R> {
topology_node.node_id,
topology_node.reliability,
topology_node.socket_address,
directory,
pipeline,
)
}
+41 -45
View File
@@ -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<Ts> = BaseNode<Ts, SimplePacket, SimpleProcessingNode>;
pub type SimpleNode = BaseNode<SimplePacket, SimpleProcessingNode>;
impl<Ts> SimpleNode<Ts> {
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<Ts> SimpleNode<Ts> {
///
/// Returns an error if the UDP socket cannot be bound or set non-blocking.
pub fn new(topology_node: TopologyNode, directory: Arc<Directory>) -> anyhow::Result<Self> {
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<Ts> SimpleNode<Ts> {
/// [`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<Directory>) -> 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<Ts: Clone> NymNodeProcessingPipeline<Ts, SimplePacket, NodeId> for SimpleProcessingNode {
type Options = SimpleInputOptions;
type MessageKind = SimpleMessage;
impl NymNodeProcessingPipeline<SimplePacket> 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<Ts>,
_timestamp: Ts,
) -> Vec<PipelinePayload<Ts, SimpleInputOptions, NodeId>> {
vec![PipelinePayload::new(
_: (),
payload: TimedPayload,
_timestamp: Instant,
) -> Vec<AddressedTimedPayload> {
vec![AddressedTimedPayload::new_addressed(
payload.timestamp,
payload.data,
SimpleInputOptions,
self.id + 1,
self.next_hop,
)]
}
}
// Delegation of subtraits
impl<Ts: Clone> Framing<Ts, SimpleInputOptions, NodeId> for SimpleProcessingNode {
impl Framing<()> for SimpleProcessingNode {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Framing<Ts, _, _>>::OVERHEAD_SIZE;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Framing<_>>::OVERHEAD_SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, SimpleInputOptions, NodeId>,
payload: AddressedTimedPayload,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, SimpleFrame, NodeId>> {
) -> Vec<AddressedTimedData<SimpleFrame>> {
self.wrapper.to_frame(payload, frame_size)
}
}
impl<Ts: Clone> Transport<Ts, SimplePacket, NodeId> for SimpleProcessingNode {
impl Transport<SimplePacket> for SimpleProcessingNode {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Transport<Ts, _, _>>::OVERHEAD_SIZE;
const OVERHEAD_SIZE: usize = <SimpleWireWrapper as Transport<_>>::OVERHEAD_SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Ts, SimpleFrame, NodeId>,
) -> AddressedTimedData<Ts, SimplePacket, NodeId> {
frame: AddressedTimedData<SimpleFrame>,
) -> AddressedTimedData<SimplePacket> {
self.wrapper.to_transport_packet(frame)
}
}
impl<Ts: Clone> WireWrappingPipeline<Ts, SimplePacket, SimpleInputOptions, NodeId>
for SimpleProcessingNode
{
impl WireWrappingPipeline<SimplePacket, ()> for SimpleProcessingNode {
fn packet_size(&self) -> usize {
<SimpleWireWrapper as WireWrappingPipeline<Ts, _, _, _>>::packet_size(&self.wrapper)
<SimpleWireWrapper as WireWrappingPipeline<_, _>>::packet_size(&self.wrapper)
}
}
impl<Ts> FramingUnwrap<Ts, SimpleMessage> for SimpleProcessingNode {
impl FramingUnwrap<()> for SimpleProcessingNode {
type Frame = SimpleFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Ts, SimpleFrame>,
) -> Option<(TimedPayload<Ts>, SimpleMessage)> {
fn frame_to_message(&mut self, frame: TimedData<SimpleFrame>) -> Option<(TimedPayload, ())> {
self.unwrapper.frame_to_message(frame)
}
}
impl<Ts: Clone> TransportUnwrap<Ts, SimplePacket> for SimpleProcessingNode {
impl TransportUnwrap<SimplePacket> for SimpleProcessingNode {
type Frame = SimpleFrame;
type Error = anyhow::Error;
fn packet_to_frame(
&mut self,
packet: SimplePacket,
timestamp: Ts,
) -> anyhow::Result<TimedData<Ts, SimpleFrame>> {
timestamp: Instant,
) -> anyhow::Result<TimedData<SimpleFrame>> {
self.unwrapper.packet_to_frame(packet, timestamp)
}
}
impl<Ts: Clone> WireUnwrappingPipeline<Ts, SimplePacket, SimpleMessage> for SimpleProcessingNode {}
impl WireUnwrappingPipeline<SimplePacket, ()> for SimpleProcessingNode {}
+73 -32
View File
@@ -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<Ts> = BaseNode<Ts, SimMixPacket, SphinxProcessingNode>;
pub type SphinxNode = BaseNode<SimMixPacket, SphinxProcessingNode>;
impl<Ts> SphinxNode<Ts> {
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<Ts> SphinxNode<Ts> {
///
/// Returns an error if the UDP socket cannot be bound or set non-blocking.
pub fn new(topology_node: TopologyNode, directory: Arc<Directory>) -> anyhow::Result<Self> {
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<Ts> SphinxNode<Ts> {
pub struct SphinxProcessingNode {
id: NodeId,
sphinx_secret: x25519::PrivateKey,
directory: Arc<Directory>,
}
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<Directory>,
) -> Self {
Self {
id: node_id,
sphinx_secret,
directory,
}
}
}
impl<Ts> NymNodeProcessingPipeline<Ts, SimMixPacket, NodeId> for SphinxProcessingNode
where
Ts: AddDelay + Clone,
{
type MessageKind = SphinxMessage;
impl NymNodeProcessingPipeline<SimMixPacket> 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<Ts>,
timestamp: Ts,
) -> Vec<AddressedTimedPayload<Ts, NodeId>> {
_: (),
payload: TimedPayload,
timestamp: Instant,
) -> Vec<AddressedTimedPayload> {
// 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) => {
+1 -1
View File
@@ -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;
+18 -31
View File
@@ -1,11 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Ts: Clone> Framing<Ts, SimpleInputOptions, NodeId> for SimpleWireWrapper {
impl Framing<()> for SimpleWireWrapper {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = SimpleFrame::HEADER.len();
fn to_frame(
&mut self,
payload: PipelinePayload<Ts, SimpleInputOptions, NodeId>,
payload: AddressedTimedPayload,
frame_size: usize,
) -> Vec<AddressedTimedData<Ts, SimpleFrame, NodeId>> {
) -> Vec<AddressedTimedData<SimpleFrame>> {
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<Ts: Clone> Framing<Ts, SimpleInputOptions, NodeId> for SimpleWireWrapper {
/// Transport wraps a [`SimpleFrame`] into a [`SimplePacket`].
/// Overhead = 16 bytes (UUID), so effective payload = 48 bytes.
impl<Ts: Clone> Transport<Ts, SimplePacket, NodeId> for SimpleWireWrapper {
impl Transport<SimplePacket> for SimpleWireWrapper {
type Frame = SimpleFrame;
const OVERHEAD_SIZE: usize = 16;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Ts, SimpleFrame, NodeId>,
) -> AddressedTimedData<Ts, SimplePacket, NodeId> {
frame: AddressedTimedData<SimpleFrame>,
) -> AddressedTimedData<SimplePacket> {
// 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<Ts: Clone> WireWrappingPipeline<Ts, SimplePacket, SimpleInputOptions, NodeId>
for SimpleWireWrapper
{
impl WireWrappingPipeline<SimplePacket, ()> for SimpleWireWrapper {
fn packet_size(&self) -> usize {
SimplePacket::SIZE
}
@@ -247,30 +237,27 @@ impl<Ts: Clone> WireWrappingPipeline<Ts, SimplePacket, SimpleInputOptions, NodeI
/// `SimpleWireUnwrapper`.
pub struct SimpleWireUnwrapper;
impl<Ts> FramingUnwrap<Ts, SimpleMessage> for SimpleWireUnwrapper {
impl FramingUnwrap<()> for SimpleWireUnwrapper {
type Frame = SimpleFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Ts, SimpleFrame>,
) -> Option<(TimedPayload<Ts>, SimpleMessage)> {
fn frame_to_message(&mut self, frame: TimedData<SimpleFrame>) -> Option<(TimedPayload, ())> {
Some((
TimedPayload {
data: frame.data.data,
timestamp: frame.timestamp,
},
SimpleMessage,
(),
))
}
}
impl<Ts: Clone> TransportUnwrap<Ts, SimplePacket> for SimpleWireUnwrapper {
impl TransportUnwrap<SimplePacket> for SimpleWireUnwrapper {
type Frame = SimpleFrame;
type Error = anyhow::Error;
fn packet_to_frame(
&mut self,
packet: SimplePacket,
timestamp: Ts,
) -> anyhow::Result<TimedData<Ts, SimpleFrame>> {
timestamp: Instant,
) -> anyhow::Result<TimedData<SimpleFrame>> {
// packet.data holds the framed bytes (HEADER + payload)
Ok(TimedData::new(
timestamp,
@@ -279,4 +266,4 @@ impl<Ts: Clone> TransportUnwrap<Ts, SimplePacket> for SimpleWireUnwrapper {
}
}
impl<Ts: Clone> WireUnwrappingPipeline<Ts, SimplePacket, SimpleMessage> for SimpleWireUnwrapper {}
impl WireUnwrappingPipeline<SimplePacket, ()> for SimpleWireUnwrapper {}
+12 -118
View File
@@ -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<Ts: GenerateDelay, R>(
pub fn construct<R>(
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::<Vec<_>>();
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::<Vec<_>>();
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<Self::Delay, Output = Self> {
/// 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<f64> = 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<f64> = 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<f64> = 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<f64> = 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<f64> = Exp::new(1.0 / 200.0).unwrap();
Duration::from_millis(exp.sample(rng).round() as u64)
}
}
+4 -14
View File
@@ -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<NodeId> {
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
+4 -5
View File
@@ -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<Vec<AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr>>, MalformedLpPacketError>;
type WorkerOutput = Result<Vec<AddressedTimedData<EncryptedLpPacket>>, 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<WorkerOutput>,
outgoing_pkt_buffer: Vec<AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr>>,
outgoing_pkt_buffer: Vec<AddressedTimedData<EncryptedLpPacket>>,
/// Shutdown token
shutdown: nym_task::ShutdownToken,
@@ -244,8 +243,8 @@ impl LpDataHandler {
input_rx: mpsc::Receiver<WorkerInput>,
output_tx: mpsc::SyncSender<WorkerOutput>,
) where
P: NymNodeProcessingPipeline<Instant, EncryptedLpPacket, SocketAddr>
+ TransportUnwrap<Instant, EncryptedLpPacket, Error = MalformedLpPacketError>, // This is needed to specify the error type
P: NymNodeProcessingPipeline<EncryptedLpPacket>
+ TransportUnwrap<EncryptedLpPacket, Error = MalformedLpPacketError>, // 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
@@ -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<R: Rng> MixingNodeDataPipeline<R> {
}
// Processing logic - stubbed; to be implemented.
impl<R: Rng> NymNodeProcessingPipeline<Instant, EncryptedLpPacket, SocketAddr>
for MixingNodeDataPipeline<R>
{
impl<R: Rng> NymNodeProcessingPipeline<EncryptedLpPacket> for MixingNodeDataPipeline<R> {
type Options = MixMessage;
type MessageKind = MixMessage;
fn mix(
&mut self,
message_kind: MixMessage,
payload: TimedPayload<Instant>,
payload: TimedPayload,
_: Instant,
) -> Vec<PipelinePayload<Instant, MixMessage, SocketAddr>> {
) -> Vec<PipelinePayload<MixMessage>> {
// Everything specific to a given packet type should happen here
let processing_result =
NymNodeDataPipeline::<R>::process_mix_packet(&self.state, message_kind, payload);
@@ -101,15 +99,15 @@ impl<R: Rng> NymNodeProcessingPipeline<Instant, EncryptedLpPacket, SocketAddr>
// ============== Wire wrap/unwrap: pure delegation to WirePipeline ==============
impl<R: Rng> Framing<Instant, MixMessage, SocketAddr> for MixingNodeDataPipeline<R> {
impl<R: Rng> Framing<MixMessage> for MixingNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Instant, MixMessage, SocketAddr>,
payload: PipelinePayload<MixMessage>,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, Self::Frame, SocketAddr>> {
) -> Vec<AddressedTimedData<Self::Frame>> {
let frame = LpFrame {
header: payload.options.into(),
content: payload.data.data.into(),
@@ -119,27 +117,25 @@ impl<R: Rng> Framing<Instant, MixMessage, SocketAddr> for MixingNodeDataPipeline
}
}
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for MixingNodeDataPipeline<R> {
impl<R: Rng> Transport<EncryptedLpPacket> for MixingNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Instant, Self::Frame, SocketAddr>,
) -> AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr> {
frame: AddressedTimedData<Self::Frame>,
) -> AddressedTimedData<EncryptedLpPacket> {
self.wire.frame_to_packet(frame)
}
}
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, MixMessage, SocketAddr>
for MixingNodeDataPipeline<R>
{
impl<R: Rng> WireWrappingPipeline<EncryptedLpPacket, MixMessage> for MixingNodeDataPipeline<R> {
fn packet_size(&self) -> usize {
nym_lp_data::packet::MTU
}
}
impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for MixingNodeDataPipeline<R> {
impl<R: Rng> TransportUnwrap<EncryptedLpPacket> for MixingNodeDataPipeline<R> {
type Frame = LpFrame;
type Error = MalformedLpPacketError;
@@ -147,18 +143,18 @@ impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for MixingNodeDataPipel
&mut self,
packet: EncryptedLpPacket,
timestamp: Instant,
) -> Result<TimedData<Instant, Self::Frame>, Self::Error> {
) -> Result<TimedData<Self::Frame>, Self::Error> {
self.wire.packet_to_frame(packet, timestamp)
}
}
impl<R: Rng> FramingUnwrap<Instant, MixMessage> for MixingNodeDataPipeline<R> {
impl<R: Rng> FramingUnwrap<MixMessage> for MixingNodeDataPipeline<R> {
type Frame = LpFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Instant, Self::Frame>,
) -> Option<(TimedPayload<Instant>, MixMessage)> {
frame: TimedData<Self::Frame>,
) -> 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<R: Rng> FramingUnwrap<Instant, MixMessage> for MixingNodeDataPipeline<R> {
}
}
impl<R: Rng> WireUnwrappingPipeline<Instant, EncryptedLpPacket, MixMessage>
for MixingNodeDataPipeline<R>
{
}
impl<R: Rng> WireUnwrappingPipeline<EncryptedLpPacket, MixMessage> for MixingNodeDataPipeline<R> {}
@@ -1,7 +1,7 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<R: Rng> NymNodeDataPipeline<R> {
pub fn process_mix_packet(
shared_state: &SharedLpDataState,
message_kind: MixMessage,
payload: TimedPayload<Instant>,
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError>
{
payload: TimedPayload,
) -> Result<PipelinePayload<MixMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
match message_kind {
MixMessage::Sphinx(metadata) => {
processing::sphinx::process(shared_state, payload, metadata)
@@ -67,18 +66,16 @@ impl<R: Rng> NymNodeDataPipeline<R> {
}
// Processing logic
impl<R: Rng> NymNodeProcessingPipeline<Instant, EncryptedLpPacket, SocketAddr>
for NymNodeDataPipeline<R>
{
impl<R: Rng> NymNodeProcessingPipeline<EncryptedLpPacket> for NymNodeDataPipeline<R> {
type Options = NymNodeMessage;
type MessageKind = NymNodeMessage;
fn mix(
&mut self,
message_kind: NymNodeMessage,
payload: TimedPayload<Instant>,
payload: TimedPayload,
_: Instant,
) -> Vec<PipelinePayload<Instant, NymNodeMessage, SocketAddr>> {
) -> Vec<PipelinePayload<NymNodeMessage>> {
// 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<R: Rng> NymNodeProcessingPipeline<Instant, EncryptedLpPacket, SocketAddr>
// ============== Wire wrap/unwrap: pure delegation to WirePipeline ==============
impl<R: Rng> Framing<Instant, NymNodeMessage, SocketAddr> for NymNodeDataPipeline<R> {
impl<R: Rng> Framing<NymNodeMessage> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
fn to_frame(
&mut self,
payload: PipelinePayload<Instant, NymNodeMessage, SocketAddr>,
payload: PipelinePayload<NymNodeMessage>,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, Self::Frame, SocketAddr>> {
) -> Vec<AddressedTimedData<Self::Frame>> {
let frame = LpFrame {
header: payload.options.into(),
content: payload.data.data.into(),
@@ -168,27 +165,25 @@ impl<R: Rng> Framing<Instant, NymNodeMessage, SocketAddr> for NymNodeDataPipelin
}
}
impl<R: Rng> Transport<Instant, EncryptedLpPacket, SocketAddr> for NymNodeDataPipeline<R> {
impl<R: Rng> Transport<EncryptedLpPacket> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
fn to_transport_packet(
&mut self,
frame: AddressedTimedData<Instant, Self::Frame, SocketAddr>,
) -> AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr> {
frame: AddressedTimedData<Self::Frame>,
) -> AddressedTimedData<EncryptedLpPacket> {
self.wire.frame_to_packet(frame)
}
}
impl<R: Rng> WireWrappingPipeline<Instant, EncryptedLpPacket, NymNodeMessage, SocketAddr>
for NymNodeDataPipeline<R>
{
impl<R: Rng> WireWrappingPipeline<EncryptedLpPacket, NymNodeMessage> for NymNodeDataPipeline<R> {
fn packet_size(&self) -> usize {
nym_lp_data::packet::MTU
}
}
impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for NymNodeDataPipeline<R> {
impl<R: Rng> TransportUnwrap<EncryptedLpPacket> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
type Error = MalformedLpPacketError;
@@ -196,18 +191,18 @@ impl<R: Rng> TransportUnwrap<Instant, EncryptedLpPacket> for NymNodeDataPipeline
&mut self,
packet: EncryptedLpPacket,
timestamp: Instant,
) -> Result<TimedData<Instant, Self::Frame>, Self::Error> {
) -> Result<TimedData<Self::Frame>, Self::Error> {
self.wire.packet_to_frame(packet, timestamp)
}
}
impl<R: Rng> FramingUnwrap<Instant, NymNodeMessage> for NymNodeDataPipeline<R> {
impl<R: Rng> FramingUnwrap<NymNodeMessage> for NymNodeDataPipeline<R> {
type Frame = LpFrame;
fn frame_to_message(
&mut self,
frame: TimedData<Instant, Self::Frame>,
) -> Option<(TimedPayload<Instant>, NymNodeMessage)> {
frame: TimedData<Self::Frame>,
) -> 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<R: Rng> FramingUnwrap<Instant, NymNodeMessage> for NymNodeDataPipeline<R> {
}
}
impl<R: Rng> WireUnwrappingPipeline<Instant, EncryptedLpPacket, NymNodeMessage>
for NymNodeDataPipeline<R>
{
}
impl<R: Rng> WireUnwrappingPipeline<EncryptedLpPacket, NymNodeMessage> for NymNodeDataPipeline<R> {}
// ================================================================================================================================================
@@ -42,8 +42,8 @@ impl<R: Rng> WirePipeline<R> {
/// Wrap an [`LpFrame`] into an [`EncryptedLpPacket`] for the wire.
pub fn frame_to_packet(
&mut self,
frame: AddressedTimedData<Instant, LpFrame, SocketAddr>,
) -> AddressedTimedData<Instant, EncryptedLpPacket, SocketAddr> {
frame: AddressedTimedData<LpFrame>,
) -> AddressedTimedData<EncryptedLpPacket> {
// 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<R: Rng> WirePipeline<R> {
&mut self,
packet: EncryptedLpPacket,
timestamp: Instant,
) -> Result<TimedData<Instant, LpFrame>, MalformedLpPacketError> {
) -> Result<TimedData<LpFrame>, 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<R: Rng> WirePipeline<R> {
frame: LpFrame,
dst: SocketAddr,
frame_size: usize,
) -> Vec<AddressedTimedData<Instant, LpFrame, SocketAddr>> {
) -> Vec<AddressedTimedData<LpFrame>> {
let output_frames = if frame.len() > frame_size {
fragment_lp_message(&mut self.rng, frame, frame_size)
.into_iter()
@@ -94,8 +94,8 @@ impl<R: Rng> WirePipeline<R> {
/// fragment.
pub fn frame_to_maybe_message(
&mut self,
frame: TimedData<Instant, LpFrame>,
) -> Option<TimedData<Instant, LpFrame>> {
frame: TimedData<LpFrame>,
) -> Option<TimedData<LpFrame>> {
let reassembled = if frame.data.kind() == LpFrameKind::FragmentedData {
let fragment = frame
.data
@@ -1,6 +1,5 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Instant>,
outfox_packet: TimedPayload,
metadata: OutfoxMixMessage,
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
) -> Result<PipelinePayload<MixMessage, NymNodeRoutingAddress>, 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<Instant>,
sphinx_packet: TimedPayload,
metadata: ForwardOutfoxMessage,
) -> Result<PipelinePayload<Instant, NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
) -> Result<PipelinePayload<NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
// SW TODO add bandwidth check here
let node = shared_gateway_state
@@ -1,8 +1,6 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<Instant>,
sphinx_packet: TimedPayload,
metadata: SphinxMixMessage,
) -> Result<PipelinePayload<Instant, MixMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
) -> Result<PipelinePayload<MixMessage, NymNodeRoutingAddress>, 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<Instant>,
sphinx_packet: TimedPayload,
metadata: ForwardSphinxMessage,
) -> Result<PipelinePayload<Instant, NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
) -> Result<PipelinePayload<NymNodeMessage, NymNodeRoutingAddress>, LpDataHandlerError> {
// SW TODO add bandwidth check here
let node = shared_gateway_state
+3 -6
View File
@@ -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<Instant, Duration>,
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<Instant, impl Clone + Into<PacketKind>, NymNodeRoutingAddress>,
PipelinePayload<impl Clone + Into<PacketKind>, NymNodeRoutingAddress>,
LpDataHandlerError,
>,
) {