Design documentation illustration and review pass

Wire format diagrams:
- Add 24 SVG diagrams covering every FMP and FSP wire format: common
  prefix, established frame headers, Noise IK handshake messages,
  handshake flow, TreeAnnounce, AncestryEntry, FilterAnnounce,
  LookupRequest/Response, SessionDatagram, Disconnect, SenderReport,
  ReceiverReport, FSP complete message, SessionSetup/Ack/Msg3,
  PathMtuNotification, CoordsRequired, PathBroken, and MtuExceeded
- Replace ASCII art in fips-wire-formats.md with SVG references
- Apply text edits to fips-mesh-layer.md, fips-mesh-operation.md,
  fips-transport-layer.md, and fips-ipv6-adapter.md

Spanning tree dynamics:
- Add 12 topology SVG diagrams: node join (overview + 3-panel steps),
  three-node convergence (4-panel), link addition with depth labels,
  link removal, partition formation, and 6 real-world example diagrams
  (office, mixed-link, two-site WAN topologies)
- Rewrite all code blocks to narrative prose with diagram references
- Add inline prior art attributions distinguishing Yggdrasil-derived
  concepts from FIPS-novel contributions
- Add 3 new references (De Couto ETX, IEEE 802.1D, RFC 2328 OSPF) and
  Prior Art summary
- Remove outdated sections: indirect partition note, integration test
  gaps, DHT-based lookup reference
- Change "must elect a new root" to "must rediscover its new root"

Spanning tree design review (fips-spanning-tree.md):
- Rename "Root Election" to "Root Discovery" across docs
- Add "What Is a Spanning Tree?" introductory section
- Add parent selection intro explaining self-organization role
- Fix tree distance example: 4 hops, not 2
- Clarify timestamp field as advisory only
- Remove unimplemented ROOT_TIMEOUT and TREE_ENTRY_TTL from timing
  parameters and implementation status tables

Bloom filter design review (fips-bloom-filters.md):
- Add "What Is a Bloom Filter?" intro section
- Rewrite Purpose section to frame filters as routing path identification
- Correct FPR analysis (old values were 3-50x overstated)
- Add Filter Occupancy Model based on network size and tree position
- Fix filter expiration to describe actual MMP-based cleanup
- Combine Scale Considerations with Size Classes after Wire Format
- Fix stale FPR values in src/bloom/mod.rs comments

Session layer review (fips-session-layer.md):
- Add inline prior art attributions: Noise Protocol Framework, WireGuard,
  DTLS (RFC 6347), IKEv2 (RFC 7296), RFC 1191 PMTUD, Yggdrasil, NIP-44
- Replace warmup state machine ASCII art with SVG diagram
- Convert CoordsWarmup wire format code block to prose
- Add External References section with full citations

Level 5 implementation doc cleanup:
- Delete fips-software-architecture.md (redundant with protocol layer docs)
- Delete fips-state-machines.md (Rust tutorial, not protocol design)
- Add fipsctl command reference to README.md
- Update cross-references in fips-intro.md, docs/design/README.md,
  fips-transport-layer.md, fips-configuration.md

Fixes:
- Correct fd::/8 to fd00::/8 in fips-session-layer.md,
  fips-identity-derivation.svg, and fips-node-architecture.svg
- Fix config example MTU: 1197 → 1472 in fips-configuration.md

File organization:
- Move all SVG diagrams into docs/design/diagrams/ subdirectory
- Update all diagram references to use new paths
This commit is contained in:
Johnathan Corgan
2026-02-24 17:32:37 +00:00
parent c1820e689d
commit 00e26765bd
62 changed files with 3232 additions and 1754 deletions

View File

@@ -69,6 +69,28 @@ sudo ./target/release/fips -c fips.yaml
See [docs/design/fips-configuration.md](docs/design/fips-configuration.md) for
the full configuration reference.
### Inspect
While a node is running, use `fipsctl` to inspect its state:
```
fipsctl show status # Node status overview
fipsctl show peers # Authenticated peers
fipsctl show links # Active links
fipsctl show tree # Spanning tree state
fipsctl show sessions # End-to-end sessions
fipsctl show bloom # Bloom filter state
fipsctl show mmp # MMP metrics summary
fipsctl show cache # Coordinate cache stats
fipsctl show connections # Pending handshake connections
fipsctl show transports # Transport instances
fipsctl show routing # Routing table summary
```
`fipsctl` communicates with the node via a Unix domain control socket
(enabled by default). All queries are read-only. Use `-s <path>` to
override the socket path.
### Multi-node Testing
See [testing/](testing/) for Docker-based integration test harnesses including

View File

@@ -31,15 +31,13 @@ specific topics.
| Document | Description |
| -------- | ----------- |
| [fips-spanning-tree.md](fips-spanning-tree.md) | Spanning tree algorithms: root election, parent selection, coordinates |
| [fips-spanning-tree.md](fips-spanning-tree.md) | Spanning tree algorithms: root discovery, parent selection, coordinates |
| [fips-bloom-filters.md](fips-bloom-filters.md) | Bloom filter math: FPR analysis, size classes, split-horizon |
### Implementation
| Document | Description |
| -------- | ----------- |
| [fips-software-architecture.md](fips-software-architecture.md) | Stable architectural decisions guiding the codebase |
| [fips-state-machines.md](fips-state-machines.md) | Phase-based state machine pattern (Rust enum-of-structs) |
| [fips-configuration.md](fips-configuration.md) | YAML configuration reference |
### Supplemental
@@ -54,16 +52,16 @@ specific topics.
fips-intro.md
┌──────────────┼──────────────┐
▼ ▼
fips-transport-layer fips-mesh- fips-software-
│ operation architecture
▼ ▼
fips-transport-layer fips-mesh-
│ operation
▼ │ │
fips-mesh-layer ◄────────┤
│ │ fips-state-machines
fips-session-layer ├──► fips-spanning-tree
│ │
└──► fips-bloom-filters
fips-mesh-layer ◄────────┤
│ │
├──► fips-spanning-tree
fips-session-layer │ │
└──► fips-bloom-filters
fips-ipv6-adapter
fips-wire-formats
(referenced by all layer docs)

