Update design docs and diagrams for Noise XX and forklift changes

Update CHANGELOG Breaking section with all v0.4.0 wire format
changes. Comprehensive update to fips-wire-formats.md (handshake,
MMP, FilterAnnounce, discovery, size tables).

Update fips-mesh-layer.md, fips-session-layer.md, fips-intro.md
for IK/XK→XX transition and spin bit removal. Document
disable_routing and leaf_only profile config in
fips-configuration.md. Update fips-transport-layer.md for 4-byte
beacon and unified header. Update fips-bloom-filters.md for delta
compression and variable sizing. Update fips-mesh-operation.md for
node profiles and discovery min_mtu.

Create/update 8 SVG diagrams (XX msg1/2/3, handshake flow, session
setup/ack/msg3, filter-announce, sender/receiver report). Update
README.md Noise pattern references.
This commit is contained in:
Johnathan Corgan
2026-04-11 13:21:47 +00:00
parent 10122d7d87
commit 44667d9cc2
22 changed files with 847 additions and 612 deletions
+73 -1
View File
@@ -7,7 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Breaking
_Wire-format and API breaking changes for the next major release._
Wire-format breaking changes for v0.4.0. All nodes in a mesh must
run the same major version — these changes are not backward compatible
with v0.2.x peers.
### Changed
#### Noise XX Handshake (FMP and FSP)
- FMP link handshake switched from Noise IK (2 messages) to Noise XX
(3 messages). Neither side requires prior knowledge of the peer's
static key. Responder identity revealed in msg2, initiator in msg3.
FMP wire version incremented to 1.
- FSP session handshake switched from Noise XK to Noise XX. Same
3-message flow with post-handshake identity verification using
x-only key comparison (parity-independent for npub compatibility).
- Protocol negotiation payload added to XX msg2/msg3 for both layers:
format byte, packed version min/max, 64-bit feature bitfield, and
forward-compatible TLV extensions. Enables rolling protocol upgrades
in future releases.
- FMP msg1 reduced from 106 to 33 bytes (ephemeral key only, no
encrypted static key or DH products).
#### FMP Node Profiles
- Node profile enum (Full, NonRouting, Leaf) advertised in FMP feature
bitfield bits 0-2. At least one side of a link must be Full.
- MMP report flow gated by wants/provides bits (bits 3-6): reports
only sent when the sender can provide and the receiver wants them.
- Non-routing nodes receive bloom filters (one-way) but do not send
them; the full peer inserts their identity as a leaf dependent.
- Leaf nodes enforce single-peer constraint with no tree, bloom, or
transit participation.
#### MMP Report Format
- Spin bit removed. Reclaims FMP flags bit 2 and FSP inner flags
bit 0. Superseded by MMP receiver report timestamp echo for RTT.
- SenderReport reduced from 48 to 20 bytes (3 fields: interval
packets/bytes sent, cumulative packets sent).
- ReceiverReport reduced from 68 to 54 bytes (10 fields retained;
removed max/mean burst loss and interval recv counters).
- Both report types use extensibility header: `[format_version:1]
[total_length:2 LE]` replacing reserved bytes. Decoders skip
unknown trailing bytes for forward compatibility.
#### Discovery Wire Format
- Dropped `origin_coords` from LookupRequest (saves 2 + 16*depth
bytes per request). Reverse-path routing via `recent_requests` is
the primary response mechanism.
- `min_mtu` field wired up in LookupRequest: transit nodes skip peers
whose link MTU is below the request's minimum.
- TLV extension section added to LookupRequest and LookupResponse
after fixed fields. Transit nodes forward TLV bytes verbatim.
#### Shared-Media Beacons
- Ethernet frame header unified to 4 bytes `[type][flags][length:2
LE]` for all frame types. Beacons reduced from 34 to 5 bytes
(pubkey stripped — identity learned from XX handshake).
- BLE pre-handshake pubkey exchange removed. Cross-probe tie-breaker
eliminated (unnecessary with XX).
#### Bloom Filter Wire Format
- FilterAnnounce gains flags byte (delta bit), `base_seq` field, and
RLE-compressed payload for XOR-diff delta compression.
- New FilterNack message type (0x21) for out-of-sequence delta
recovery (triggers full retransmit).
- Filter size decoupled from FMP negotiation: announced dynamically in
filter updates. Bit 7 and TLV field 1 removed from handshake.
- Variable filter sizes (512 bytes to 32 KB) with adaptive sizing
based on outgoing fill ratio (step-up at 20%, step-down at 5%).
## [Unreleased]
+4 -3
View File
@@ -40,8 +40,9 @@ endpoints.
filter guided discovery, no global routing tables
- **Multi-transport** — UDP, TCP, Ethernet, Tor, and Bluetooth (BLE L2CAP)
today; designed for serial and radio
- **Noise encryption** — hop-by-hop link encryption (IK) plus independent
end-to-end session encryption (XK), with periodic rekey for forward secrecy
- **Noise encryption** — hop-by-hop link encryption and independent
end-to-end session encryption (both Noise XX), with periodic rekey for
forward secrecy and protocol negotiation in the handshake
- **Nostr-native identity** — secp256k1 keypairs as node addresses, no
registration or central authority
- **IPv6 adaptation** — TUN interface maps npubs to fd00::/8 addresses for
@@ -322,7 +323,7 @@ Ethernet, Tor, and Bluetooth (BLE) with a small live mesh of deployed nodes.
- Spanning tree construction with greedy coordinate routing
- Bloom filter guided discovery (no flooding, single-path with retry)
- Noise IK (link layer) and Noise XK (session layer) encryption
- Noise XX encryption at both layers with protocol negotiation
- Periodic Noise rekey with hitless cutover for forward secrecy (FMP + FSP)
- Persistent node identity with key file management
- IPv6 TUN adapter with DNS resolution of `.fips` names
+34 -25
View File
@@ -1,43 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="270" fill="#1a1a2e" rx="4"/>
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FilterAnnounce (0x20) — 11 + filter bytes</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FilterAnnounce (0x20) &#8212; 19-byte header + RLE payload</text>
<!-- Row 0 (03): msg_type(1) + sequence starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<!-- Row 0 (0-1): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;1</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<rect x="185" y="36" width="390" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="380" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<rect x="185" y="36" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="380" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte &#8212; bit 0: delta (XOR diff)</text>
<!-- Row 1 (48): sequence continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</text>
<!-- Row 1 (2-9): sequence -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">2&#8211;9</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — monotonic counter</text>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE &#8212; per-peer monotonic counter</text>
<!-- Row 2 (910): hash_count + size_class -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">910</text>
<!-- Row 2 (10-17): base_seq -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">10&#8211;17</text>
<rect x="55" y="140" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="185" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">hash_count</text>
<text x="185" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="55" y="140" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">base_seq</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE &#8212; reference sequence for delta (0 if full)</text>
<rect x="315" y="140" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">size_class</text>
<text x="445" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 3 (18): size_class -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">18</text>
<!-- Row 3 (11): filter_bits -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">11</text>
<rect x="55" y="192" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">size_class</text>
<text x="315" y="232" fill="#8af8f8" text-anchor="middle" font-size="10">1 byte &#8212; filter size = 512 &#171; size_class bytes</text>
<rect x="55" y="198" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="222" fill="#8af8f8" text-anchor="middle" font-size="11">filter_bits (variable, 512 &lt;&lt; size_class bytes)</text>
<!-- Row 4 (19-): compressed_payload -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">19&#8211;</text>
<rect x="55" y="250" width="520" height="40" fill="#1f1f3a" stroke="#4a9090" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="274" fill="#8ad8d8" text-anchor="middle" font-size="11">compressed_payload (RLE: [count:2 LE][word:8 LE] per run)</text>
<!-- Explanation -->
<rect x="55" y="304" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="324" fill="#888" text-anchor="middle" font-size="10">delta: XOR diff of current vs last-sent filter &#8212; full: raw filter words</text>
<!-- Total -->
<text x="310" y="258" fill="#777" font-size="10" text-anchor="middle">v1 payload: 1,035 bytes (11 header + 1,024 filter)</text>
<text x="310" y="362" fill="#777" font-size="10" text-anchor="middle">19-byte header + variable compressed payload</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

+43 -34
View File
@@ -1,26 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 500" font-family="monospace" font-size="13">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 620" font-family="monospace" font-size="13">
<defs>
<marker id="arrowR" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<polygon points="0,0 10,4 0,8" fill="#e0e0e0"/>
</marker>
<marker id="arrowL" markerWidth="10" markerHeight="8" refX="1" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<polygon points="10,0 0,4 10,8" fill="#e0e0e0"/>
</marker>
</defs>
<!-- Background -->
<rect width="620" height="500" fill="#1a1a2e" rx="4"/>
<rect width="620" height="620" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise IK)</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise XX)</text>
<!-- Column headers -->
<text x="100" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Initiator</text>
<text x="520" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Responder</text>
<!-- Lifelines -->
<line x1="100" y1="62" x2="100" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="520" y1="62" x2="520" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="100" y1="62" x2="100" y2="600" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="520" y1="62" x2="520" y2="600" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<!-- Step 1: Initiator prepares -->
<text x="20" y="88" fill="#8af8c8" font-size="10">generates sender_idx</text>
@@ -28,39 +25,51 @@
<!-- Arrow 1: msg1 (initiator -> responder) -->
<line x1="100" y1="120" x2="520" y2="120" stroke="#4a90d9" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="130" y="128" width="360" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x01|flags=0|len] | sender_idx | noise_msg1</text>
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 — 114 bytes</text>
<rect x="145" y="128" width="330" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x11|flags=0|len] | sender_idx | noise_msg1</text>
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 &#8212; 41 bytes &#8212; pattern: &#8594; e</text>
<!-- Step 2: Responder processes (right-aligned to stay in bounds) -->
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1</text>
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator's static key</text>
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
<text x="600" y="234" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
<!-- Step 2: Responder processes msg1 -->
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1 (ephemeral only)</text>
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
<!-- Arrow 2: msg2 (responder -> initiator) -->
<!-- Draw line right-to-left so orient="auto" points the arrowhead left -->
<line x1="520" y1="256" x2="100" y2="256" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="130" y="264" width="360" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
<text x="310" y="280" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x02|flags=0|len] | sender_idx | receiver_idx | noise_msg2</text>
<text x="310" y="300" fill="#666" text-anchor="middle" font-size="9">phase 0x2 — 69 bytes</text>
<line x1="520" y1="244" x2="100" y2="244" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="252" width="390" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
<text x="310" y="268" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x12|flags=0|len] | sender_idx | receiver_idx | noise_msg2 + negotiation</text>
<text x="310" y="288" fill="#666" text-anchor="middle" font-size="9">phase 0x2 &#8212; 144 bytes &#8212; pattern: &#8592; e, ee, s, es</text>
<text x="600" y="300" fill="#d8a0f8" font-size="9" text-anchor="end">responder identity revealed</text>
<!-- Step 3: Initiator completes -->
<text x="20" y="324" fill="#8af8c8" font-size="10">validates msg2</text>
<text x="20" y="338" fill="#8af8c8" font-size="10">derives session keys</text>
<!-- Step 3: Initiator processes msg2, learns responder identity -->
<text x="20" y="322" fill="#8af8c8" font-size="10">validates msg2</text>
<text x="20" y="336" fill="#8af8c8" font-size="10">learns responder identity</text>
<text x="20" y="350" fill="#8af8c8" font-size="10">checks epoch (restart detection)</text>
<!-- Arrow 3: msg3 (initiator -> responder) -->
<line x1="100" y1="368" x2="520" y2="368" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="376" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
<text x="310" y="392" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x13|flags=0|len] | sender_idx | receiver_idx | noise_msg3 + neg</text>
<text x="310" y="412" fill="#666" text-anchor="middle" font-size="9">phase 0x3 &#8212; 111 bytes &#8212; pattern: &#8594; s, se</text>
<text x="20" y="424" fill="#d8a0f8" font-size="9">initiator identity revealed</text>
<!-- Step 4: Responder processes msg3, learns initiator identity -->
<text x="600" y="440" fill="#8af8c8" font-size="10" text-anchor="end">validates msg3</text>
<text x="600" y="454" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator identity</text>
<text x="600" y="468" fill="#8af8c8" font-size="10" text-anchor="end">checks epoch, derives session keys</text>
<!-- Handshake complete separator -->
<line x1="30" y1="360" x2="230" y2="360" stroke="#d9904a" stroke-width="1.5"/>
<text x="310" y="364" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
<line x1="390" y1="360" x2="590" y2="360" stroke="#d9904a" stroke-width="1.5"/>
<line x1="30" y1="488" x2="230" y2="488" stroke="#d9904a" stroke-width="1.5"/>
<text x="310" y="492" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
<line x1="390" y1="488" x2="590" y2="488" stroke="#d9904a" stroke-width="1.5"/>
<!-- Arrow 3: first encrypted frame -->
<text x="20" y="392" fill="#777" font-size="10">first encrypted frame:</text>
<line x1="100" y1="406" x2="520" y2="406" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="414" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
<text x="310" y="430" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
<text x="310" y="450" fill="#666" text-anchor="middle" font-size="9">phase 0x0 established frame</text>
<!-- Arrow 4: first encrypted frame -->
<text x="20" y="516" fill="#777" font-size="10">first encrypted frame:</text>
<line x1="100" y1="530" x2="520" y2="530" stroke="#d9b04a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="538" width="390" height="24" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1" rx="3"/>
<text x="310" y="554" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
<text x="310" y="574" fill="#666" text-anchor="middle" font-size="9">phase 0x0 &#8212; established frame</text>
<!-- Legend -->
<text x="310" y="478" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch exchange enables restart detection.</text>
<text x="310" y="600" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch + negotiation exchanged in msg2/msg3.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

