Skip to content

[LP-DATA] Mix simulator, pipeline traits#6712

Open
simonwicky wants to merge 11 commits intodevelopfrom
simon/lp/msg-pipeline
Open

[LP-DATA] Mix simulator, pipeline traits#6712
simonwicky wants to merge 11 commits intodevelopfrom
simon/lp/msg-pipeline

Conversation

@simonwicky
Copy link
Copy Markdown
Contributor

@simonwicky simonwicky commented Apr 27, 2026

Summary

Adds two new workspace crates that together form an abstract pipeline framework for the upcoming Lewes Protocol (LP) plus a runnable simulator that consumes it.

  • common/nym-lp-data — a trait-only "vocabulary" crate that defines the shape of client and mix-node packet-processing pipelines. No concrete cryptography, transport, or networking code.
  • nym-mix-sim — a discrete-time mixnet simulator running on localhost UDP. Doubles as the reference implementation of every trait in nym-lp-data.

⚠️ Disclaimer : the traits introduced in nym-lp-data are not stable yet, they might be subject to changes and tweaks in the future

nym-lp-data — the vocabulary

Three modules:

  • common — wire-layer traits shared by clients and nodes (Framing / Transport and their unwrap counterparts) plus the supertraits WireWrappingPipeline / WireUnwrappingPipeline. common/helpers.rs adds NoOpWireWrapper / NoOpWireUnwrapper markers for packet types that are already self-contained on the wire (e.g. a Sphinx packet).
  • clients — the six-stage outbound pipeline Chunking → Reliability → Obfuscation → RoutingSecurity → Framing → Transport, the supertrait ClientWrappingPipeline that ties them together with a default process(), an inbound counterpart, no-op marker traits per stage, a Pipeline composition struct, and a tick-driven ClientWrappingPipelineDriver.
  • mixnodesMixnodeProcessingPipeline (unwrap → mix() → re-wrap).

A few generic wrappers thread per-packet state through every stage: TimedData, AddressedTimedData, and PipelineData (which adds per-message InputOptions).

A couple of design points worth flagging

  • process() is called every tick, with or without input. Reliability and Obfuscation are explicitly invoked twice per tick (once with the real input, once with None) so retransmissions and cover-traffic loops can fire on idle ticks without the caller knowing what's in flight.
  • InputOptions are per-message, not per-pipeline. They toggle whether reliability / obfuscation / routing-security run for a given payload and carry the next-hop id, which is what lets a single pipeline interleave real and cover packets with different routing rules.
  • The Frame type is an associated item cross-constrained by the wire-pipeline supertraits, so the parameter list stays short at the cost of forcing implementors to spell out the cross-constraint when composing. Open to feedback.

A small synthetic integration test wires a mock pipeline against the nym-lp packet types as a self-contained example of every trait being implemented.

nym-mix-sim — the simulator

Discrete-time mixnet simulator that runs a configurable number of mix nodes and clients on localhost UDP. Time advances in ticks; each tick runs the client phase, then drains sockets, mixes buffered packets, and dispatches outgoing ones.

Two binaries: nym-mix-sim (generates topology.json and runs the loop) and mix-client (stdin → app socket of a running client).

Three driver flavours, controlled by --driver:

Driver Timestamp Encryption Cover traffic Reliability Manual mode
simple u32 tick counter none no no yes
sphinx wall-clock Instant full Sphinx Poisson SURB ACKs no
discrete-sphinx (default) u32 tick counter (1 tick = 1 ms) full Sphinx Poisson SURB ACKs yes

--manual pauses for ENTER between ticks and pretty-prints each node's buffer state per phase, which is useful for stepping a Sphinx packet hop-by-hop. The Sphinx driver implements 3-hop onion encryption, two-loop Poisson cover traffic, a SURB-ACK reliability layer, and fragment reconstruction — exercising every trait in nym-lp-data.

See nym-mix-sim/README.md and common/nym-lp-data/README.md for full docs and quick-start examples.