View File

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 192" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="192" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">AncestryEntry (32 bytes)</text>
<!-- Row 0 (015): node_addr -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">015</text>
<rect x="55" y="36" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">node_addr</text>
<text x="315" y="76" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — routing identifier</text>
<!-- Row 1 (1623): sequence -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">1623</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">sequence</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 2 (2431): timestamp -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">2431</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">timestamp</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE — Unix seconds</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 160" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="160" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="290" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP/FSP Common Prefix (4 bytes)</text>
<!-- Bit ruler -->
<text x="65" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="130" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="195" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="325" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="530" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Byte 0: version (high nibble) -->
<rect x="35" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="82" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="82" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<!-- Byte 0: phase (low nibble) -->
<rect x="130" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="177" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="177" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<!-- Byte 1: flags -->
<rect x="225" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="290" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="290" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<!-- Bytes 2-3: payload_len -->
<rect x="355" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="455" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="455" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Byte offset labels -->
<text x="82" y="130" fill="#666" font-size="10" text-anchor="middle">byte 0</text>
<text x="290" y="130" fill="#666" font-size="10" text-anchor="middle">byte 1</text>
<text x="455" y="130" fill="#666" font-size="10" text-anchor="middle">bytes 2-3</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 218" font-family="monospace" font-size="13">
<!-- Background -->
<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">CoordsRequired (0x20) — 34 bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr we couldn't route to</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</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">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Note -->
<text x="310" y="212" fill="#777" font-size="10" text-anchor="middle">plaintext error signal (U flag set) · total on wire: 38 bytes (4 prefix + 34 payload)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 114" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="114" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Disconnect (0x50) — 2 bytes</text>
<!-- Row 0 (01): msg_type + reason -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x50</text>
<rect x="315" y="36" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="445" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">reason</text>
<text x="445" y="78" fill="#f88aaa" text-anchor="middle" font-size="10">1 byte</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,100 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 168" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="780" height="168" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="390" y="22" text-anchor="middle" font-size="14" font-weight="bold" fill="#e0e0e0">Three-Node Convergence</text>
<!-- Panel separators -->
<line x1="193" y1="32" x2="193" y2="160" stroke="#303050" stroke-width="1"/>
<line x1="390" y1="32" x2="390" y2="160" stroke="#303050" stroke-width="1"/>
<line x1="587" y1="32" x2="587" y2="160" stroke="#303050" stroke-width="1"/>
<!-- ======================== -->
<!-- Panel 1: T0 — Isolated -->
<!-- ======================== -->
<text x="95" y="44" text-anchor="middle" font-size="10" fill="#909090">T0 &#x2014; Isolated</text>
<text x="45" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<text x="95" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<text x="145" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<circle cx="45" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="45" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">A</text>
<circle cx="95" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="95" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">B</text>
<circle cx="145" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="145" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">C</text>
<text x="95" y="150" text-anchor="middle" font-size="9" fill="#606060">node_addr: A &lt; B &lt; C</text>
<!-- ======================== -->
<!-- Panel 2: T1 — Links form -->
<!-- ======================== -->
<text x="292" y="44" text-anchor="middle" font-size="10" fill="#909090">T1 &#x2014; Links form</text>
<text x="242" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<text x="292" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<text x="342" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<!-- Peer link A—B (dashed gray) -->
<line x1="260" y1="105" x2="274" y2="105" stroke="#404860" stroke-width="1.5" stroke-dasharray="6,3"/>
<!-- Peer link B—C (dashed gray) -->
<line x1="310" y1="105" x2="324" y2="105" stroke="#404860" stroke-width="1.5" stroke-dasharray="6,3"/>
<circle cx="242" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="242" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">A</text>
<circle cx="292" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="292" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">B</text>
<circle cx="342" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="342" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">C</text>
<!-- ======================== -->
<!-- Panel 3: T2 — B re-parents -->
<!-- ======================== -->
<text x="490" y="44" text-anchor="middle" font-size="10" fill="#909090">T2 &#x2014; B re-parents</text>
<text x="440" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<text x="540" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<!-- Tree link A—B (gold solid) -->
<line x1="458" y1="105" x2="472" y2="105" stroke="#d0a040" stroke-width="2"/>
<!-- Peer link B—C (dashed gray) -->
<line x1="508" y1="105" x2="522" y2="105" stroke="#404860" stroke-width="1.5" stroke-dasharray="6,3"/>
<circle cx="440" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="440" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">A</text>
<circle cx="490" cy="105" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="490" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">B</text>
<circle cx="540" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="540" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">C</text>
<!-- ======================== -->
<!-- Panel 4: T3 — Converged -->
<!-- ======================== -->
<text x="685" y="44" text-anchor="middle" font-size="10" fill="#909090">T3 &#x2014; Converged</text>
<text x="635" y="72" text-anchor="middle" font-size="8" fill="#d0a040">&#x2605; root</text>
<!-- Tree link A—B (gold solid) -->
<line x1="653" y1="105" x2="667" y2="105" stroke="#d0a040" stroke-width="2"/>
<!-- Tree link B—C (gold solid) -->
<line x1="703" y1="105" x2="717" y2="105" stroke="#d0a040" stroke-width="2"/>
<circle cx="635" cy="105" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="635" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">A</text>
<circle cx="685" cy="105" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="685" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">B</text>
<circle cx="735" cy="105" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="735" y="109" text-anchor="middle" font-weight="bold" fill="#e0e0e0">C</text>
<text x="685" y="150" text-anchor="middle" font-size="9" fill="#909090">A &#x2190; B &#x2190; C</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 320">
<rect width="500" height="320" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="250" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Office Network — Physical Topology</text>
<text x="250" y="48" text-anchor="middle" font-family="monospace" font-size="11" fill="#909090">node_addr: A &lt; C &lt; B &lt; E &lt; D</text>
<!-- Node positions: A(100,110) B(340,110) D(100,250) C(340,250) E(460,250) -->
<!-- Mesh links (gray) -->
<line x1="100" y1="110" x2="340" y2="110" stroke="#404860" stroke-width="1.5"/><!-- A—B -->
<line x1="100" y1="110" x2="100" y2="250" stroke="#404860" stroke-width="1.5"/><!-- A—D -->
<line x1="100" y1="110" x2="340" y2="250" stroke="#404860" stroke-width="1.5"/><!-- A—C -->
<line x1="340" y1="110" x2="340" y2="250" stroke="#404860" stroke-width="1.5"/><!-- B—C -->
<line x1="340" y1="110" x2="100" y2="250" stroke="#404860" stroke-width="1.5"/><!-- B—D -->
<line x1="100" y1="250" x2="340" y2="250" stroke="#404860" stroke-width="1.5"/><!-- D—C -->
<line x1="340" y1="250" x2="460" y2="250" stroke="#404860" stroke-width="1.5"/><!-- C—E -->
<!-- Node A -->
<circle cx="100" cy="110" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<!-- Node B -->
<circle cx="340" cy="110" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="340" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<!-- Node D -->
<circle cx="100" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">D</text>
<!-- Node C -->
<circle cx="340" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="340" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
<!-- Node E -->
<circle cx="460" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="460" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 450 320">
<rect width="450" height="320" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="225" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Office Network — Converged Tree</text>
<!-- Node positions: A(225,90) B(100,190) C(225,190) D(350,190) E(225,280) -->
<!-- Mesh links (background, faint) -->
<line x1="225" y1="90" x2="100" y2="190" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- A—B -->
<line x1="225" y1="90" x2="350" y2="190" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- A—D -->
<line x1="100" y1="190" x2="225" y2="190" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- B—C -->
<line x1="100" y1="190" x2="350" y2="190" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- B—D -->
<line x1="350" y1="190" x2="225" y2="190" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- D—C -->
<line x1="225" y1="190" x2="225" y2="280" stroke="#404860" stroke-width="1" opacity="0.4"/><!-- C—E -->
<!-- Tree links (gold) -->
<line x1="225" y1="90" x2="100" y2="190" stroke="#d0a040" stroke-width="2.5"/><!-- A→B -->
<line x1="225" y1="90" x2="225" y2="190" stroke="#d0a040" stroke-width="2.5"/><!-- A→C -->
<line x1="225" y1="90" x2="350" y2="190" stroke="#d0a040" stroke-width="2.5"/><!-- A→D -->
<line x1="225" y1="190" x2="225" y2="280" stroke="#d0a040" stroke-width="2.5"/><!-- C→E -->
<!-- Node A (root) -->
<circle cx="225" cy="90" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="225" y="95" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<text x="225" y="56" text-anchor="middle" font-family="monospace" font-size="11" fill="#d0a040">&#9733; root</text>
<!-- Node B -->
<circle cx="100" cy="190" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="195" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<!-- Node C -->
<circle cx="225" cy="190" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="225" y="195" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
<!-- Node D -->
<circle cx="350" cy="190" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="350" y="195" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">D</text>
<!-- Node E -->
<circle cx="225" cy="280" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="225" y="285" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 550 320">
<rect width="550" height="320" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="275" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Mixed Network — Physical Topology</text>
<text x="275" y="48" text-anchor="middle" font-family="monospace" font-size="11" fill="#909090">node_addr: B &lt; A &lt; D &lt; E &lt; C</text>
<!-- Node positions: A(100,110) B(420,110) C(100,250) D(275,250) E(450,250) -->
<!-- Fiber links (thick solid green) -->
<line x1="100" y1="110" x2="420" y2="110" stroke="#60a060" stroke-width="3"/><!-- A═B fiber -->
<line x1="420" y1="110" x2="275" y2="250" stroke="#60a060" stroke-width="3"/><!-- B═D fiber -->
<line x1="275" y1="250" x2="450" y2="250" stroke="#60a060" stroke-width="3"/><!-- D═E fiber -->
<!-- DSL link (medium dashed yellow) -->
<line x1="100" y1="250" x2="275" y2="250" stroke="#a0a060" stroke-width="2" stroke-dasharray="8,3"/><!-- C—D DSL -->
<!-- Radio link (thin dotted red) -->
<line x1="100" y1="110" x2="100" y2="250" stroke="#a06060" stroke-width="1.5" stroke-dasharray="3,3"/><!-- A~C radio -->
<!-- Link labels -->
<text x="260" y="100" text-anchor="middle" font-family="monospace" font-size="10" fill="#60a060">fiber</text>
<text x="365" y="178" text-anchor="start" font-family="monospace" font-size="10" fill="#60a060">fiber</text>
<text x="362" y="240" text-anchor="middle" font-family="monospace" font-size="10" fill="#60a060">fiber</text>
<text x="187" y="268" text-anchor="middle" font-family="monospace" font-size="10" fill="#a0a060">DSL 1Mbps</text>
<text x="78" y="183" text-anchor="end" font-family="monospace" font-size="10" fill="#a06060">radio</text>
<text x="78" y="195" text-anchor="end" font-family="monospace" font-size="10" fill="#a06060">9600bps</text>
<!-- Node A -->
<circle cx="100" cy="110" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<!-- Node B -->
<circle cx="420" cy="110" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="420" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<!-- Node C -->
<circle cx="100" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
<!-- Node D -->
<circle cx="275" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="275" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">D</text>
<!-- Node E -->
<circle cx="450" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="450" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 450 320">
<rect width="450" height="320" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="225" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Mixed Network — Cost-Weighted Tree</text>
<!--
Tree layout:
B (225,90) root
/ \
A D
(120,180) (330,180)
|\
E C
(380,270) (270,270)
-->
<!-- Non-tree physical links (faint background) -->
<!-- A~C radio (non-tree) -->
<line x1="120" y1="180" x2="270" y2="270" stroke="#404860" stroke-width="1" opacity="0.3" stroke-dasharray="3,3"/>
<!-- C—D DSL (non-tree, now C is child of D so this is tree; but the physical DSL link exists) -->
<!-- Actually C→D is tree, so only A~C is non-tree -->
<!-- Tree links (gold) -->
<!-- B→A -->
<line x1="225" y1="90" x2="120" y2="180" stroke="#d0a040" stroke-width="2.5"/>
<!-- B→D -->
<line x1="225" y1="90" x2="330" y2="180" stroke="#d0a040" stroke-width="2.5"/>
<!-- D→E -->
<line x1="330" y1="180" x2="380" y2="270" stroke="#d0a040" stroke-width="2.5"/>
<!-- D→C -->
<line x1="330" y1="180" x2="270" y2="270" stroke="#d0a040" stroke-width="2.5"/>
<!-- Cost labels on tree links -->
<text x="155" y="128" text-anchor="middle" font-family="monospace" font-size="10" fill="#d0a040">eff 1.01</text>
<text x="295" y="128" text-anchor="middle" font-family="monospace" font-size="10" fill="#d0a040">eff 1.01</text>
<text x="375" y="228" text-anchor="start" font-family="monospace" font-size="10" fill="#d0a040">eff 2.01</text>
<text x="278" y="228" text-anchor="end" font-family="monospace" font-size="10" fill="#d0a040">eff 2.25</text>
<!-- Node B (root) -->
<circle cx="225" cy="90" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="225" y="95" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<text x="225" y="56" text-anchor="middle" font-family="monospace" font-size="10" fill="#d0a040">★ root</text>
<!-- Node A -->
<circle cx="120" cy="180" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="120" y="185" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<!-- Node D -->
<circle cx="330" cy="180" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="330" y="185" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">D</text>
<!-- Node E -->
<circle cx="380" cy="270" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="380" y="275" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
<!-- Node C -->
<circle cx="270" cy="270" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="270" y="275" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 300">
<rect width="600" height="300" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Network Partition — Independent Trees</text>
<!-- Partition separator (vertical dashed line) -->
<line x1="300" y1="50" x2="300" y2="290" stroke="#404060" stroke-width="1" stroke-dasharray="6,4"/>
<!-- Partition labels -->
<text x="150" y="68" text-anchor="middle" font-family="monospace" font-size="11" fill="#606880">Partition 1</text>
<text x="450" y="68" text-anchor="middle" font-family="monospace" font-size="11" fill="#606880">Partition 2</text>
<!-- Node positions:
Partition 1: A(150,110) B(150,180) C(150,250)
Partition 2: E(450,110) F(450,180) G(450,250)
-->
<!-- Tree links — Partition 1 (gold) -->
<line x1="150" y1="132" x2="150" y2="158" stroke="#d0a040" stroke-width="2.5"/><!-- A→B -->
<line x1="150" y1="202" x2="150" y2="228" stroke="#d0a040" stroke-width="2.5"/><!-- B→C -->
<!-- Tree links — Partition 2 (gold) -->
<line x1="450" y1="132" x2="450" y2="158" stroke="#d0a040" stroke-width="2.5"/><!-- E→F -->
<line x1="450" y1="202" x2="450" y2="228" stroke="#d0a040" stroke-width="2.5"/><!-- F→G -->
<!-- WAN failed indicator -->
<!-- Red X between the two partitions -->
<line x1="288" y1="134" x2="312" y2="146" stroke="#c04040" stroke-width="2.5" stroke-linecap="round"/>
<line x1="312" y1="134" x2="288" y2="146" stroke="#c04040" stroke-width="2.5" stroke-linecap="round"/>
<text x="300" y="165" text-anchor="middle" font-family="monospace" font-size="10" fill="#c04040">WAN failed</text>
<!-- Node A (root) -->
<circle cx="150" cy="110" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="150" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<text x="193" y="108" text-anchor="start" font-family="monospace" font-size="10" fill="#d0a040">★ root</text>
<!-- Node B -->
<circle cx="150" cy="180" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="150" y="185" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<!-- Node C -->
<circle cx="150" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="150" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
<!-- Node E (root) -->
<circle cx="450" cy="110" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="450" y="115" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
<text x="493" y="108" text-anchor="start" font-family="monospace" font-size="10" fill="#d0a040">★ root</text>
<!-- Node F -->
<circle cx="450" cy="180" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="450" y="185" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">F</text>
<!-- Node G -->
<circle cx="450" cy="250" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="450" y="255" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">G</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 280">
<rect width="650" height="280" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="325" y="30" text-anchor="middle" font-family="monospace" font-size="15" font-weight="bold" fill="#e0e0e0">Two-Site Network — Physical Topology</text>
<text x="325" y="48" text-anchor="middle" font-family="monospace" font-size="11" fill="#909090">node_addr: A &lt; E &lt; B &lt; F &lt; C &lt; G</text>
<!-- Site 1 grouping -->
<rect x="42" y="68" width="200" height="185" rx="8" fill="none" stroke="#303050" stroke-width="1" stroke-dasharray="4,2"/>
<text x="142" y="86" text-anchor="middle" font-family="monospace" font-size="11" fill="#606880">Site 1</text>
<!-- Site 2 grouping -->
<rect x="408" y="68" width="200" height="185" rx="8" fill="none" stroke="#303050" stroke-width="1" stroke-dasharray="4,2"/>
<text x="508" y="86" text-anchor="middle" font-family="monospace" font-size="11" fill="#606880">Site 2</text>
<!-- Node positions:
Site 1: A(100,140) B(200,140) C(200,220)
Site 2: E(450,140) F(550,140) G(550,220)
-->
<!-- Local links — Site 1 -->
<line x1="100" y1="140" x2="200" y2="140" stroke="#404860" stroke-width="1.5"/><!-- A—B -->
<line x1="200" y1="140" x2="200" y2="220" stroke="#404860" stroke-width="1.5"/><!-- B—C -->
<!-- Local links — Site 2 -->
<line x1="450" y1="140" x2="550" y2="140" stroke="#404860" stroke-width="1.5"/><!-- E—F -->
<line x1="550" y1="140" x2="550" y2="220" stroke="#404860" stroke-width="1.5"/><!-- F—G -->
<!-- WAN link B—E (dashed) -->
<line x1="200" y1="140" x2="450" y2="140" stroke="#404860" stroke-width="1.5" stroke-dasharray="8,4"/>
<text x="325" y="132" text-anchor="middle" font-family="monospace" font-size="10" fill="#909090">WAN</text>
<!-- Node A -->
<circle cx="100" cy="140" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="100" y="145" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">A</text>
<!-- Node B -->
<circle cx="200" cy="140" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="200" y="145" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">B</text>
<!-- Node C -->
<circle cx="200" cy="220" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="200" y="225" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">C</text>
<!-- Node E -->
<circle cx="450" cy="140" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="450" y="145" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">E</text>
<!-- Node F -->
<circle cx="550" cy="140" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="550" y="145" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">F</text>
<!-- Node G -->
<circle cx="550" cy="220" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="550" y="225" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#e0e0e0">G</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 410" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="650" height="410" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="325" y="30" fill="#e0e0e0" text-anchor="middle" font-size="15" font-weight="bold">Link Addition &#x2014; D Re-parents to A</text>
<!-- Section titles -->
<text x="165" y="58" fill="#909090" text-anchor="middle" font-size="14">Before</text>
<text x="485" y="58" fill="#909090" text-anchor="middle" font-size="14">After</text>
<!-- Arrow between halves -->
<text x="325" y="255" fill="#909090" text-anchor="middle" font-size="24">&#x2192;</text>
<!-- ======================== -->
<!-- BEFORE (left side) -->
<!-- ======================== -->
<!-- Root label at y=82, node A at cy=120 (top of circle at 98, 16px below label) -->
<!-- Vertical chain: A(120) - B(200) - C(280) - D(360) at x=165 -->
<text x="165" y="82" fill="#d0a040" text-anchor="middle" font-size="10">&#x2605; root</text>
<!-- Depth labels (left side) -->
<text x="205" y="124" fill="#606060" font-size="10" text-anchor="start">depth 0</text>
<text x="205" y="204" fill="#606060" font-size="10" text-anchor="start">depth 1</text>
<text x="205" y="284" fill="#606060" font-size="10" text-anchor="start">depth 2</text>
<text x="205" y="364" fill="#606060" font-size="10" text-anchor="start">depth 3</text>
<!-- Tree links (gold) -->
<line x1="165" y1="142" x2="165" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<line x1="165" y1="222" x2="165" y2="258" stroke="#d0a040" stroke-width="2.5"/>
<line x1="165" y1="302" x2="165" y2="338" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node A (root) -->
<circle cx="165" cy="120" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="165" y="125" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Node B -->
<circle cx="165" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="165" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Node C -->
<circle cx="165" cy="280" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="165" y="285" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- Node D -->
<circle cx="165" cy="360" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="165" y="365" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- ======================== -->
<!-- AFTER (right side) -->
<!-- ======================== -->
<!-- A at (485,120), B at (435,200), D at (535,200), C at (435,280) -->
<text x="485" y="82" fill="#d0a040" text-anchor="middle" font-size="10">&#x2605; root</text>
<!-- Depth labels (right side) -->
<text x="595" y="124" fill="#606060" font-size="10" text-anchor="start">depth 0</text>
<text x="595" y="204" fill="#606060" font-size="10" text-anchor="start">depth 1</text>
<text x="595" y="284" fill="#606060" font-size="10" text-anchor="start">depth 2</text>
<text x="595" y="364" fill="#606060" font-size="10" text-anchor="start">depth 3</text>
<!-- Ghost D at old position (depth 3) -->
<circle cx="485" cy="360" r="22" fill="none" stroke="#404040" stroke-width="1.5" stroke-dasharray="4,3"/>
<text x="485" y="365" fill="#404040" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Tree links (gold) -->
<line x1="473" y1="138" x2="447" y2="182" stroke="#d0a040" stroke-width="2.5"/>
<line x1="497" y1="138" x2="523" y2="182" stroke="#d0a040" stroke-width="2.5"/>
<line x1="435" y1="222" x2="435" y2="258" stroke="#d0a040" stroke-width="2.5"/>
<!-- Dashed line from D new to ghost D old -->
<line x1="535" y1="222" x2="497" y2="342" stroke="#404040" stroke-width="1" stroke-dasharray="4,3"/>
<!-- Node A (root) -->
<circle cx="485" cy="120" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="485" y="125" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Node B -->
<circle cx="435" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="435" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Node D (re-parented to depth 1) -->
<circle cx="535" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="535" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Node C -->
<circle cx="435" cy="280" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="435" y="285" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- "new link" annotation on A→D -->
<text x="530" y="155" fill="#5a9f5a" text-anchor="start" font-size="9">new link</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 310" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="700" height="310" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="350" y="28" fill="#e0e0e0" text-anchor="middle" font-size="15" font-weight="bold">Link Removal &#8212; B&#8596;C Link Fails</text>
<!-- Section divider between Before and After -->
<line x1="300" y1="42" x2="300" y2="300" stroke="#404060" stroke-width="1" stroke-dasharray="6,4"/>
<!-- ======================== BEFORE (left side) ======================== -->
<text x="150" y="55" fill="#909090" text-anchor="middle" font-size="14">Before</text>
<!-- Node A (root) at y=110 -->
<text x="150" y="86" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="150" cy="110" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="150" y="115" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Link A-B -->
<line x1="150" y1="130" x2="150" y2="160" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node B at y=180 -->
<circle cx="150" cy="180" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="150" y="185" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Link B-D -->
<line x1="130" y1="196" x2="90" y2="235" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node D at y=255 -->
<circle cx="80" cy="255" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="80" y="260" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Link B-C (failing - dashed) -->
<line x1="170" y1="196" x2="210" y2="235" stroke="#d0a040" stroke-width="2.5" stroke-dasharray="4,3" opacity="0.5"/>
<!-- Red X on B-C link -->
<line x1="178" y1="208" x2="202" y2="228" stroke="#c04040" stroke-width="3" stroke-linecap="round"/>
<line x1="202" y1="208" x2="178" y2="228" stroke="#c04040" stroke-width="3" stroke-linecap="round"/>
<!-- Node C at y=255 -->
<circle cx="220" cy="255" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="220" y="260" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- ======================== AFTER (right side) ======================== -->
<text x="500" y="55" fill="#909090" text-anchor="middle" font-size="14">After</text>
<!-- Node A (root) at y=110 -->
<text x="440" y="86" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="440" cy="110" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="440" y="115" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Link A-B -->
<line x1="440" y1="130" x2="440" y2="160" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node B at y=180 -->
<circle cx="440" cy="180" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="440" y="185" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Link B-D -->
<line x1="440" y1="200" x2="440" y2="235" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node D at y=255 -->
<circle cx="440" cy="255" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="440" y="260" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Node C (isolated, its own root) at y=180 -->
<text x="600" y="148" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="600" cy="180" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="600" y="185" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- "isolated" label under C -->
<text x="600" y="215" fill="#909090" text-anchor="middle" font-size="10">(isolated)</text>
<!-- Ghost link where B-C used to be -->
<line x1="460" y1="180" x2="580" y2="180" stroke="#404040" stroke-width="1.5" stroke-dasharray="4,3" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,140 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 310" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="780" height="310" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="390" y="24" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Node B Joins via D &#x2014; State Progression</text>
<!-- Panel subtitles -->
<text x="130" y="46" fill="#909090" text-anchor="middle" font-size="11">T1 &#x2014; Link established</text>
<text x="390" y="46" fill="#909090" text-anchor="middle" font-size="11">T2&#x2013;T3 &#x2014; B re-parents to D</text>
<text x="650" y="46" fill="#909090" text-anchor="middle" font-size="11">T4&#x2013;T5 &#x2014; Filter propagation</text>
<!-- Vertical separators -->
<line x1="260" y1="36" x2="260" y2="300" stroke="#303050" stroke-width="1"/>
<line x1="520" y1="36" x2="520" y2="300" stroke="#303050" stroke-width="1"/>
<!-- ======================== -->
<!-- PANEL 1: Link established -->
<!-- ======================== -->
<!-- A at (130, 110), children C(65,190) D(130,190) E(195,190), B below D at (130,265) -->
<!-- Root label -->
<text x="130" y="82" fill="#d0a040" text-anchor="middle" font-size="9">&#x2605; root</text>
<!-- Tree links A to children -->
<line x1="130" y1="128" x2="65" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="130" y1="128" x2="130" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="130" y1="128" x2="195" y2="172" stroke="#d0a040" stroke-width="2"/>
<!-- New peer link D to B (dashed green, vertical) -->
<line x1="130" y1="208" x2="130" y2="247" stroke="#60a060" stroke-width="2" stroke-dasharray="6,3"/>
<text x="152" y="233" fill="#60a060" font-size="9">new link</text>
<!-- Node A (root) -->
<circle cx="130" cy="110" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="130" y="114" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">A</text>
<!-- Node C -->
<circle cx="65" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="65" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">C</text>
<!-- Node D -->
<circle cx="130" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="130" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">D</text>
<!-- Node E -->
<circle cx="195" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="195" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">E</text>
<!-- Node B (not yet in tree, dashed border) -->
<circle cx="130" cy="265" r="18" fill="#1a1a2e" stroke="#808890" stroke-width="2" stroke-dasharray="4,3"/>
<text x="130" y="269" fill="#808890" text-anchor="middle" font-weight="bold" font-size="13">B</text>
<!-- ======================== -->
<!-- PANEL 2: B re-parents to D -->
<!-- ======================== -->
<!-- A at (390, 110), children C(325,190) D(390,190) E(455,190), B at (390,265) -->
<!-- Root label -->
<text x="390" y="82" fill="#d0a040" text-anchor="middle" font-size="9">&#x2605; root</text>
<!-- Tree links A to children -->
<line x1="390" y1="128" x2="325" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="390" y1="128" x2="390" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="390" y1="128" x2="455" y2="172" stroke="#d0a040" stroke-width="2"/>
<!-- Tree link D to B (gold) -->
<line x1="390" y1="208" x2="390" y2="247" stroke="#d0a040" stroke-width="2"/>
<!-- Node A (root) -->
<circle cx="390" cy="110" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="390" y="114" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">A</text>
<!-- Node C -->
<circle cx="325" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="325" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">C</text>
<!-- Node D -->
<circle cx="390" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="390" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">D</text>
<!-- Node E -->
<circle cx="455" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="455" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">E</text>
<!-- Node B (child of D) -->
<circle cx="390" cy="265" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="390" y="269" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">B</text>
<!-- Annotation near B -->
<text x="416" y="278" fill="#909090" font-size="9">parent=D, seq=2</text>
<!-- ======================== -->
<!-- PANEL 3: Filter propagation -->
<!-- ======================== -->
<!-- A at (650, 110), children C(585,190) D(650,190) E(715,190), B at (650,265) -->
<!-- Root label -->
<text x="650" y="82" fill="#d0a040" text-anchor="middle" font-size="9">&#x2605; root</text>
<!-- Tree links A to children -->
<line x1="650" y1="128" x2="585" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="650" y1="128" x2="650" y2="172" stroke="#d0a040" stroke-width="2"/>
<line x1="650" y1="128" x2="715" y2="172" stroke="#d0a040" stroke-width="2"/>
<!-- Tree link D to B (gold) -->
<line x1="650" y1="208" x2="650" y2="247" stroke="#d0a040" stroke-width="2"/>
<!-- Node A (root) -->
<circle cx="650" cy="110" r="18" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="650" y="114" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">A</text>
<!-- Node C -->
<circle cx="585" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="585" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">C</text>
<!-- Node D -->
<circle cx="650" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="650" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">D</text>
<!-- Node E -->
<circle cx="715" cy="190" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="715" y="194" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">E</text>
<!-- Node B (child of D) -->
<circle cx="650" cy="265" r="18" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="650" y="269" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="13">B</text>
<!-- FilterAnnounce arrow and label (drawn last so they render on top) -->
<defs>
<marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#6090c0"/>
</marker>
</defs>
<line x1="640" y1="177" x2="640" y2="132" stroke="#6090c0" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrowBlue)"/>
<text x="622" y="158" fill="#6090c0" font-size="9" text-anchor="end">FilterAnnounce</text>
<!-- Annotation near A -->
<text x="688" y="102" fill="#909090" font-size="9">B reachable via D</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 320" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="700" height="320" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="350" y="28" fill="#e0e0e0" text-anchor="middle" font-size="15" font-weight="bold">Node Joining — B Connects to D</text>
<!-- Section titles -->
<text x="175" y="56" fill="#909090" text-anchor="middle" font-size="14">Before</text>
<text x="525" y="56" fill="#909090" text-anchor="middle" font-size="14">After</text>
<!-- Arrow between halves -->
<text x="350" y="180" fill="#909090" text-anchor="middle" font-size="22"></text>
<!-- ======================== -->
<!-- BEFORE (left half) -->
<!-- ======================== -->
<!-- Root A at top center of left half -->
<!-- A: (175, 110) -->
<text x="175" y="78" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="175" cy="110" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="175" y="115" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Children: C (105, 200), D (175, 200), E (245, 200) -->
<!-- Links from A to children -->
<line x1="175" y1="132" x2="105" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<line x1="175" y1="132" x2="175" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<line x1="175" y1="132" x2="245" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<!-- C -->
<circle cx="105" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="105" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- D -->
<circle cx="175" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="175" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- E -->
<circle cx="245" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="245" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">E</text>
<!-- B isolated (dashed, off to the side) -->
<circle cx="60" cy="280" r="22" fill="#1a1a2e" stroke="#808890" stroke-width="2" stroke-dasharray="4,3"/>
<text x="60" y="285" fill="#808890" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- ======================== -->
<!-- AFTER (right half) -->
<!-- ======================== -->
<!-- Root A at top center of right half -->
<!-- A: (525, 110) -->
<text x="525" y="78" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="525" cy="110" r="22" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="525" y="115" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Children: C (455, 200), D (525, 200), E (595, 200) -->
<!-- Links from A to children -->
<line x1="525" y1="132" x2="455" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<line x1="525" y1="132" x2="525" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<line x1="525" y1="132" x2="595" y2="178" stroke="#d0a040" stroke-width="2.5"/>
<!-- C -->
<circle cx="455" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="455" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- D -->
<circle cx="525" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="525" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- E -->
<circle cx="595" cy="200" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="595" y="205" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">E</text>
<!-- B as child of D -->
<!-- Link from D to B -->
<line x1="525" y1="222" x2="525" y2="268" stroke="#d0a040" stroke-width="2.5"/>
<!-- B -->
<circle cx="525" cy="290" r="22" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="525" y="295" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 370" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="700" height="370" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="350" y="28" fill="#e0e0e0" text-anchor="middle" font-size="15" font-weight="bold">Network Partition &#8212; Link C&#8596;D Fails</text>
<!-- Section divider between Before and After -->
<line x1="310" y1="42" x2="310" y2="360" stroke="#404060" stroke-width="1" stroke-dasharray="6,4"/>
<!-- ======================== BEFORE (left side) ======================== -->
<text x="155" y="55" fill="#909090" text-anchor="middle" font-size="14">Before</text>
<!-- Node A (root) at y=100 -->
<text x="155" y="76" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="155" cy="100" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="155" y="105" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Link A&#8594;B -->
<line x1="155" y1="120" x2="155" y2="140" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node B at y=160 -->
<circle cx="155" cy="160" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="155" y="165" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Link B&#8594;C -->
<line x1="155" y1="180" x2="155" y2="200" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node C at y=220 -->
<circle cx="155" cy="220" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="155" y="225" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- Link C&#8594;D (failing &#8212; dashed) -->
<line x1="155" y1="240" x2="155" y2="260" stroke="#d0a040" stroke-width="2.5" stroke-dasharray="4,3" opacity="0.5"/>
<!-- Red X on C&#8596;D link -->
<line x1="143" y1="243" x2="167" y2="259" stroke="#c04040" stroke-width="3" stroke-linecap="round"/>
<line x1="167" y1="243" x2="143" y2="259" stroke="#c04040" stroke-width="3" stroke-linecap="round"/>
<!-- Node D at y=280 -->
<circle cx="155" cy="280" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="155" y="285" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Link D&#8594;E -->
<line x1="155" y1="300" x2="155" y2="320" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node E at y=340 -->
<circle cx="155" cy="340" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="155" y="345" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">E</text>
<!-- ======================== AFTER (right side) ======================== -->
<text x="505" y="55" fill="#909090" text-anchor="middle" font-size="14">After</text>
<!-- Partition labels -->
<text x="415" y="80" fill="#909090" text-anchor="middle" font-size="11">Partition 1</text>
<text x="595" y="80" fill="#909090" text-anchor="middle" font-size="11">Partition 2</text>
<!-- Partition separator -->
<line x1="505" y1="88" x2="505" y2="360" stroke="#404060" stroke-width="1" stroke-dasharray="6,4"/>
<!-- === Partition 1: A &#8592; B &#8592; C === -->
<!-- Node A (root) at y=130 -->
<text x="415" y="106" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="415" cy="130" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="415" y="135" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">A</text>
<!-- Link A&#8594;B -->
<line x1="415" y1="150" x2="415" y2="175" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node B at y=195 -->
<circle cx="415" cy="195" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="415" y="200" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">B</text>
<!-- Link B&#8594;C -->
<line x1="415" y1="215" x2="415" y2="240" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node C at y=260 -->
<circle cx="415" cy="260" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="415" y="265" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">C</text>
<!-- === Partition 2: D &#8592; E === -->
<!-- Node D (new root) at y=130 -->
<text x="595" y="106" fill="#d0a040" text-anchor="middle" font-size="10">&#9733; root</text>
<circle cx="595" cy="130" r="20" fill="#1a1a2e" stroke="#d0a040" stroke-width="2.5"/>
<text x="595" y="135" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">D</text>
<!-- Link D&#8594;E -->
<line x1="595" y1="150" x2="595" y2="175" stroke="#d0a040" stroke-width="2.5"/>
<!-- Node E at y=195 -->
<circle cx="595" cy="195" r="20" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="595" y="200" fill="#e0e0e0" text-anchor="middle" font-weight="bold" font-size="14">E</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 498" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="498" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Complete Encrypted Frame (37 + body bytes)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: receiver_idx (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="150" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: counter low (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 3: counter high (bytes 12-15) -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">1215</text>
<rect x="55" y="212" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="242" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
<!-- AAD bracket -->
<text x="10" y="166" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 166)">AAD</text>
<line x1="18" y1="56" x2="18" y2="264" stroke="#555" stroke-width="1"/>
<line x1="18" y1="56" x2="23" y2="56" stroke="#555" stroke-width="1"/>
<line x1="18" y1="264" x2="23" y2="264" stroke="#555" stroke-width="1"/>
<!-- Separator: cleartext above / ciphertext below -->
<line x1="55" y1="272" x2="575" y2="272" stroke="#555" stroke-width="1" stroke-dasharray="6,4"/>
<text x="315" y="288" fill="#777" font-size="10" text-anchor="middle">── ciphertext (encrypted with ChaCha20-Poly1305) ──</text>
<!-- Row 4: timestamp (bytes 16-19, encrypted) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">1619</text>
<rect x="55" y="296" width="520" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="338" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 5: msg_type (1 byte) + body starts -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">20</text>
<rect x="55" y="348" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="372" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="390" fill="#f8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="348" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="380" y="374" fill="#666" text-anchor="middle" font-size="11">message body ...</text>
<!-- Row 6: body variable -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="400" width="520" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="430" fill="#666" text-anchor="middle" font-size="11">message body (variable length)</text>
<!-- AEAD tag -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">+16</text>
<rect x="55" y="452" width="520" height="30" fill="#4a2d4a" stroke="#b04ab0" stroke-width="1.5" rx="3"/>
<text x="315" y="472" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Poly1305 AEAD tag (16 bytes)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 186" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="186" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Established Frame Inner Header (5 bytes, plaintext after decrypt)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: timestamp (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 1: msg_type (byte 4) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">4</text>
<rect x="55" y="108" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="120" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="150" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 1: body starts -->
<rect x="185" y="108" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="380" y="134" fill="#666" text-anchor="middle" font-size="11">message body ...</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 290" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="290" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Established Frame Outer Header (16 bytes, AEAD AAD)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: receiver_idx (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="150" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: counter low (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 3: counter high (bytes 12-15) -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">1215</text>
<rect x="55" y="212" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="242" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="270" 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>
<!-- Row 0 (03): msg_type(1) + sequence starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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>
<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>
<!-- Row 1 (48): sequence continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</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>
<!-- Row 2 (910): hash_count + size_class -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">910</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="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 (11): filter_bits -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">11</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>
<!-- 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>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -95,7 +95,7 @@
<!-- Arrow down from node_addr -->
<line x1="400" y1="226" x2="400" y2="276" class="derive"/>
<polygon points="396,274 400,282 404,274" class="dhead"/>
<text x="416" y="254" class="op">add fd::/8 prefix</text>
<text x="416" y="254" class="op">add fd00::/8 prefix</text>
<!-- IPv6 address box -->
<rect x="280" y="282" width="240" height="50" class="box compat"/>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -40,7 +40,7 @@
<rect x="400" y="90" width="200" height="50" class="layer tun"/>
<text x="500" y="108" text-anchor="middle" class="label">IPv6 TUN Adapter</text>
<text x="500" y="121" text-anchor="middle" class="sub">fips0 + DNS (.fips)</text>
<text x="500" y="133" text-anchor="middle" class="sub">npub → fd::/8 IPv6</text>
<text x="500" y="133" text-anchor="middle" class="sub">npub → fd00::/8 IPv6</text>
<!-- External app labels -->
<text x="280" y="74" text-anchor="middle" class="sub">FIPS-aware apps</text>

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 524" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="524" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FSP Complete Encrypted Message (34 + body bytes)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: counter low (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="138" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 2: counter high (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
<!-- AAD bracket -->
<text x="10" y="134" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 134)">AAD</text>
<line x1="18" y1="56" x2="18" y2="212" stroke="#555" stroke-width="1"/>
<line x1="18" y1="56" x2="23" y2="56" stroke="#555" stroke-width="1"/>
<line x1="18" y1="212" x2="23" y2="212" stroke="#555" stroke-width="1"/>
<!-- Optional coords -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">opt</text>
<rect x="55" y="220" width="520" height="40" fill="#1f1f3a" stroke="#4ad99a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="244" fill="#8af8c8" text-anchor="middle" font-size="11">cleartext coordinates (if CP flag set, variable length)</text>
<!-- Separator: ciphertext below -->
<line x1="55" y1="268" x2="575" y2="268" stroke="#555" stroke-width="1" stroke-dasharray="6,4"/>
<text x="315" y="284" fill="#777" font-size="10" text-anchor="middle">── ciphertext (encrypted with ChaCha20-Poly1305) ──</text>
<!-- Row 3: timestamp (4 bytes, encrypted) -->
<text x="50" y="318" fill="#666" font-size="10" text-anchor="end">+03</text>
<rect x="55" y="292" width="520" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="316" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="334" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 4: msg_type (1) + inner_flags (1) + body starts -->
<text x="50" y="370" fill="#666" font-size="10" text-anchor="end">+4</text>
<rect x="55" y="344" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="368" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="386" fill="#f8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="344" width="130" height="52" fill="#3d3d2d" stroke="#a0a04a" stroke-width="1.5" rx="3"/>
<text x="250" y="368" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">inner_flags</text>
<text x="250" y="386" fill="#d8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="344" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="445" y="374" fill="#666" text-anchor="middle" font-size="11">body ...</text>
<!-- Row 5: body variable -->
<text x="50" y="422" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="396" width="520" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="426" fill="#666" text-anchor="middle" font-size="11">message body (variable length)</text>
<!-- AEAD tag -->
<text x="50" y="468" fill="#666" font-size="10" text-anchor="end">+16</text>
<rect x="55" y="452" width="520" height="30" fill="#4a2d4a" stroke="#b04ab0" stroke-width="1.5" rx="3"/>
<text x="315" y="472" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Poly1305 AEAD tag (16 bytes)</text>
<!-- Totals -->
<text x="310" y="510" fill="#777" font-size="10" text-anchor="middle">overhead: 34 bytes (12 header + 6 inner + 16 tag) · coords add variable length</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 500" 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"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise IK)</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"/>
<!-- Step 1: Initiator prepares -->
<text x="20" y="88" fill="#8af8c8" font-size="10">generates sender_idx</text>
<text x="20" y="102" fill="#8af8c8" font-size="10">generates ephemeral keypair</text>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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"/>
<!-- 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>
<!-- 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>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,72 @@
<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">LookupRequest (0x30) — 303 + 16n bytes</text>
<!-- Row 0 (03): msg_type + request_id starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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">0x30</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">request_id</text>
<!-- Row 1 (48): request_id continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</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 — unique random identifier</text>
<!-- Row 2 (924): target -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">924</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">target</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr being sought</text>
<!-- Row 3 (2540): origin -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2540</text>
<rect x="55" y="192" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">origin</text>
<text x="315" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — requester's NodeAddr</text>
<!-- Row 4 (4143): ttl + min_mtu -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">4143</text>
<rect x="55" y="244" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="120" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ttl</text>
<text x="120" y="286" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="244" width="390" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="380" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">min_mtu</text>
<text x="380" y="286" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 5 (4445): origin_coords_cnt -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">4445</text>
<rect x="55" y="296" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="185" y="326" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">origin_coords_cnt</text>
<!-- Row 6 (46): origin_coords -->
<text x="50" y="370" fill="#666" font-size="10" text-anchor="end">46</text>
<rect x="55" y="348" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="372" fill="#8af8f8" text-anchor="middle" font-size="11">origin_coords × n (16 bytes each — NodeAddr only)</text>
<!-- Row 7: visited bloom -->
<text x="50" y="414" fill="#666" font-size="10" text-anchor="end">+1+256</text>
<rect x="55" y="394" width="130" height="40" fill="#3d3d2d" stroke="#a0a04a" stroke-width="1.5" rx="3"/>
<text x="120" y="418" fill="#d8d88a" text-anchor="middle" font-size="10">hash_cnt</text>
<rect x="185" y="394" width="390" height="40" fill="#1f1f3a" stroke="#a0a04a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="380" y="418" fill="#d8d88a" text-anchor="middle" font-size="11">visited_bits (256 bytes)</text>
<!-- Total -->
<text x="310" y="462" fill="#777" font-size="10" text-anchor="middle">total: 303 + (n × 16) bytes, where n = origin depth + 1</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<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">LookupResponse (0x31) — 93 + 16n bytes</text>
<!-- Row 0 (03): msg_type + request_id starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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">0x31</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">request_id</text>
<!-- Row 1 (48): request_id continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</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 — echoes the request's ID</text>
<!-- Row 2 (924): target -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">924</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">target</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr that was found</text>
<!-- Row 3 (2528): path_mtu + target_coords_cnt -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2528</text>
<rect x="55" y="192" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="185" y="232" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<rect x="315" y="192" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="445" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">target_coords_cnt</text>
<text x="445" y="232" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 4 (29): target_coords -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">29</text>
<rect x="55" y="250" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="274" fill="#8af8f8" text-anchor="middle" font-size="11">target_coords × n (16 bytes each — NodeAddr only)</text>
<!-- Row 5: proof -->
<text x="50" y="316" fill="#666" font-size="10" text-anchor="end">+64</text>
<rect x="55" y="296" width="520" height="40" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Schnorr proof (64 bytes)</text>
<!-- Total -->
<text x="310" y="358" fill="#777" font-size="10" text-anchor="middle">total: 93 + (n × 16) bytes · proof signs (request_id || target || target_coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="270" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">MtuExceeded (0x22) — 36 bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x22</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — destination being forwarded to</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</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">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Row 3 (3435): mtu -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="192" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">mtu</text>
<text x="185" y="232" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE — bottleneck MTU</text>
<!-- Note -->
<text x="310" y="264" fill="#777" font-size="10" text-anchor="middle">plaintext error signal (U flag set) · total on wire: 40 bytes (4 prefix + 36 payload)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,67 @@
<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>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<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">Noise IK Message 2 — phase 0x2 (69 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>
<!-- Row 2: receiver_idx (bytes 8-11) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">811</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>
<!-- 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>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 296" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="296" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">PathBroken (0x21) — 36 + 16n bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x21</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — unreachable NodeAddr</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</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">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Row 3 (3435): last_coords_count -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="192" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="185" y="222" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">last_coords_count</text>
<!-- Row 4 (36): last_known_coords -->
<text x="50" y="264" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="248" width="520" height="40" fill="#1f1f3a" stroke="#d9904a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="272" fill="#f8c88a" text-anchor="middle" font-size="11">last_known_coords × n (16 bytes each — stale coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 114" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="114" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">PathMtuNotification (0x13) — 2 bytes body</text>
<!-- Row 0 (01): path_mtu -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="315" y="76" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE — minimum MTU observed along forward path</text>
<!-- Note -->
<text x="310" y="108" fill="#777" font-size="10" text-anchor="middle">body only — carried inside FSP encrypted message (prefix + inner header + AEAD tag)</text>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

View File

@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 634" font-family="monospace" font-size="13">
<!-- Background -->
<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>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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>
<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>
<!-- Row 1 (411): highest_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</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>
<!-- Row 2 (1219): cumulative_packets_recv -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</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>
<!-- Row 3 (2027): cumulative_bytes_recv -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</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="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>
<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="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 (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="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>
<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"/>
<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>
<rect x="315" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" 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>
<!-- Row 7 (4855): owd_trend + burst_loss_count -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">4855</text>
<rect x="55" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" 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>
<rect x="315" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" 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>
<!-- Row 8 (5663): cumulative_reorder_count + interval_packets_recv -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">5663</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="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>
<!-- 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>
<!-- Total -->
<text x="310" y="586" fill="#777" font-size="10" text-anchor="middle">total: 68 bytes</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,66 @@
<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">SenderReport (0x01) — 48 bytes</text>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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>
<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>
<!-- Row 1 (411): interval_start_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</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>
<!-- Row 2 (1219): interval_end_counter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</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>
<!-- Row 3 (2027): interval_start_timestamp + interval_end_timestamp -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</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="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>
<!-- Total -->
<text x="310" y="422" fill="#777" font-size="10" text-anchor="middle">total: 48 bytes</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
<!-- Background -->
<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>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x2, flags=0, payload_len</text>
<!-- Row 1 (4): flags -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4</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">flags</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte — reserved</text>
<!-- Row 2 (5): src_coords (acknowledger) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">5</text>
<rect x="55" y="140" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="164" fill="#8af8f8" text-anchor="middle" font-size="11">src_coords_count(2) + src_coords(16 × n) — acknowledger</text>
<!-- Row 3: dest_coords (initiator) -->
<text x="50" y="206" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="186" width="520" height="40" fill="#1f1f3a" stroke="#b04ad9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="210" fill="#d8a0f8" text-anchor="middle" font-size="11">dest_coords_count(2) + dest_coords(16 × m) — initiator</text>
<!-- Row 4: handshake_len + handshake_payload -->
<text x="50" y="252" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="232" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="254" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">hs_len</text>
<text x="120" y="272" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<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>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~190 bytes (depth-dependent, carries both endpoints' coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<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">SessionDatagram (0x00) — 36 + payload bytes</text>
<!-- Row 0 (03): msg_type + ttl + path_mtu -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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">0x00</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ttl</text>
<text x="250" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="36" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="445" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="445" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1-4 (419): src_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">419</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">src_addr</text>
<text x="315" y="128" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — source NodeAddr</text>
<!-- Row 5-8 (2035): dest_addr -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">2035</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">dest_addr</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — destination NodeAddr</text>
<!-- Row 9 (36): payload -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="198" width="520" height="40" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="222" fill="#666" text-anchor="middle" font-size="11">payload (variable — session-layer message)</text>
<!-- Notes -->
<text x="310" y="262" fill="#777" font-size="10" text-anchor="middle">fixed header: 36 bytes · payload opaque to transit nodes</text>
<text x="310" y="278" fill="#777" font-size="10" text-anchor="middle">ttl decremented and path_mtu min'd at each forwarding hop</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 244" font-family="monospace" font-size="13">
<!-- Background -->
<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>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x3, flags=0, payload_len</text>
<!-- Row 1 (46): flags + handshake_len -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">46</text>
<rect x="55" y="88" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="120" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="120" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="88" width="390" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="380" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_len</text>
<text x="380" y="128" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 2 (7): handshake_payload -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">7</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">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>
<!-- Total -->
<text x="310" y="218" fill="#777" font-size="10" text-anchor="middle">~80 bytes · no coordinates (both endpoints already have them)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
<!-- Background -->
<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>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x1, flags=0, payload_len</text>
<!-- Row 1 (4): flags -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4</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">flags</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte — bit 0: REQUEST_ACK, bit 1: BIDIRECTIONAL</text>
<!-- Row 2 (5): src_coords -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">5</text>
<rect x="55" y="140" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="164" fill="#8af8f8" text-anchor="middle" font-size="11">src_coords_count(2) + src_coords(16 × n)</text>
<!-- Row 3: dest_coords -->
<text x="50" y="206" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="186" width="520" height="40" fill="#1f1f3a" stroke="#b04ad9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="210" fill="#d8a0f8" text-anchor="middle" font-size="11">dest_coords_count(2) + dest_coords(16 × m)</text>
<!-- Row 4: handshake_len + handshake_payload -->
<text x="50" y="252" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="232" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="254" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">hs_len</text>
<text x="120" y="272" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<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>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~170 bytes (depth-dependent)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 340" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="520" height="340" fill="#0d1117" rx="8"/>
<!-- Title -->
<text x="260" y="28" fill="#e0e0e0" text-anchor="middle" font-size="15" font-weight="bold">Coordinate Warmup State Machine</text>
<!-- ======================== -->
<!-- WARMUP state (top) -->
<!-- ======================== -->
<rect x="160" y="50" width="200" height="40" rx="6" fill="#1a1a2e" stroke="#d0a040" stroke-width="2"/>
<text x="260" y="75" fill="#d0a040" text-anchor="middle" font-weight="bold">WARMUP</text>
<!-- Annotation right of WARMUP -->
<text x="375" y="75" fill="#909090" text-anchor="start" font-size="10">send coords via CP</text>
<text x="375" y="87" fill="#909090" text-anchor="start" font-size="10">or CoordsWarmup</text>
<!-- Arrow: WARMUP &#8594; MINIMAL -->
<line x1="260" y1="90" x2="260" y2="140" stroke="#e0e0e0" stroke-width="1.5"/>
<polygon points="260,148 254,138 266,138" fill="#e0e0e0"/>
<!-- Transition label -->
<text x="268" y="120" fill="#909090" text-anchor="start" font-size="10">N packets sent</text>
<!-- ======================== -->
<!-- MINIMAL state (middle) -->
<!-- ======================== -->
<rect x="160" y="148" width="200" height="40" rx="6" fill="#1a1a2e" stroke="#e0e0e0" stroke-width="2"/>
<text x="260" y="173" fill="#e0e0e0" text-anchor="middle" font-weight="bold">MINIMAL</text>
<!-- Annotation right of MINIMAL -->
<text x="375" y="173" fill="#909090" text-anchor="start" font-size="10">no coords attached</text>
<!-- Arrow: MINIMAL &#8594; RE-WARM -->
<line x1="260" y1="188" x2="260" y2="238" stroke="#e0e0e0" stroke-width="1.5"/>
<polygon points="260,246 254,236 266,236" fill="#e0e0e0"/>
<!-- Transition label -->
<text x="268" y="218" fill="#c04040" text-anchor="start" font-size="10">CoordsRequired</text>
<text x="268" y="230" fill="#c04040" text-anchor="start" font-size="10">or PathBroken</text>
<!-- ======================== -->
<!-- RE-WARM state (bottom) -->
<!-- ======================== -->
<rect x="110" y="246" width="300" height="40" rx="6" fill="#1a1a2e" stroke="#c04040" stroke-width="2"/>
<text x="260" y="271" fill="#e0e0e0" text-anchor="middle" font-weight="bold">SEND CoordsWarmup (0x14)</text>
<!-- Annotation right of RE-WARM -->
<text x="425" y="271" fill="#909090" text-anchor="start" font-size="10">rate-limited</text>
<!-- Arrow: RE-WARM &#8594; back to WARMUP (curved left side) -->
<path d="M 110,266 L 60,266 L 60,70 L 160,70" fill="none" stroke="#d0a040" stroke-width="1.5"/>
<polygon points="160,70 150,64 150,76" fill="#d0a040"/>
<!-- Transition label on return arrow -->
<text x="48" y="170" fill="#d0a040" text-anchor="end" font-size="10" transform="rotate(-90, 48, 170)">counter reset</text>
<!-- Entry arrow into WARMUP (from above) -->
<line x1="260" y1="38" x2="260" y2="50" stroke="#d0a040" stroke-width="1.5"/>
<text x="268" y="46" fill="#909090" text-anchor="start" font-size="10">session established</text>
<!-- LookupResponse annotation on return path -->
<text x="68" y="310" fill="#909090" text-anchor="start" font-size="10">also on LookupResponse</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<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">TreeAnnounce (0x10) — 100 + 32n bytes</text>
<!-- Row 0 (03): msg_type(1) + version(1) + sequence starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</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">0x10</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">version</text>
<text x="250" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">0x01</text>
<rect x="315" y="36" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="445" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<!-- Row 1 (49): sequence continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">49</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>
<!-- Row 2 (1017): timestamp -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1017</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">timestamp</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE — Unix seconds</text>
<!-- Row 3 (1833): parent -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">1833</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">parent</text>
<text x="315" y="232" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr (self = root)</text>
<!-- Row 4 (3435): ancestry_count -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="250" width="260" height="40" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="274" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ancestry_count</text>
<!-- Row 5 (36): ancestry entries -->
<text x="50" y="316" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="296" width="520" height="40" fill="#1f1f3a" stroke="#d9904a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="320" fill="#f8c88a" text-anchor="middle" font-size="11">AncestryEntry × n (32 bytes each)</text>
<!-- Signature -->
<text x="50" y="360" fill="#666" font-size="10" text-anchor="end">+64</text>
<rect x="55" y="342" width="520" height="26" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="360" fill="#e0e0e0" text-anchor="middle" font-size="10" font-weight="bold">Schnorr signature (64 bytes)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -6,12 +6,31 @@ selection. It is a supporting reference — for how bloom filters fit into
the overall routing system, see
[fips-mesh-operation.md](fips-mesh-operation.md).
## What Is a Bloom Filter?
A bloom filter is a space-efficient probabilistic data structure that
tests whether an element is a member of a set. It uses a bit array and
multiple hash functions to represent a set compactly, with one key
tradeoff:
- **No false negatives** — if the filter says "not present," the
element is definitely not in the set
- **Possible false positives** — if the filter says "present," the
element is *probably* in the set, but might not be
This makes bloom filters well-suited for routing: a definitive "no"
eliminates a peer from consideration, while a "maybe" simply means the
peer is worth checking. The false positive rate is controlled by the
filter size and number of hash functions relative to the number of
entries.
## Purpose
Each node maintains bloom filters summarizing which destinations are
reachable through each of its peers. Bloom filters provide **candidate
selection**: they narrow which peers are worth considering when forwarding
a packet to a given destination. The actual forwarding decision is made by
reachable through each of its peers. When forwarding a packet, a node
checks its peers' filters to identify potential routing paths — typically
up or down the spanning tree, unless a mesh peer provides a shortcut
directly to the destination. The actual forwarding decision is made by
tree coordinate distance ranking.
Bloom filters answer a single question: "can peer P possibly reach
@@ -23,55 +42,85 @@ destination D?" The answer is either "no" (definitive) or "maybe"
| Parameter | Value | Rationale |
| --------- | ----- | --------- |
| Size | 1 KB (8,192 bits) | Balance between accuracy and bandwidth |
| Hash functions (k) | 5 | Compromise between optimal k=7 for 800 entries and accommodating up to ~1,600 entries |
| Hash functions (k) | 5 | Optimal at ~1,200 entries; good compromise for 8001,600 range |
| Size class | 1 (v1 mandated) | `512 << 1 = 1024` bytes |
### Why k = 5
The optimal number of hash functions depends on the expected occupancy:
The optimal number of hash functions is k_opt = (m/n) × ln(2). For
m = 8,192 bits:
- At 800 entries (typical for moderate-degree nodes), optimal k ≈ 7
- At 1,600 entries (hub nodes), optimal k ≈ 4
| n (entries) | k_opt | Rounded |
| ----------- | ----- | ------- |
| 800 | 7.1 | 7 |
| 1,200 | 4.7 | **5** |
| 1,600 | 3.5 | 4 |
k = 5 is a practical compromise that provides reasonable false positive
rates across the expected range of occupancy.
k = 5 is optimal at n ≈ 1,200 and a practical compromise across the
8001,600 range. The FPR penalty versus the per-n optimal k is under
1 percentage point throughout this range.
## False Positive Rate (FPR) Analysis
The false positive rate for a bloom filter with m bits, k hash functions,
and n entries is approximately:
and n entries is:
```text
FPR (1 - e^(-kn/m))^k
FPR = (1 - e^(-kn/m))^k
```
With m = 8,192 bits and k = 5:
| Node Degree | Expected Entries | FPR | Impact |
| ----------- | ---------------- | --- | ------ |
| 5 (IoT) | 100200 | ~0.02% | Negligible |
| 8 (typical) | 250400 | ~0.3% | Negligible |
| 12 (well-connected) | 500800 | ~2.4% | Minor — occasional unnecessary discovery |
| 20+ (hub) | 1,2001,800 | 7.515% | Elevated — more false positive candidates |
| n (entries) | Fill % | FPR | Impact |
| ----------- | ------ | --- | ------ |
| 200 | 11.5% | 0.002% | Negligible |
| 400 | 21.7% | 0.048% | Negligible |
| 800 | 38.6% | 0.86% | Negligible |
| 1,200 | 51.9% | 3.8% | Minor — occasional unnecessary candidate |
| 1,600 | 62.3% | 9.4% | Elevated |
| 2,400 | 78.3% | 27% | Poor — filter losing discrimination |
| 3,350 | 87% | 50% | Useless for candidate selection |
At moderate network sizes, filters are highly accurate. At larger scales
(~1M nodes), hub nodes with many peers see elevated FPR. False positives
cause unnecessary candidate evaluation (and potentially unnecessary
discovery attempts) but do not affect routing correctness — the tree
distance calculation makes the actual forwarding decision.
### Filter Occupancy Model
Filter entries are determined by **network size and tree position**, not
node degree. Under tree-only merge with split-horizon:
- **Upward filter** (to parent): contains the node's subtree — small
for deep nodes, at most N for the root
- **Downward filter** (to a child): contains the complement of the
child's subtree — approximately N × (b-1)/b entries for branching
factor b
The worst-case filter is the downward direction from the root's child,
containing roughly N × (b-1)/b entries. Upward filters remain compact
even in large networks.
For a tree with branching factor b ≈ 5:
| Network size (N) | Worst-case n | FPR | Assessment |
| ----------------- | ------------ | --- | ---------- |
| 500 | 400 | 0.048% | Negligible |
| 1,000 | 800 | 0.86% | Good |
| 2,000 | 1,600 | 9.4% | Acceptable |
| 3,000 | 2,400 | 27% | Poor |
| 5,000 | 4,000 | 63% | Filter nearly useless |
Upward filters remain excellent: a depth-2 node in a 10,000-node tree
has a subtree of ~400 entries (FPR 0.048%).
### Saturation Behavior
A bloom filter with 8,192 bits and k = 5 saturates (FPR approaches 100%)
around 3,0004,000 entries. Beyond this, every query returns "maybe" and the
filter provides no candidate selection value.
At 50% FPR (n ≈ 3,350), the filter provides no better than coin-flip
discrimination and is effectively useless for candidate selection.
Full saturation (FPR > 99%) occurs around n ≈ 10,000.
Tree-only merge propagation mitigates saturation by limiting each filter's
content to tree-relevant entries. A node's outgoing filter to its parent
contains only its subtree; its outgoing filter to a child contains the
complement. Neither filter contains entries from mesh shortcuts' transitive
information, keeping filter occupancy proportional to the node's position
in the tree rather than the total mesh connectivity.
information. This keeps upward filters compact regardless of network size,
but downward filters grow proportionally with N.
## Per-Peer Filter Model
@@ -179,38 +228,10 @@ property of the data structure). Entries are expired through:
removed, and outbound filters are recomputed
- **Filter replacement**: Each FilterAnnounce completely replaces the
previous filter for that peer
- **Implicit timeout**: If no FilterAnnounce is received from a peer within
a threshold period, the peer's filter may be considered stale (tied to
link liveness detection)
## Size Classes and Folding
### 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 |
v1 nodes MUST use size_class = 1 and MUST reject FilterAnnounce messages
with any other size_class.
### Folding (Forward Compatibility)
Larger filters can be **folded** to smaller sizes by OR-ing the two halves
together. A 2 KB filter folds to 1 KB by OR-ing the upper and lower
halves. This preserves the "maybe present" property (no false negatives
introduced) but increases the false positive rate.
Folding enables future protocol versions where hub nodes maintain larger
filters (lower FPR) while constrained nodes fold down to the mandated size.
A node receiving a larger filter folds it locally before use if needed.
The hash function design supports folding: membership tests at a smaller
size use `hash(item, i) % smaller_bit_count`, which maps to the same bit
positions that folding produces.
- **Implicit timeout**: If a peer becomes unresponsive, the MMP link
liveness detector eventually declares the link dead and removes the
peer, which triggers filter cleanup as a side effect of peer removal.
There is no independent filter staleness timer.
## Membership Test
@@ -244,30 +265,75 @@ overhead.
See [fips-wire-formats.md](fips-wire-formats.md) for the complete wire
format reference.
## Scale Considerations
## Scale and Size Classes
### Small Networks (< 1,000 nodes)
### v1 Scale Limits
Filters are very accurate (FPR < 1% for typical node degrees). Candidate
selection effectively identifies the correct forwarding peer on the first
try for most destinations.
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.
### Medium Networks (1,000100,000 nodes)
With the v1 mandatory 1 KB filter (size_class 1):
Filters remain accurate for typical nodes. Hub nodes (20+ peers) may see
elevated FPR but the tree distance ranking correctly selects the best
candidate regardless.
- **Small networks (< 1,000 nodes)**: Both upward and downward filters
are highly accurate (worst-case FPR < 1%). Filters effectively narrow
candidates to the correct forwarding peer on the first try.
### Large Networks (> 100,000 nodes)
- **Medium networks (1,0002,000 nodes)**: Upward filters remain
excellent. Downward filters approach 10% FPR at the worst case
(near-root nodes). The additional false positives add minor overhead
to candidate evaluation but do not affect routing correctness.
Hub nodes approach filter saturation. The filter still provides value
(some peers can be definitively excluded) but the candidate set grows
larger. The tree distance calculation becomes the primary routing
discriminator.
- **Large networks (> 2,000 nodes)**: Downward filters lose most of
their discrimination value. At N ≈ 5,000, downward filters for
near-root nodes have ~63% FPR, providing little benefit over evaluating
all peers. Upward filters remain compact and accurate at any scale.
Routing correctness is unaffected — greedy tree distance still selects
the right next hop — but the efficiency gain from bloom filter
pre-selection diminishes.
Future mitigation: hub nodes could use larger filters (size_class 2 or 3)
while constrained nodes fold to size_class 1. This requires protocol
negotiation not present in v1.
### 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 |
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.
### Scaling Strategy
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).
### Folding
Larger filters can be **folded** to smaller sizes by OR-ing the two halves
together. A 2 KB filter folds to 1 KB by OR-ing the upper and lower
halves. This preserves the "maybe present" property (no false negatives
introduced) but increases the false positive rate.
The hash function design supports folding: membership tests at a smaller
size use `hash(item, i) % smaller_bit_count`, which maps to the same bit
positions that folding produces.
## Implementation Status

