End-to-End Data Flow
This page traces a single anonymous JSON-RPC request from user to Substrate full node and back.
Complete Sequence
Phase-by-Phase Breakdown
Phase 1: Request Construction (Client Side)
- The smoldot light client sends a JSON-RPC request to
blindhop-proxyover WebSocket - The proxy checks the current
PrivacyMode - For None mode: the request is forwarded directly to the target RPC endpoint
- For Fast/Full mode: the request is wrapped in a binary
MixnetMessageframe with a unique correlation ID:pub struct MixnetMessage {pub payload: Vec<u8>,pub msg_type: MessageType,pub correlation_id: u64,pub accepts_compression: bool,} - The frame is sent through the Nym SDK client, while the proxy awaits the specific correlation ID on a background receiver task.
Phase 2: Mixnet Traversal
The Nym SDK handles all mixnet operations transparently:
- The message is split into Sphinx packets (fixed-size, indistinguishable)
- Each packet traverses 5 hops (Full mode) or 2 hops (Fast mode: gateway to gateway)
- At each hop (in Full mode):
- The mix node decrypts one Sphinx layer
- Applies a Poisson-distributed delay (Loopix protocol)
- Forwards to the next hop
- In Full mode, cover traffic (dummy packets) flows continuously, making real traffic indistinguishable
Phase 3: RPC Forwarding (Exit Service)
- The
blindhop-exitreceives the message as a Nym Service Provider - Messages without reply SURBs are dropped immediately
- Policy checks: request size must be ≤ 1 MiB; single JSON-RPC requests with a numeric or string
idonly; method must be in the allowlist - Concurrency limiter: acquires a permit (max 16 global concurrent requests, 8 per client tag, waits up to 5s before returning
BUSY -32005) - The request is processed concurrently on its own task using a pooled WebSocket connection (
UpstreamPool) to the Substrate full node - The full node processes the request and returns a response (≤ 4 MiB, 30 s upstream timeout)
Phase 4: Anonymous Reply
- If the request set the
0x40compression flag and the response is ≥ 1 KiB, the exit compresses the payload using raw deflate (only when that makes it smaller) - The exit builds a binary response frame containing the same correlation ID
- The response is sent via
send_reply()using the sender's anonymous SURB tag - The exit sends replies immediately (without Poisson distribution delays) to prevent buffering bottlenecks
- The reply traverses the mixnet via the pre-built SURB return path
- The proxy's background receiver decompresses if needed (capped at 8 MiB to prevent decompression bombs), matches the correlation ID, and delivers the response to the waiting caller
Latency
Measured on Nym mainnet with small requests (e.g. chain_getHeader):
| None | Fast (2-hop) | Full (5-hop) | |
|---|---|---|---|
| Round-trip p50 | RPC node latency (~50–200 ms) | ~1.4–1.8 s | ~2.0 s |
| Round-trip p90 | — | ~1.5–1.9 s | ~3.1 s |
Where the time goes in Nym modes: the request crosses the entry and exit gateways (plus three mix layers with Poisson delays in Full mode), the exit forwards it over a pooled WebSocket, and the reply returns along the same kind of path on the client's SURBs. Fast mode also skips mix nodes on the return path, since its SURBs are built without mix hops.
Large replies take longer: state_getMetadata (~1.2 MB) completes in ~9–11 s in Full mode with deflate compression. The proxy fails a request that gets no reply within 60 s.
MixnetMessage Wire Format
┌────────────────────────────────────────────────────────┐
│ Type byte (1 byte) │
│ 0x01 = Request, 0x02 = Response │
│ | 0x40 = Accepts compression (client -> exit flag) │
│ | 0x80 = Payload is raw-deflate compressed │
├────────────────────────────────────────────────────────┤
│ Correlation ID (8 bytes, big-endian u64) │
├────────────────────────────────────────────────────────┤
│ Payload (raw JSON-RPC bytes or compressed bytes) │
└────────────────────────────────────────────────────────┘
This 9-byte header replaces earlier JSON serialization, cutting payload overhead by ~3.5× and enabling raw deflate compression for large responses. (For backwards compatibility, incoming messages starting with { are still parsed as legacy JSON envelopes).