Small workspace touches

  • Cargo.toml: register both new crates and expose nym-lp-data as a workspace dependency.
  • common/crypto: new bs58_x25519_private_key serde helper used by topology.json to round-trip per-node Sphinx private keys.
  • common/nym-lp: expose LpHeader::SIZE as a pub const so the integration test can size frames against it.

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added a reusable nym-lp-data crate with generic timed/pipeline data types, client/mixnode pipeline traits, helpers, drivers, and tests.
    • Added a discrete-time nym-mix-sim simulator with CLI binaries (nym-mix-sim, mix-client), multiple drivers (simple, sphinx, discrete-sphinx), nodes, clients, packet formats, and topology support.
    • Added BaseClient/BaseNode and concrete simple/Sphinx pipeline implementations.
  • Documentation

    • Added READMEs and crate-level docs describing pipeline architecture and simulator usage.
  • Tests

    • Added integration tests and test helpers for pipeline validation.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nym-explorer-v2 Ready Ready Preview, Comment May 6, 2026 1:34pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs-nextra Ignored Ignored Preview May 6, 2026 1:34pm
nym-node-status Ignored Ignored Preview May 6, 2026 1:34pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Adds a new generic Lewes Protocol pipeline crate (common/nym-lp-data) and a discrete-time UDP-based mixnet simulator (nym-mix-sim); registers the crates in the workspace and adds Base58 serde helpers for x25519 private keys and a small header constant.

Changes

Workspace & Serialization

Layer / File(s) Summary
Workspace Manifest
Cargo.toml
Adds common/nym-lp-data to workspace members, extends default-members to include nym-mix-sim and tools/internal/localnet-orchestrator, and adds workspace dependency entry nym-lp-data = { version = "1.20.4", path = "common/nym-lp-data" }.
x25519 Serde Helper
common/crypto/src/asymmetric/x25519/serde_helpers.rs
Introduces bs58_x25519_private_key serde helpers to (de)serialize x25519::PrivateKey as Base58 strings and brings PrivateKey into scope.
Header Size Constant
common/nym-lp/src/packet/header.rs
Adds pub const SIZE to LpHeader computed from OuterHeader::SIZE + InnerHeader::SIZE.

LP Data Library (common/nym-lp-data)