-67
View File
@@ -1,67 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 426" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="426" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 1 — phase 0x1 (114 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx (bytes 4-7) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise IK msg1 label -->
<text x="10" y="250" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 250)">Noise IK msg1 (106 bytes)</text>
<line x1="18" y1="140" x2="18" y2="348" stroke="#555" stroke-width="1"/>
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
<!-- Rows 2-3: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">840</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="170" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="222" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Rows 4-5: encrypted_static (bytes 41-89, 49 bytes) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">4189</text>
<rect x="55" y="244" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="274" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<rect x="55" y="296" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="326" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 6: encrypted_epoch (bytes 90-113, 24 bytes) -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">90113</text>
<rect x="55" y="348" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="372" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="390" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Total -->
<text x="310" y="418" fill="#777" font-size="10" text-anchor="middle">total: 114 bytes · payload_len = 110 · pattern: e, es, s, ss</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

@@ -1,12 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 218" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<rect width="580" height="218" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 2 — phase 0x2 (69 bytes)</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 1 &#8212; phase 0x1 (41 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
@@ -25,41 +25,19 @@
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx (bytes 4-7) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">47</text>
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: receiver_idx (bytes 8-11) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">811</text>
<!-- Row 2: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;40</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="182" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise IK msg2 bracket -->
<text x="10" y="270" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 270)">Noise IK msg2 (57 bytes)</text>
<line x1="18" y1="192" x2="18" y2="348" stroke="#555" stroke-width="1"/>
<line x1="18" y1="192" x2="23" y2="192" stroke="#555" stroke-width="1"/>
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
<!-- Rows 3-4: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">1244</text>
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="222" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="274" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Row 5: encrypted_epoch (bytes 45-68, 24 bytes) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">4568</text>
<rect x="55" y="296" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="338" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Total -->
<text x="310" y="366" fill="#777" font-size="10" text-anchor="middle">total: 69 bytes · payload_len = 65 · pattern: e, ee, se</text>
<text x="310" y="210" fill="#777" font-size="10" text-anchor="middle">total: 41 bytes &#183; payload_len = 37 &#183; pattern: &#8594; e</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+82
View File
@@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 478" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="478" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 2 &#8212; phase 0x2 (118&#8211;144 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx (bytes 4-7) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</text>
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- receiver_idx (bytes 8-11) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end"/>
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise XX msg2 bracket -->
<text x="10" y="290" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 290)">Noise XX msg2 (106 bytes base)</text>
<line x1="18" y1="140" x2="18" y2="400" stroke="#555" stroke-width="1"/>
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
<line x1="18" y1="400" x2="23" y2="400" stroke="#555" stroke-width="1"/>
<!-- Row 2: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12&#8211;44</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Row 3: encrypted_static (bytes 45-93, 49 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">45&#8211;93</text>
<rect x="55" y="192" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<text x="315" y="234" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 4: encrypted_epoch (bytes 94-117, 24 bytes) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">94&#8211;117</text>
<rect x="55" y="244" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="286" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Row 5: negotiation payload (variable, optional) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">118+</text>
<rect x="55" y="296" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
<text x="315" y="338" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
<!-- Row 6: AEAD explanation -->
<rect x="55" y="362" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="386" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() &#8212; extends Noise hash chain</text>
<!-- Total -->
<text x="310" y="425" fill="#777" font-size="10" text-anchor="middle">base: 118 bytes &#183; with FMP negotiation: 144 bytes</text>
<text x="310" y="442" fill="#777" font-size="10" text-anchor="middle">pattern: &#8592; e, ee, s, es</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 426" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="426" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 3 &#8212; phase 0x3 (85&#8211;111 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx + receiver_idx (bytes 4-11) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;11</text>
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise XX msg3 bracket -->
<text x="10" y="240" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 240)">Noise XX msg3 (73 bytes base)</text>
<line x1="18" y1="140" x2="18" y2="348" stroke="#555" stroke-width="1"/>
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
<!-- Row 2: encrypted_static (bytes 12-60, 49 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12&#8211;60</text>
<rect x="55" y="140" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<text x="315" y="182" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 3: encrypted_epoch (bytes 61-84, 24 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">61&#8211;84</text>
<rect x="55" y="192" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="234" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Row 4: negotiation payload (variable, optional) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">85+</text>
<rect x="55" y="244" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
<text x="315" y="286" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
<!-- Row 5: explanation -->
<rect x="55" y="310" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="334" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() &#8212; extends Noise hash chain</text>
<!-- Total -->
<text x="310" y="378" fill="#777" font-size="10" text-anchor="middle">base: 85 bytes &#183; with FMP negotiation: 111 bytes</text>
<text x="310" y="395" fill="#777" font-size="10" text-anchor="middle">pattern: &#8594; s, se</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+58 -67
View File
@@ -3,104 +3,95 @@
<rect width="580" height="634" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">ReceiverReport (0x02) — 68 bytes</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">ReceiverReport (0x02) &#8212; 54 bytes</text>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 1 (411): highest_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</text>
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 50)</text>
<!-- Row 1 (4-7): timestamp_echo -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">highest_counter</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp_echo</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE (echoed sender timestamp for RTT)</text>
<!-- Row 2 (1219): cumulative_packets_recv -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</text>
<!-- Row 2 (8-9): dwell_time -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;9</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dwell_time</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE (ms between receive and echo)</text>
<!-- Row 3 (2027): cumulative_bytes_recv -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</text>
<!-- Row 3 (10-17): highest_counter -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">10&#8211;17</text>
<rect x="55" y="192" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">highest_counter</text>
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 4 (2833): timestamp_echo + dwell_time -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">2833</text>
<!-- Row 4 (18-25): cumulative_packets_recv -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">18&#8211;25</text>
<rect x="55" y="244" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="185" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp_echo</text>
<text x="185" y="284" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
<text x="315" y="284" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<rect x="315" y="244" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="445" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dwell_time</text>
<text x="445" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE ms</text>
<!-- Row 5 (26-33): cumulative_bytes_recv -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">26&#8211;33</text>
<!-- Row 5 (3439): max_burst_loss + mean_burst_loss + reserved -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">3439</text>
<rect x="55" y="296" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
<text x="315" y="336" fill="#8af8f8" text-anchor="middle" font-size="10">8 bytes LE</text>
<rect x="55" y="296" width="173" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="141" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">max_burst</text>
<text x="141" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 6 (34-41): jitter + ecn_ce_count -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">34&#8211;41</text>
<rect x="228" y="296" width="174" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">mean_burst</text>
<text x="315" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes u8.8</text>
<rect x="402" y="296" width="173" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="488" y="322" fill="#666" text-anchor="middle" font-size="10">reserved</text>
<!-- Row 6 (4047): jitter + ecn_ce_count -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">4047</text>
<rect x="55" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<rect x="55" y="348" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="185" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">jitter</text>
<text x="185" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE µs</text>
<text x="185" y="388" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE (&#181;s)</text>
<rect x="315" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<rect x="315" y="348" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="445" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ecn_ce_count</text>
<text x="445" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<text x="445" y="388" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 7 (4855): owd_trend + burst_loss_count -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">4855</text>
<!-- Row 7 (42-49): owd_trend + burst_loss_count -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">42&#8211;49</text>
<rect x="55" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<rect x="55" y="400" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="185" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">owd_trend</text>
<text x="185" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes i32 LE µs/s</text>
<text x="185" y="440" fill="#f8c88a" text-anchor="middle" font-size="10">i32 LE (&#181;s/s)</text>
<rect x="315" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<rect x="315" y="400" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="445" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">burst_loss_count</text>
<text x="445" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<text x="445" y="440" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 8 (5663): cumulative_reorder_count + interval_packets_recv -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">5663</text>
<!-- Row 8 (50-53): cumulative_reorder_count -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">50&#8211;53</text>
<rect x="55" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">reorder_count</text>
<text x="185" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE cumulative</text>
<rect x="55" y="452" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="474" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_reorder_count</text>
<text x="315" y="492" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="315" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="445" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_pkts_recv</text>
<text x="445" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Forward compat note -->
<rect x="55" y="518" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="538" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
<!-- Row 9 (6467): interval_bytes_recv -->
<text x="50" y="530" fill="#666" font-size="10" text-anchor="end">6467</text>
<rect x="55" y="504" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="526" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_bytes_recv</text>
<text x="185" y="544" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Removed fields note -->
<rect x="55" y="558" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="578" fill="#666" text-anchor="middle" font-size="10">removed from v0.2: max_burst_loss, mean_burst_loss, interval_packets/bytes_recv</text>
<!-- Total -->
<text x="310" y="586" fill="#777" font-size="10" text-anchor="middle">total: 68 bytes</text>
<text x="310" y="618" fill="#777" font-size="10" text-anchor="middle">total: 54 bytes &#183; format_version = 0 &#183; total_length = 50</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

+32 -48
View File
@@ -1,66 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 478" font-family="monospace" font-size="13">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="478" fill="#1a1a2e" rx="4"/>
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SenderReport (0x01) — 48 bytes</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SenderReport (0x01) &#8212; 20 bytes</text>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 1 (411): interval_start_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</text>
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 16)</text>
<!-- Row 1 (4-7): interval_packets_sent -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_counter</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_packets_sent</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2 (1219): interval_end_counter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</text>
<!-- Row 2 (8-11): interval_bytes_sent -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;11</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_counter</text>
<text x="315" y="180" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_bytes_sent</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 3 (2027): interval_start_timestamp + interval_end_timestamp -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</text>
<!-- Row 3 (12-19): cumulative_packets_sent -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">12&#8211;19</text>
<rect x="55" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_ts</text>
<text x="185" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="55" y="192" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_sent</text>
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<rect x="315" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_ts</text>
<text x="445" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 4 (2831): interval_bytes_sent -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">2831</text>
<rect x="55" y="244" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_bytes_sent</text>
<text x="315" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 5 (3239): cumulative_packets_sent -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">3239</text>
<rect x="55" y="296" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_sent</text>
<text x="315" y="336" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 6 (4047): cumulative_bytes_sent -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">4047</text>
<rect x="55" y="348" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_sent</text>
<text x="315" y="388" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Forward compat note -->
<rect x="55" y="258" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="278" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
<!-- Total -->
<text x="310" y="422" fill="#777" font-size="10" text-anchor="middle">total: 48 bytes</text>
<text x="310" y="314" fill="#777" font-size="10" text-anchor="middle">total: 20 bytes &#183; format_version = 0 &#183; total_length = 16</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+3 -3
View File
@@ -3,7 +3,7 @@
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionAck (phase 0x2) Noise XK msg2</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionAck (phase 0x2) &#8212; Noise XX msg2</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
@@ -40,8 +40,8 @@
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg2 (57 bytes ephemeral + epoch)</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg2 (106+ bytes &#8212; ephemeral + static + epoch)</text>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~190 bytes (depth-dependent, carries both endpoints' coords)</text>
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~240 bytes (depth-dependent, carries both endpoints' coords)</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+2 -2
View File
@@ -3,7 +3,7 @@
<rect width="580" height="244" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionMsg3 (phase 0x3) Noise XK msg3</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionMsg3 (phase 0x3) &#8212; Noise XX msg3</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
@@ -28,7 +28,7 @@
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg3 (73 bytes encrypted static + encrypted epoch)</text>
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg3 (73+ bytes &#8212; encrypted static + epoch + optional negotiation)</text>
<!-- Total -->
<text x="310" y="218" fill="#777" font-size="10" text-anchor="middle">~80 bytes · no coordinates (both endpoints already have them)</text>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+2 -2
View File
@@ -3,7 +3,7 @@
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionSetup (phase 0x1) Noise XK msg1</text>
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionSetup (phase 0x1) &#8212; Noise XX msg1</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
@@ -40,7 +40,7 @@
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg1 (33 bytes ephemeral key)</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg1 (33 bytes &#8212; ephemeral key)</text>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~170 bytes (depth-dependent)</text>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+37 -35
View File
@@ -245,7 +245,7 @@ for i in 0..hash_count:
return true // Maybe present (possible false positive)
```
Where `filter_bits = 8 × (512 << size_class)` — 8,192 for v1.
Where `filter_bits = 8 × (512 << size_class)` — 8,192 for size_class 1 (default).
## Wire Format
@@ -254,27 +254,30 @@ FilterAnnounce messages are carried inside encrypted link-layer frames:
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x20 |
| 1 | sequence | 8 bytes LE | Monotonic counter for freshness |
| 9 | hash_count | 1 byte | Number of hash functions (5 in v1) |
| 10 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
| 11 | filter_bits | 1,024 bytes | Bloom filter bit array (v1) |
| 1 | flags | 1 byte | Bit 0: delta (XOR diff), bits 1-7 reserved |
| 2 | sequence | 8 bytes LE | Monotonic counter, per-peer |
| 10 | base_seq | 8 bytes LE | Reference filter sequence for delta (0 if full) |
| 18 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
| 19 | compressed_payload | variable | RLE-encoded filter or XOR diff |
**v1 total**: 1,035 bytes payload, 1,064 bytes with link encryption
overhead.
Payloads are RLE-compressed: each run = `[count:2 LE][word:8 LE]`
(10 bytes per run). XOR diffs between consecutive filters are mostly
zero words, compressing to very few runs. A FilterNack (msg_type 0x21)
requests full retransmission when a sequence gap is detected.
See [fips-wire-formats.md](fips-wire-formats.md) for the complete wire
format reference.
## Scale and Size Classes
### v1 Scale Limits
### Scale Limits
Coordinate-based tree distance checking ensures correct routing decisions
at all network sizes — bloom filters are an optimization that narrows the
set of peers considered, not a correctness requirement. As filters
saturate, routing still works; it just evaluates more candidates per hop.
With the v1 mandatory 1 KB filter (size_class 1):
With the default 1 KB filter (size_class 1):
- **Small networks (< 1,000 nodes)**: Both upward and downward filters
are highly accurate (worst-case FPR < 1%). Filters effectively narrow
@@ -295,34 +298,29 @@ With the v1 mandatory 1 KB filter (size_class 1):
### Size Class Table
| size_class | Bytes | Bits | Status |
| ---------- | ----- | ---- | ------ |
| 0 | 512 | 4,096 | Reserved |
| 1 | 1,024 | 8,192 | **v1 (MUST use)** |
| 2 | 2,048 | 16,384 | Reserved |
| 3 | 4,096 | 32,768 | Reserved |
| size_class | Bytes | Bits | Notes |
| ---------- | ----- | ---- | ----- |
| 0 | 512 | 4,096 | Minimum |
| 1 | 1,024 | 8,192 | Default |
| 2 | 2,048 | 16,384 | |
| 3 | 4,096 | 32,768 | |
| 4 | 8,192 | 65,536 | |
| 5 | 16,384 | 131,072 | |
| 6 | 32,768 | 262,144 | Maximum |
FMP v1 mandates size_class = 1. Nodes MUST use size_class = 1 and MUST
reject FilterAnnounce messages with any other size_class. The size_class
field is reserved in the wire format to support future protocol versions
with larger default filter sizes.
### Adaptive Sizing
### Scaling Strategy
Filter size is a node property, not a link property. Each node selects
its own size class based on outgoing filter fill ratio: step up above
~20%, step down below ~5%, with hysteresis to prevent oscillation.
Nodes near the root of the spanning tree — which carry larger combined
filters — naturally upsize, while leaf and edge nodes stay small.
The 1 KB filter becomes a practical limitation beyond ~2,000 nodes. The
size class mechanism provides the path forward: future FMP versions may
use larger default filters (size_class 2 or 3) to support larger networks
while remaining compatible with constrained nodes through folding.
Size_class 2 (2 KB, 16,384 bits) would roughly double the practical
network size limit.
The envisioned approach is that hub nodes near the root — which carry the
largest downward filters — would use larger size classes, while leaf nodes
and resource-constrained nodes continue with smaller filters. A node
receiving a filter larger than its own size class folds it down locally.
The mechanism by which heterogeneous filter sizes propagate through the
tree is a future design direction not specified in v1. See
[IDEA-0043](../../ideas/IDEA-0043-heterogeneous-filter-propagation.md).
When a node receives a filter at a different size class than its own,
it converts on receipt: larger filters are folded down, smaller filters
are expanded via bit duplication. Routing queries use the peer's filter
at its native (advertised) size for full resolution; conversion happens
only when building the node's own outgoing filter.
### Folding
@@ -339,7 +337,7 @@ positions that folding produces.
| Feature | Status |
| ------- | ------ |
| 1 KB bloom filter (size_class 1) | **Implemented** |
| Variable-size bloom filters (512 B 32 KB) | **Implemented** |
| 5 hash functions | **Implemented** |
| Split-horizon filter computation | **Implemented** |
| Tree-only merge propagation | **Implemented** |
@@ -347,6 +345,10 @@ positions that folding produces.
| Per-peer filter maintenance | **Implemented** |
| Event-driven updates | **Implemented** |
| 500ms rate limiting | **Implemented** |
| Delta compression (XOR diff + RLE) | **Implemented** |
| FilterNack sequence recovery | **Implemented** |
| Adaptive sizing (fill-ratio heuristic) | **Implemented** |
| Fold/duplicate size conversion | **Implemented** |
| FilterAnnounce gossip (all peers) | **Implemented** |
| Filter cardinality logging | **Implemented** |
| Size class negotiation | Future direction |
+6 -4
View File
@@ -90,7 +90,8 @@ to the highest-priority config file for operator visibility, even in ephemeral m
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `node.leaf_only` | bool | `false` | Leaf-only mode: node does not forward traffic or participate in routing |
| `node.disable_routing` | bool | `false` | Non-routing mode: participates in spanning tree but does not forward transit traffic or send bloom filters |
| `node.leaf_only` | bool | `false` | Leaf mode: single upstream peer, no tree/bloom/transit participation. Implies `disable_routing: true` |
| `node.tick_interval_secs` | u64 | `1` | Periodic maintenance tick interval (retry checks, timeout cleanup, tree refresh) |
| `node.base_rtt_ms` | u64 | `100` | Initial RTT estimate for new links before measurements converge |
| `node.heartbeat_interval_secs` | u64 | `10` | Heartbeat send interval per peer for liveness detection |
@@ -109,7 +110,7 @@ Controls capacity for connections, peers, and links.
### Rate Limiting (`node.rate_limit.*`)
Handshake rate limiting protects against DoS on the Noise IK handshake path.
Handshake rate limiting protects against DoS on the Noise XX handshake path.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
@@ -204,7 +205,7 @@ stays set for all subsequent hops to the destination.
### Rekey (`node.rekey.*`)
Controls periodic Noise rekey for forward secrecy. When enabled, both FMP
(link-layer IK) and FSP (session-layer XK) sessions perform fresh Diffie-Hellman
(link-layer XX) and FSP (session-layer XX) sessions perform fresh Diffie-Hellman
key exchanges after a time or message count threshold, whichever comes first.
A 10-second drain window keeps the old session active for decryption during
cutover.
@@ -238,7 +239,7 @@ Metrics Measurement Protocol for per-peer link measurement. See
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `node.mmp.mode` | string | `"full"` | Operating mode: `full` (sender + receiver reports), `lightweight` (receiver reports only), or `minimal` (spin bit + CE echo only, no reports) |
| `node.mmp.mode` | string | `"full"` | Operating mode: `full` (sender + receiver reports), `lightweight` (receiver reports only), or `minimal` (CE echo only, no reports) |
| `node.mmp.log_interval_secs` | u64 | `30` | Periodic operator log interval for link metrics |
| `node.mmp.owd_window_size` | usize | `32` | One-way delay trend ring buffer size |
@@ -666,6 +667,7 @@ node:
identity:
nsec: null # secret key in nsec or hex (null = depends on persistent)
persistent: false # true = load/save fips.key; false = ephemeral each start
disable_routing: false
leaf_only: false
tick_interval_secs: 1
base_rtt_ms: 100
+29 -45
View File
@@ -134,7 +134,7 @@ See [fips-transport-layer.md](fips-transport-layer.md) for the transport layer
specification.
**FIPS Mesh Protocol (FMP)**: Manages peer connections, authenticates peers
via Noise IK handshakes, and encrypts all traffic on each link. FMP is where
via Noise XX handshakes, and encrypts all traffic on each link. FMP is where
the mesh organizes itself — nodes exchange spanning tree announcements and
bloom filters with their direct peers, and FMP makes forwarding decisions
for transit traffic. FMP provides authenticated, encrypted forwarding to FSP
@@ -203,7 +203,7 @@ same keypair.
![Identity Derivation](diagrams/fips-identity-derivation.svg)
The pubkey is the node's cryptographic identity, used in Noise IK handshakes
The pubkey is the node's cryptographic identity, used in Noise XX handshakes
for both link and session encryption. It is never exposed beyond the
endpoints of an encrypted channel. The node_addr, a one-way SHA-256 hash
truncated to 16 bytes, serves as the routing identifier in packet headers
@@ -243,31 +243,27 @@ FIPS uses independent encryption at two protocol layers:
| Layer | Scope | Pattern | Purpose |
| ----- | ----- | ------- | ------- |
| **FMP (Mesh)** | Hop-by-hop | Noise IK | Encrypt all traffic on each peer link |
| **FSP (Session)** | End-to-end | Noise XK | Encrypt application payload between endpoints |
| **FMP (Mesh)** | Hop-by-hop | Noise XX | Encrypt all traffic on each peer link |
| **FSP (Session)** | End-to-end | Noise XX | Encrypt application payload between endpoints |
### Link Layer (Hop-by-Hop)
When two nodes establish a direct connection, they perform a [Noise
IK](https://noiseprotocol.org/) handshake. This authenticates both parties
XX](https://noiseprotocol.org/) handshake. This authenticates both parties
and establishes symmetric keys for encrypting all traffic on that link.
Every packet between direct peers is encrypted — gossip messages, routing
queries, and forwarded session datagrams alike.
The IK pattern is used because outbound connections know the peer's npub
from configuration, while inbound connections learn the initiator's identity
from the first handshake message.
queries, and forwarded session datagrams alike. Neither side requires prior
knowledge of the other's static key — both identities are revealed during
the three-message handshake, along with a protocol negotiation payload
that enables rolling upgrades.
### Session Layer (End-to-End)
FIPS establishes end-to-end encrypted sessions between any two communicating
nodes using Noise XK, regardless of how many hops separate them. The
initiator knows the destination's npub (required for XK's pre-message);
the responder learns the initiator's identity from the third handshake
message. Unlike the link-layer IK pattern where the initiator's identity
is revealed in msg1, XK delays identity disclosure until msg3, providing
stronger initiator identity protection for traffic traversing untrusted
intermediate nodes.
nodes using Noise XX, regardless of how many hops separate them. The same
three-message XX pattern is used at both layers — neither side reveals its
identity until msg2 (responder) or msg3 (initiator), providing mutual
identity protection for traffic traversing untrusted intermediate nodes.
A packet from A to D through intermediate nodes B and C:
@@ -455,7 +451,7 @@ MMP operates in three modes. **Full** mode exchanges both SenderReports and
ReceiverReports to compute all metrics including RTT. **Lightweight** mode
exchanges only ReceiverReports, providing loss and jitter but not RTT — useful
for constrained links. **Minimal** mode disables reports entirely, relying
only on spin bit and congestion echo flags in the frame header.
only on congestion echo flags in the frame header.
Reports are sent at RTT-adaptive intervals (clamped to 100 ms2 s), so
high-latency links don't generate excessive measurement traffic while
@@ -472,7 +468,7 @@ evaluates `effective_depth = depth + link_cost` using
`find_next_hop()` candidate ranking for data forwarding.
See [fips-mesh-layer.md](fips-mesh-layer.md) for MMP operating modes, report
scheduling, and the spin bit design.
scheduling, and RTT measurement.
---
@@ -488,7 +484,7 @@ A **transport** is a driver for a particular medium. A **link** is a peer
connection established over a transport. Transport addresses (IP:port, MAC
address, .onion) are opaque to all layers above FMP — they exist only to
deliver datagrams and are discarded once FMP has authenticated the peer via
the Noise IK handshake. From that point on, the peer is identified solely by
the Noise XX handshake. From that point on, the peer is identified solely by
its cryptographic identity.
Transports fall into three categories based on their connectivity model:
@@ -543,7 +539,7 @@ different layer of the protocol.
A passive observer on the underlying transport — someone monitoring a WiFi
network, tapping an Ethernet segment, or inspecting UDP traffic — sees only
encrypted packets. The FMP link-layer Noise IK session encrypts all traffic
encrypted packets. The FMP link-layer Noise XX session encrypts all traffic
between direct peers, including routing gossip and forwarded session
datagrams. The observer can infer timing, packet sizes, and which transport
endpoints are exchanging traffic, but cannot read content or determine
@@ -555,7 +551,7 @@ infer communication relationships — is not defended against (see
### Active Attackers on the Transport
An adversary who can inject, modify, drop, or replay packets on the
transport is also defeated by the FMP link-layer Noise IK session. Mutual
transport is also defeated by the FMP link-layer Noise XX session. Mutual
authentication prevents impersonation, AEAD encryption detects tampering,
and counter-based nonces with a sliding replay window reject replayed
packets.
@@ -565,7 +561,7 @@ packets.
The most important adversary class is the operators of other nodes in the
mesh — the peers that forward your traffic. FIPS treats every intermediate
router as potentially adversarial. The FSP session layer establishes a
completely independent Noise XK session between the communicating endpoints,
completely independent Noise XX session between the communicating endpoints,
so intermediate nodes cannot read application payloads even though they
decrypt and re-encrypt the link-layer envelope at each hop.
@@ -678,20 +674,16 @@ confidentiality and integrity rather than hiding traffic patterns.
### Noise Protocol Framework
FIPS uses the [Noise Protocol Framework](https://noiseprotocol.org/) at both
protocol layers, with different handshake patterns chosen for each layer's
threat model. FMP link encryption uses **Noise IK**, providing mutual
authentication with a single round trip where the initiator knows the
responder's static key in advance.
[WireGuard](https://www.wireguard.com/) uses the same IK base pattern
(extended with a pre-shared key as IKpsk2) for VPN tunnels. FSP session
encryption uses **Noise XK**, the same pattern used by the
[Lightning Network](https://github.com/lightning/bolts/blob/master/08-transport.md),
where the initiator's static key is transmitted in a third message rather
than the first. XK provides stronger initiator identity hiding at the cost
of an additional round trip — a worthwhile tradeoff for session-layer traffic
that traverses untrusted intermediate nodes. At the link layer, where both
peers are configured and directly connected, IK's single round trip is
preferred.
protocol layers with the **Noise XX** handshake pattern. XX requires no
prior knowledge of the peer's static key — both identities are revealed
during a three-message handshake (responder in msg2, initiator in msg3).
This enables anonymous peer discovery on shared-media transports and
allows a protocol negotiation payload to be exchanged alongside the
handshake, supporting rolling protocol upgrades without extra round trips.
[WireGuard](https://www.wireguard.com/) uses the related IK pattern for
VPN tunnels;
[Lightning Network](https://github.com/lightning/bolts/blob/master/08-transport.md)
uses XK for transport encryption.
### Index-Based Session Dispatch
@@ -728,13 +720,6 @@ computation used in TCP for retransmission timeout calculation since 1988.
MMP derives RTT from timestamp-echo in ReceiverReports with dwell-time
compensation, rather than from packet round-trips.
The spin bit in the FMP frame header follows the
[QUIC](https://www.rfc-editor.org/rfc/rfc9000) spin bit
([RFC 9312](https://www.rfc-editor.org/rfc/rfc9312)) — a single bit that
alternates each round trip, enabling passive latency measurement. FIPS
implements the spin bit state machine but relies on timestamp-echo for SRTT,
as irregular mesh traffic makes spin bit RTT unreliable.
The Expected Transmission Count (ETX) metric, computed from bidirectional
delivery ratios, was introduced by
[De Couto et al. (2003)](https://pdos.csail.mit.edu/papers/grid:mobicom03/paper.pdf)
@@ -809,7 +794,6 @@ of self-sovereign identity systems. No novel cryptography is introduced.
- [WireGuard Whitepaper](https://www.wireguard.com/papers/wireguard.pdf)
- [Lightning Network BOLT #8 — Transport](https://github.com/lightning/bolts/blob/master/08-transport.md)
- [QUIC (RFC 9000)](https://www.rfc-editor.org/rfc/rfc9000)
- [QUIC Spin Bit (RFC 9312)](https://www.rfc-editor.org/rfc/rfc9312)
- [RTCP (RFC 3550)](https://www.rfc-editor.org/rfc/rfc3550)
- [TCP SRTT / RTO (RFC 6298)](https://www.rfc-editor.org/rfc/rfc6298)
- [ECN (RFC 3168)](https://www.rfc-editor.org/rfc/rfc3168)
+48 -42
View File
@@ -9,7 +9,7 @@ the mesh self-organizes, and where forwarding decisions are made.
FMP manages direct peer connections over transports. When a transport delivers
a datagram from an unknown address, FMP authenticates the sender through a
Noise IK handshake, establishing a cryptographic link. Once authenticated, the
Noise XX handshake, establishing a cryptographic link. Once authenticated, the
link carries all inter-peer communication: spanning tree gossip, bloom filter
updates, coordinate discovery, and forwarded session datagrams — all encrypted
per-hop.
@@ -82,7 +82,7 @@ for the encrypted frame wrapper: 16-byte outer header + 5-byte inner header +
### Connection Lifecycle
For connection-oriented transports, the transport must establish the underlying
connection before FMP can begin the Noise IK handshake. For connectionless
connection before FMP can begin the Noise XX handshake. For connectionless
transports, datagrams can flow immediately.
### Endpoint Discovery (Optional)
@@ -94,46 +94,52 @@ through configuration.
## Peer Authentication
### Noise IK Handshake
### Noise XX Handshake
Every peer connection begins with a Noise IK handshake that mutually
Every peer connection begins with a Noise XX handshake that mutually
authenticates both parties and establishes symmetric keys for link encryption.
The IK pattern is chosen because:
The XX pattern is chosen because:
- The **initiator** knows the responder's static public key from configuration
or discovery, and sends their own static key encrypted in the first message
- The **responder** learns the initiator's identity from the first message,
then responds with their own ephemeral key
- **Neither side** requires prior knowledge of the other's static public key
- The **responder** reveals its identity in msg2; the **initiator** reveals
its identity in msg3
- A protocol negotiation payload (version range, feature bitfield, TLV
extensions) is appended to msg2 and msg3, enabling rolling protocol
upgrades without additional round-trips
After the two-message handshake completes, both parties share symmetric
session keys derived from four DH operations (es, ss, ee, se). The handshake
provides mutual authentication, forward secrecy, and identity hiding for the
initiator.
After the three-message handshake completes, both parties share symmetric
session keys derived from three DH operations (ee, es, se). The handshake
provides mutual authentication, forward secrecy, and identity hiding for
both parties until they choose to reveal.
### Epoch Exchange and Peer Restart Detection
Both IK handshake messages carry an encrypted epoch payload — an 8-byte
random value generated once at node startup:
XX handshake messages msg2 and msg3 carry an encrypted epoch payload — an
8-byte random value generated once at node startup:
- **msg1**: Ephemeral key (33 bytes) + encrypted static key (49 bytes) +
encrypted epoch (24 bytes) = 106 bytes total
- **msg2**: Ephemeral key (33 bytes) + encrypted epoch (24 bytes) = 57 bytes
total
- **msg1**: Ephemeral key only (33 bytes) — no identity or epoch
- **msg2**: Ephemeral key (33 bytes) + encrypted static key (49 bytes) +
encrypted epoch (24 bytes) = 106 bytes base, plus negotiation payload
- **msg3**: Encrypted static key (49 bytes) + encrypted epoch (24 bytes)
= 73 bytes base, plus negotiation payload
The encrypted epoch (EPOCH_ENCRYPTED_SIZE = 24 bytes) consists of the
8-byte epoch value plus a 16-byte AEAD tag.
On reconnection, each peer compares the received epoch with the previously
stored epoch for that peer. An epoch mismatch indicates the peer has
restarted (generated a new epoch), triggering full link re-establishment
rather than treating the handshake as a simple reconnection. This prevents
stale session state from persisting across restarts.
Because msg1 carries no identity, restart detection is deferred: the
initiator checks the responder's epoch after msg2, and the responder
checks the initiator's epoch after msg3. An epoch mismatch indicates the
peer has restarted, triggering full link re-establishment rather than
treating the handshake as a simple reconnection. This prevents stale
session state from persisting across restarts.
### Identity Binding
The Noise handshake binds the link to the peer's cryptographic identity. After
handshake completion:
The Noise handshake binds the link to the peer's cryptographic identity.
With XX, identity confirmation happens at different points: the initiator
learns the responder's identity from msg2, and the responder learns the
initiator's identity from msg3. After handshake completion:
- The peer's public key (FIPS identity) is confirmed
- The node_addr is computed from the public key (SHA-256, truncated to 128 bits)
@@ -143,8 +149,12 @@ handshake completion:
### Reconnection
When a Noise IK msg1 arrives from a peer that already has an authenticated
link, FMP accepts the new handshake alongside the existing session. If the new
When a Noise XX msg1 arrives from an address that already has an
authenticated link, FMP accepts the new handshake alongside the existing
session. With XX, the peer's identity is not known at msg1 time — FMP can
only detect the duplicate by transport address. Identity-based checks
(restart detection, rekey recognition, cross-connection resolution) are
deferred to msg3 when the initiator's identity is revealed. If the new
handshake completes successfully, it replaces the old session. This handles
legitimate reconnection (network change, process restart, NAT rebinding)
without disrupting ongoing traffic until the new session is confirmed.
@@ -163,7 +173,7 @@ The auto-reconnect path:
3. If eligible, the peer is fed into the retry system with unlimited retries
and exponential backoff (same base interval and max backoff as startup
retries, configured via `node.retry.*`)
4. On each retry tick, a fresh Noise IK handshake is initiated toward the
4. On each retry tick, a fresh Noise XX handshake is initiated toward the
peer's configured transport addresses
Auto-reconnect only applies to peers in the static peer list with
@@ -173,8 +183,8 @@ config) is responsible for re-establishing the link.
### Handshake Message Retry
Both link-layer (Noise IK msg1/msg2) and session-layer (SessionSetup/
SessionAck) handshakes use message-level retry with exponential backoff
Both link-layer (Noise XX msg1/msg2/msg3) and session-layer (SessionSetup/
SessionAck/SessionMsg3) handshakes use message-level retry with exponential backoff
within the handshake timeout window. This handles packet loss on the
underlying transport without waiting for the full handshake timeout to
expire.
@@ -446,7 +456,7 @@ MMP supports three modes, configured via `node.mmp.mode`:
| ---- | ----------------- | ----------------- |
| **Full** (default) | SenderReport + ReceiverReport | All metrics including RTT, loss, jitter, goodput, OWD trend |
| **Lightweight** | ReceiverReport only | Loss (from counter gaps), jitter, OWD trend. No RTT. |
| **Minimal** | None | Spin bit and CE echo flags only. No computed metrics. |
| **Minimal** | None | CE echo flags only. No computed metrics. |
### Report Scheduling
@@ -454,16 +464,10 @@ Reports are sent at RTT-adaptive intervals, clamped to [100ms, 2s]. A
cold-start interval of 500ms is used before SRTT converges. The interval
formula is `clamp(2 × SRTT, 100ms, 2000ms)`.
### Spin Bit and RTT
### RTT Measurement
The SP (spin bit) flag in the FMP inner header follows the QUIC spin bit
pattern: reflected on receive, toggled on send when the reflected value
matches the last sent value. The spin bit state machine runs for TX
reflection, but **RTT samples from the spin bit are discarded**. In a mesh
protocol where frames are sent irregularly (tree announces, bloom filters,
MMP reports on different timers), inter-frame processing delays inflate spin
bit RTT measurements unpredictably. Timestamp-echo from ReceiverReports
(with dwell-time compensation) is the sole SRTT source.
RTT is measured exclusively via timestamp-echo in ReceiverReports with
dwell-time compensation.
### ECN Congestion Signaling
@@ -555,7 +559,9 @@ an attacker sends invalid packets to elicit responses.
| Feature | Status |
| ------- | ------ |
| Noise IK handshake (with epoch) | **Implemented** |
| Noise XX handshake (with epoch and negotiation) | **Implemented** |
| Protocol negotiation (version + features + TLV) | **Implemented** |
| Node profiles (Full, NonRouting, Leaf) | **Implemented** |
| Peer restart detection (epoch mismatch) | **Implemented** |
| Link encryption (ChaCha20-Poly1305) | **Implemented** |
| Index-based session dispatch | **Implemented** |
+22 -15
View File
@@ -347,8 +347,8 @@ The source creates a LookupRequest containing:
- **request_id**: Unique identifier for deduplication
- **target**: The node_addr being sought
- **origin**: The requester's node_addr
- **origin_coords**: The requester's current tree coordinates (so the
response can route back)
- **min_mtu**: Minimum transport MTU the origin requires (transit nodes
skip peers whose link MTU is below this)
- **TTL**: Bounds the forwarding radius
### Bloom-Guided Tree Routing
@@ -433,8 +433,8 @@ the primary mechanism: each transit node looks up the `request_id` in its
`recent_requests` table to find the peer that forwarded the original request,
and sends the response back through that peer. This ensures the response
follows the same path as the request. Greedy tree routing toward the
`origin_coords` is used only as a fallback if the reverse-path entry has
expired.
greedy tree routing toward the origin's coordinates is used only as a
fallback if the reverse-path entry has expired.
**Response-forwarded flag**: Each `recent_requests` entry tracks whether a
response has already been forwarded for that `request_id`. If a second
@@ -663,16 +663,23 @@ When traffic resumes:
3. Coordinates: discovery may be needed if cache has expired
4. SessionSetup re-warms transit caches on the new path
## Leaf-Only Operation *(under development)*
## Node Profiles
Leaf-only operation is an optimization for resource-constrained nodes
(sensors, battery-powered devices). The core infrastructure exists (config
flag, node constructor, bloom filter support) but is not yet enabled in
normal operation.
Nodes advertise a profile during FMP negotiation (bits 0-2 of the feature
bitfield): **Full** (default), **NonRouting**, or **Leaf**. At least one
side of a link must be Full. Config mapping: `disable_routing: true`
NonRouting, `leaf_only: true` → Leaf.
### Concept
### Non-Routing Nodes
A leaf-only node connects to a single upstream peer that handles all routing
A non-routing node participates in the spanning tree but does not forward
transit traffic or send bloom filters. Its full peer inserts the
non-routing node's identity as a leaf dependent. MMP report flow is gated
by wants/provides bits negotiated during the handshake.
### Leaf Nodes
A leaf node connects to a single upstream peer that handles all routing
on its behalf:
- **No bloom filter storage or processing**: The upstream peer includes the
@@ -695,7 +702,7 @@ The upstream peer:
Even as a leaf-only node, it still:
- Maintains its own Noise IK link session with the upstream peer (FMP layer)
- Maintains its own Noise XX link session with the upstream peer (FMP layer)
- Can establish end-to-end FSP sessions with arbitrary destinations
- Has its own identity (npub, node_addr)
@@ -707,8 +714,8 @@ routing decisions but retains its own end-to-end encryption and identity.
| Message | Typical Size | When | Forwarded? |
| ------- | ------------ | ---- | ---------- |
| TreeAnnounce | Variable (depth-dependent) | Topology changes | No (peer-to-peer) |
| FilterAnnounce | ~1 KB | Topology changes | No (peer-to-peer) |
| LookupRequest | ~300 bytes | First contact, recovery | Yes (bloom-guided tree) |
| FilterAnnounce | variable (RLE compressed) | Topology changes | No (peer-to-peer) |
| LookupRequest | 44 bytes + TLV | First contact, recovery | Yes (bloom-guided tree) |
| LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) |
| SessionDatagram + SessionSetup | ~232402 bytes | Session establishment | Yes (routed) |
| SessionDatagram + SessionAck | ~170 bytes | Session confirmation | Yes (routed) |
@@ -766,7 +773,7 @@ recovery).
| Discovery originator backoff | **Implemented** |
| Discovery transit-side rate limiting | **Implemented** |
| Discovery response-forwarded dedup | **Implemented** |
| Leaf-only operation | Under development |
| Node profiles (Full, NonRouting, Leaf) | **Implemented** |
| Link cost in parent selection (ETX) | **Implemented** |
| Link cost in candidate ranking | **Implemented** |
+54 -47
View File
@@ -120,24 +120,29 @@ node, FMP delivers it to FSP for session-layer processing.
Sessions are established on demand when the first datagram needs to be sent to
a destination with no existing session.
FSP uses Noise XK for session key agreement (Noise Protocol Framework;
Perrin 2018). The initiator knows the destination's npub (required for
XK's pre-message `s` token); the responder learns the initiator's
identity from msg3 (not msg1, unlike IK at the link layer). This provides stronger initiator identity hiding
— the initiator's static key is encrypted under the established shared
secret rather than under only the responder's static key.
FSP uses Noise XX for session key agreement (Noise Protocol Framework;
Perrin 2018). Neither side requires prior knowledge of the other's
static key — both identities are revealed during the handshake
(responder in msg2, initiator in msg3). An optional protocol negotiation
payload may be appended to msg2/msg3 (omitted for rekey handshakes).
The handshake is a three-message flow carried in SessionSetup, SessionAck,
and SessionMsg3:
1. **Initiator** sends SessionSetup containing Noise XK msg1 (ephemeral key
1. **Initiator** sends SessionSetup containing Noise XX msg1 (ephemeral key
only) and both parties' tree coordinates
2. **Responder** processes msg1, sends SessionAck containing Noise XK msg2
(ephemeral key + encrypted epoch) and both parties' tree coordinates.
The responder transitions to AwaitingMsg3 state.
3. **Initiator** processes msg2, sends SessionMsg3 containing the encrypted
static key and encrypted epoch. Both parties derive identical symmetric
session keys and the session is established.
2. **Responder** processes msg1, sends SessionAck containing Noise XX msg2
(ephemeral key + encrypted static key + encrypted epoch) and both
parties' tree coordinates. The responder transitions to AwaitingMsg3
state.
3. **Initiator** processes msg2 (learning the responder's identity), sends
SessionMsg3 containing its encrypted static key and encrypted epoch.
The responder learns the initiator's identity from msg3. Both parties
derive identical symmetric session keys and the session is established.
Post-handshake identity verification uses x-only key comparison
(parity-independent) to confirm the revealed identity matches the
expected npub.
Each side's epoch (an 8-byte random value generated at startup) is
exchanged encrypted in msg2 and msg3. On subsequent handshakes, an epoch
@@ -218,29 +223,30 @@ than network addresses. A session survives:
## End-to-End Encryption
### Noise XK Pattern
### Noise XX Pattern
FSP uses Noise XK for session encryption, distinct from the Noise IK
pattern used at the link layer. The full Noise descriptor is
`Noise_XK_secp256k1_ChaChaPoly_SHA256`.
FSP uses the same Noise XX pattern as the link layer (FMP). The full
Noise descriptor is `Noise_XX_secp256k1_ChaChaPoly_SHA256`.
The XK pattern (pre-message: `← s`):
The XX pattern (no pre-message):
- **msg1** (`→ e, es`): Initiator sends ephemeral key only. The initiator's
static identity is not revealed in this message.
- **msg2** (`← e, ee`): Responder sends ephemeral key and encrypted epoch.
- **msg1** (`→ e`): Initiator sends ephemeral key only. No identity
disclosed, no DH with static keys.
- **msg2** (`← e, ee, s, es`): Responder sends ephemeral key, encrypted
static key, and encrypted epoch. The initiator learns the responder's
identity.
- **msg3** (`→ s, se`): Initiator sends encrypted static key and encrypted
epoch. Both parties now share identical session keys.
epoch. The responder learns the initiator's identity. Both parties now
share identical session keys.
After the handshake, Noise produces two directional symmetric keys
(`send_key`, `recv_key`) used with ChaCha20-Poly1305 for all subsequent data.
(`send_key`, `recv_key`) used with ChaCha20-Poly1305 for all subsequent
data.
The XK pattern requires the initiator to know the responder's static key
in advance (the `← s` pre-message), which is satisfied by the discovery
or DNS lookup that precedes session establishment. In exchange, XK
provides stronger initiator identity protection than IK — the initiator's
static key is encrypted under the full shared secret (after three DH
operations) rather than under only the responder's static key.
XX requires no prior knowledge of the peer's static key. The initiator
still needs the destination's npub to address the SessionSetup, but the
Noise handshake itself does not depend on it — identity is verified
post-handshake by comparing the revealed key against the expected npub.
### Cryptographic Primitives
@@ -258,26 +264,27 @@ messaging standard.
### secp256k1 Parity Normalization
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity). The
Noise XK pre-message mixes the responder's static key as a 33-byte compressed
key, and the default secp256k1 ECDH hash includes a parity-dependent version
byte.
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity).
When the Noise XX handshake reveals a peer's static key via
`public_key().serialize()`, the key has its actual parity (0x02 or 0x03
prefix). The default secp256k1 ECDH hash also includes a parity-dependent
version byte.
Both operations are normalized to be parity-independent: the pre-message hash
uses even parity (`0x02` prefix), and ECDH hashes only the x-coordinate of the
result point. This ensures handshakes succeed regardless of the responder's
actual key parity.
Both operations are normalized to be parity-independent: ECDH hashes only
the x-coordinate of the result point, and post-handshake identity
verification uses `x_only_public_key()` to strip parity before comparing
against the expected npub. This ensures handshakes and identity checks
succeed regardless of key parity.
### Privacy Note
Noise XK provides stronger initiator identity protection than IK. In XK, the
initiator's static key is encrypted in msg3 under the full shared secret
(derived from three DH operations), so an attacker who compromises only the
responder's nsec cannot decrypt the initiator's identity from captured
handshake messages (they would also need the responder's ephemeral key).
This is the primary reason FSP uses XK rather than IK — session-layer
traffic traverses untrusted intermediate nodes, making initiator identity
protection more valuable than at the link layer.
Noise XX provides mutual identity protection — both the initiator's and
responder's static keys are encrypted under the evolving shared secret
(derived from DH operations completed in earlier messages). An attacker
who compromises only one side's nsec cannot decrypt the other side's
identity from captured handshake messages without also obtaining the
corresponding ephemeral key. Since session-layer traffic traverses
untrusted intermediate nodes, this mutual identity hiding is valuable.
### Data Packet Authentication
@@ -534,7 +541,7 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
| Feature | Status |
| ------- | ------ |
| Session establishment (Noise XK) | **Implemented** |
| Session establishment (Noise XX) | **Implemented** |
| Peer restart detection (epoch exchange) | **Implemented** |
| MtuExceeded handling | **Implemented** |
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
@@ -571,7 +578,7 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
- Perrin, T. ["The Noise Protocol Framework"](https://noiseprotocol.org/noise.html).
Revision 34, 2018. *Framework for building crypto protocols using Diffie-Hellman
key agreement and AEAD ciphers. FSP uses the XK handshake pattern.*
key agreement and AEAD ciphers. FSP uses the XX handshake pattern.*
- Donenfeld, J.A. ["WireGuard: Next Generation Kernel Network Tunnel"](https://www.wireguard.com/papers/wireguard.pdf).
NDSS 2017. *Transport-independent cryptographic sessions bound to identity keys
+1 -1
View File
@@ -165,7 +165,7 @@ When a node receives a TreeAnnounce from peer P:
1. **Validate version**: Reject if version ≠ 0x01
2. **Verify signature**: Check P's declaration signature using P's known
public key (established during Noise IK handshake)
public key (established during Noise XX handshake)
3. **Verify identity**: Confirm the declaration's node_addr matches the
sender's known identity
4. **Check freshness**: If `sequence ≤ stored sequence for P`, discard
+17 -19
View File
@@ -16,7 +16,7 @@ to the FIPS Mesh Protocol (FMP) above.
The transport layer deals exclusively in **transport addresses** — IP:port
or hostname:port addresses, MAC addresses, .onion identifiers, radio device addresses. These are
opaque to every layer above FMP. The mapping from transport address to FIPS
identity happens at the link layer after the Noise IK link handshake completes.
identity happens at the link layer after the Noise XX link handshake completes.
The word "peer" belongs to the link layer and above; the transport layer
knows only about remote endpoints identified by transport addresses.
@@ -69,7 +69,7 @@ forwarding and LookupResponse transit annotation.
For connection-oriented transports, manage the underlying connection: TCP
handshake, Tor circuit establishment, Bluetooth pairing. FMP cannot begin
the Noise IK link handshake until the transport-layer connection is
the Noise XX link handshake until the transport-layer connection is
established.
Connection-oriented transports expose a non-blocking connect interface.
@@ -153,7 +153,7 @@ and duplication at the routing layer.
**Connection model**: Connectionless transports (UDP, raw Ethernet) allow
immediate datagram exchange. Connection-oriented transports (TCP, Tor, BLE)
require connection setup before FMP can begin the Noise IK link handshake,
require connection setup before FMP can begin the Noise XX link handshake,
adding startup latency.
**Stream vs. datagram**: Datagram transports have natural packet boundaries.
@@ -292,30 +292,28 @@ EtherType 0x2121. SOCK_DGRAM mode
lets the kernel handle Ethernet header construction and parsing — the
transport deals only with payloads and MAC addresses.
Data frames use a 3-byte header: a 1-byte frame type (`0x00`) followed by
a 2-byte little-endian payload length. The length field allows the receiver
to trim Ethernet minimum-frame padding that would otherwise corrupt AEAD
verification. Beacon frames (`0x01`) use only the 1-byte type prefix
(fixed 34-byte payload). Beacons and data share the same EtherType and
socket.
All frames use a unified 4-byte header: `[type:1][flags:1][length:2 LE]`.
The length field allows the receiver to trim Ethernet minimum-frame padding
that would otherwise corrupt AEAD verification. Frame types: `0x00` (data),
`0x01` (beacon). Beacons and data share the same EtherType and socket.
| Property | Value |
| -------- | ----- |
| EtherType | 0x2121 |
| Socket type | AF_PACKET SOCK_DGRAM |
| Data frame header | `[type:1][length:2 LE][payload]` |
| Beacon frame header | `[type:1][payload]` (fixed 34 bytes) |
| Effective MTU | Interface MTU - 3 (typically 1497) |
| Frame header | `[type:1][flags:1][length:2 LE][payload]` |
| Effective MTU | Interface MTU - 4 (typically 1496) |
| Addressing | 6-byte MAC address |
| Platform | Linux only (`CAP_NET_RAW` required) |
### Beacon Discovery
Ethernet nodes discover peers via broadcast beacons sent to
ff:ff:ff:ff:ff:ff. Each beacon is a 34-byte frame containing the sender's
x-only public key. Receiving nodes extract the MAC source address from the
frame and the public key from the payload, then report the discovered peer
to FMP.
ff:ff:ff:ff:ff:ff. Beacons are minimal 5-byte frames (4-byte header +
1-byte beacon type) — no public key is included. The peer's identity is
learned from the Noise XX handshake after the connection is established.
Receiving nodes extract the MAC source address from the frame and report
the discovered address to FMP.
Four configuration flags control discovery behavior:
@@ -400,7 +398,7 @@ every tick, `poll_pending_connects()` calls `connection_state(addr)` to
check progress. When the transport reports `Connected`, the completed
connection is promoted to the established pool (stream split into
read/write halves, per-connection receive task spawned), and the node
initiates the Noise IK link handshake. If the transport reports `Failed`,
initiates the Noise XX link handshake. If the transport reports `Failed`,
the node schedules a retry with exponential backoff.
As a fallback, `send(addr, data)` still performs synchronous
@@ -447,7 +445,7 @@ socket is created).
The Tor transport routes FIPS traffic through the Tor network, hiding
a node's IP address from its peers. A node behind Tor connects outbound
through a local Tor SOCKS5 proxy; the remote peer sees the Tor exit
node's IP, not the initiator's. After the Noise IK handshake, the remote
node's IP, not the initiator's. After the Noise XX handshake, the remote
peer knows the initiator's FIPS identity (npub) but not its network
location.
@@ -528,7 +526,7 @@ The inbound accept loop mirrors the TCP transport's pattern: accept
connection, configure socket (TCP_NODELAY, keepalive), spawn a
per-connection receive loop using the shared FMP stream reader. Inbound
connections arrive from `127.0.0.1` (Tor daemon's local forwarding); peer
identity is resolved during the Noise IK handshake, not from the transport
identity is resolved during the Noise XX handshake, not from the transport
address.
Configuration requires coordinating `torrc` and `fips.yaml`:
+217 -119
View File
@@ -24,14 +24,14 @@ byte stream; the common prefix `payload_len` field provides this
framing directly. TCP and Tor share a common stream reader
(`tcp/stream.rs`) that implements this framing.
**Ethernet data frame header.** The Ethernet transport prepends a 3-byte
header before the FMP payload on data frames: a 1-byte frame type
(`0x00`) followed by a 2-byte little-endian payload length. The length
field allows the receiver to trim Ethernet minimum-frame padding that
would otherwise corrupt AEAD verification. Beacon frames (`0x01`) have
no length field (fixed 34-byte payload). These bytes are consumed by the
transport layer and are not visible to FMP. The effective MTU for FMP is
the interface MTU minus three bytes (typically 1497).
**Ethernet frame header.** The Ethernet transport prepends a 4-byte
unified header before the payload: `[type:1][flags:1][length:2 LE]`.
The length field allows the receiver to trim Ethernet minimum-frame
padding that would otherwise corrupt AEAD verification. Frame types:
`0x00` (data), `0x01` (beacon). Beacons are 5 bytes total (4-byte
header + 1-byte beacon type). These bytes are consumed by the
transport layer and are not visible to FMP. The effective MTU for FMP
is the interface MTU minus four bytes (typically 1496).
## Link-Layer Formats
@@ -45,7 +45,7 @@ length.
| Field | Size | Description |
| ----- | ---- | ----------- |
| version | 4 bits (high) | Protocol version. Currently 0x0 |
| version | 4 bits (high) | Protocol version. Currently 0x1 |
| phase | 4 bits (low) | Session lifecycle phase (see table) |
| flags | 1 byte | Per-packet signal flags (zero during handshake) |
| payload_len | 2 bytes LE | Length of payload after phase-specific header, excluding AEAD tag |
@@ -55,8 +55,9 @@ length.
| Phase | Type | Description |
| ----- | ---- | ----------- |
| 0x0 | Established frame | Post-handshake encrypted traffic |
| 0x1 | Noise IK msg1 | Handshake initiation |
| 0x2 | Noise IK msg2 | Handshake response |
| 0x1 | Noise XX msg1 | Handshake initiation (ephemeral only) |
| 0x2 | Noise XX msg2 | Handshake response (responder identity + negotiation) |
| 0x3 | Noise XX msg3 | Handshake completion (initiator identity + negotiation) |
### Flags (Established Phase Only)
@@ -64,10 +65,9 @@ length.
| --- | ---- | ----------- |
| 0 | K (key epoch) | Selects active key during rekeying |
| 1 | CE | Congestion Experienced echo |
| 2 | SP (spin bit) | RTT measurement |
| 3-7 | — | Reserved (must be zero) |
| 2-7 | — | Reserved (must be zero) |
Flags must be zero in handshake packets (phase 0x1 and 0x2).
Flags must be zero in handshake packets (phase 0x1, 0x2, and 0x3).
### Established Frame (phase 0x0)
@@ -80,7 +80,7 @@ encrypted link-layer message.
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=0, flags, payload_len |
| common prefix | 4 bytes | ver=1, phase=0, flags, payload_len |
| receiver_idx | 4 bytes LE | Session index for O(1) lookup |
| counter | 8 bytes LE | Monotonic nonce, used as AEAD nonce and for replay detection |
@@ -108,67 +108,141 @@ the 1-byte message type and message-specific fields.
| Type | Message | Description |
| ---- | ------- | ----------- |
| 0x00 | SessionDatagram | Encapsulated session-layer payload for forwarding |
| 0x01 | SenderReport | MMP sender-side metrics report (48 bytes) |
| 0x02 | ReceiverReport | MMP receiver-side metrics report (68 bytes) |
| 0x01 | SenderReport | MMP sender-side metrics report (20 bytes) |
| 0x02 | ReceiverReport | MMP receiver-side metrics report (54 bytes) |
| 0x10 | TreeAnnounce | Spanning tree state announcement |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x21 | FilterNack | Bloom filter delta NACK (retransmit request) |
| 0x30 | LookupRequest | Coordinate discovery request |
| 0x31 | LookupResponse | Coordinate discovery response |
| 0x50 | Disconnect | Orderly link teardown |
| 0x51 | Heartbeat | Link liveness probe |
### Noise IK Message 1 (phase 0x1)
### Noise XX Message 1 (phase 0x1)
Handshake initiation from connecting party.
Handshake initiation from connecting party. The initiator sends only its
ephemeral key — neither side's static identity is revealed in msg1.
![Noise IK message 1](diagrams/noise-ik-msg1.svg)
![Noise XX message 1](diagrams/noise-xx-msg1.svg)
Common prefix: ver=0, phase=0x1, flags=0, payload_len=110 (4 + 106).
Common prefix: ver=1, phase=0x1, flags=0, payload_len=37 (4 + 33).
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=1, flags=0, payload_len |
| common prefix | 4 bytes | ver=1, phase=1, flags=0, payload_len |
| sender_idx | 4 bytes LE | Initiator's session index (becomes receiver's `receiver_idx`) |
| noise_msg1 | 106 bytes | Noise IK first message |
| noise_msg1 | 33 bytes | Noise XX first message |
**Noise msg1 breakdown** (106 bytes):
**Noise msg1 breakdown** (33 bytes):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | ephemeral_pubkey | 33 bytes | Initiator's ephemeral key (compressed secp256k1) |
| 33 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
| 82 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
Noise pattern: `-> e, es, s, ss` with epoch payload
Noise pattern: `-> e`
### Noise IK Message 2 (phase 0x2)
**Total wire size**: 41 bytes (4 prefix + 4 sender_idx + 33 noise).
Handshake response from responder.
### Noise XX Message 2 (phase 0x2)
![Noise IK message 2](diagrams/noise-ik-msg2.svg)
Handshake response from responder. The responder reveals its identity
(ephemeral key, encrypted static key, and encrypted epoch) plus an
optional protocol negotiation payload.
Common prefix: ver=0, phase=0x2, flags=0, payload_len=65 (4 + 4 + 57).
![Noise XX message 2](diagrams/noise-xx-msg2.svg)
Common prefix: ver=1, phase=0x2, flags=0, payload_len varies.
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=2, flags=0, payload_len |
| common prefix | 4 bytes | ver=1, phase=2, flags=0, payload_len |
| sender_idx | 4 bytes LE | Responder's session index |
| receiver_idx | 4 bytes LE | Echo of initiator's sender_idx from msg1 |
| noise_msg2 | 57 bytes | Noise IK second message |
| noise_msg2 | 106+ bytes | Noise XX second message (variable with negotiation) |
**Noise msg2 breakdown** (57 bytes):
**Noise msg2 breakdown** (106 bytes base):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | ephemeral_pubkey | 33 bytes | Responder's ephemeral key (compressed secp256k1) |
| 33 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
| 33 | encrypted_static | 49 bytes | Responder's static key (33) + AEAD tag (16) |
| 82 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
Noise pattern: `<- e, ee, se` with epoch payload
Noise pattern: `<- e, ee, s, es` with epoch and negotiation payload
After msg2, both parties derive identical symmetric session keys. The
encrypted epoch in msg1 and msg2 enables peer restart detection — if a
peer's epoch changes, the other side knows it restarted and must
re-establish the link.
**Negotiation payload** (variable, appended via `encrypt_payload()`):
encrypted negotiation bytes + AEAD tag. Minimum 26 bytes (10 payload +
16 tag) when present. See [Protocol Negotiation Payload](#protocol-negotiation-payload).
**Total wire size**: 118 bytes minimum (without negotiation), 144 bytes
typical (with FMP negotiation: 118 + 26).
After msg2, the responder's identity is known to the initiator.
### Noise XX Message 3 (phase 0x3)
Handshake completion from initiator. The initiator reveals its encrypted
static identity and epoch, plus an optional negotiation payload.
![Noise XX message 3](diagrams/noise-xx-msg3.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=1, phase=3, flags=0, payload_len |
| sender_idx | 4 bytes LE | Echo of initiator's sender_idx |
| receiver_idx | 4 bytes LE | Echo of responder's sender_idx from msg2 |
| noise_msg3 | 73+ bytes | Noise XX third message (variable with negotiation) |
**Noise msg3 breakdown** (73 bytes base):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
| 49 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
Noise pattern: `-> s, se` with epoch and negotiation payload
**Negotiation payload** (variable, appended via `encrypt_payload()`):
same format as msg2. Minimum 26 bytes when present.
**Total wire size**: 85 bytes minimum (without negotiation), 111 bytes
typical (with FMP negotiation: 85 + 26).
After msg3, both parties derive identical symmetric session keys and
have exchanged negotiation payloads. The encrypted epoch in msg2 and
msg3 enables peer restart detection — if a peer's epoch changes, the
other side knows it restarted and must re-establish the link.
### Protocol Negotiation Payload
Appended to XX msg2 and msg3 via Noise `encrypt_payload()` — extends
the Noise hash chain so negotiation data is authenticated alongside the
handshake transcript.
**Wire format**: `[format:1][versions:1][features:8][TLV entries...]`
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | format | 1 byte | Must be 0 |
| 1 | versions | 1 byte | High nibble = version_min, low nibble = version_max |
| 2 | features | 8 bytes LE | 64-bit feature bitfield |
| 10 | tlv_entries | variable | TLV extensions: `[field_num:2 LE][length:2 LE][value:N]` per entry |
**Minimum size**: 10 bytes (no TLV entries). With AEAD tag: 26 bytes on
the wire.
**FMP feature bits**:
| Bits | Name | Description |
| ---- | ---- | ----------- |
| 0-2 | Node profile | Full (0), NonRouting (1), Leaf (2) |
| 3-6 | MMP wants/provides | MMP capability negotiation |
| 7 | Bloom | Bloom filter capability |
| 8-63 | — | Reserved |
**Critical**: `decrypt_payload()` MUST be called even if the result is
discarded, to maintain hash chain consistency between the two Noise
transport channels.
### Index Semantics
@@ -230,28 +304,49 @@ includes self)
### FilterAnnounce (0x20)
Bloom filter reachability update, exchanged between direct peers only.
Supports both full sends and delta (XOR diff) updates with RLE
compression.
![FilterAnnounce](diagrams/filter-announce.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x20 |
| 1 | sequence | 8 bytes LE | Monotonic counter for freshness |
| 9 | hash_count | 1 byte | Number of hash functions (5 in v1) |
| 10 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
| 11 | filter_bits | variable | Bloom filter bit array |
| 1 | flags | 1 byte | Bit 0: delta (XOR diff), bits 1-7 reserved |
| 2 | sequence | 8 bytes LE | Monotonic counter, per-peer (only increments on actual send) |
| 10 | base_seq | 8 bytes LE | Sequence of reference filter for delta (0 if full send) |
| 18 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
| 19 | compressed_payload | variable | RLE-encoded filter or XOR diff |
**RLE format**: each run = `[count:2 LE][word:8 LE]` (10 bytes per run).
Sparse XOR diffs compress to very few runs.
**Size class table**:
| size_class | Bytes | Bits | Status |
| ---------- | ----- | ---- | ------ |
| 0 | 512 | 4,096 | Reserved |
| 1 | 1,024 | 8,192 | **v1 (MUST use)** |
| 2 | 2,048 | 16,384 | Reserved |
| 3 | 4,096 | 32,768 | Reserved |
| size_class | Bytes | Bits | Notes |
| ---------- | ----- | ---- | ----- |
| 0 | 512 | 4,096 | Minimum |
| 1 | 1,024 | 8,192 | Default |
| 2 | 2,048 | 16,384 | |
| 3 | 4,096 | 32,768 | |
| 4 | 8,192 | 65,536 | |
| 5 | 16,384 | 131,072 | |
| 6 | 32,768 | 262,144 | Maximum |
**v1 payload**: 1,035 bytes (11 header + 1,024 filter).
With link overhead: 1,072 bytes.
**Size**: variable (19-byte header + RLE-compressed payload).
### FilterNack (0x21)
Request full filter retransmission. Sent when a node receives an
out-of-sequence delta update (sequence gap detected), triggering a
full retransmit from the sender.
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x21 |
| 1 | expected_seq | 8 bytes LE | Sequence number the receiver expected |
**Total**: 9 bytes.
### LookupRequest (0x30)
@@ -271,16 +366,10 @@ restructuring.
| 25 | origin | 16 bytes | Requester's NodeAddr |
| 41 | ttl | 1 byte | Remaining hops (default 64) |
| 42 | min_mtu | 2 bytes LE | Minimum transport MTU the origin requires (0 = no requirement) |
| 44 | origin_coords_cnt | 2 bytes LE | Number of coordinate entries |
| 46 | origin_coords | 16 x n bytes | Requester's ancestry (NodeAddr only) |
| 44 | tlv_entries | variable | TLV extensions, forwarded verbatim by transit |
**Size**: `46 + (n x 16)` bytes, where n = origin depth + 1
| Origin Depth | Payload |
| ------------ | ------- |
| 3 | 110 bytes |
| 5 | 142 bytes |
| 10 | 222 bytes |
**Size**: 44 bytes fixed + TLV entries. TLV format:
`[field_num:2 LE][length:2 LE][value:N]` per entry.
### LookupResponse (0x31)
@@ -298,11 +387,12 @@ requester via the transit nodes that forwarded the request.
| 27 | target_coords_cnt | 2 bytes LE | Number of coordinate entries |
| 29 | target_coords | 16 x n bytes | Target's ancestry (NodeAddr only) |
| 29 + 16n | proof | 64 bytes | Schnorr signature over `(request_id \|\| target \|\| target_coords)` |
| 93 + 16n | tlv_entries | variable | TLV extensions, forwarded verbatim by transit |
**Size**: `93 + (n x 16)` bytes
**Size**: `93 + (n x 16)` bytes + TLV entries
| Target Depth | Payload |
| ------------ | ------- |
| Target Depth | Payload (no TLV) |
| ------------ | ---------------- |
| 3 | 141 bytes |
| 5 | 173 bytes |
| 10 | 253 bytes |
@@ -375,16 +465,16 @@ Sent by the frame sender to provide interval-based transmission statistics.
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x01` |
| 1 | reserved | 3 | Zero |
| 4 | interval_start_counter | 8 | u64 LE — first counter in this interval |
| 12 | interval_end_counter | 8 | u64 LE — last counter in this interval |
| 20 | interval_start_timestamp | 4 | u32 LE — timestamp at interval start |
| 24 | interval_end_timestamp | 4 | u32 LE — timestamp at interval end |
| 28 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval |
| 32 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link |
| 40 | cumulative_bytes_sent | 8 | u64 LE — total bytes sent on this link |
| 1 | format_version | 1 | 0 (current) |
| 2 | total_length | 2 | u16 LE — 16 (v0 payload size) |
| 4 | interval_packets_sent | 4 | u32 LE — packets sent in interval |
| 8 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval |
| 12 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link |
**Total: 48 bytes.**
**Total: 20 bytes.**
Decoders skip trailing bytes beyond `total_length` for forward
compatibility with future format versions.
### ReceiverReport (0x02)
@@ -395,24 +485,26 @@ Sent by the frame receiver to provide loss, jitter, and timing feedback.
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x02` |
| 1 | reserved | 3 | Zero |
| 4 | highest_counter | 8 | u64 LE — highest counter value received |
| 12 | cumulative_packets_recv | 8 | u64 LE — total packets received |
| 20 | cumulative_bytes_recv | 8 | u64 LE — total bytes received |
| 28 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT |
| 32 | dwell_time | 2 | u16 LE — time between receive and echo (ms) |
| 34 | max_burst_loss | 2 | u16 LE — largest loss burst in interval |
| 36 | mean_burst_loss | 2 | u16 LE — mean burst length (u8.8 fixed-point) |
| 38 | reserved | 2 | Zero |
| 40 | jitter | 4 | u32 LE — interarrival jitter (microseconds) |
| 44 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets |
| 48 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) |
| 52 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval |
| 56 | cumulative_reorder_count | 4 | u32 LE — total reordered packets |
| 60 | interval_packets_recv | 4 | u32 LE — packets received in interval |
| 64 | interval_bytes_recv | 4 | u32 LE — bytes received in interval |
| 1 | format_version | 1 | 0 (current) |
| 2 | total_length | 2 | u16 LE — 50 (v0 payload size) |
| 4 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT |
| 8 | dwell_time | 2 | u16 LE — time between receive and echo (ms) |
| 10 | highest_counter | 8 | u64 LE — highest counter value received |
| 18 | cumulative_packets_recv | 8 | u64 LE — total packets received |
| 26 | cumulative_bytes_recv | 8 | u64 LE — total bytes received |
| 34 | jitter | 4 | u32 LE — interarrival jitter (microseconds) |
| 38 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets |
| 42 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) |
| 46 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval |
| 50 | cumulative_reorder_count | 4 | u32 LE — total reordered packets |
**Total: 68 bytes.**
**Total: 54 bytes.**
Fields removed from v0.2: `max_burst_loss`, `mean_burst_loss`,
`interval_packets_recv`, `interval_bytes_recv`.
Decoders skip trailing bytes beyond `total_length` for forward
compatibility with future format versions.
## Session-Layer Message Formats
@@ -434,9 +526,9 @@ protocol version, session lifecycle phase, per-packet flags, and payload length.
| Phase | Type | Description |
| ----- | ---- | ----------- |
| 0x0 | Established | Post-handshake encrypted traffic or plaintext error signals |
| 0x1 | Handshake msg1 | SessionSetup (Noise XK msg1) |
| 0x2 | Handshake msg2 | SessionAck (Noise XK msg2) |
| 0x3 | Handshake msg3 | SessionMsg3 (Noise XK msg3) |
| 0x1 | Handshake msg1 | SessionSetup (Noise XX msg1) |
| 0x2 | Handshake msg2 | SessionAck (Noise XX msg2) |
| 0x3 | Handshake msg3 | SessionMsg3 (Noise XX msg3) |
### FSP Flags (Established Phase Only)
@@ -479,7 +571,7 @@ Transit nodes parse the CP flag and extract coordinates without decryption.
| ----- | ---- | ----------- |
| timestamp | 4 bytes LE | Session-relative milliseconds (u32) |
| msg_type | 1 byte | Session-layer message type |
| inner_flags | 1 byte | Bit 0: SP (spin bit for RTT measurement) |
| inner_flags | 1 byte | Reserved (must be zero) |
After the inner header, the remaining plaintext is the message-type-specific
body.
@@ -513,8 +605,8 @@ happens at the session level based on the FSP message type.
### SessionSetup (phase 0x1)
Establishes a session and warms transit coordinate caches. Contains the
first message of the Noise XK handshake (ephemeral key only — the
initiator's static identity is not revealed until msg3).
first message of the Noise XX handshake (ephemeral key only — neither
side's static identity is revealed until msg2/msg3).
SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
field in the FSP common prefix (0x1, 0x2, 0x3), not by a message type
@@ -535,12 +627,13 @@ Encoded with FSP prefix: ver=0, phase=0x1, flags=0, payload_len.
| ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
| ... | handshake_len | 2 bytes LE | Noise payload length |
| ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) |
| ... | handshake_payload | variable | Noise XX msg1 (33 bytes — ephemeral key only) |
### SessionAck (phase 0x2)
Second message of the Noise XK handshake. The responder sends its
ephemeral key and encrypted epoch.
Second message of the Noise XX handshake. The responder reveals its
identity (ephemeral key, encrypted static key, and encrypted epoch).
Optional FSP negotiation payload may be appended (omitted for rekey).
Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
![SessionAck](diagrams/session-ack.svg)
@@ -555,12 +648,13 @@ Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
| ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries |
| ... | dest_coords | 16 x m bytes | Initiator's ancestry (for return-path cache warming) |
| ... | handshake_len | 2 bytes LE | Noise payload length |
| ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) |
| ... | handshake_payload | variable | Noise XX msg2 (106+ bytes — ephemeral + encrypted static + encrypted epoch + optional negotiation) |
### SessionMsg3 (phase 0x3)
Third and final message of the Noise XK handshake. The initiator reveals
its encrypted static identity and epoch. After msg3, both parties derive
Third and final message of the Noise XX handshake. The initiator reveals
its encrypted static identity and epoch. Optional FSP negotiation payload
may be appended (omitted for rekey). After msg3, both parties derive
identical symmetric session keys and the session is established.
Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
@@ -572,9 +666,9 @@ Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | handshake_len | 2 bytes LE | Noise payload length |
| 3 | handshake_payload | variable | Noise XK msg3 (73 bytes — encrypted static + encrypted epoch) |
| 3 | handshake_payload | variable | Noise XX msg3 (73+ bytes — encrypted static + encrypted epoch + optional negotiation) |
**Noise XK msg3 breakdown** (73 bytes):
**Noise XX msg3 breakdown** (73 bytes base):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
@@ -823,29 +917,33 @@ endpoint session keys).
## Size Summary
### FMP Handshake Messages (Noise IK)
### FMP Handshake Messages (Noise XX)
| Message | Raw Noise | Wire Frame |
| ------- | --------- | ---------- |
| IK msg1 (ephemeral + encrypted static + encrypted epoch) | 106 bytes | 114 bytes |
| IK msg2 (ephemeral + encrypted epoch) | 57 bytes | 69 bytes |
| XX msg1 (ephemeral only) | 33 bytes | 41 bytes |
| XX msg2 (ephemeral + encrypted static + epoch) | 106 bytes | 144 bytes (with negotiation) |
| XX msg3 (encrypted static + epoch) | 73 bytes | 111 bytes (with negotiation) |
### FSP Handshake Messages (Noise XK)
### FSP Handshake Messages (Noise XX)
| Message | Raw Noise | Notes |
| ------- | --------- | ----- |
| XK msg1 (ephemeral only) | 33 bytes | Carried in SessionSetup |
| XK msg2 (ephemeral + encrypted epoch) | 57 bytes | Carried in SessionAck |
| XK msg3 (encrypted static + encrypted epoch) | 73 bytes | Carried in SessionMsg3 |
| XX msg1 (ephemeral only) | 33 bytes | Carried in SessionSetup |
| XX msg2 (ephemeral + encrypted static + epoch) | 106 bytes | Carried in SessionAck |
| XX msg3 (encrypted static + epoch) | 73 bytes | Carried in SessionMsg3 |
### Link-Layer Messages (inside encrypted frame)
| Message | Size | Notes |
| ------- | ---- | ----- |
| TreeAnnounce | 100 + 32n bytes | n = depth + 1 |
| FilterAnnounce | 1,035 bytes | v1 (1KB filter) |
| LookupRequest | 303 + 16n bytes | n = origin depth + 1 |
| LookupResponse | 93 + 16n bytes | n = target depth + 1 |
| FilterAnnounce | variable | 19-byte header + RLE-compressed payload |
| FilterNack | 9 bytes | |
| LookupRequest | 44 bytes + TLV | Fixed (no longer depth-dependent) |
| LookupResponse | 93 + 16n bytes + TLV | n = target depth + 1 |
| SenderReport | 20 bytes | Extensibility header |
| ReceiverReport | 54 bytes | Extensibility header |
| SessionDatagram | 36 + payload bytes | Fixed 36-byte header |
| Disconnect | 2 bytes | |
@@ -853,13 +951,13 @@ endpoint session keys).
| Message | Typical Size | Notes |
| ------- | ------------ | ----- |
| SessionSetup | ~170 bytes | Depth-dependent (XK msg1 = 33 bytes) |
| SessionAck | ~190 bytes | Depth-dependent, carries both endpoints' coords (XK msg2 = 57 bytes) |
| SessionMsg3 | ~80 bytes | Fixed (XK msg3 = 73 bytes, no coords) |
| SessionSetup | ~170 bytes | Depth-dependent (XX msg1 = 33 bytes) |
| SessionAck | ~240 bytes | Depth-dependent, carries both endpoints' coords (XX msg2 = 106+ bytes) |
| SessionMsg3 | ~80 bytes | Fixed (XX msg3 = 73+ bytes, no coords) |
| Data (minimal) | 12 + 6 + 4 + payload + 16 bytes | Steady state (port header included) |
| Data (with coords) | 12 + ~130 + 6 + 4 + payload + 16 bytes | Warmup/recovery (port header included) |
| SenderReport | 12 + 6 + 46 + 16 bytes | MMP metrics |
| ReceiverReport | 12 + 6 + 66 + 16 bytes | MMP metrics |
| SenderReport | 12 + 6 + 20 + 16 bytes | MMP metrics |
| ReceiverReport | 12 + 6 + 54 + 16 bytes | MMP metrics |
| PathMtuNotification | 12 + 6 + 2 + 16 bytes | MTU signal |
| CoordsWarmup | 12 + coords + 6 + 16 bytes | Standalone warmup (empty body) |
| CoordsRequired | 38 bytes | Fixed (prefix + msg_type + body) |