a70e68c7bd
* Smolmix documentation * Add smolmix docs: landing page, tutorials, and developer page links * Add Exit Gateway services page (NR vs IPR) and link from existing docs * Update auto-generated command and API outputs * Reorg of tutorials and architecture pages * License information + remove TODO from docs.rs visibile comment + reorg readme * Add versions file for doc-wide versioning * Relative -> absolute links * Relative -> absolute links * Update license + add old tutorial code as examples * Streamline smolmix docs * Clippy * Clean up doc comments * Last pass * Add larger file download to list * set new versions * Clippy * Remove blake pin from docs + add version range to root Cargo.toml * Format example logging * Remove crate blocked component * Loose whitespace * Add doc verification script for inline mdx * Formatting * Components regen * Reorg + tighten text * Voicing cohesion pass + remove bloated examples * Voicing cont. * Reduce max download size * Small suggested clarifications * Max/docs voicing consistency (#6769) * Reduce max download size * voicing consistency across docs * New landing order w smolmix * Tweaks * Final tweaks
93 lines
4.1 KiB
Plaintext
93 lines
4.1 KiB
Plaintext
---
|
|
title: "Stream Module Architecture"
|
|
description: "Internal architecture of the Nym Stream subsystem: wire protocol, multiplexing, router task, and how concurrent byte channels share a single MixnetClient."
|
|
schemaType: "TechArticle"
|
|
section: "Developers"
|
|
lastUpdated: "2026-03-15"
|
|
---
|
|
|
|
# Stream Architecture
|
|
|
|
import { Callout } from 'nextra/components'
|
|
|
|
{/* Canonical source: sdk/rust/nym-sdk/src/mixnet/stream/ARCHITECTURE.md */}
|
|
|
|
## Overview
|
|
|
|
The stream subsystem gives each `MixnetClient` the ability to hold many concurrent byte channels (`AsyncRead + AsyncWrite`) to different remote peers, multiplexed over a single client connection.
|
|
|
|
```mermaid
|
|
---
|
|
config:
|
|
theme: neo-dark
|
|
---
|
|
flowchart TD
|
|
subgraph MixnetClient
|
|
SA["MixnetStream A"] -->|writes| CI["Client input channel"]
|
|
SB["MixnetStream B"] -->|writes| CI
|
|
CI --> MX["── Mixnet ──"]
|
|
MX --> RT["Router task"]
|
|
RT -->|Open messages| ML["MixnetListener.accept()"]
|
|
RT -->|Data messages| SM["Stream routing table"]
|
|
SM --> SA
|
|
SM --> SB
|
|
end
|
|
```
|
|
|
|
## Wire protocol
|
|
|
|
Every stream message has a fixed 16-byte LP frame header prepended to the payload:
|
|
|
|
```
|
|
[LpFrameKind: 2 bytes LE][StreamId: 8 bytes BE][MsgType: 1 byte][SequenceNum: 4 bytes BE][Reserved: 1 byte][payload ...]
|
|
```
|
|
|
|
- **LpFrameKind:** `3` (SphinxStream). Distinguishes stream traffic from other LP frame types (Opaque, Registration, Forward).
|
|
- **StreamId:** random `u64` generated by the opener, used to multiplex streams.
|
|
- **MsgType:** `Open` (0) or `Data` (1).
|
|
- **SequenceNum:** `u32` counter, incremented per write. Used by the receiver's per-stream reorder buffer to deliver data in the correct order.
|
|
- **Reserved:** must be `0x00`.
|
|
|
|
There is no `Close` message type; see [Known Limitations](#known-limitations) for why.
|
|
|
|
## Stream mode
|
|
|
|
Stream mode is activated lazily on the first call to `open_stream()` or `listener()`. This is a **one-way transition**:
|
|
|
|
1. The client's message receiver is handed off to a background router task
|
|
2. `stream_mode` flag is set to `true`
|
|
3. Message-based methods (`send_plain_message`, `wait_for_messages`) are disabled and return errors
|
|
|
|
There is no switching back without disconnecting and creating a new client.
|
|
|
|
## Opening and accepting streams
|
|
|
|
**Opening (outbound):**
|
|
1. `open_stream(recipient, surbs)` generates a random `StreamId`
|
|
2. An `Open` message is sent through the Mixnet to the recipient
|
|
3. A `MixnetStream` is returned, ready for writing and reading
|
|
|
|
**Accepting (inbound):**
|
|
1. `listener.accept()` waits for an `Open` message from a remote peer
|
|
2. A `MixnetStream` is created with the opener's `sender_tag` for anonymous replies
|
|
3. The stream is ready for bidirectional I/O
|
|
|
|
## Cleanup
|
|
|
|
- **On `drop`:** the stream deregisters from the routing table. No close message is sent over the wire.
|
|
- **Idle timeout:** streams idle for longer than the configured timeout (default: 30 minutes) are automatically cleaned up. Configure with [`MixnetClientBuilder::with_stream_idle_timeout()`](https://docs.rs/nym-sdk/latest/nym_sdk/mixnet/struct.MixnetClientBuilder.html).
|
|
|
|
## Known limitations
|
|
|
|
<Callout type="info">
|
|
The Mixnet does not guarantee message ordering at the transport level, but each stream write includes a `sequence_num` in the LP frame header. The receiver maintains a per-stream reorder buffer (BTreeMap keyed by sequence number) that buffers out-of-order messages and drains them in sequence, so protocols that depend on byte ordering (HTTP, TLS, protobuf) work correctly over streams.
|
|
|
|
- **Buffer cap:** 256 messages per stream. If the buffer fills (e.g. a large gap in sequence numbers), the receiver skips ahead to the lowest buffered sequence.
|
|
- **Duplicates:** messages with a sequence number below the next expected are dropped.
|
|
- There is no `Close` message type, since a close could race ahead of in-flight data.
|
|
</Callout>
|
|
|
|
## Internal details
|
|
|
|
For the full implementation details (router task, `StreamMap`, `PollSender` usage, base-client type rationale), see the `ARCHITECTURE.md` file next to the module source code, or the [docs.rs](https://docs.rs/nym-sdk/latest/nym_sdk/) API reference.
|