adds some new notes about sphinx (mc2)
61
10 Notes/DHT Bug in Logos Storage.md
Normal file
@ -0,0 +1,61 @@
|
||||
This code in `vendor/logos-storage-nim-dht/codexdht/private/eth/p2p/discoveryv5/spr.nim`
|
||||
|
||||
```nim
|
||||
proc ip*(r: SignedPeerRecord): Option[array[4, byte]] =
|
||||
for address in r.data.addresses:
|
||||
let ma = address.address
|
||||
let code = ma[0].get.protoCode()
|
||||
if code.isOk and code.get == multiCodec("ip4"):
|
||||
var ipbuf: array[4, byte]
|
||||
let res = ma[0].get.protoArgument(ipbuf)
|
||||
if res.isOk:
|
||||
return some(ipbuf)
|
||||
|
||||
# err("Incorrect IPv4 address")
|
||||
# else:
|
||||
# if (?(?ma[1]).protoArgument(pbuf)) == 0:
|
||||
# err("Incorrect port number")
|
||||
# else:
|
||||
# res.port = Port(fromBytesBE(uint16, pbuf))
|
||||
# ok(res)
|
||||
# else:
|
||||
|
||||
# else:
|
||||
# err("MultiAddress must be wire address (tcp, udp or unix)")
|
||||
|
||||
proc udp*(r: SignedPeerRecord): Option[int] =
|
||||
for address in r.data.addresses:
|
||||
let ma = address.address
|
||||
|
||||
let code = ma[1].get.protoCode()
|
||||
if code.isOk and code.get == multiCodec("udp"):
|
||||
var pbuf: array[2, byte]
|
||||
let res = ma[1].get.protoArgument(pbuf)
|
||||
if res.isOk:
|
||||
let p = fromBytesBE(uint16, pbuf)
|
||||
return some(p.int)
|
||||
```
|
||||
|
||||
```nim
|
||||
proc ip*(r: SignedPeerRecord): Option[array[4, byte]] =
|
||||
for address in r.data.addresses:
|
||||
let ma = address.address
|
||||
let code = ma[0].get.protoCode()
|
||||
if code.isOk and code.get == multiCodec("ip4"):
|
||||
var ipbuf: array[4, byte]
|
||||
let res = ma[0].get.protoArgument(ipbuf)
|
||||
if res.isOk:
|
||||
return some(ipbuf)
|
||||
|
||||
proc udp*(r: SignedPeerRecord): Option[int] =
|
||||
for address in r.data.addresses:
|
||||
let ma = address.address
|
||||
|
||||
let code = ma[1].get.protoCode()
|
||||
if code.isOk and code.get == multiCodec("udp"):
|
||||
var pbuf: array[2, byte]
|
||||
let res = ma[1].get.protoArgument(pbuf)
|
||||
if res.isOk:
|
||||
let p = fromBytesBE(uint16, pbuf)
|
||||
return some(p.int)
|
||||
```
|
||||
72
10 Notes/MixNets - Sphinx YouTube video.md
Normal file
@ -0,0 +1,72 @@
|
||||
A youtube video about Mix and Sphinx: https://youtu.be/34TKXELJa2c?is=J8G4cNyG3djsWdtm
|
||||
|
||||
Notes:
|
||||
|
||||
Mixing strategies and how mixing nodes can add security to the messages:
|
||||
- buffer the messages and send them or regular intervals,
|
||||
- Buffer messages till a selected number of the is collected and then send all of them or just a selection of them - pool model
|
||||
- Messages can be tagged by the sender to indicate special security requirements
|
||||
|
||||
Mix format
|
||||
- has to hide the length of the message
|
||||
- where you are in the path - if you just add a layer of encryption, the message gets longer and longer leaking its potential position in the mix path
|
||||
- includes mechanism that allows the recipient to reply to the message in an anonymous way - it should be indistinguishable from the *forward messages*. It comes from the concept called anonymity set: number of people that use the system - that could have send the given message. E.g. if only, say 10 people use the system it is obviously way easier to figure out the potential recipient and the sender. Thus, the larger the number of people using the system, the bigger the anonymity set and the better protection. If the reply messages are distinguishable from the forward messages, we are effectively dealing with two mix nets with effectively lower corresponding anonymity sets.
|
||||
|
||||
Existing Mix format
|
||||
|
||||
- Mixmaster (1998)
|
||||
- No replies
|
||||
- Heuristic security (you define the level of security by how many known attacks the network survives)
|
||||
- Möller (2003), Camenish & Lysyanskaya (2005)
|
||||
- provable security - a mathematical proof that if you can break some aspect of the system then you can solve some problems believed to be hard (like factoring or discrete logs)
|
||||
- still no (indistinguishable) replies
|
||||
- never actually deployed
|
||||
- Mixminion (2003)
|
||||
- a follow-up to Mixmaster
|
||||
- Heuristic security
|
||||
- has indistinguishable replies
|
||||
- deployed
|
||||
- Minx (2004)
|
||||
- First attempt of a *compact* mix format - the overhead - the headers you need to route the message through the network - has a certain size (kB or tens of kB), which may not be ideal if your messages are very small
|
||||
- indistinguishable replies
|
||||
- heuristic security
|
||||
- BROKEN
|
||||
- Shimsock, Staats, Hopper (2008) - ”SSH 08”
|
||||
- Broke Minx, provided provable security fix
|
||||
- has indistinguishable replies
|
||||
- But: very expensive to construct mix messages
|
||||
- The only way add an e.g. address of the next mix to the message was to share a cryptographic hash with the next mix - that hash has to contain the information (routing info) Alice wants to convey, e.g. to convey a 32 bit address, Alice would have to create mix packets that hash to that 32 bit address (2^32 - 4 billion tries in the worst case)
|
||||
- to make that easier, the only information that Alice is allowed to convey is the identity of the next hop and there are at most 255 different mix nodes - we store their addresses in a table and Alice just has to convey 8 bits of information (indicating the row in the table) - so 2^8 x number of hops.
|
||||
- Sphinx takes the best of the all above approaches
|
||||
- Provable security
|
||||
- supports indistinguishable replies
|
||||
- compact
|
||||
- Computationally cheap
|
||||
|
||||
![[Pasted image 20260331141209.png]]
|
||||
|
||||
- r - maximum length of the path (common value 3, 5, 10)
|
||||
- s - size of the symmetric key in bytes - how much we choose for s depends on the level of security we want - commonly at that time 80-bit security - 2^80 operations for an attacker to brake the system. Today we prefer to use around 128 bits (16 bytes)
|
||||
- p - size of the public key - also depends on the security level you want - e.g. for 80 bits security, you may need to have something like 1000-bit public key if you are using RSA or something like that… It also depends on which system you use: for RSA, Elgamal, or Diffie-Hellman, the key size will be rather big, for elliptic curves the key will be much smaller for the same security.
|
||||
|
||||
Some examples:
|
||||
|
||||
RSA-like…
|
||||
|
||||
![[Pasted image 20260331142627.png]]
|
||||
|
||||
EC:
|
||||
|
||||
![[Pasted image 20260331142912.png]]
|
||||
|
||||
Around 100 bytes for 80-bit security.
|
||||
|
||||
### Sphinx message format
|
||||
|
||||
![[Pasted image 20260331143337.png]]
|
||||
|
||||
![[Pasted image 20260331144536.png]]
|
||||
|
||||
Now, we could be tempted to use the same alpha for each hop (and different for different message/recipient), but then the message coming into the mix node and one coming out to the mix node could be correlated.
|
||||
|
||||
![[Pasted image 20260331145433.png]]
|
||||
@ -0,0 +1,4 @@
|
||||
Giuliano made an excellent analysis:
|
||||
|
||||
- [New Block Protocol - Summary](https://hackmd.io/YKEKMFGgSAaHjVxKcz8n2w)
|
||||
- [Bandwidth-Delay Product Estimations: Are we Doing It Right?](https://hackmd.io/2brFqqXzS9GQ4Ei5xe0gLw) - see my own note [[Bandwidth-Delay Product for Block Exchange Protocol]].
|
||||
@ -0,0 +1,20 @@
|
||||
Regarding the segmentation fault, I had some iterations with gpt-5-codex and here is an edited summary:
|
||||
|
||||
- The crash happens immediately after `Switch.start()` completes in `vendor/nim-codex/codex/codex.nim` (line `238`), before any of the block‑exchange or discovery logs appear. The next code that runs is `nattedAddress(...)`, so the blow‑up is occurring while we compute NAT mappings, not inside libp2p.
|
||||
- `vendor/nim-codex/codex/nat.nim` keeps all runtime state in module‑level globals (`strategy`, `extIp`, `natClosed`, `activeMappings`, `natThreads`) plus a single `addQuitProc(stopNatThreads)` hook. That design only works when one node lives in the process. As soon as you create a second node, both instances mutate the same sequences and atomics:
|
||||
- `redirectPorts` appends to the shared `activeMappings/natThreads` and spawns a renewal thread for every caller. Those renewal threads and any later redirectPorts invocations then race over the same global state.
|
||||
- When the first node shuts down (or when the quit proc runs) `stopNatThreads()` sets the shared `natClosed` atomic to `true`, tears down all threads and deletes every mapping in `activeMappings`, regardless of which node created them. The other live node continues executing with stale NAT state and dangling C pointers, which is consistent with the `SIGSEGV` you’re observing when the second node starts up.
|
||||
- Because the globals are never re‑initialised (`natClosed` never flips back to `false`, `activeMappings` is never cleared safely), the second node inherits half torn‑down structures. That’s why the bug only shows up once you create another node in the same process.
|
||||
- The behaviour matches your platform reports: on Ubuntu (CI) we don’t hit the race because the NAT code exits early (CI runs in environments without UPnP/NAT‑PMP), whereas on your AMD desktops the NAT calls succeed, meaning we actually enter the broken multi‑instance path and crash.
|
||||
- Also notice that the crash only happens once we actually go down the NAT path far enough to spin up per-process state and threads. Arnaud's Fedora does not suffer from the crash (regardless if he uses Nix or without Nix - I am **not** using Nix BTW), but that's can also be explained: if his Fedora is missing/blocked UPnP/NAT-PMP support, or the router refuses those protocols, `getExternalIP()` simply returns `none`. In that case `redirectPorts` and the renewal threads never run, so the global state never gets into a bad cross-node mix — the code just keeps the default `nat=any` without doing any mapping, and everything appears fine. On my AMD boxes the call likely succeeds (UPnP works), so the brittle shared state is exercised and I am hitting the segfault.
|
||||
|
||||
To make multiple nodes safe, we need to turn NAT management into a per-node resource.
|
||||
|
||||
The code on this branch is mostly authored by "gpt-5-codex" and so, it is just an entry point to actual work. The initial fix compiles and appears to work, but we had to temporarily disable the NAT port‑mapping renewal thread so we no longer spawn a background thread with `repeatPortMapping`. This cannot go to production like this as NAT will not be able to adjust to changing conditions, but, I think it is a great start.
|
||||
|
||||
With this initial fix, I can at least start two nodes one after another with libcodex and even two of them at the same time, even with NAT set to `any`. Also apparently it fixed some logging issues that I have observed - for some combination of log levels for the codex nodes in one process, the node that stops second could in some circumstances hang on `stop` and `destroy` operations. With this initial fix, it seems not to be a problem any more.
|
||||
|
||||
To experiments with libcodex, and to experience the problem yourself, you may like to check the following two repos:
|
||||
|
||||
- https://github.com/codex-storage/codex-go-bindings
|
||||
- https://github.com/codex-storage/go-codex-client (can be good for isolated experiments and test runs)
|
||||
2231
10 Notes/Sphinx Header Processing Example.md
Normal file
8
10 Notes/Sphinx Header Processing Infographic.md
Normal file
@ -0,0 +1,8 @@
|
||||
Related to: [[Sphinx Header Processing Example]].
|
||||
|
||||
Here is the Excalidraw drawing demonstrating the creation of the filler, the header, and the subsequent header processing.
|
||||
|
||||
This is editable version (requires Exalidraw plugin for Obsidian). For a non editable shared version
|
||||
that you can use in a browser, please visit: https://link.excalidraw.com/readonly/6y7DRQlUhkkBMZScEONS.
|
||||
|
||||
![[Sphinx Header Processing Infographic 2026-05-11 02.50.17.excalidraw]]
|
||||
1745
10 Notes/Sphinx SURBs implementation in the libp2p MIX protocol.md
Normal file
2
10 Notes/Understanding NAT.md
Normal file
@ -0,0 +1,2 @@
|
||||
Some learning resources about [[NAT]]:
|
||||
- https://tailscale.com/blog/how-nat-traversal-works
|
||||
552
10 Notes/libp2p MIX Architecture and API.md
Normal file
@ -0,0 +1,552 @@
|
||||
The copy of this document can be found in [https://github.com/logos-storage/libp2p-storage-mix-transport/blob/master/docs/mix.md](https://github.com/logos-storage/libp2p-storage-mix-transport/blob/master/docs/mix.md). The content of this document should contain the most recent version.
|
||||
|
||||
> This document belong to the series of [[Mix Implementation]] notes.
|
||||
|
||||
This document describes the MIX implementation vendored with this [project] under
|
||||
`nimbledeps/pkgs2/libp2p-*/libp2p/protocols/mix`. It focuses on how the code is
|
||||
structured, what API surface is intended for callers, and what constraints matter
|
||||
when using MIX as a transport-like wrapper for other libp2p protocols.
|
||||
|
||||
## Overview
|
||||
|
||||
MIX adds an anonymous message routing layer on top of libp2p streams. A sender
|
||||
wraps an application protocol message in a fixed-size Sphinx packet, sends it to
|
||||
a randomly selected mix path, and lets each hop peel one routing layer before
|
||||
forwarding the packet. The final mix hop acts as an exit node and forwards the
|
||||
inner message to the real destination protocol. Replies are optional and are
|
||||
returned through Single Use Reply Blocks (SURBs), so the destination can respond
|
||||
without learning the sender.
|
||||
|
||||
The libp2p protocol ID is:
|
||||
|
||||
```nim
|
||||
const MixProtocolID* = "/mix/1.0.0"
|
||||
```
|
||||
|
||||
At a high level:
|
||||
|
||||
```text
|
||||
sender protocol
|
||||
|
|
||||
| writes to MixEntryConnection
|
||||
v
|
||||
entry MixProtocol
|
||||
|
|
||||
| Sphinx packet over /mix/1.0.0
|
||||
v
|
||||
mix hop 1 -> mix hop 2 -> ... -> exit mix hop
|
||||
|
|
||||
| original codec
|
||||
v
|
||||
destination protocol
|
||||
|
|
||||
| optional response via SURBs
|
||||
v
|
||||
sender MixEntryConnection
|
||||
```
|
||||
|
||||
## Main Modules
|
||||
|
||||
The public import is `libp2p/protocols/mix`. It re-exports the main types and
|
||||
helpers from the implementation modules:
|
||||
|
||||
- `mix_protocol.nim`: main `MixProtocol` implementation, path selection,
|
||||
Sphinx packet handling, SURB creation, forwarding, and reply processing.
|
||||
- `mix_node.nim`: private/public mix node identity records.
|
||||
- `pool.nim`: `MixNodePool`, backed by the libp2p `PeerStore`.
|
||||
- `entry_connection.nim`: `MixEntryConnection`, a `Connection` facade used by
|
||||
local protocols to write through MIX and optionally read replies.
|
||||
- `exit_layer.nim`: destination forwarding and reply dispatch from the exit
|
||||
node.
|
||||
- `reply_connection.nim`: `MixReplyConnection`, a write-only connection facade
|
||||
used by the exit layer to send replies over SURBs.
|
||||
- `serialization.nim`, `sphinx.nim`, `fragmentation.nim`, `mix_message.nim`:
|
||||
fixed packet formats, Sphinx processing, padding, and application message
|
||||
encoding.
|
||||
- `multiaddr.nim`: compact encoding for next-hop and destination addresses.
|
||||
- `delay_strategy.nim`: pluggable per-hop delay strategy.
|
||||
- `spam_protection.nim`: optional per-hop proof interface.
|
||||
- `tag_manager.nim`: replay detection state for Sphinx tags.
|
||||
|
||||
## Core Types
|
||||
|
||||
### `MixProtocol`
|
||||
|
||||
`MixProtocol` is an `LPProtocol` mounted on a libp2p `Switch`. Each running mix
|
||||
node owns one instance.
|
||||
|
||||
Important fields and collaborators:
|
||||
|
||||
- `mixNodeInfo`: this node's libp2p identity and Curve25519 MIX keypair.
|
||||
- `switch`: the underlying libp2p switch used for dialing peers and destinations.
|
||||
- `nodePool`: public information for candidate mix nodes.
|
||||
- `tagManager`: replay detection for processed Sphinx packets.
|
||||
- `exitLayer`: forwards exit messages to destination protocols and sends replies.
|
||||
- `connCreds`: temporary SURB credentials for expected replies.
|
||||
- `destReadBehavior`: per-codec callbacks that tell the exit node how to read a
|
||||
destination response.
|
||||
- `spamProtection`: optional proof generator/verifier.
|
||||
- `delayStrategy`: pluggable encoded/actual delay policy.
|
||||
|
||||
Create and mount it with:
|
||||
|
||||
```nim
|
||||
let proto = MixProtocol.new(nodeInfo, switch)
|
||||
await proto.start()
|
||||
proto.nodePool.add(otherMixNodes)
|
||||
switch.mount(proto)
|
||||
```
|
||||
|
||||
`MixProtocol.new` defaults to `NoSamplingDelayStrategy` and no spam protection.
|
||||
|
||||
### `MixNodeInfo` and `MixPubInfo`
|
||||
|
||||
`MixNodeInfo` contains private node data and is used by the local mix node:
|
||||
|
||||
```nim
|
||||
type MixNodeInfo* = object
|
||||
peerId*: PeerId
|
||||
multiAddr*: MultiAddress
|
||||
mixPubKey*: FieldElement
|
||||
mixPrivKey*: FieldElement
|
||||
libp2pPubKey*: SkPublicKey
|
||||
libp2pPrivKey*: SkPrivateKey
|
||||
```
|
||||
|
||||
`MixPubInfo` is the shareable subset stored in `MixNodePool`:
|
||||
|
||||
```nim
|
||||
type MixPubInfo* = object
|
||||
peerId*: PeerId
|
||||
multiAddr*: MultiAddress
|
||||
mixPubKey*: FieldElement
|
||||
libp2pPubKey*: SkPublicKey
|
||||
```
|
||||
|
||||
Convenience helpers:
|
||||
|
||||
- `MixNodeInfo.generateRandom(port)`
|
||||
- `MixNodeInfo.generateRandomMany(count, basePort = 4242)`
|
||||
- `initMixNodeInfo(...)`
|
||||
- `toMixPubInfo(info)`
|
||||
- `includeAllExcept(allNodes, exceptNode)`
|
||||
|
||||
In local examples, switches are started first so wildcard listen addresses are
|
||||
resolved, then `initMixNodeInfo` rebuilds each node record with a dialable
|
||||
address from `switch.peerInfo.addrs`.
|
||||
|
||||
### `MixNodePool`
|
||||
|
||||
`MixNodePool` manages public mix node information through the switch peer store.
|
||||
It writes to the peer store's mix public key book, address book, and key book.
|
||||
|
||||
Public operations:
|
||||
|
||||
```nim
|
||||
let pool = MixNodePool.new(switch.peerStore)
|
||||
pool.add(info)
|
||||
pool.add(infos)
|
||||
discard pool.remove(peerId)
|
||||
let infoOpt = pool.get(peerId)
|
||||
let ids = pool.peerIds()
|
||||
let n = pool.len
|
||||
```
|
||||
|
||||
Supported addresses are IPv4 TCP and IPv4 QUIC-v1, including supported
|
||||
circuit-relay addresses over those base transports. IPv6, DNS, WebSocket, and
|
||||
related address forms are not supported by the compact address encoder in this
|
||||
implementation.
|
||||
|
||||
The implementation requires Secp256k1 libp2p peer IDs when serializing hop
|
||||
addresses.
|
||||
|
||||
### `MixDestination`
|
||||
|
||||
`MixDestination` identifies the final target of a mixed message.
|
||||
|
||||
Normal usage forwards from the exit node to a destination address:
|
||||
|
||||
```nim
|
||||
let destination = MixDestination.init(destPeerId, destAddr)
|
||||
```
|
||||
|
||||
This is equivalent to `MixDestination.forwardToAddr(destPeerId, destAddr)`.
|
||||
|
||||
There is also compile-time experimental support for `exit == destination` behind
|
||||
`-d:libp2p_mix_experimental_exit_is_dest`. Without that define, the destination
|
||||
must be a separate forwarded address.
|
||||
|
||||
### `MixParameters`
|
||||
|
||||
`MixParameters` controls entry connection behavior:
|
||||
|
||||
```nim
|
||||
type MixParameters* = object
|
||||
expectReply*: Opt[bool]
|
||||
numSurbs*: Opt[uint8]
|
||||
```
|
||||
|
||||
If `expectReply` is true, `toConnection` creates an incoming queue and the
|
||||
sender embeds SURBs in the outgoing payload. If `numSurbs` is omitted while a
|
||||
reply is expected, `MixEntryConnection` uses a default of 4 SURBs. If no reply is
|
||||
expected, the number of SURBs is forced to 0.
|
||||
|
||||
### `DestReadBehavior`
|
||||
|
||||
When a request expects a reply, the exit node needs to know how to read a
|
||||
response from the destination protocol connection. Register that per application
|
||||
codec on every mix node that may be selected as an exit:
|
||||
|
||||
```nim
|
||||
proto.registerDestReadBehavior(PingCodec, readExactly(32))
|
||||
```
|
||||
|
||||
The public helpers are:
|
||||
|
||||
```nim
|
||||
readExactly(nBytes: int): DestReadBehavior
|
||||
readLp(maxSize: int): DestReadBehavior
|
||||
```
|
||||
|
||||
Use `readExactly` for fixed-size response protocols. Use `readLp` when the
|
||||
destination protocol returns length-prefixed messages using libp2p's varint LP
|
||||
framing. For `readLp`, the exit layer restores the length prefix before sending
|
||||
the reply through MIX, allowing the original caller to call `readLp()` on the
|
||||
returned `MixEntryConnection`.
|
||||
|
||||
## Sending Through MIX
|
||||
|
||||
### What "writes to `MixEntryConnection`" means
|
||||
|
||||
In libp2p Nim, application protocols usually talk to a generic
|
||||
`Connection`. For example, the Ping client does not know whether the connection
|
||||
is a direct TCP stream, QUIC stream, relay stream, or MIX-backed connection:
|
||||
|
||||
```nim
|
||||
proc ping*(p: Ping, conn: Connection): Future[Duration] =
|
||||
await conn.write(randomBytes)
|
||||
await conn.readExactly(addr resultBuf[0], 32)
|
||||
```
|
||||
|
||||
`MixEntryConnection` is a custom `Connection` implementation. It has the same
|
||||
`write` and `readOnce`/`readExactly` behavior that application protocols expect,
|
||||
but its `write` method does not write directly to the destination. Instead, it
|
||||
calls into `MixProtocol.anonymizeLocalProtocolSend`, which wraps that write in a
|
||||
Sphinx packet and sends it through the mixnet.
|
||||
|
||||
So "writes to `MixEntryConnection`" normally happens indirectly:
|
||||
|
||||
1. Application code calls an existing protocol helper, such as
|
||||
`pingProto.ping(conn)`.
|
||||
2. That helper calls `conn.write(...)`.
|
||||
3. Because `conn` is actually a `MixEntryConnection`, the write becomes a MIX
|
||||
send.
|
||||
4. If the helper later calls `conn.readExactly(...)`, `MixEntryConnection` waits
|
||||
for a SURB reply and serves the reply bytes from its local incoming queue.
|
||||
|
||||
This is why `mix_ping_tcp.nim` can reuse the normal Ping client logic:
|
||||
|
||||
```nim
|
||||
let pingProto = Ping.new()
|
||||
destNode.mount(pingProto)
|
||||
|
||||
let conn = mixProtos[senderIndex]
|
||||
.toConnection(
|
||||
MixDestination.init(destNode.peerInfo.peerId, destNode.peerInfo.addrs[0]),
|
||||
PingCodec,
|
||||
MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))),
|
||||
)
|
||||
.expect("could not build connection")
|
||||
|
||||
let response = await pingProto.ping(conn)
|
||||
```
|
||||
|
||||
There are two different uses of the same `Ping` instance here:
|
||||
|
||||
- Mounted on `destNode`, `Ping` acts as the destination server handler. It reads
|
||||
32 bytes and writes the same 32 bytes back.
|
||||
- Called as `pingProto.ping(conn)`, `Ping` acts as client-side helper code. It
|
||||
only needs a `Connection`, so it can use the MIX-backed connection returned by
|
||||
`toConnection`.
|
||||
|
||||
### Preferred API: `toConnection`
|
||||
|
||||
The ergonomic API is `toConnection`, which returns a libp2p `Connection`
|
||||
implementation. Use this when you want an existing libp2p protocol helper to run
|
||||
over MIX:
|
||||
|
||||
```nim
|
||||
let conn = mixProto
|
||||
.toConnection(
|
||||
MixDestination.init(destNode.peerInfo.peerId, destNode.peerInfo.addrs[0]),
|
||||
PingCodec,
|
||||
MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))),
|
||||
)
|
||||
.expect("could not build connection")
|
||||
|
||||
let response = await pingProto.ping(conn)
|
||||
await conn.close()
|
||||
```
|
||||
|
||||
`MixEntryConnection.write` sends each write as one independent MIX message by
|
||||
calling `MixProtocol.anonymizeLocalProtocolSend`. `readOnce` waits for one reply
|
||||
if the connection was created with `expectReply = true`.
|
||||
|
||||
If `expectReply = true` and no destination read behavior has been registered on
|
||||
the sender's local `MixProtocol`, `toConnection` returns an error. The actual
|
||||
response read happens on the selected exit node, so practical deployments should
|
||||
register the same behavior on all candidate exit nodes. If no reply is expected,
|
||||
missing read behavior only logs a warning.
|
||||
|
||||
### Lower-level API: `anonymizeLocalProtocolSend`
|
||||
|
||||
`MixProtocol.anonymizeLocalProtocolSend` is also exported from
|
||||
`mix_protocol.nim` itself. It sends one byte message through MIX without first
|
||||
constructing a `Connection` facade:
|
||||
|
||||
```nim
|
||||
let incoming = newAsyncQueue[seq[byte]]()
|
||||
let sendRes = await mixProto.anonymizeLocalProtocolSend(
|
||||
incoming,
|
||||
msg,
|
||||
codec,
|
||||
destination,
|
||||
numSurbs,
|
||||
)
|
||||
```
|
||||
|
||||
This is useful if you are building a custom adapter and want direct control over
|
||||
the incoming reply queue. It is less convenient for normal libp2p protocols
|
||||
because those protocols usually expect to read and write through a `Connection`.
|
||||
|
||||
In this implementation, the practical outbound interaction models are:
|
||||
|
||||
- Use `toConnection` and pass the returned connection to an existing protocol
|
||||
client helper. This is the normal path.
|
||||
- Use `toConnection` directly and call `conn.write`, `conn.readExactly`,
|
||||
`conn.readLp`, etc. yourself.
|
||||
- Call `anonymizeLocalProtocolSend` from `mix_protocol.nim` for a lower-level
|
||||
one-message send.
|
||||
|
||||
Inbound mixnet traffic is not handled by application code directly. Mix nodes
|
||||
receive `/mix/1.0.0` streams through the mounted `MixProtocol`; the exit layer
|
||||
then dials the destination using the original application codec.
|
||||
|
||||
## Internal Message Flow
|
||||
|
||||
### Entry
|
||||
|
||||
`anonymizeLocalProtocolSend` performs the sender-side work:
|
||||
|
||||
1. Check that the node pool has at least `PathLength` available public mix nodes.
|
||||
2. Randomly select a path from `nodePool`, excluding the final destination if it
|
||||
is also present in the pool.
|
||||
3. Use the last selected mix node as the exit node.
|
||||
4. Encode each hop as compact bytes containing IPv4 address, transport, port,
|
||||
optional relay peer ID, and peer ID.
|
||||
5. Generate per-hop delay values using `delayStrategy.generateForEntry`.
|
||||
6. Optionally create SURBs for replies and prepend them to the application
|
||||
message.
|
||||
7. Encode the application message as `MixMessage(message, codec)`.
|
||||
8. Pad it into one fixed-size `MessageChunk`.
|
||||
9. Wrap it into a Sphinx packet using the selected public keys, delays, hops,
|
||||
and final destination.
|
||||
10. Optionally append a spam protection proof.
|
||||
11. Send the packet to the first hop over `/mix/1.0.0`.
|
||||
|
||||
### Intermediate Hop
|
||||
|
||||
Each mix node handles inbound `/mix/1.0.0` streams in
|
||||
`handleMixNodeConnection`. For every length-prefixed packet read from the stream,
|
||||
it spawns `handleMixMessages`.
|
||||
|
||||
For intermediate packets:
|
||||
|
||||
1. Extract the optional spam proof.
|
||||
2. Deserialize the Sphinx packet.
|
||||
3. Check replay tags with the node's MIX private key and `TagManager`.
|
||||
4. Verify spam protection after the replay check.
|
||||
5. Process the Sphinx packet to reveal the next hop and transformed packet.
|
||||
6. Compute the actual delay using `delayStrategy.generateForIntermediate`.
|
||||
7. Generate a fresh per-hop spam proof for the outgoing packet if enabled.
|
||||
8. Sleep for the computed delay.
|
||||
9. Forward to the next hop over `/mix/1.0.0`.
|
||||
|
||||
Connections to next hops are cached by peer ID in `connPool`. If a cached stream
|
||||
is closed or reset, the implementation dials a fresh stream and retries the
|
||||
write.
|
||||
|
||||
### Exit
|
||||
|
||||
When Sphinx processing returns `Exit`, the node:
|
||||
|
||||
1. Deserializes and unpads the `MessageChunk`.
|
||||
2. Deserializes the `MixMessage` to recover `codec` and `message`.
|
||||
3. Extracts any embedded SURBs.
|
||||
4. Passes the request to `ExitLayer.onMessage`.
|
||||
|
||||
For normal forwarded destinations, `ExitLayer` dials the destination peer using
|
||||
the recovered codec, writes the raw inner message, and optionally reads a
|
||||
response using the registered `DestReadBehavior`.
|
||||
|
||||
If SURBs were provided, the exit layer sends the response back through every SURB
|
||||
using a `MixReplyConnection`.
|
||||
|
||||
### Reply
|
||||
|
||||
Replies do not contain an application codec; they are associated with a SURB
|
||||
identifier generated by the original sender.
|
||||
|
||||
The original sender stores each generated SURB identifier in `connCreds` with
|
||||
the SURB key, secret, identifier group, and incoming queue. When a `Reply` packet
|
||||
arrives:
|
||||
|
||||
1. The sender looks up the SURB identifier.
|
||||
2. It processes the reply using the stored SURB key and secret.
|
||||
3. It deletes all credentials in the same identifier group.
|
||||
4. It deserializes, unpads, and decodes the reply message.
|
||||
5. It places the reply bytes on the entry connection's incoming queue.
|
||||
|
||||
Because the credentials are single-use, one response consumes the associated SURB
|
||||
group.
|
||||
|
||||
## Packet and Payload Limits
|
||||
|
||||
Important constants from `serialization.nim` and `fragmentation.nim`:
|
||||
|
||||
```nim
|
||||
PacketSize = 4608
|
||||
MessageSize = PacketSize - HeaderSize - k
|
||||
DataSize = MessageSize - PaddingLengthSize - SeqNoSize
|
||||
PathLength = 3
|
||||
```
|
||||
|
||||
Current code builds a single padded `MessageChunk` for each `write`; it does not
|
||||
automatically split large application messages in `anonymizeLocalProtocolSend`.
|
||||
`MixEntryConnection.write` rejects messages larger than `DataSize`, and
|
||||
`buildMessage` rejects serialized `MixMessage` values larger than `DataSize`.
|
||||
|
||||
Because MIX adds codec and SURB overhead inside the fixed payload, callers should
|
||||
use:
|
||||
|
||||
```nim
|
||||
let maxPayload = getMaxMessageSizeForCodec(codec, numberOfSurbs)
|
||||
```
|
||||
|
||||
This returns the maximum application message size available for the given codec
|
||||
and number of embedded SURBs.
|
||||
|
||||
## Delay Strategies
|
||||
|
||||
Delay behavior is pluggable through `DelayStrategy`.
|
||||
|
||||
Built-in strategies:
|
||||
|
||||
- `NoSamplingDelayStrategy`: default. The entry node encodes a random delay in
|
||||
the range 0-2 ms, and intermediate nodes use that value directly.
|
||||
- `ExponentialDelayStrategy`: the entry node encodes a mean delay, and
|
||||
intermediate nodes sample from an exponential distribution with that mean.
|
||||
The default mean is `DefaultMeanDelayMs = 100`.
|
||||
|
||||
Example:
|
||||
|
||||
```nim
|
||||
let rng = newRng()
|
||||
let delay = ExponentialDelayStrategy.new(meanDelayMs = 100, rng = rng)
|
||||
let proto = MixProtocol.new(nodeInfo, switch, rng = rng, delayStrategy = Opt.some(delay))
|
||||
```
|
||||
|
||||
When spam protection is enabled, proof generation runs in parallel with the
|
||||
intermediate delay. If proof generation takes longer than the configured delay,
|
||||
the effective delay becomes the proof generation time.
|
||||
|
||||
## Spam Protection
|
||||
|
||||
Spam protection is optional and is represented by an abstract `SpamProtection`
|
||||
object:
|
||||
|
||||
```nim
|
||||
type SpamProtection* = ref object of RootObj
|
||||
proofSize*: int
|
||||
|
||||
method generateProof(self: SpamProtection, bindingData: seq[byte]): Result[seq[byte], string]
|
||||
method verifyProof(self: SpamProtection, encodedProofData: seq[byte], bindingData: seq[byte]): Result[bool, string]
|
||||
```
|
||||
|
||||
If provided to `MixProtocol.new`, the wire packet becomes:
|
||||
|
||||
```text
|
||||
[Sphinx packet: 4608 bytes][proof: proofSize bytes]
|
||||
```
|
||||
|
||||
Each hop extracts and verifies the incoming proof, processes the Sphinx packet,
|
||||
then generates a fresh proof for the next hop. If no spam protection instance is
|
||||
provided, packets are just the fixed-size Sphinx packet.
|
||||
|
||||
## Address Constraints
|
||||
|
||||
Hop and destination addresses are encoded into a fixed `AddrSize` field. The
|
||||
implementation currently supports:
|
||||
|
||||
- `/ip4/.../tcp/...`
|
||||
- `/ip4/.../udp/.../quic-v1`
|
||||
- circuit-relay variants over those base transports
|
||||
|
||||
Current limitations:
|
||||
|
||||
- No IPv6 support.
|
||||
- No DNS or DNS4 support.
|
||||
- No WebSocket, WebSocket Secure, or SNI support.
|
||||
- Peer IDs must serialize to the expected Secp256k1 multihash length.
|
||||
|
||||
Invalid or unsupported mix node addresses are removed from consideration during
|
||||
path construction.
|
||||
|
||||
## Minimal Setup Pattern
|
||||
|
||||
The local TCP and QUIC examples follow this shape:
|
||||
|
||||
```nim
|
||||
let mixNodeInfos = MixNodeInfo.generateRandomMany(NumMixNodes)
|
||||
|
||||
for nodeInfo in mixNodeInfos:
|
||||
let switch = createSwitch(nodeInfo.multiAddr, Opt.some(nodeInfo.libp2pPrivKey))
|
||||
await switch.start()
|
||||
switches.add(switch)
|
||||
|
||||
let resolvedInfos = collect:
|
||||
for i, nodeInfo in mixNodeInfos:
|
||||
initMixNodeInfo(
|
||||
nodeInfo.peerId,
|
||||
switches[i].peerInfo.addrs[0],
|
||||
nodeInfo.mixPubKey,
|
||||
nodeInfo.mixPrivKey,
|
||||
nodeInfo.libp2pPubKey,
|
||||
nodeInfo.libp2pPrivKey,
|
||||
)
|
||||
|
||||
for i, nodeInfo in resolvedInfos:
|
||||
let proto = MixProtocol.new(nodeInfo, switches[i])
|
||||
await proto.start()
|
||||
proto.nodePool.add(resolvedInfos.includeAllExcept(nodeInfo))
|
||||
proto.registerDestReadBehavior(PingCodec, readExactly(32))
|
||||
switches[i].mount(proto)
|
||||
```
|
||||
|
||||
For QUIC, build switches with `TransportType.QUIC` and use
|
||||
`/ip4/0.0.0.0/udp/0/quic-v1` listen addresses.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Every mix participant must mount `MixProtocol` on its switch.
|
||||
- The sender's node pool must contain enough usable mix nodes for `PathLength`.
|
||||
- Destination protocols do not need to know about MIX when using normal
|
||||
exit-forwarding mode; the exit node dials them by their original codec.
|
||||
- Replies require a registered `DestReadBehavior` for the application codec.
|
||||
Register it on all candidate exit nodes; the sender also checks for a local
|
||||
registration before creating a reply-capable connection.
|
||||
- MIX messages are independent and stateless at the routing layer, apart from
|
||||
replay protection tags, cached outbound streams, and temporary SURB credentials.
|
||||
- `MixProtocol.stop` marks the protocol stopped and stops the tag manager, but
|
||||
it does not perform broad connection-pool or credential cleanup.
|
||||
- Benchmark-only metadata framing is compiled in with `-d:enable_mix_benchmarks`.
|
||||
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_0.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_3.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_4.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
90 Extras/92 Assets/MixNets - Sphinx YouTube video_5.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 642 KiB |