trait rework, removed Ts and NdId generic
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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>,
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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: {:?}",
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
pub mod client;
|
||||
pub mod driver;
|
||||
pub mod helpers;
|
||||
pub mod node;
|
||||
pub mod packet;
|
||||
pub mod topology;
|
||||
|
||||
@@ -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
@@ -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:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
>,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user