View File

@@ -57,9 +57,8 @@ peers: # Static peer list
| `node.control.socket_path` | string | *(auto)* | Socket file path. Default: `$XDG_RUNTIME_DIR/fips/control.sock` if `XDG_RUNTIME_DIR` is set, otherwise `/tmp/fips-control.sock` |
The control socket provides read-only access to node state via the
`fipsctl` command-line tool. See
[fips-software-architecture.md](fips-software-architecture.md) for the
protocol and command list.
`fipsctl` command-line tool. See the project
[README](../../README.md#inspect) for the command list.
All tunable protocol parameters live under `node.*`, organized as sysctl-style
dotted paths. The top-level sections (`tun`, `dns`, `transports`, `peers`)
@@ -285,7 +284,7 @@ dns:
transports:
udp:
bind_addr: "0.0.0.0:4000"
mtu: 1197
mtu: 1472
peers:
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"

View File

@@ -108,14 +108,14 @@ and applications know nothing about which physical media carry their traffic.
This separation means new transports, protocol features, and application
interfaces can be added independently.
![Protocol Stack](fips-protocol-stack.svg)
![Protocol Stack](diagrams/fips-protocol-stack.svg)
### Mapping to Traditional Networking
Readers familiar with the OSI model or TCP/IP networking may find it helpful
to see how FIPS concepts relate to traditional layers:
![OSI Mapping](fips-osi-mapping.svg)
![OSI Mapping](diagrams/fips-osi-mapping.svg)
Note that FMP spans what would traditionally be separate link and network
layers. This is intentional — in a self-organizing mesh, the same layer that
@@ -168,7 +168,7 @@ Two application interfaces sit at the top of the stack: a native datagram
API addressed by npub, and an IPv6 TUN adapter that maps npubs to `fd00::/8`
addresses so unmodified IP applications can use the network transparently.
![Node Architecture](fips-node-architecture.svg)
![Node Architecture](diagrams/fips-node-architecture.svg)
The mesh routes application traffic across heterogeneous transports
transparently. A packet may traverse WiFi, Ethernet, UDP/IP, and Tor links
@@ -177,7 +177,7 @@ which transports are involved. Each hop is independently encrypted at the
link layer, while a single end-to-end session protects the payload across
the entire path.
![Architecture Overview](fips-architecture-overview.svg)
![Architecture Overview](diagrams/fips-architecture-overview.svg)
---
@@ -198,7 +198,7 @@ same keypair.
### FIPS Identity Handling
![Identity Derivation](fips-identity-derivation.svg)
![Identity Derivation](diagrams/fips-identity-derivation.svg)
The pubkey is the node's cryptographic identity, used in Noise IK handshakes
for both link and session encryption. It is never exposed beyond the
@@ -299,7 +299,7 @@ complementary mechanisms provide the information each node needs.
### Spanning Tree: The Coordinate System
![Mesh Topology](fips-mesh-topology.svg)
![Mesh Topology](diagrams/fips-mesh-topology.svg)
Nodes self-organize into a spanning tree through gossip — each node
exchanges announcements with its direct peers and independently selects a
@@ -363,7 +363,7 @@ the full network.
See [fips-bloom-filters.md](fips-bloom-filters.md) for filter parameters and
mathematical properties.
![Bloom filter propagation on a spanning tree](fips-bloom-propagation.svg)
![Bloom filter propagation on a spanning tree](diagrams/fips-bloom-propagation.svg)
The outbound filter for peer Q merges this node's identity with tree peer
inbound filters except Q's (split-horizon exclusion). This creates
@@ -405,7 +405,7 @@ These are cached at each node after being learned through discovery
(LookupRequest/LookupResponse) or session establishment (SessionSetup). The
coordinate cache is the critical piece that enables efficient forwarding.
![Per-hop routing decision flowchart](fips-routing-decision.svg)
![Per-hop routing decision flowchart](diagrams/fips-routing-decision.svg)
### Coordinate Caching and Discovery
@@ -419,7 +419,7 @@ Session establishment (SessionSetup) also carries coordinates, warming
transit node caches along the path so that data packets can be forwarded
without individual discovery at each hop.
![Coordinate discovery and cache warming sequence](fips-coordinate-discovery.svg)
![Coordinate discovery and cache warming sequence](diagrams/fips-coordinate-discovery.svg)
### Error Recovery
@@ -783,8 +783,6 @@ of self-sovereign identity systems. No novel cryptography is introduced.
| Document | Description |
| -------- | ----------- |
| [fips-software-architecture.md](fips-software-architecture.md) | Stable architectural decisions guiding the codebase |
| [fips-state-machines.md](fips-state-machines.md) | Phase-based state machine pattern (Rust) |
| [fips-configuration.md](fips-configuration.md) | YAML configuration reference |
### External References

View File

@@ -10,7 +10,8 @@ can communicate over the mesh without modification.
The adapter bridges two worlds: IPv6 applications that address destinations by
IP address, and the FIPS mesh that addresses destinations by public key
(npub). The adapter handles the translation: DNS resolution from npub to
`fd::/8` address, identity cache management so FIPS can route IPv6 packets,
`fd00::/8` ULA (Unique Local Address) address, identity cache management so
FIPS can route IPv6 packets,
MTU enforcement so packets fit through the mesh, and the TUN interface that
connects to the kernel's IPv6 stack.
@@ -21,7 +22,7 @@ native FIPS datagram API, addressing destinations directly by npub.
### The Problem
IPv6 addresses in the `fd::/8` range are derived from public keys via a
IPv6 addresses in the `fd00::/8` range are derived from public keys via a
one-way hash (SHA-256). Given only an IPv6 address, the public key cannot be
recovered — and without the public key, FIPS cannot compute the node_addr
needed for routing.
@@ -35,7 +36,7 @@ DNS resolution serves as the "routing intent" signal. When an application
resolves `npub1xxx...xxx.fips`, the FIPS DNS service:
1. Extracts the npub from the `.fips` domain name
2. Derives the `fd::/8` IPv6 address from the public key
2. Derives the `fd00::/8` IPv6 address from the public key
3. Primes the identity cache with the mapping
(IPv6 address prefix ↔ NodeAddr ↔ PublicKey)
4. Returns the IPv6 address to the application
@@ -54,7 +55,7 @@ for address derivation.
### Traffic Without Prior DNS Lookup
A packet may arrive at the TUN for an `fd::/8` destination without a prior DNS
A packet may arrive at the TUN for an `fd00::/8` destination without a prior DNS
lookup — cached address, manual configuration, etc. Since the address derivation
is one-way, the npub cannot be recovered from the address alone.
@@ -82,24 +83,37 @@ SHA-256 → node_addr (16 bytes, truncated)
fd + node_addr[0..15] → IPv6 address (16 bytes)
```
The `fd` prefix ensures no collision with addresses in use on the underlying
transport network. These are overlay identifiers — they appear in the TUN
interface for application compatibility but are not routable on the underlying
transport.
The `fd` prefix places FIPS addresses in the IPv6 Unique Local Address (ULA)
space defined by RFC 4193. ULAs are the IPv6 equivalent of RFC 1918 private
addresses (10.x, 172.16.x, 192.168.x) — they are reserved for local use and
are not routable over the public Internet. This means FIPS overlay addresses
cannot conflict with native IPv6 traffic that may be present on the same host
or network, and they will not leak beyond the local system even if routing is
misconfigured. These are overlay identifiers — they appear in the TUN
interface for application compatibility but have no meaning outside the FIPS
mesh.
## Identity Cache
The identity cache maps the FIPS address prefix (15 bytes — the IPv6 address
minus the `fd` prefix) to `(NodeAddr, PublicKey)`. This cache is needed only
when using the IPv6 adapter; the native FIPS API provides the public key
directly.
The derivation from public key to NodeAddr and IPv6 address is one-way
(SHA-256 truncation). Given a destination IPv6 address from an outbound packet
on the TUN interface, the adapter cannot recover the public key or NodeAddr
needed for FIPS routing. The identity cache provides the reverse lookup:
it maps the FIPS address prefix (15 bytes — the IPv6 address minus the `fd`
prefix) back to `(NodeAddr, PublicKey)`, allowing the adapter to route
IPv6 traffic into the mesh. This cache is needed only when using the IPv6
adapter; the native FIPS API provides the public key directly.
### Eviction Policy
The mapping is deterministic (derived from the public key) and never becomes
stale. The cache uses **LRU-only eviction** bounded by a configurable size
(default 10K entries). There is no TTL — entries are evicted only when the
cache is full and space is needed for a new entry.
cache is full and space is needed for a new entry. LRU-only eviction is
necessary because there is no other way for the FIPS router to recover the
routing identity from an IPv6 address, and IPv6 traffic for a destination may
arrive an arbitrarily long time after the DNS resolution that populated the
cache entry.
### Relationship to DNS TTL
@@ -111,9 +125,17 @@ entry hasn't been evicted by memory pressure.
## MTU Enforcement
FIPS encapsulation adds overhead to every packet. The adapter must ensure
that IPv6 packets from applications fit within the FIPS encapsulation budget
after all layers of wrapping.
FIPS does not provide fragmentation or reassembly at the session or mesh
protocol layers — every datagram must fit in a single transport-layer packet.
Some transports may perform fragmentation and reassembly internally (e.g., BLE
L2CAP) and can advertise a larger virtual MTU than the physical medium
supports, but this is transparent to FIPS. The mesh layer provides two
facilities to manage MTU across heterogeneous paths: route discovery can
constrain results to paths that support a required minimum MTU, and transit
nodes that cannot forward an oversized datagram send an MtuExceeded error
signal back to the source. The adapter must ensure that IPv6 packets from
applications fit within the FIPS encapsulation budget after all layers of
wrapping.
### Encapsulation Overhead
@@ -156,8 +178,9 @@ transport path MTU for the IPv6 adapter is therefore:
```
Transports with smaller MTUs (radio at ~250 bytes, serial at 256 bytes) cannot
support the IPv6 adapter — applications on those transports must use the
native FIPS datagram API.
support the IPv6 adapter without some form of internal fragmentation and
reassembly. Otherwise, applications on those transports must use the native
FIPS datagram API.
### ICMP Packet Too Big
@@ -206,10 +229,10 @@ defining feature.
### Architecture
```text
Applications (sockets using fd::/8 addresses)
Applications (sockets using fd00::/8 addresses)
Kernel IPv6 Stack (routing: fd::/8 → fips0)
Kernel IPv6 Stack (routing: fd00::/8 → fips0)
TUN Device (fips0)
@@ -222,7 +245,7 @@ TUN Device (fips0)
The TUN reader receives raw IPv6 packets from applications and processes them:
1. Validate IPv6 header
2. Extract destination `fd::/8` address
2. Extract destination `fd00::/8` address
3. Look up identity cache — miss returns ICMPv6 Destination Unreachable
4. Retrieve NodeAddr and PublicKey from cache
5. Look up or establish FSP session
@@ -247,7 +270,7 @@ handle.
The Linux kernel routing table processes rules in priority order:
1. **Local table**: Intercepts traffic to addresses assigned to this machine
2. **Main table**: Routes `fd::/8` to the TUN device
2. **Main table**: Routes `fd00::/8` to the TUN device
This means every packet arriving at the TUN reader is guaranteed to be for a
*remote* FIPS destination. No "is this for me?" check is needed on the read
@@ -283,10 +306,10 @@ TUN device creation requires `CAP_NET_ADMIN`. Options:
| TCP MSS clamping (SYN + SYN-ACK) | **Implemented** |
| DNS service (.fips domain) | **Implemented** |
| Per-destination route MTU (netlink) | Planned |
| Transit MTU error signal | Planned |
| Transit MTU error signal | **Implemented** |
| Path MTU tracking (SessionDatagram field) | **Implemented** |
| Path MTU notification (end-to-end echo) | **Implemented** |
| Endpoint fragmentation/reassembly | Future direction |
| Endpoint fragmentation/reassembly | Transport drivers |
## Design Considerations
@@ -318,11 +341,6 @@ plaintext. Fragmenting encrypted datagrams would require either exposing
plaintext structure to transit nodes (unacceptable) or reassembly before
decryption (opens attack surface).
Endpoint-only fragmentation (fragment before session encryption, reassemble
after decryption) is a future direction that avoids these objections. Each
fragment would be independently encrypted and look like a normal data packet
to transit nodes.
## References
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture

View File

@@ -280,8 +280,8 @@ authenticated peer regardless of what transport address the packet arrived
from. FMP updates the peer's current address to the packet's source address,
and subsequent outbound packets use the updated address.
This allows peers to change transport addresses (IP:port for UDP, connection
handle for TCP) without session interruption. The mechanism is:
This allows peers to change transport addresses (e.g., IP:port for UDP)
without session interruption. The mechanism is:
1. Packet arrives from a different address than expected
2. Receiver index lookup finds the session

View File

@@ -23,8 +23,8 @@ Two complementary mechanisms provide the information each node needs:
ancestry path from itself to the root. These coordinates enable distance
calculations between any two nodes without global topology knowledge.
- **Bloom filters** summarize which destinations are reachable through each
peer. They provide candidate selection — narrowing which peers are worth
considering for forwarding a given destination.
peer. Because they propagate along tree edges, they encode directional
reachability — which subtree contains a given destination.
Together, they enable a routing decision process that is local, efficient,
and self-healing.
@@ -48,7 +48,7 @@ enable:
Nodes self-organize into a spanning tree through distributed parent selection:
1. **Root election**: The node with the smallest node_addr becomes the root.
1. **Root discovery**: The node with the smallest node_addr becomes the root.
No election protocol — this is a consequence of each node independently
preferring lower-addressed roots.
2. **Parent selection**: Each node selects a single parent from among its
@@ -75,10 +75,76 @@ Changes cascade through the tree:
TreeAnnounce propagation is rate-limited at 500ms minimum interval per peer.
A tree of depth D reconverges in roughly D×0.5s to D×1.0s.
### How the Tree Adapts to Link Quality
The initial tree forms based on hop count alone — all links default to a
cost of 1.0 before measurements are available. As the Metrics Measurement
Protocol (MMP) accumulates bidirectional delivery ratios and round-trip
time estimates, each node computes a per-link cost:
```text
link_cost = ETX × (1.0 + SRTT_ms / 100.0)
```
ETX (Expected Transmission Count) captures loss — a perfect link has
ETX = 1.0, while 10% loss in each direction yields ETX ≈ 1.23. The SRTT
term weights latency so that a low-loss but high-latency link (e.g., a
satellite hop) costs more than a low-loss, low-latency link.
Parent selection uses **effective depth** rather than raw hop count:
```text
effective_depth = peer.depth + link_cost_to_peer
```
This allows a node to trade a shorter but lossy path for a longer but
higher-quality one. A node two hops from the root over clean links
(effective depth ≈ 3.0) is preferred over a node one hop away over a
degraded link (effective depth ≈ 4.5).
Parent reselection is triggered by three paths:
1. **TreeAnnounce**: When a peer announces a new tree position, the node
re-evaluates using current link costs
2. **Periodic re-evaluation**: Every 60s (configurable), the node
re-evaluates its parent choice using the latest MMP metrics, catching
gradual link degradation that doesn't trigger TreeAnnounce
3. **Parent loss**: When the current parent is removed, the node
immediately selects the best alternative
To prevent oscillation from metric noise, parent switches are subject to
**hysteresis**: a candidate must offer an effective depth at least 20%
better than the current parent to trigger a switch. A **hold-down period**
(default 30s) suppresses non-mandatory re-evaluation after a switch,
allowing MMP metrics to stabilize on the new link before reconsidering.
### Flap Dampening
Unstable links that repeatedly connect and disconnect can cause cascading
tree reconvergence. The spanning tree uses flap dampening with hysteresis
and hold-down periods to suppress rapid parent oscillation. Links that flap
above a configurable threshold are temporarily penalized, preventing them
from being selected as parent until the link stabilizes.
### Link Liveness
Each node sends a dedicated **Heartbeat** message (0x51, 1 byte, no
payload) to every peer at a fixed interval (default 10s). Any
authenticated encrypted frame — heartbeat, MMP report, TreeAnnounce,
data packet — resets the peer's liveness timer. On an idle link with no
application data or topology changes, the heartbeat is the only traffic
that keeps the link alive.
Peers that are silent for a configurable dead timeout (default 30s) are
considered dead and removed from the peer table. With the default 10s
heartbeat interval, a peer must miss three consecutive heartbeats before
removal. This triggers tree reconvergence and bloom filter recomputation
for the affected subtree.
### Partition Handling
If the network partitions, each segment independently elects its own root
(the smallest node_addr in the segment) and reconverges. When segments
If the network partitions, each segment independently rediscovers its own
root (the smallest node_addr in the segment) and reconverges. When segments
rejoin, nodes discover the globally-smallest root through TreeAnnounce
exchange and reconverge to a single tree.
@@ -94,9 +160,10 @@ Each node maintains a bloom filter per peer, answering: "can peer P possibly
reach destination D?" The answer is either "no" (definitive) or "maybe"
(probabilistic — false positives are possible).
This is **candidate selection**, not routing. Bloom filters identify which
peers are worth considering for a destination, but the actual forwarding
decision uses tree coordinate distance to rank those candidates.
Because filters propagate along tree edges with split-horizon exclusion,
they encode directional reachability: a bloom hit on a tree peer reliably
indicates which subtree contains the destination. When multiple peers match,
tree coordinate distance ranks them.
### How Filters Propagate
@@ -135,9 +202,9 @@ Updates are rate-limited at 500ms to prevent storms during topology changes.
At moderate network sizes, bloom filters are highly accurate. At larger
scales (~1M nodes), hub nodes with many peers may see elevated false positive
rates (715% for nodes with 20+ peers). False positives cause unnecessary
discovery attempts but do not affect routing correctness — the tree distance
calculation makes the actual forwarding decision.
rates (715% for nodes with 20+ peers). False positives may cause a packet
to be forwarded toward the wrong subtree, but the self-distance check at
each hop prevents loops and the packet falls through to greedy tree routing.
See [fips-bloom-filters.md](fips-bloom-filters.md) for filter parameters,
FPR calculations, and size class folding.
@@ -155,8 +222,8 @@ priority chain. This is the core routing algorithm.
2. **Direct peer** — The destination is an authenticated neighbor. Forward
directly. No coordinates or bloom filters needed.
3. **Bloom-guided candidate selection** — One or more peers' bloom filters
contain the destination. Select the best candidate by composite key:
3. **Bloom-guided routing** — One or more peers' bloom filters contain the
destination. Select the best peer by composite key:
`(link_cost, tree_distance, node_addr)`. This requires the destination's
tree coordinates to be in the local coordinate cache.
@@ -189,10 +256,12 @@ through to greedy tree routing (functional but suboptimal).
When bloom filters identify multiple candidate peers, they are ranked by a
composite key:
1. **link_cost** — Per-link quality metric. ETX is computed from bidirectional
delivery ratios in MMP metrics and is used in cost-based parent selection
via `effective_depth = depth + link_cost`, but is not yet wired into
`find_next_hop()` candidate ranking.
1. **link_cost** — Per-link quality metric derived from ETX (Expected
Transmission Count), computed from bidirectional delivery ratios in MMP
metrics. In practice this is an uncommon tie-breaker: most forwarding
decisions are resolved by tree distance alone, and link_cost only
differentiates candidates when multiple peers offer the same tree distance
to the destination.
2. **tree_distance** — Coordinate-based distance to destination through this
peer
3. **node_addr** — Deterministic tie-breaker
@@ -285,9 +354,10 @@ to a visited filter (preventing loops on a single path), and forwards to all
peers not in the visited filter. Bloom filters may help direct the flood
toward likely candidates.
**Deduplication**: Nodes maintain a short-lived request_id dedup cache to
drop convergent duplicates (the same request arriving via different paths).
This is a protocol requirement, not an optimization.
**Deduplication**: Nodes maintain a short-lived request_id dedup cache
(default 10s window) to drop convergent duplicates (the same request
arriving via different paths). This is a protocol requirement, not an
optimization.
### LookupResponse
@@ -528,10 +598,12 @@ 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 *(future direction)*
## Leaf-Only Operation *(under development)*
Leaf-only operation is a planned optimization for resource-constrained nodes
(sensors, battery-powered devices). Not currently implemented.
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.
### Concept
@@ -558,7 +630,7 @@ The upstream peer:
Even as a leaf-only node, it still:
- Maintains its own Noise IK link session with the upstream peer
- Maintains its own Noise IK 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)
@@ -621,9 +693,12 @@ recovery).
| LookupResponse proof verification | **Implemented** |
| Discovery reverse-path routing | **Implemented** |
| Error signal rate limiting | **Implemented** |
| Leaf-only operation | Future direction |
| Flap dampening (hysteresis + hold-down) | **Implemented** |
| Link liveness (dead timeout) | **Implemented** |
| Discovery request deduplication | **Implemented** |
| Leaf-only operation | Under development |
| Link cost in parent selection (ETX) | **Implemented** |
| Link cost in candidate ranking | Future direction |
| Link cost in candidate ranking | **Implemented** |
| Discovery path accumulation | Future direction |
## References

View File

@@ -38,7 +38,7 @@ encrypts the payload, and routes through FMP. No DNS involvement.
### IPv6 Adapter
Unmodified IPv6 applications use a TUN device with `fd::/8` routing. A local
Unmodified IPv6 applications use a TUN device with `fd00::/8` routing. A local
DNS service maps npub → IPv6 address and primes the identity cache. Packets
arriving at the TUN are translated to FIPS datagrams and routed through FSP.
@@ -111,10 +111,10 @@ 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. 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
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.
@@ -133,7 +133,8 @@ and SessionMsg3:
Each side's epoch (an 8-byte random value generated at startup) is
exchanged encrypted in msg2 and msg3. On subsequent handshakes, an epoch
mismatch indicates the peer has restarted, triggering session
re-establishment.
re-establishment — similar to IKEv2's INITIAL_CONTACT notification for
peer restart detection (RFC 7296 §2.4).
Packets that trigger session establishment are queued (with bounded buffer)
and transmitted after the session is established.
@@ -154,14 +155,15 @@ the forward path (e.g., after tree reconvergence).
### Simultaneous Initiation
When both nodes attempt to establish a session simultaneously ("crossing
hellos"), a deterministic tie-breaker resolves the conflict:
hellos"), a deterministic tie-breaker resolves the conflict, mirroring
IKEv2's simultaneous initiation resolution (RFC 7296 §2.8):
- If `local_node_addr < remote_node_addr`: Continue as initiator, ignore
incoming setup
- If `local_node_addr > remote_node_addr`: Abort own initiation, switch to
responder role
This ensures exactly one handshake completes.
This lowest-address-wins approach ensures exactly one handshake completes.
### Data Transfer
@@ -194,7 +196,9 @@ The fresh SessionSetup re-warms the caches, maintaining routing continuity.
### Session Independence from Transport
Sessions exist above the routing layer and are bound to npub identities, not
transport addresses or routing paths. A session survives:
transport addresses or routing paths — following WireGuard's approach
(Donenfeld 2017) of binding cryptographic sessions to identity keys rather
than network addresses. A session survives:
- Transport failover (UDP → Ethernet → back to UDP)
- Route changes (different intermediate hops)
@@ -237,7 +241,9 @@ operations) rather than under only the responder's static key.
| Hash | SHA-256 | Nostr-native |
| Key derivation | HKDF-SHA256 | Standard Noise KDF |
These choices prioritize compatibility with the Nostr cryptographic stack.
These choices prioritize compatibility with the Nostr cryptographic stack
secp256k1 + ChaCha20-Poly1305 + SHA-256 aligns with the NIP-44 encrypted
messaging standard.
### secp256k1 Parity Normalization
@@ -271,14 +277,16 @@ authentication for every packet, matching WireGuard and Lightning's approach.
### Forward Secrecy
Ephemeral keys in the Noise handshake provide forward secrecy. Compromise of
static keys (nsec) does not reveal past session keys, because session keys are
derived in part from ephemeral-ephemeral DH (`ee`), and ephemeral keys are
discarded after the handshake.
Ephemeral keys in the Noise handshake provide forward secrecy — a standard
property of Noise handshake patterns that include ephemeral key exchange.
Compromise of static keys (nsec) does not reveal past session keys, because
session keys are derived in part from ephemeral-ephemeral DH (`ee`), and
ephemeral keys are discarded after the handshake.
## Replay Protection
FSP uses explicit 8-byte counters on the wire for replay protection. Each side
FSP uses explicit 8-byte counters on the wire for replay protection, adapted
from the DTLS anti-replay mechanism (Rescorla & Modadugu, RFC 6347). Each side
maintains a monotonically increasing send counter, included in the 12-byte
cleartext header of every encrypted message. The receiver maintains a sliding
window (2048-entry bitmap) tracking which counters have been seen.
@@ -287,8 +295,9 @@ This design is critical for operation over unreliable transports. Under UDP
packet loss or reordering, implicit nonce counters (where the receiver
increments on each decrypt attempt) would desynchronize permanently — a failed
`decrypt()` increments the nonce, and the desync grows with each lost packet.
Explicit counters allow the receiver to decrypt any packet independently,
regardless of what packets were lost or reordered.
Explicit counters with a sliding bitmap window — the standard DTLS approach —
allow the receiver to decrypt any packet independently, regardless of what
packets were lost or reordered.
The same `ReplayWindow` and `decrypt_with_replay_check()` implementation is
used at both the link and session layers.
@@ -302,7 +311,9 @@ entry expires, it cannot forward data packets (which carry only addresses, not
coordinates) and sends a CoordsRequired error.
FSP uses a hybrid warmup strategy combining proactive piggybacking with
reactive standalone messages to keep transit caches populated:
reactive standalone messages to keep transit caches populated (Yggdrasil
uses a similar approach of embedding coordinates in session traffic to
warm transit node caches):
### Proactive Warmup Phase
@@ -329,15 +340,12 @@ Transit nodes extract coordinates via the existing `try_warm_coord_cache()`
code path with zero changes to the transit forwarding logic. CoordsWarmup is
indistinguishable from any other CP-flagged message at the transit layer.
Wire format:
```text
FSP header (12 bytes, AAD): ver=0, phase=0, flags=CP, counter, payload_len
Cleartext coords: src_coords + dst_coords (same encoding as CP flag)
AEAD ciphertext: inner_header(6) + Poly1305 tag(16) = 22 bytes
Total FSP payload: 12 + coords + 22
```
On the wire, a CoordsWarmup message consists of the standard 12-byte FSP
header (used as AEAD AAD) with ver=0, phase=0, flags=CP, followed by
cleartext source and destination coordinates (same encoding as any
CP-flagged packet), followed by 22 bytes of AEAD ciphertext (6-byte inner
header and 16-byte Poly1305 tag). The total FSP payload is 12 bytes of
header, the coordinate data, and 22 bytes of ciphertext.
### Steady State
@@ -368,30 +376,11 @@ signal generation (100ms per destination).
### Warmup State Machine
```text
┌──────────────┐
│ WARMUP │ ◄── Send first N packets with coords (CP or CoordsWarmup)
└──────┬───────┘
│ N packets sent without CoordsRequired
┌──────────────┐
│ MINIMAL │ ◄── Send packets without coords
└──────┬───────┘
│ CoordsRequired or PathBroken received
┌──────────────────────────┐
│ SEND CoordsWarmup (0x14) │ ◄── Immediate standalone warmup (rate-limited)
└──────────┬───────────────┘
┌──────────────┐
│ WARMUP │ ◄── Counter reset, piggyback coords again
└──────────────┘
```
![Coordinate warmup state machine](diagrams/session-warmup-fsm.svg)
## Identity Cache
The identity cache maps FIPS address prefix (15 bytes, the `fd::/8` IPv6
The identity cache maps FIPS address prefix (15 bytes, the `fd00::/8` IPv6
address minus the `fd` prefix) to `(NodeAddr, PublicKey)`. This cache is
needed only when using the IPv6 adapter — the native FIPS API provides the
public key directly.
@@ -482,7 +471,8 @@ bandwidth cost: clamped to [500ms, 10s] with a cold-start interval of 1s
### Path MTU Tracking
PathMtuNotification (message type 0x13) provides end-to-end path MTU
feedback:
feedback, adapting RFC 1191 Path MTU Discovery for overlay networks — the
transit-node `min()` propagation replaces ICMP Packet Too Big:
1. The source sets `path_mtu` in each SessionDatagram envelope to its
outbound link MTU.
@@ -500,7 +490,8 @@ feedback:
### Send Failure Backoff
When a session MMP report cannot be delivered (destination unreachable, no
route), the sender applies exponential backoff to the probe interval:
route), the sender applies exponential backoff to the probe interval (a
standard distributed systems pattern for transient failure handling):
- Each consecutive failure doubles the interval: 2x, 4x, 8x, 16x, 32x
- Backoff caps at 32x the base interval (5 consecutive failures)
@@ -553,6 +544,8 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
## References
### FIPS Internal Documentation
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
- [fips-mesh-layer.md](fips-mesh-layer.md) — FMP specification (below FSP)
- [fips-ipv6-adapter.md](fips-ipv6-adapter.md) — IPv6 adaptation layer (above FSP)
@@ -560,3 +553,29 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
error recovery
- [fips-wire-formats.md](fips-wire-formats.md) — Wire format reference for all
session message types
### External References
- 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.*
- 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
rather than network addresses; AEAD-only authentication model.*
- Rescorla, E., Modadugu, N. [RFC 6347](https://datatracker.ietf.org/doc/html/rfc6347):
"Datagram Transport Layer Security Version 1.2". 2012. *Explicit sequence numbers
with sliding bitmap window for replay protection over unreliable transports.*
- Kaufman, C., Hoffman, P., Nir, Y., Eronen, P., Kivinen, T.
[RFC 7296](https://datatracker.ietf.org/doc/html/rfc7296):
"Internet Key Exchange Protocol Version 2 (IKEv2)". 2014. *Simultaneous
initiation resolution (§2.8) and INITIAL_CONTACT peer restart detection (§2.4).*
- Mogul, J., Deering, S. [RFC 1191](https://datatracker.ietf.org/doc/html/rfc1191):
"Path MTU Discovery". 1990. *End-to-end path MTU discovery; FSP adapts this for
overlay networks using transit-node min() propagation.*
- [Yggdrasil Network](https://yggdrasil-network.github.io/). *Coordinate-based
overlay routing with session traffic used to warm transit node coordinate caches.*

View File

@@ -1,365 +0,0 @@
# FIPS Software Architecture
This document describes the stable architectural decisions that guide the
FIPS codebase — the "why" behind the code's shape. It covers design
principles and patterns that are expected to remain stable as the
implementation evolves. For protocol behavior and wire formats, see the
protocol layer documents.
## Ownership and Entity Hierarchy
A FIPS node owns transports, which produce links, which authenticate into
peers:
```text
Node
├── Transports (HashMap<TransportId, TransportHandle>)
│ └── Each transport instance manages one communication medium
├── Links (HashMap<LinkId, Link>)
│ └── Each link is a connection to a remote endpoint over a transport
├── Peers (HashMap<NodeAddr, PeerSlot>)
│ └── Each peer is an authenticated remote FIPS node
├── TreeState — local view of the spanning tree
├── CoordCache — destination coordinates for routing
├── Sessions — end-to-end FSP sessions (HashMap by NodeAddr)
└── Identity — this node's cryptographic identity
```
**Key ownership rules**:
- A transport exists for the lifetime of the node (configured at startup)
- A link is created when connecting to a remote endpoint and destroyed when
the connection terminates
- A peer is created when a link successfully authenticates (Noise IK
handshake) and destroyed when the link goes down
- Links and peers have a one-to-one mapping with coupled lifecycles —
peer teardown implies link teardown
## Event-Driven Execution Model
The node uses an async select loop as its main event loop, multiplexing
events from all sources into a single processing stream:
- **Transport events**: Inbound datagrams from all transports arrive via a
shared mpsc channel
- **Timer events**: Periodic and one-shot timers for keepalive, stale peer
detection, cache expiry, handshake timeouts
- **TUN events**: Outbound IPv6 packets from local applications
- **Control events**: Identity registrations from DNS, shutdown signals
Within the select loop, events are dispatched to focused handler functions
organized by concern (handshake processing, gossip handling, forwarding,
session management, timeout handling). Each handler operates on the node's
state directly — there is no separate message-passing between internal
components.
**Why a single select loop**: FIPS protocol operations frequently need to
read and modify multiple pieces of state (e.g., forwarding a packet reads
the coordinate cache, peer ancestry, and bloom filters simultaneously).
A single-threaded event loop avoids the complexity of locking and provides
deterministic ordering of state changes.
**Exceptions**: The TUN reader and writer run in separate blocking threads
because TUN I/O is blocking (kernel file descriptor). They communicate with
the main event loop via channels.
## Phase-Based State Machine Pattern
FIPS entities use a Rust enum-of-structs pattern for state machines where
each phase carries only the data relevant to that phase:
```rust
enum PeerSlot {
Connecting(PeerConnection), // handshake in progress
Active(ActivePeer), // authenticated, participating
}
```
Each variant holds a different struct with phase-appropriate fields. The
`PeerConnection` struct carries handshake state; `ActivePeer` carries tree
position, bloom filters, and link statistics. Transitioning between phases
consumes the old struct and produces the new one, making it impossible to
access handshake state after authentication is complete.
This pattern enforces at the type level that code handling an authenticated
peer cannot accidentally reference handshake state, and vice versa.
See [fips-state-machines.md](fips-state-machines.md) for a detailed
treatment of this pattern.
## Two-Layer Encryption Rationale
FIPS uses independent Noise encryption at two layers (IK at FMP, XK at FSP):
| Layer | Scope | What It Protects |
| ----- | ----- | ---------------- |
| FMP (mesh) | Hop-by-hop | All traffic on each peer link |
| FSP (session) | End-to-end | Application payload between endpoints |
**Why two layers instead of one**:
- **Link encryption** protects all traffic from passive observers on the
underlying transport — including routing metadata (TreeAnnounce, bloom
filters, discovery messages) that would otherwise be visible
- **Session encryption** protects application payloads from intermediate
routing nodes, which must decrypt link encryption to read routing headers
- Both layers always apply. For adjacent peers, traffic is encrypted twice.
This eliminates special cases ("local peer" vs. "remote destination") and
means topology changes (a direct peer becomes multi-hop) don't affect
sessions.
**Why different patterns**: FMP uses Noise IK (2-message) because link peers
are configured and directly connected — the single round trip is preferred.
FSP uses Noise XK (3-message) because session traffic traverses untrusted
intermediate nodes — the extra round trip buys stronger initiator identity
hiding. Both layers reuse the same cryptographic stack (secp256k1,
ChaCha20-Poly1305, SHA-256), minimizing cryptographic dependencies.
## Identity Model
FIPS uses three related but distinct identifiers at different layers:
```text
keypair (secp256k1)
├── pubkey (32 bytes) — the endpoint identity, used in Noise handshakes
├── node_addr = SHA-256(pubkey)[0..16] — routing identifier, visible to
│ transit nodes, cannot be reversed to pubkey
└── IPv6 address = fd + node_addr[0..15] — overlay address for IPv6
applications
```
**Privacy property**: Transit nodes see only node_addrs in packet headers.
They can forward traffic without knowing the Nostr identities of the
endpoints. An observer can verify "does this node_addr belong to pubkey X?"
but cannot enumerate communicating identities from traffic alone.
**Self-sovereign**: Nodes generate their own identities without coordination.
The identity system uses Nostr keypairs (secp256k1), so existing npub/nsec
pairs work directly.
## Protocol Self-Healing Design
FIPS control protocols are designed for eventual consistency, tolerating
packet loss without acknowledgment/retry machinery:
| Protocol | Self-Healing Property |
| -------- | --------------------- |
| TreeAnnounce | Full state with monotonic sequence; lost announcement recovered on next send |
| FilterAnnounce | Full filter replacement with sequence; stale filter recovered on next update |
| LookupRequest | Timeout-based retry at application level |
| SessionSetup | Timeout-based retry; lost setup triggers re-establishment on first data failure |
| CoordsRequired/PathBroken | Rate-limited, best-effort; lost error recovered by session idle timeout |
**Why no ack/retry**: FIPS operates over unreliable transports (primarily
UDP). Adding reliability to control messages would require per-message state,
retransmission timers, and acknowledgment tracking — complexity that gossip
protocols avoid by sending full state periodically. A lost TreeAnnounce is
simply replaced by the next one, which carries the same or newer state.
## Metrics Measurement Protocol
MMP is instantiated at two independent layers, each with its own
configuration and state:
- **Link layer**: One `MmpPeerState` per `ActivePeer`. Measures per-hop
quality using the FMP counter and timestamp fields that already exist on
every encrypted frame. No additional message overhead beyond periodic
SenderReport/ReceiverReport exchanges.
- **Session layer**: One `MmpSessionState` per established `SessionEntry`.
Measures end-to-end quality using the FSP counter and timestamp fields.
Reports are encrypted and forwarded through every transit link.
Both instantiations use identical algorithms (SRTT, loss, jitter, dual EWMA,
OWD trend) but are configured independently via `node.mmp.*` and
`node.session_mmp.*`. This allows operators to run Full mode on links (low
overhead, single hop) while using Lightweight mode for sessions (reduces
bandwidth cost on transit links).
### Peer Display Names
The node maintains a `peer_aliases` map (`HashMap<NodeAddr, String>`) populated
from the `peers[].alias` field in configuration. All log output uses
`peer_display_name()` to show human-readable names (e.g., "node-b") instead
of truncated public keys, improving operator experience.
### Buffer Sizing Chain
Under high forwarding load, back-pressure propagates through:
1. **UDP socket receive buffer** (`transports.udp.recv_buf_size`, default 2 MB) —
kernel-level buffer for incoming datagrams.
2. **Packet channel** (`node.buffers.packet_channel`, default 1024) — async
channel from transport receive loop to the node's RX event loop.
3. **Processing** — decryption, routing decision, forwarding.
If the packet channel fills (RX loop can't keep up), the transport receive
loop blocks, and the kernel receive buffer absorbs bursts. If the kernel
buffer also fills, incoming datagrams are silently dropped
(`RcvbufErrors` in `/proc/net/snmp`). The 2 MB default socket buffer was
chosen to handle ~85 MB/s forwarding throughput without kernel drops.
## Bounded State Principle
FIPS nodes maintain state proportional to O(P × D), where P is the number
of direct peers and D is the tree depth — not O(N) where N is the network
size.
What each node stores:
| State | Size | Scope |
| ----- | ---- | ----- |
| Peer ancestry (TreeAnnounce) | P × D entries | Direct peers only |
| Bloom filters | P × 1 KB | One per peer |
| Coordinate cache | Configurable (50K default) | Destinations actively routed |
| Identity cache | Configurable (10K default) | IPv6 adapter only |
| Sessions | Configurable (10K default) | Active end-to-end sessions |
A node does not know about nodes in distant parts of the network. It knows
its direct peers, their tree positions, and the destinations it has recently
routed traffic to. This scales naturally: adding nodes to the network does
not increase the per-node state of existing nodes (except for a slight
increase in bloom filter occupancy).
## Transport Opacity
Transport addresses are opaque byte vectors above FMP. The transport layer
interprets them (e.g., UDP parses "ip:port" strings); all layers above treat
them as handles passed back to the transport for sending.
**Architectural boundary**: Adding a new transport type (e.g., BLE) requires
implementing the transport trait and potentially a new `TransportHandle`
variant. No changes to FMP, FSP, or any routing logic. The transport trait
defines the interface:
- `send(addr, data)` — send a datagram
- `mtu()` — maximum datagram size
- `start()` / `stop()` — lifecycle
- `discover()` — optional endpoint discovery
Inbound datagrams are pushed via a shared channel, aggregating all transports
into a single event stream for the main loop.
## Cache Architecture
### Unified Coordinate Cache
The coordinate cache maps `NodeAddr → TreeCoordinate`. It was originally two
separate caches (session-populated and discovery-populated) but was merged
into a single cache because both stored the same type of data and the
distinction was conceptual, not functional.
Key properties:
- **TTL-based expiration** (300s default) with **refresh on use** — active
routing resets the TTL, keeping hot entries alive
- **LRU eviction** when full — least recently used entries are evicted first
- **Flush on parent change** — when the local node's tree parent changes,
the entire cache is flushed because the node's own coordinates have
changed, making cached distance calculations potentially invalid
### Identity Cache (LRU-Only)
The identity cache maps FIPS address prefix → (NodeAddr, PublicKey). The
mapping is deterministic (derived from public key) and never becomes stale,
so there is no TTL — only LRU eviction bounded by a configurable size.
This cache is needed only by the IPv6 adapter. The native FIPS API provides
the public key directly.
### Timer Ordering
Cache and session timers are ordered to ensure correct lifecycle behavior:
```text
Session idle timeout (90s) < Coordinate cache TTL (300s) ≤ DNS TTL (300s)
```
When traffic stops, the session tears down first (90s). When traffic
resumes, a fresh SessionSetup re-warms transit caches that are still within
their TTL (300s). This ordering prevents the case where a session outlives
its transit cache entries, which would cause routing failures.
## Receive Path Design
Transports use a channel-push model rather than a poll/receive method. Each
transport takes a sender handle (`PacketTx`) at construction and spawns an
internal receive loop that pushes inbound datagrams onto the channel. The
node's main select loop reads from the corresponding receiver.
**Why push, not poll**: Async Rust cannot express async methods on trait
objects (the `Transport` trait is synchronous). The channel-push model works
around this limitation: the concrete transport implementation (e.g.,
`UdpTransport`) spawns its own async receive task and pushes to a channel,
while the trait surface remains synchronous for `send()`, `mtu()`, etc.
The `TransportHandle` enum provides async dispatch for methods that need it
(like `send_async()`) without requiring dyn dispatch.
## Control Socket
FIPS exposes a read-only observability interface through a Unix domain
socket. The control socket provides runtime visibility into node state
without affecting protocol operation.
### Architecture
The control socket is a `UnixListener` integrated into the node's async
runtime. It accepts connections, reads a single JSON request per
connection, dispatches to query handlers that read (but never modify)
node state, and returns a JSON response.
- **Max request size**: 4,096 bytes
- **I/O timeout**: 5 seconds per connection
- **Socket cleanup**: Automatically removes stale sockets on startup
### fipsctl Binary
The `fipsctl` command-line tool communicates with the control socket:
```text
fipsctl [OPTIONS] <COMMAND>
OPTIONS:
-s, --socket <SOCKET> Control socket path override
COMMANDS:
show status Node status overview
show peers Authenticated peers
show links Active links
show tree Spanning tree state
show sessions End-to-end sessions
show bloom Bloom filter state
show mmp MMP metrics summary
show cache Coordinate cache stats
show connections Pending handshake connections
show transports Transport instances
show routing Routing table summary
```
### Protocol
Request and response are line-delimited JSON:
```json
{"command":"show_status"}
{"status":"ok","data":{...}}
```
Error responses use `{"status":"error","message":"..."}`.
All queries are read-only — the control socket cannot modify node state,
trigger actions, or shut down the node.
## References
- [fips-intro.md](fips-intro.md) — Protocol overview
- [fips-mesh-layer.md](fips-mesh-layer.md) — FMP specification
- [fips-session-layer.md](fips-session-layer.md) — FSP specification
- [fips-state-machines.md](fips-state-machines.md) — Phase-based state
machine pattern
- [fips-configuration.md](fips-configuration.md) — YAML configuration
reference

View File

@@ -6,9 +6,29 @@ for readers who want to understand the tree internals — for how the
spanning tree fits into the overall mesh operation, see
[fips-mesh-operation.md](fips-mesh-operation.md).
## What Is a Spanning Tree?
A spanning tree is a subset of the links in a mesh network that:
- **Reaches every node** — no node is disconnected
- **Contains no cycles** — there is exactly one path between any two nodes
- **Has a single root** — one distinguished node from which all paths descend
- **Assigns every non-root node exactly one parent** — creating a
hierarchy from leaves to root
Because a tree has no cycles, any node's position can be described by
its path to the root. This path serves as a coordinate in a virtual
address space, enabling distance calculations and routing decisions
without global topology knowledge.
There is nothing special about the root node other than providing the
center of the coordinate system. Being root implies no additional
processing, routing, or operational burden — the root runs the same
protocol as every other node.
## Purpose
The spanning tree gives every node in the mesh a **coordinate** — its
The FIPS spanning tree gives every node in the mesh a **coordinate** — its
ancestry path from itself to the root. These coordinates enable:
- Distance calculation between any two nodes without global topology
@@ -16,24 +36,31 @@ ancestry path from itself to the root. These coordinates enable:
- Greedy routing where each hop reduces distance to the destination
- Loop-free forwarding guaranteed by strictly-decreasing distance
## Root Election
## Root Discovery
The root is the node with the **lexicographically smallest node_addr** among
all reachable nodes. There is no election protocol, no voting, no
negotiation. Each node independently evaluates the TreeAnnounce messages
from its peers and selects the minimum root.
negotiation — each node independently discovers the same root by
evaluating the TreeAnnounce messages from its peers and selecting the
minimum root.
When a node first joins the network with no peers, it is its own root. As
it connects to peers and receives their TreeAnnounce messages, it discovers
smaller node_addrs and converges to the global root.
If the network partitions, each segment independently elects its own root
If the network partitions, each segment independently discovers its own root
(the smallest node_addr in that segment). When segments rejoin, all nodes
discover the globally-smallest root through TreeAnnounce exchange and
reconverge to a single tree.
## Parent Selection
Parent selection and reselection is the primary means by which the mesh
self-organizes into an efficient routing structure. By having each node
choose the parent with the best measured link performance, packet
routing up and down the tree follows the best available path that
reduces distance to the destination.
Each node selects a single parent from among its direct peers. Parent
selection uses cost-weighted depth to balance tree depth against link
quality.
@@ -79,9 +106,9 @@ immediate parent reselection:
delaying legitimate reconvergence. Mandatory switches (parent loss,
root change) bypass dampening.
- **Local-only metrics**: Link costs use only locally measured MMP data
(ETX and SRTT). No cumulative path costs are propagated and no wire
format changes are required. This avoids the trust problems inherent
in self-reported cost metrics in a permissionless network.
(ETX and SRTT). No cumulative path costs are propagated, avoiding
the trust problems inherent in self-reported cost metrics in a
permissionless network.
### After Parent Change
@@ -126,7 +153,7 @@ tree_distance(a, b):
Example: If A has coordinates `[A, X, Y, Root]` and B has coordinates
`[B, Z, Y, Root]`, the common suffix is `[Y, Root]` (length 2). Distance =
(3 - 2) + (3 - 2) = 2 hops.
(4 - 2) + (4 - 2) = 4 hops.
The self-distance check in greedy routing uses this calculation: a packet is
forwarded to a peer only if the peer is strictly closer to the destination
@@ -193,10 +220,8 @@ for stronger verification.
### Timestamp
- Type: u64, Unix seconds
- Used for stale detection, not versioning
- A root declaration is considered stale after `ROOT_TIMEOUT` (60 minutes)
without refresh. In practice, heartbeat cascading handles root departure
for the common case (see spanning-tree-dynamics.md §11)
- Advisory only — not used in any decision logic, as there is no way to
verify its accuracy from peers
## Reconvergence
@@ -214,7 +239,7 @@ When a node fails (link timeout or disconnect):
When the network partitions:
1. Nodes in each segment lose peers across the partition boundary
2. If the root was in the other segment, affected nodes elect a new segment
2. If the root was in the other segment, affected nodes discover a new segment
root (smallest node_addr in their segment)
3. Each segment reconverges independently
@@ -262,14 +287,12 @@ Example: In a 1000-node network with depth 10 and 5 peers, a node stores
| FLAP_WINDOW_SECS | 60s | Sliding window for counting parent switches |
| FLAP_DAMPENING_SECS | 120s | Extended hold-down duration when flap threshold exceeded |
| ANNOUNCE_MIN_INTERVAL | 500ms | Minimum between announcements to same peer |
| ROOT_TIMEOUT | 60 min | Root declaration considered stale (not yet enforced; heartbeat cascading covers common case) |
| TREE_ENTRY_TTL | 510 min | Individual entry expiration |
## Implementation Status
| Feature | Status |
| ------- | ------ |
| Root election (smallest node_addr) | **Implemented** |
| Root discovery (smallest node_addr) | **Implemented** |
| Cost-based parent selection with hysteresis | **Implemented** |
| Hold-down timer after parent change | **Implemented** |
| Periodic cost-based parent re-evaluation | **Implemented** |
@@ -280,8 +303,6 @@ Example: In a 1000-node network with depth 10 and 5 peers, a node stores
| Rate limiting (500ms per peer) | **Implemented** |
| Coord cache flush on parent change | **Implemented** |
| Flap dampening (extended hold-down on rapid switches) | **Implemented** |
| Root timeout enforcement | Planned |
| Tree entry TTL enforcement | Planned |
| Per-ancestry-entry signatures | Future direction |
## References

View File

@@ -1,381 +0,0 @@
# FIPS State Machine Design
This document describes the phase-based state machine pattern used throughout
FIPS, where different lifecycle phases are represented by distinct struct types
wrapped in an enum rather than a single struct with a state field.
## Pattern Overview
### Traditional Approach (Single Struct + State Enum)
```rust
enum PeerState {
Connecting,
Authenticating,
Active,
Disconnected,
}
struct Peer {
identity: PeerIdentity,
state: PeerState,
// Fields needed by ALL states
ephemeral_key: Option<Keypair>, // Only used during auth
session_keys: Option<SessionKeys>, // Only valid when Active
tree_coords: Option<TreeCoordinate>, // Only valid when Active
// ...
}
impl Peer {
fn handle_packet(&mut self, packet: &[u8]) {
match self.state {
PeerState::Connecting => { /* must check we're not Active */ }
PeerState::Active => { /* must check session_keys.is_some() */ }
// ...
}
}
}
```
**Problems:**
- Fields that only apply to certain states are `Option<T>` or uninitialized
- Methods must check state before operating (runtime errors possible)
- Auth-phase secrets (ephemeral keys) persist in memory after auth completes
- Single struct grows to accommodate all phases
### Phase-Based Approach (Enum of Structs)
```rust
/// What the Node stores per peer slot
enum PeerSlot {
Connecting(Box<PeerConnection>),
Active(Box<ActivePeer>),
}
/// Handles authentication handshake only
struct PeerConnection {
link_id: LinkId,
direction: LinkDirection,
// Handshake-specific state
handshake_state: HandshakeState,
expected_identity: Option<PeerIdentity>,
noise_handshake: Option<noise::HandshakeState>,
noise_session: Option<NoiseSession>,
// Timing
started_at: u64,
last_activity: u64,
}
/// Fully authenticated peer
struct ActivePeer {
identity: PeerIdentity,
link_id: LinkId,
connectivity: ConnectivityState,
noise_session: Option<NoiseSession>,
// Routing state
declaration: Option<ParentDeclaration>,
ancestry: Option<TreeCoordinate>,
inbound_filter: Option<BloomFilter>,
last_seen: u64,
}
```
**Benefits:**
- Each struct only contains fields relevant to that phase
- Methods can't be called in wrong state (compile-time safety)
- Noise handshake state automatically dropped when `PeerConnection``ActivePeer`
- Each phase struct is smaller, simpler, independently testable
## Transition Pattern
In FIPS, connections and peers are stored in separate maps rather than a
unified `PeerSlot` map (though `PeerSlot` exists for cases where either phase
is needed). PeerConnection uses `&mut self` methods for handshake steps, and
the Node orchestrates promotion when the handshake completes:
```rust
impl PeerConnection {
/// Start handshake as initiator, returns msg1 to send.
fn start_handshake(&mut self, our_keypair: Keypair, current_time_ms: u64)
-> Result<Vec<u8>, NoiseError>;
/// Process incoming msg1 (responder), returns msg2 to send.
fn receive_handshake_init(&mut self, our_keypair: Keypair, message: &[u8],
current_time_ms: u64) -> Result<Vec<u8>, NoiseError>;
/// Complete handshake by processing msg2 (initiator).
fn complete_handshake(&mut self, message: &[u8], current_time_ms: u64)
-> Result<(), NoiseError>;
/// Take the completed NoiseSession for use in ActivePeer.
fn take_session(&mut self) -> Option<NoiseSession>;
}
/// Result of promoting a connection to active peer.
enum PromotionResult {
/// New peer created successfully.
Promoted(NodeAddr),
/// Cross-connection detected, this connection lost tie-breaker.
CrossConnectionLost { winner_link_id: LinkId },
/// Cross-connection detected, this connection won, old one replaced.
CrossConnectionWon { loser_link_id: LinkId, node_addr: NodeAddr },
}
```
The Node promotes a completed connection by moving data between maps:
```rust
// In the Node's handshake completion handler:
// 1. Remove from connections map
let conn = self.connections.remove(&link_id).unwrap();
// 2. Extract session and identity from completed connection
let session = conn.take_session().unwrap();
let identity = conn.expected_identity().unwrap().clone();
// 3. Create active peer with the session
let peer = ActivePeer::with_session(
identity, link_id, current_time_ms,
session, our_index, their_index,
transport_id, source_addr, conn.link_stats().clone(),
);
// 4. Insert into peers map (checking for cross-connections)
self.peers.insert(*peer.node_addr(), peer);
```
## Timeout Handling
Each phase struct tracks its own timing using Unix milliseconds (`u64`).
PeerConnection provides a simple boolean timeout check:
```rust
impl PeerConnection {
/// Check if the connection has timed out.
fn is_timed_out(&self, current_time_ms: u64, timeout_ms: u64) -> bool {
self.idle_time(current_time_ms) > timeout_ms
}
/// Time since last activity.
fn idle_time(&self, current_time_ms: u64) -> u64 {
current_time_ms.saturating_sub(self.last_activity)
}
}
```
The Node's event loop periodically scans the connections map for timeouts:
```rust
// In the Node's periodic maintenance:
let now_ms = current_time_ms();
let mut timed_out = vec![];
for (link_id, conn) in &self.connections {
if conn.is_timed_out(now_ms, HANDSHAKE_TIMEOUT_MS) {
timed_out.push(*link_id);
}
}
for link_id in timed_out {
self.connections.remove(&link_id);
// Clean up link, free index, etc.
}
```
## Application in FIPS
### Peer Lifecycle
```text
PeerSlot::Connecting(PeerConnection)
│ Noise IK handshake (2 messages)
PeerSlot::Active(ActivePeer)
│ Disconnect message (0x50) / link failure / timeout
[removed from peers map, index freed, link cleaned up]
```
**PeerConnection** contains:
- Noise IK handshake state (`noise_handshake`, `handshake_state`)
- Expected identity (known for outbound, learned for inbound)
- Direction (`LinkDirection::Inbound` vs `LinkDirection::Outbound`)
- Session index tracking (`our_index`, `their_index`)
- Timing (`started_at`, `last_activity` in Unix milliseconds)
**ActivePeer** contains:
- `NoiseSession` (symmetric keys for encrypt/decrypt)
- `ConnectivityState` (Connected, Stale, Reconnecting, Disconnected)
- Tree position (`declaration`, `ancestry`)
- Bloom filter (`inbound_filter`, with sequence tracking)
- Session indices and transport address (for wire protocol dispatch)
- Statistics (`last_seen`, `link_stats`, `authenticated_at`)
### End-to-End Session Lifecycle (FSP)
```text
EndToEndState::Initiating(HandshakeState)
│ Noise XK msg1 sent (SessionSetup)
│ Awaiting msg2 (SessionAck)
│ msg2 received, processed
│ msg3 sent (SessionMsg3)
EndToEndState::Established(NoiseSession)
```
```text
[msg1 received from remote initiator]
│ Noise XK msg1 processed, msg2 sent (SessionAck)
EndToEndState::AwaitingMsg3(HandshakeState)
│ msg3 received, processed
EndToEndState::Established(NoiseSession)
```
**EndToEndState** is an enum with three variants:
- **Initiating(HandshakeState)** — We initiated the session, sent
SessionSetup with XK msg1, awaiting SessionAck (msg2)
- **AwaitingMsg3(HandshakeState)** — We are the XK responder, processed
msg1, sent msg2, awaiting msg3 from initiator
- **Established(NoiseSession)** — Handshake complete, symmetric session
keys derived, ready for application data
The three-message flow (XK pattern) means the responder has an intermediate
`AwaitingMsg3` state that does not exist in the link-layer IK handshake
(which completes in two messages). The initiator transitions directly from
Initiating to Established upon processing msg2 and sending msg3.
Each side's epoch (8-byte random startup value) is exchanged encrypted in
msg2 and msg3. Epoch mismatch on subsequent handshakes indicates peer
restart.
### Link Lifecycle (Connection-Oriented Transports)
Links use a simple state enum rather than phase-based structs, since link
state transitions are straightforward:
```rust
enum LinkState {
Connecting, // Connection in progress (connection-oriented only)
Connected, // Ready for traffic
Disconnected, // Was connected, now gone
Failed, // Connection attempt failed
}
struct Link {
link_id: LinkId,
transport_id: TransportId,
remote_addr: TransportAddr,
direction: LinkDirection,
state: LinkState,
stats: LinkStats,
// ...
}
```
For connectionless transports (UDP), links are immediately `Connected` -
no `Connecting` phase needed. A phase-based approach (separate structs for
connecting vs established) would be valuable for transports with complex
connection setup like Tor circuit building.
### Node Lifecycle
```rust
enum NodeState {
Created,
Starting,
Running,
Stopping,
Stopped,
}
```
The Node uses a simple state enum because startup/shutdown are brief and
don't need complex per-phase logic. A phase-based approach (separate structs
per phase) would be useful if startup involved multi-step async operations
with retries.
### Transport Lifecycle
```rust
enum TransportState {
Configured,
Starting,
Up,
Down,
Failed,
}
```
Like `NodeState`, this uses a simple state enum because transport startup is
straightforward. A phase-based approach would be valuable for transports with
complex initialization (e.g., Tor bootstrap with multi-step circuit building).
## When to Use This Pattern
**Use phase-based structs when:**
- Different phases have different fields (auth secrets vs session keys)
- Phase-specific logic is complex enough to benefit from isolation
- Security-sensitive data should be dropped after phase completion
- You want compile-time enforcement of valid operations per phase
**Use simple state enum when:**
- All phases share the same fields
- Phase transitions are simple (just flip a flag)
- The struct is small and phase logic is trivial
## Lookup Tables
Rather than a unified `PeerSlot` map, the Node stores connections and peers
in separate maps optimized for their phase:
```rust
struct Node {
// Handshake phase: indexed by LinkId (identity not yet known)
connections: HashMap<LinkId, PeerConnection>,
// Active phase: indexed by NodeAddr (verified identity)
peers: HashMap<NodeAddr, ActivePeer>,
// Reverse lookup: (transport_id, remote_addr) → LinkId
addr_to_link: HashMap<AddrKey, LinkId>,
// Wire protocol dispatch: (transport_id, our_index) → NodeAddr
peers_by_index: HashMap<(TransportId, u32), NodeAddr>,
}
```
For inbound connections from unknown addresses:
1. Receive Noise IK msg1 → decrypt to extract sender's static key (identity)
2. Create new PeerConnection with discovered identity
3. Add to `connections` (by LinkId) and `addr_to_link`
4. After handshake completes, promote to `peers` (indexed by NodeAddr) and
`peers_by_index` (for session index dispatch)
## Summary
The phase-based state machine pattern provides:
1. **Type safety** - Can't call auth methods on active peer
2. **Memory efficiency** - Phase-specific data dropped on transition
3. **Clarity** - Each struct is focused and comprehensible
4. **Security** - Handshake state (ephemeral keys) dropped after auth
5. **Testability** - Each phase testable in isolation
The cost is slightly more complex transition handling in the event loop,
but this is offset by simpler per-phase logic.

View File

@@ -16,7 +16,7 @@ to the FIPS Mesh Protocol (FMP) above.
The transport layer deals exclusively in **transport addresses** — IP:port
tuples, 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 handshake completes.
identity happens at the link layer after the Noise IK 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 handshake until the transport-layer connection is established.
the Noise IK link handshake until the transport-layer connection is established.
Connectionless transports (UDP, raw Ethernet) skip this — datagrams can flow
immediately to any reachable address.
@@ -133,7 +133,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 handshake,
require connection setup before FMP can begin the Noise IK link handshake,
adding startup latency.
**Stream vs. datagram**: Datagram transports have natural packet boundaries.
@@ -422,5 +422,3 @@ quality difference is significant. Link cost is not yet used in
- [fips-intro.md](fips-intro.md) — Protocol overview and layer architecture
- [fips-mesh-layer.md](fips-mesh-layer.md) — FMP specification (the layer above)
- [fips-wire-formats.md](fips-wire-formats.md) — Transport framing details
- [fips-software-architecture.md](fips-software-architecture.md) — Transport
trait implementation details

View File

@@ -17,18 +17,11 @@ how application data is wrapped through each layer.
## Transport Framing
### UDP
FIPS packets are carried directly in UDP datagrams. No additional framing is
needed — each UDP datagram contains exactly one FIPS link-layer packet.
### Stream Transports *(future direction)*
TCP, WebSocket, and Tor transports provide a byte stream, not datagrams. The
common prefix `payload_len` field provides integrated stream framing — the
receiver reads the 4-byte common prefix, then reads exactly the number of
bytes indicated by `payload_len` (plus any phase-specific header and AEAD
tag). No separate length prefix is needed.
Datagram-oriented transports (UDP, raw Ethernet, radio) preserve natural
packet boundaries and require no additional framing. Stream-oriented
transports (TCP, WebSocket, Tor) must delineate FIPS packets within the
byte stream; the common prefix `payload_len` field provides this
framing directly.
## Link-Layer Formats
@@ -38,12 +31,7 @@ length.
### Common Prefix (4 bytes)
```text
┌──────────────────────┬───────────┬───────────────┐
│ ver(4) + phase(4) │ flags │ payload_len │
│ 1 byte │ 1 byte │ 2 bytes LE │
└──────────────────────┴───────────┴───────────────┘
```
![Common Prefix](diagrams/common-prefix.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
@@ -78,12 +66,7 @@ encrypted link-layer message.
**Outer header** (16 bytes, used as AEAD AAD):
```text
┌──────────────────────┬───────────┬───────────────┬──────────────┬──────────┐
│ ver(4) + phase(4) │ flags │ payload_len │ receiver_idx │ counter │
│ 1 byte │ 1 byte │ 2 bytes LE │ 4 bytes LE │ 8 bytes LE│
└──────────────────────┴───────────┴───────────────┴──────────────┴──────────┘
```
![Established frame outer header](diagrams/established-outer-header.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
@@ -96,12 +79,7 @@ ChaCha20-Poly1305 AEAD construction.
**Encrypted inner header** (5 bytes, first bytes of plaintext):
```text
┌───────────────┬──────────┐
│ timestamp │ msg_type │
│ 4 bytes LE │ 1 byte │
└───────────────┴──────────┘
```
![Established frame inner header](diagrams/established-inner-header.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
@@ -113,15 +91,7 @@ the 1-byte message type and message-specific fields.
**Complete encrypted frame**:
```text
┌──────────────────────────────────────┬───────────────────────────┐
│ outer header (16 bytes, used as AAD) │ ciphertext + AEAD tag │
│ │ (inner_hdr + body) + 16 │
└──────────────────────────────────────┴───────────────────────────┘
Total overhead: 37 bytes (16 outer + 5 inner + 16 AEAD tag)
Minimum frame: 37 bytes (empty body)
```
![Complete encrypted frame](diagrams/established-complete-frame.svg)
### Message Type Table
@@ -141,14 +111,7 @@ Minimum frame: 37 bytes (empty body)
Handshake initiation from connecting party.
```text
┌──────────────────────┬─────────────┬─────────────────────────────────────────┐
│ common prefix │ sender_idx │ Noise IK message 1 │
│ 4 bytes │ 4 bytes LE │ 106 bytes │
└──────────────────────┴─────────────┴─────────────────────────────────────────┘
Total: 114 bytes
```
![Noise IK message 1](diagrams/noise-ik-msg1.svg)
Common prefix: ver=0, phase=0x1, flags=0, payload_len=110 (4 + 106).
@@ -172,14 +135,7 @@ Noise pattern: `-> e, es, s, ss` with epoch payload
Handshake response from responder.
```text
┌──────────────────────┬─────────────┬──────────────┬──────────────────────────┐
│ common prefix │ sender_idx │ receiver_idx │ Noise IK message 2 │
│ 4 bytes │ 4 bytes LE │ 4 bytes LE │ 57 bytes │
└──────────────────────┴─────────────┴──────────────┴──────────────────────────┘
Total: 69 bytes
```
![Noise IK message 2](diagrams/noise-ik-msg2.svg)
Common prefix: ver=0, phase=0x2, flags=0, payload_len=65 (4 + 4 + 57).
@@ -215,32 +171,7 @@ Each party in a link session maintains two indices:
### Handshake Flow
```text
Initiator Responder
--------- ---------
generates sender_idx
generates ephemeral keypair
[0x01|flags=0|len] | sender_idx | noise_msg1
------------------------------------------------>
validates msg1
learns initiator's static key
generates sender_idx
generates ephemeral keypair
[0x02|flags=0|len] | sender_idx | receiver_idx | noise_msg2
<------------------------------------------------
validates msg2
derives session keys
=============== HANDSHAKE COMPLETE ===============
First encrypted frame:
[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag
------------------------------------------------>
```
![Handshake flow](diagrams/handshake-flow.svg)
## Link-Layer Message Types
@@ -253,6 +184,8 @@ message-specific fields.
Spanning tree state announcement, exchanged between direct peers only.
![TreeAnnounce](diagrams/tree-announce.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x10 |
@@ -266,6 +199,8 @@ Spanning tree state announcement, exchanged between direct peers only.
**AncestryEntry** (32 bytes):
![AncestryEntry](diagrams/ancestry-entry.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | node_addr | 16 bytes | Node's routing identifier |
@@ -286,6 +221,8 @@ includes self)
Bloom filter reachability update, exchanged between direct peers only.
![FilterAnnounce](diagrams/filter-announce.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x20 |
@@ -310,6 +247,8 @@ With link overhead: 1,072 bytes.
Coordinate discovery request, flooded through the mesh.
![LookupRequest](diagrams/lookup-request.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x30 |
@@ -335,6 +274,8 @@ Coordinate discovery request, flooded through the mesh.
Coordinate discovery response, greedy-routed back to requester.
![LookupResponse](diagrams/lookup-response.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x31 |
@@ -367,6 +308,8 @@ The source verifies the proof upon receipt.
Encapsulated session-layer payload for multi-hop forwarding.
![SessionDatagram](diagrams/session-datagram.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x00 |
@@ -389,6 +332,8 @@ independently of link encryption.
Orderly link teardown with reason code.
![Disconnect](diagrams/disconnect.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x50 |
@@ -412,6 +357,8 @@ Orderly link teardown with reason code.
Sent by the frame sender to provide interval-based transmission statistics.
![SenderReport](diagrams/sender-report.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x01` |
@@ -430,6 +377,8 @@ Sent by the frame sender to provide interval-based transmission statistics.
Sent by the frame receiver to provide loss, jitter, and timing feedback.
![ReceiverReport](diagrams/receiver-report.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x02` |
@@ -524,12 +473,7 @@ body.
**Complete encrypted message**:
```text
┌─────────────────────────────────┬─────────────────┬───────────────────────────┐
│ header (12 bytes, used as AAD) │ [coords if CP] │ ciphertext + AEAD tag │
│ │ │ (inner_hdr + body) + 16 │
└─────────────────────────────────┴─────────────────┴───────────────────────────┘
```
![FSP complete encrypted message](diagrams/fsp-complete-message.svg)
### FSP Session Message Types
@@ -558,8 +502,16 @@ happens at the session level based on the FSP message type.
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).
SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
field in the FSP common prefix (0x1, 0x2, 0x3), not by a message type
byte. The `msg_type` field in the encrypted inner header applies only to
established-phase (0x0) messages.
Encoded with FSP prefix: ver=0, phase=0x1, flags=0, payload_len.
![SessionSetup](diagrams/session-setup.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
@@ -578,6 +530,8 @@ Second message of the Noise XK handshake. The responder sends its
ephemeral key and encrypted epoch.
Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
![SessionAck](diagrams/session-ack.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
@@ -597,6 +551,8 @@ its encrypted static identity and epoch. 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.
![SessionMsg3](diagrams/session-msg3.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
@@ -625,6 +581,8 @@ The body after the inner header is delivered directly to the TUN interface.
Sent by the destination to report the observed forward-path MTU.
![PathMtuNotification](diagrams/path-mtu-notification.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | path_mtu | 2 | u16 LE — minimum MTU observed along the forward path |
@@ -662,50 +620,61 @@ on the wire.
### CoordsRequired (0x20)
Plaintext error signal — transit node lacks coordinates for destination.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
Identified by the FSP phase field, not a separate message type byte
(same as SessionSetup/SessionAck/SessionMsg3).
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len=34.
**Body** (after 4-byte FSP prefix + 1-byte msg_type):
![CoordsRequired](diagrams/coords-required.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | dest_addr | 16 bytes | NodeAddr we couldn't route to |
| 17 | reporter | 16 bytes | NodeAddr of reporting router |
| 0 | msg_type | 1 byte | 0x20 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | NodeAddr we couldn't route to |
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
**Body size**: 33 bytes. Total with prefix + msg_type: 38 bytes.
**Payload size**: 34 bytes. **Total on wire**: 38 bytes (4 prefix + 34 payload).
### PathBroken (0x21)
Plaintext error signal — greedy routing reached a dead end.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
**Body** (after 4-byte FSP prefix + 1-byte msg_type):
![PathBroken](diagrams/path-broken.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | dest_addr | 16 bytes | Unreachable NodeAddr |
| 17 | reporter | 16 bytes | NodeAddr of reporting router |
| 33 | last_coords_count | 2 bytes LE | Number of stale coordinate entries |
| 35 | last_known_coords | 16 x n bytes | Stale coordinates that failed |
| 0 | msg_type | 1 byte | 0x21 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | Unreachable NodeAddr |
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
| 34 | last_coords_count | 2 bytes LE | Number of stale coordinate entries |
| 36 | last_known_coords | 16 x n bytes | Stale coordinates that failed |
### MtuExceeded (0x22)
Plaintext error signal — forwarded packet exceeds the next-hop link MTU.
Sent by a transit router back to the source when a SessionDatagram cannot
be forwarded because its size exceeds the outgoing link's MTU.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len=36.
**Body** (after 4-byte FSP prefix + 1-byte msg_type):
![MtuExceeded](diagrams/mtu-exceeded.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | dest_addr | 16 bytes | NodeAddr of the destination being forwarded to |
| 17 | reporter | 16 bytes | NodeAddr of the router that detected the MTU violation |
| 33 | mtu | 2 bytes LE | Bottleneck MTU at the reporting router |
| 0 | msg_type | 1 byte | 0x22 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | NodeAddr of the destination being forwarded to |
| 18 | reporter | 16 bytes | NodeAddr of the router that detected the MTU violation |
| 34 | mtu | 2 bytes LE | Bottleneck MTU at the reporting router |
**Body size**: 35 bytes. Total with prefix + msg_type: 40 bytes.
**Payload size**: 36 bytes. **Total on wire**: 40 bytes (4 prefix + 36 payload).
The source uses the reported MTU to adjust its session-layer path MTU
estimate. MtuExceeded is the reactive complement to the proactive

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
//! ## v1 Parameters
//!
//! - Size: 1 KB (8,192 bits) - sized for actual ~400-800 entry occupancy
//! - Hash functions: k=5 - optimal for 800-1,600 entries at 1KB
//! - Hash functions: k=5 - optimal at ~1,200 entries, good for 800-1,600
//! - Bandwidth: 1 KB/announce (75% reduction from original 4KB design)
//!
//! These parameters are right-sized for typical network occupancy of
@@ -24,7 +24,7 @@ pub use state::BloomState;
/// Default filter size in bits (1KB = 8,192 bits).
///
/// Sized for ~800-1,600 entries with <5% FPR at typical occupancy (~400 entries).
/// Sized for ~800-1,600 entries. FPR ~0.05% at 400 entries, ~0.9% at 800.
/// This is v1 protocol default (size_class=1).
pub const DEFAULT_FILTER_SIZE_BITS: usize = 8192;
@@ -33,8 +33,8 @@ pub const DEFAULT_FILTER_SIZE_BYTES: usize = DEFAULT_FILTER_SIZE_BITS / 8;
/// Default number of hash functions.
///
/// k=5 is optimal for 800-1,600 entries at 1KB filter size.
/// At 400 entries: FPR ~0.3%. At 800 entries: FPR ~2.4%.
/// k=5 is optimal at ~1,200 entries and a good compromise for 800-1,600.
/// At 400 entries: FPR ~0.05%. At 800 entries: FPR ~0.9%.
pub const DEFAULT_HASH_COUNT: u8 = 5;
/// Size class for v1 protocol (1 KB filters).