Layer / File(s) Summary
Crate Manifest & Docs
common/nym-lp-data/Cargo.toml, common/nym-lp-data/README.md
Adds nym-lp-data crate manifest and README documenting crate purpose, module layout, pipeline semantics, and examples.
Core Types
common/nym-lp-data/src/lib.rs
Introduces TimedData, PipelineData, AddressedTimedData aliases and helper constructors/transform methods.
Wire Traits
common/nym-lp-data/src/common/traits.rs, common/nym-lp-data/src/common/mod.rs
Defines Framing, FramingUnwrap, Transport, TransportUnwrap, WireWrappingPipeline, and WireUnwrappingPipeline traits.
Wire Helpers
common/nym-lp-data/src/common/helpers.rs
Adds NoOpWireWrapper/NoOpWireUnwrapper markers with blanket impls for framing/transport/wire pipeline no-ops.
Client Pipeline Traits
common/nym-lp-data/src/clients/traits.rs, common/nym-lp-data/src/clients/mod.rs
Adds InputOptions, Chunking, Reliability, Obfuscation, RoutingSecurity, ClientWrappingPipeline, DynClientWrappingPipeline, and ClientUnwrappingPipeline with chunk-size derivation and tick-aware process.
Client Types & Composition
common/nym-lp-data/src/clients/types.rs
Adds generic Pipeline<C,R,O,Rs,F,T> composing six stages and delegating implementations.
Client Helpers
common/nym-lp-data/src/clients/helpers.rs
Adds NoOpReliability, NoOpRoutingSecurity, NoOpObfuscation marker traits and blanket impls.
Driver
common/nym-lp-data/src/clients/driver.rs
Adds ClientWrappingPipelineDriver that accepts external inputs via a zero-capacity mpsc::sync_channel, buffers produced addressed packets, and on tick(timestamp) returns due packets.
Mixnode Pipeline
common/nym-lp-data/src/mixnodes/traits.rs, common/nym-lp-data/src/mixnodes/mod.rs
Adds MixnodeProcessingPipeline trait with required mix and provided process that unwraps, mixes, and re-wraps payloads.
Integration Tests
common/nym-lp-data/tests/integration/*
Adds test helpers/mocks (MockChunking, KcpReliability, SphinxSecurity, KekwObfuscation, LpFraming, LpTransport, etc.) and an integration test empty_input_yields_empty_output.

Simulator Implementation (nym-mix-sim)

Layer / File(s) Summary
Crate Manifest & Root
nym-mix-sim/Cargo.toml, nym-mix-sim/src/lib.rs, nym-mix-sim/README.md
Adds nym-mix-sim crate manifest with two binaries (mix-client, nym-mix-sim), crate docs, and README describing simulator architecture, binaries, and examples.
Topology Model
nym-mix-sim/src/topology/mod.rs, nym-mix-sim/src/topology/directory.rs
Adds Topology, TopologyNode, TopologyClient (stores x25519 private key via Base58 serde helper), and Directory immutable routing table with lookups and random hop/route sampling.
Wire Packet Abstractions
nym-mix-sim/src/packet/mod.rs
Adds WirePacketFormat trait and impl WirePacketFormat for Vec<u8>.
Simple Packet Format
nym-mix-sim/src/packet/simple.rs
Adds SimplePacket fixed 64-byte format, SimpleFrame framing header, SimpleWireWrapper/SimpleWireUnwrapper and implementations for wrapping/unwrapping pipeline traits.
Sphinx Packet Utilities
nym-mix-sim/src/packet/sphinx.rs
Adds SimMixPacket, SurbAck construction/recovery utilities, SphinxMessage marker, and timing traits AddDelay/GenerateDelay with implementations for u32 and Instant.
Node Core
nym-mix-sim/src/node/mod.rs
Adds MixSimNode and ProcessingNode traits and BaseNode implementation: UDP binding, non-blocking recv, buffering, and three-phase tick orchestration (incoming, processing, outgoing).
Simple Node
nym-mix-sim/src/node/simple.rs
Adds SimpleNode and SimpleProcessingNode wired to the simple pipeline and fixed forwarding logic.
Sphinx Node
nym-mix-sim/src/node/sphinx.rs
Adds SphinxNode and SphinxProcessingNode that decrypts Sphinx packets, forwards on delay for forward hops, and extracts/forwards client messages and recovered SURB ACK first-hop packets.
Client Core
nym-mix-sim/src/client/mod.rs
Adds MixSimClient and ProcessingClient traits and BaseClient implementation: two non-blocking UDP sockets (mix/app), outgoing queue, with_pipeline, send_to_node, recv_from_mix, recv_from_app, and three-phase tick behavior.
Simple Client
nym-mix-sim/src/client/simple.rs
Adds SimpleClient, SimpleProcessingClient, simple padding/chunking wrapping, and unwrapping that strips marker padding.
Sphinx Client
nym-mix-sim/src/client/sphinx/mod.rs, .../poisson_cover_traffic.rs, .../surb_acks.rs
Adds SphinxClient with SphinxProcessingClient that composes fragmenting, SurbAcksReliability, PoissonCoverTraffic obfuscation, and 3-hop Sphinx encryption; adds PoissonCoverTraffic generator and SurbAcksReliability reliability layer.
Driver & Runners
nym-mix-sim/src/driver/mod.rs, nym-mix-sim/src/driver/simple.rs, nym-mix-sim/src/driver/sphinx.rs
Adds MixSimDriver<Ts> orchestrator, concrete drivers SimpleMixDriver, SphinxMixDriver, DiscreteSphinxMixDriver, and SimDriver enum dispatch to load topology, construct nodes/clients, and run simulation in manual or automatic modes.
Binaries & CLI
nym-mix-sim/src/main.rs, nym-mix-sim/src/bin/mix_client.rs
Adds nym-mix-sim CLI with init-topology (generate topology.json) and run (start selected driver) subcommands; adds mix-client helper binary to send app datagrams (stdin → UDP).

Sequence Diagram

sequenceDiagram
    participant User
    participant Driver as MixSimDriver
    participant Client as BaseClient
    participant Node as BaseNode
    participant Net as UDP_Network

    User->>Driver: run / tick(timestamp)
    Driver->>Client: tick(timestamp) — app incoming
    Client->>Client: recv_from_app() / process(payload)
    Client->>Client: enqueue AddressedTimedData(timestamp)
    Driver->>Node: tick_incoming()
    Node->>Net: non-blocking recv_packet()
    Node->>Node: buffer packets_to_process
    Driver->>Node: tick_processing(timestamp)
    Node->>Node: processing_node.process(packet)
    Node->>Node: schedule outputs (timestamp +/- delay)
    Driver->>Client: tick(timestamp) — outgoing
    Client->>Client: send queued packets with pkt.timestamp <= timestamp
    Client->>Net: send_to_node(first_hop, bytes)
    Driver->>Node: tick_outgoing(timestamp)
    Node->>Net: send_to_node(next_hop, bytes)
    Driver->>Client: tick(timestamp) — mix incoming
    Client->>Net: recv_from_mix() non-blocking
    Client->>Client: unwrap(packet, timestamp) -> Maybe plaintext
    Client->>Client: log received messages
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

"I nibble bytes beneath the moonlight,
Ticks and hops make packets take flight,
With whiskers twitch and floppy ear,
The simulator hums — hop, hop, cheer! 🐰"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch simon/lp/msg-pipeline

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@common/nym-lp-data/src/clients/traits.rs`:
- Around line 143-156: In chunk_size(), replace the unchecked arithmetic with
checked operations: compute let base =
self.frame_size().checked_mul(self.nb_frames()) and if None return 0 (or a safe
default); then if input_options.routing_security() apply base =
base.and_then(|b| b.checked_sub(<Self as
RoutingSecurity<_,_,_>>::OVERHEAD_SIZE)) and if input_options.reliability()
apply base = base.and_then(|b| b.checked_sub(<Self as
Reliability<_,_,_>>::OVERHEAD_SIZE)); finally return base.unwrap_or(0). Update
the chunk_size function to use these checked_mul/checked_sub calls and the
referenced symbols (chunk_size, frame_size, nb_frames,
RoutingSecurity::OVERHEAD_SIZE, Reliability::OVERHEAD_SIZE,
input_options.routing_security, input_options.reliability) so over/underflow
cannot produce wrapped sizes.

In `@common/nym-lp-data/src/common/traits.rs`:
- Around line 126-130: The frame_size() implementation can underflow when
packet_size() is smaller than the combined OVERHEAD_SIZEs; update the frame_size
function to perform checked subtraction (e.g., use checked_sub on packet_size
then checked_sub again with the other OVERHEAD_SIZE) and fail early with a clear
error (panic/expect or return a Result) instead of letting wrapping produce a
huge usize; reference the function frame_size and the constants <Self as
Transport<Ts, Pkt, NdId>>::OVERHEAD_SIZE and <Self as Framing<Ts, Opts,
NdId>>::OVERHEAD_SIZE when constructing the error message so it’s clear which
overheads caused the underflow.

In `@nym-mix-sim/README.md`:
- Around line 36-38: Update the three fenced code blocks that show commands to
include a language identifier; specifically change the fence opening for the
blocks containing "cargo run --bin nym-mix-sim -- init-topology [OPTIONS]",
"cargo run --bin nym-mix-sim -- run [OPTIONS]", and "cargo run --bin mix-client
-- --src <ID> --dst <ID> [--topology <PATH>]" so they start with ```bash instead
of just ```, ensuring MD040 is satisfied.

In `@nym-mix-sim/src/client/mod.rs`:
- Around line 229-240: The loop over inputs calls processing_client.process(...)
for each (dst, payload), which re-triggers timer-driven stages inside
ClientWrappingPipeline::process() and causes duplicate reliable_encode/obfuscate
behavior; instead, batch all app datagrams for the current tick into a single
call that represents the tick entry point. Concretely: accumulate/collect all
payloads in inputs and call processing_client.process(...) once with a combined
or iterator-aware API (or add a new process_tick(...) on
processing_client/ClientWrappingPipeline that runs the per-tick
reliable_encode(None,..)/obfuscate(None,..) and then feeds all app messages),
then extend outgoing_queue with that single return; ensure you no longer call
processing_client.process for each (dst, payload) in the for loop.

In `@nym-mix-sim/src/client/simple.rs`:
- Around line 64-70: The constructor SimpleClient::new must validate the
simple-mode hop assumption (that next-hop node id 0 exists) and error early if
absent: before creating SimpleProcessingClient or calling
BaseClient::with_pipeline, check the supplied TopologyClient/Directory/topology
for a node with id 0 (the pipeline’s hardcoded next hop) and return an
anyhow::Error if not found; apply the same guard to the other SimpleClient
constructor variant that builds the pipeline (the block creating
SimpleProcessingClient / calling BaseClient::with_pipeline) so sends don’t
silently fail when node 0 is missing.

In `@nym-mix-sim/src/driver/mod.rs`:
- Around line 124-135: The run(...) method accepts display_state but doesn't
forward it to run_automatic(...) causing automatic mode ticks to always use
false; update run_automatic to accept a display_state: bool parameter and change
the call in run(...) to self.run_automatic(start_tick, tick_duration_ms,
display_state). Also update run_automatic's internal uses (and any helper
functions it calls that decide whether to display state) to use the passed
display_state instead of the hardcoded false; repeat the same change for the
other run/run_automatic call site referenced (the second occurrence) so both
invocation paths propagate display_state correctly.

In `@nym-mix-sim/src/driver/sphinx.rs`:
- Around line 29-36: Reject empty topologies early in the constructors: in pub
fn new (and the other constructor that currently accepts
topology.nodes.is_empty()), add a guard after deserializing Topology that checks
if topology.nodes.is_empty() and return an Err(anyhow::anyhow!("Topology
contains no nodes") ) (or equivalent anyhow::Context) so construction fails fast
with a clear message instead of allowing later panics during route sampling;
reference the Topology value and the constructors pub fn new and the other
constructor handling Topology to locate where to add the check.

In `@nym-mix-sim/src/main.rs`:
- Around line 100-101: The range expression nodes..nodes + clients used to build
client_list can overflow (u8) and produce an invalid/empty range; replace the
plain addition with a checked or saturating addition before constructing the
range (e.g., use nodes.checked_add(clients).unwrap_or_else(...) or cast to a
larger integer type and validate) so you either abort with a clear error or
clamp the upper bound; update the client_list construction to use the
validated/safe upper bound and keep references to the same variables
(client_list, nodes, clients) so the rest of the code remains unchanged.

In `@nym-mix-sim/src/node/simple.rs`:
- Around line 95-110: mix() currently forwards to self.id + 1 which assumes
sequential IDs; instead consult the topology/directory to choose the next hop so
you don't forward to a non-existent NodeId. Change the routing in the mix
function (the code constructing PipelinePayload via PipelinePayload::new) to
lookup the next hop using the Directory/topology API (or a configured routing
table) given self.id and fail/return an error or drop/metrics if no next hop
exists; ensure you reference SimpleMessage, SimpleInputOptions and NodeId when
replacing the hard-coded self.id + 1.

In `@nym-mix-sim/src/node/sphinx.rs`:
- Around line 116-120: Replace the panic-causing unwrap on
SphinxPacket::from_bytes(&payload.data) with proper error handling: call
SphinxPacket::from_bytes and match on the Result, logging the deserialization
error (include payload metadata if available) and aborting processing for that
SimMixPacket instead of panicking; ensure the logging uses the node's logger
context and simply returns/continues (drops the packet) on Err, and only
proceeds with the successfully parsed SphinxPacket on Ok so downstream code no
longer assumes from_bytes cannot fail.

In `@nym-mix-sim/src/packet/simple.rs`:
- Around line 162-171: SimpleFrame::try_from_bytes currently only checks length
and accepts any bytes with the correct minimal length; update it to verify that
the leading bytes match the expected magic header (Self::HEADER) before slicing
the payload. In the SimpleFrame::try_from_bytes function, compare
bytes[..Self::HEADER.len()] to Self::HEADER and return an
Err(anyhow::anyhow!(...)) when they differ (with a concise message like "Invalid
SimpleFrame header"), otherwise proceed to extract data =
bytes[Self::HEADER.len()..].to_vec() and return Ok(SimpleFrame { data }).

In `@nym-mix-sim/src/packet/sphinx.rs`:
- Around line 170-173: try_recover_first_hop_packet currently indexes b[0] and
will panic on empty input; guard against truncated SURB buffers by validating
the slice length before indexing. In try_recover_first_hop_packet, check that b
is non-empty (e.g., via b.is_empty() or b.get(0).ok_or(...)) and return an
anyhow::Result error when the buffer is empty or otherwise too short, then
proceed to construct NodeId from the first byte and call
SimMixPacket::try_from_bytes(&b[1..]) as before; ensure the error message
clearly indicates a malformed/truncated SURB buffer.
- Around line 196-201: The is_surb_ack function currently only checks for the
marker prefix length and can return true for payloads shorter than the full
16-byte ACK header; update is_surb_ack to first verify data.len() >= 16 (the
full ACK header length used later by SphinxClientUnwrapping when it accesses
plaintext[8..16]) and only then compare the marker bytes (Self::MARKER) against
the corresponding prefix; ensure you reference is_surb_ack and Self::MARKER and
guard against truncated payloads so the later plaintext[8..16] slice cannot
panic.

In `@nym-mix-sim/src/topology/directory.rs`:
- Around line 51-52: The Directory::client method currently accepts a NodeId but
the clients map is keyed by ClientId, causing a type-domain mismatch; change the
method signature to take a ClientId (e.g., pub fn client(&self, id: ClientId) ->
Option<&SocketAddr>) and update the internal lookup to use self.clients.get(&id)
(and rename the parameter if needed). After changing the signature, update all
callers to pass a ClientId instead of a NodeId so the API remains type-safe and
consistent with the clients field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6729ba99-6209-4d43-ab4a-0326679ed82a

📥 Commits

Reviewing files that changed from the base of the PR and between a0116f9 and 1d161f8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • Cargo.toml
  • common/crypto/src/asymmetric/x25519/serde_helpers.rs
  • common/nym-lp-data/Cargo.toml
  • common/nym-lp-data/README.md
  • common/nym-lp-data/src/clients/driver.rs
  • common/nym-lp-data/src/clients/helpers.rs
  • common/nym-lp-data/src/clients/mod.rs
  • common/nym-lp-data/src/clients/traits.rs
  • common/nym-lp-data/src/clients/types.rs
  • common/nym-lp-data/src/common/helpers.rs
  • common/nym-lp-data/src/common/mod.rs
  • common/nym-lp-data/src/common/traits.rs
  • common/nym-lp-data/src/lib.rs
  • common/nym-lp-data/src/mixnodes/mod.rs
  • common/nym-lp-data/src/mixnodes/traits.rs
  • common/nym-lp-data/tests/integration/common/mod.rs
  • common/nym-lp-data/tests/integration/main.rs
  • common/nym-lp/src/packet/header.rs
  • nym-mix-sim/Cargo.toml
  • nym-mix-sim/README.md
  • nym-mix-sim/src/bin/mix_client.rs
  • nym-mix-sim/src/client/mod.rs
  • nym-mix-sim/src/client/simple.rs
  • nym-mix-sim/src/client/sphinx/mod.rs
  • nym-mix-sim/src/client/sphinx/poisson_cover_traffic.rs
  • nym-mix-sim/src/client/sphinx/surb_acks.rs
  • nym-mix-sim/src/driver/mod.rs
  • nym-mix-sim/src/driver/simple.rs
  • nym-mix-sim/src/driver/sphinx.rs
  • nym-mix-sim/src/lib.rs
  • nym-mix-sim/src/main.rs
  • nym-mix-sim/src/node/mod.rs
  • nym-mix-sim/src/node/simple.rs
  • nym-mix-sim/src/node/sphinx.rs
  • nym-mix-sim/src/packet/mod.rs
  • nym-mix-sim/src/packet/simple.rs
  • nym-mix-sim/src/packet/sphinx.rs
  • nym-mix-sim/src/topology/directory.rs
  • nym-mix-sim/src/topology/mod.rs

Comment thread common/nym-lp-data/src/clients/traits.rs
Comment thread common/nym-lp-data/src/common/traits.rs
Comment thread nym-mix-sim/README.md Outdated
Comment thread nym-mix-sim/src/client/mod.rs
Comment thread nym-mix-sim/src/client/simple.rs
Comment thread nym-mix-sim/src/node/sphinx.rs
Comment thread nym-mix-sim/src/packet/simple.rs
Comment thread nym-mix-sim/src/packet/sphinx.rs
Comment thread nym-mix-sim/src/packet/sphinx.rs
Comment thread nym-mix-sim/src/topology/directory.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
nym-mix-sim/src/topology/directory.rs (1)

64-68: ⚡ Quick win

Avoid per-call allocation in random_next_hop to match the lock-free/allocation-free design note.

Line 67 builds a fresh Vec<NodeId> via node_ids() on every call. Sampling directly from self.nodes.keys() avoids that allocation on hot routing paths.

Suggested patch
     pub fn random_next_hop(&self, rng: &mut impl rand::Rng) -> NodeId {
         // 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.keys().choose(rng).unwrap()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nym-mix-sim/src/topology/directory.rs` around lines 64 - 68, The method
random_next_hop currently calls node_ids() which allocates a Vec<NodeId> per
call; change it to sample directly from the directory's map keys to avoid
allocations by using the IteratorRandom helper on self.nodes.keys() (e.g. import
rand::seq::IteratorRandom and call self.nodes.keys().choose(rng)), dereference
the chosen &NodeId to return NodeId, and keep the existing SAFETY/unwrap
assertion (or use expect with the same message) to preserve the guarantee that
the directory is non-empty; this removes the per-call allocation from node_ids()
while keeping semantics for random_next_hop.
common/nym-lp-data/src/clients/traits.rs (1)

184-213: ⚡ Quick win

Lift the per-tick idempotency contract into the trait docs.

The "this should be a no-op since it already has been called with the same timestamp" property is load-bearing: any Reliability / Obfuscation impl that re-emits state on a repeat call with the same timestamp will produce duplicate retransmissions or duplicate cover packets per tick. Right now this contract lives only in inline comments inside process; please document it on Reliability::reliable_encode and Obfuscation::obfuscate so implementors can't get it wrong.

Alternatively, restructure process to call each stage exactly once per tick (passing Option<chunk> per item) so the contract isn't needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/nym-lp-data/src/clients/traits.rs` around lines 184 - 213, Document
the per-tick idempotency contract directly on the trait methods: add clear doc
comments to Reliability::reliable_encode and Obfuscation::obfuscate stating that
each method must be idempotent per timestamp (calling with the same timestamp
more than once must be a no-op and must not re-emit previously produced chunks),
explain the meaning/role of the Option<Chunk> parameter (None is used to trigger
any per-tick maintenance like catching retransmissions or emitting cover
traffic), and note callers may call the method both with Some(chunk) and later
with None for the same timestamp so implementations must guard against duplicate
output; alternatively, if you prefer behavior instead of docs, refactor process
to ensure each stage (reliable_encode and obfuscate) is invoked exactly once per
tick per stage (passing Option<Chunk>) so no idempotency contract is required.
common/nym-lp-data/src/lib.rs (1)

70-89: 💤 Low value

Optional: relax FnMut to FnOnce for *_transform closures.

data_transform and ts_transform invoke op exactly once, so FnOnce is the more accurate (and looser) bound; this lets callers pass non-FnMut move closures that consume captured state.

Proposed change
-    pub fn data_transform<F, Nd>(self, mut op: F) -> TimedData<Ts, Nd>
+    pub fn data_transform<F, Nd>(self, op: F) -> TimedData<Ts, Nd>
     where
-        F: FnMut(D) -> Nd,
+        F: FnOnce(D) -> Nd,
     {
@@
-    pub fn ts_transform<F>(self, mut op: F) -> Self
+    pub fn ts_transform<F>(self, op: F) -> Self
     where
-        F: FnMut(Ts) -> Ts,
+        F: FnOnce(Ts) -> Ts,
     {

(and analogous changes on PipelineData::data_transform / ts_transform)

Also applies to: 128-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/nym-lp-data/src/lib.rs` around lines 70 - 89, The transform methods
(TimedData::data_transform and TimedData::ts_transform, and the analogous
PipelineData::data_transform/ts_transform) use a FnMut bound and a mut parameter
even though the closure is invoked exactly once; change the generic bounds from
F: FnMut(...) -> ... to F: FnOnce(...) -> ... and remove the unnecessary mut on
the op parameter (e.g., `mut op: F` -> `op: F`) so single-use, consuming move
closures are accepted; update both TimedData and PipelineData variants
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@common/nym-lp-data/src/common/traits.rs`:
- Around line 124-126: Fix the typo in the comment: change "accomodate" to
"accommodate" in the comment block that begins "// IMPORTANT NOTE : This fn can
be not constant..." in common/nym-lp-data/src/common/traits.rs so the comment
reads "must be able to accommodate the different overhead."

In `@nym-mix-sim/src/topology/directory.rs`:
- Around line 86-97: The From<&Topology> implementation for Directory must
enforce that the topology has at least one node to avoid later unwrap() panics
in random_next_hop/random_route; at the start of fn from(value: &Topology) ->
Self add an explicit check like assert!(!value.nodes.is_empty(), "Topology must
contain at least one node when constructing Directory") (or panic! with the same
clear message) so invalid Topology values fail fast, then proceed to create
Directory via Directory::default(), populate directory.nodes and
directory.clients as before.

---

Nitpick comments:
In `@common/nym-lp-data/src/clients/traits.rs`:
- Around line 184-213: Document the per-tick idempotency contract directly on
the trait methods: add clear doc comments to Reliability::reliable_encode and
Obfuscation::obfuscate stating that each method must be idempotent per timestamp
(calling with the same timestamp more than once must be a no-op and must not
re-emit previously produced chunks), explain the meaning/role of the
Option<Chunk> parameter (None is used to trigger any per-tick maintenance like
catching retransmissions or emitting cover traffic), and note callers may call
the method both with Some(chunk) and later with None for the same timestamp so
implementations must guard against duplicate output; alternatively, if you
prefer behavior instead of docs, refactor process to ensure each stage
(reliable_encode and obfuscate) is invoked exactly once per tick per stage
(passing Option<Chunk>) so no idempotency contract is required.

In `@common/nym-lp-data/src/lib.rs`:
- Around line 70-89: The transform methods (TimedData::data_transform and
TimedData::ts_transform, and the analogous
PipelineData::data_transform/ts_transform) use a FnMut bound and a mut parameter
even though the closure is invoked exactly once; change the generic bounds from
F: FnMut(...) -> ... to F: FnOnce(...) -> ... and remove the unnecessary mut on
the op parameter (e.g., `mut op: F` -> `op: F`) so single-use, consuming move
closures are accepted; update both TimedData and PipelineData variants
accordingly.

In `@nym-mix-sim/src/topology/directory.rs`:
- Around line 64-68: The method random_next_hop currently calls node_ids() which
allocates a Vec<NodeId> per call; change it to sample directly from the
directory's map keys to avoid allocations by using the IteratorRandom helper on
self.nodes.keys() (e.g. import rand::seq::IteratorRandom and call
self.nodes.keys().choose(rng)), dereference the chosen &NodeId to return NodeId,
and keep the existing SAFETY/unwrap assertion (or use expect with the same
message) to preserve the guarantee that the directory is non-empty; this removes
the per-call allocation from node_ids() while keeping semantics for
random_next_hop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a40dff51-5d4a-4e4d-80f5-d273feef3dd3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d161f8 and 8b37cf7.

📒 Files selected for processing (5)
  • common/nym-lp-data/src/clients/traits.rs
  • common/nym-lp-data/src/common/traits.rs
  • common/nym-lp-data/src/lib.rs
  • nym-mix-sim/README.md
  • nym-mix-sim/src/topology/directory.rs

Comment on lines +124 to +126
// IMPORTANT NOTE : This fn can be not constant to allow e.g. flexible MTU
// However, every possible value must be able to accomodate the different overhead.
// If it doesn't, the pipeline becomes unusable
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo: "accomodate" → "accommodate".

Proposed fix
-    // However, every possible value must be able to accomodate the different overhead.
+    // However, every possible value must be able to accommodate the different overhead.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// IMPORTANT NOTE : This fn can be not constant to allow e.g. flexible MTU
// However, every possible value must be able to accomodate the different overhead.
// If it doesn't, the pipeline becomes unusable
// 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.
// If it doesn't, the pipeline becomes unusable
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/nym-lp-data/src/common/traits.rs` around lines 124 - 126, Fix the typo
in the comment: change "accomodate" to "accommodate" in the comment block that
begins "// IMPORTANT NOTE : This fn can be not constant..." in
common/nym-lp-data/src/common/traits.rs so the comment reads "must be able to
accommodate the different overhead."

Comment on lines +86 to +97
fn from(value: &Topology) -> Self {
let mut directory = Directory::default();
for node in &value.nodes {
directory.nodes.insert(node.node_id, node.into());
}
for client in &value.clients {
directory
.clients
.insert(client.client_id, client.mixnet_address);
}
directory
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce non-empty topology invariant at directory construction.

random_next_hop/random_route rely on unwrap() and will panic when nodes is empty. Add an explicit invariant check in From<&Topology> so invalid topology fails fast with a clear message (Line 86 onward), instead of crashing later during routing.

Suggested patch
 impl From<&Topology> for Directory {
@@
     fn from(value: &Topology) -> Self {
+        assert!(
+            !value.nodes.is_empty(),
+            "topology must contain at least one node"
+        );
         let mut directory = Directory::default();
         for node in &value.nodes {
             directory.nodes.insert(node.node_id, node.into());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn from(value: &Topology) -> Self {
let mut directory = Directory::default();
for node in &value.nodes {
directory.nodes.insert(node.node_id, node.into());
}
for client in &value.clients {
directory
.clients
.insert(client.client_id, client.mixnet_address);
}
directory
}
fn from(value: &Topology) -> Self {
assert!(
!value.nodes.is_empty(),
"topology must contain at least one node"
);
let mut directory = Directory::default();
for node in &value.nodes {
directory.nodes.insert(node.node_id, node.into());
}
for client in &value.clients {
directory
.clients
.insert(client.client_id, client.mixnet_address);
}
directory
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nym-mix-sim/src/topology/directory.rs` around lines 86 - 97, The
From<&Topology> implementation for Directory must enforce that the topology has
at least one node to avoid later unwrap() panics in
random_next_hop/random_route; at the start of fn from(value: &Topology) -> Self
add an explicit check like assert!(!value.nodes.is_empty(), "Topology must
contain at least one node when constructing Directory") (or panic! with the same
clear message) so invalid Topology values fail fast, then proceed to create
Directory via Directory::default(), populate directory.nodes and
directory.clients as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant