Add detailed packet layout diagrams to protocol documentation

Add comprehensive wire format diagrams with byte offsets, field sizes,
and concrete examples to three protocol documents:

fips-wire-protocol.md (Appendix A):
- Encrypted frame (0x00) with plaintext structure breakdown
- Noise IK msg1/msg2 with payload breakdown
- Complete handshake flow diagram

fips-gossip-protocol.md (Appendix A):
- TreeAnnounce with ancestry entry structure and size table
- FilterAnnounce with bloom filter layout
- LookupRequest/LookupResponse with examples
- Message flow showing packet nesting

fips-session-protocol.md (§8):
- SessionSetup, SessionAck, DataPacket, CoordsRequired, PathBroken
- Full nested packet example showing link + session layers

fips-routing.md:
- Refactored Part 4 to "Route Cache Management"
- Moved wire format content to fips-session-protocol.md
This commit is contained in:
Johnathan Corgan
2026-02-02 14:19:04 +00:00
parent 7dc0b7fc52
commit 021bf69d73
4 changed files with 762 additions and 82 deletions

View File

@@ -368,6 +368,314 @@ Vec<T> encoding:
---
## Appendix A: Detailed Packet Layouts
All gossip messages are link-layer messages carried inside encrypted frames
(discriminator 0x00). The layouts below show the plaintext structure after
link-layer decryption.
### A.1 TreeAnnounce (0x10)
Propagates spanning tree state between directly connected peers.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ FULL PACKET (Link Layer + TreeAnnounce) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ LINK LAYER FRAME (encrypted) │ │
│ ├───────────┬──────────────┬────────────┬───────────────────────────────┤ │
│ │ 0x00 │ receiver_idx │ counter │ ciphertext + tag │ │
│ │ 1 byte │ 4 bytes LE │ 8 bytes LE │ N + 16 bytes │ │
│ └───────────┴──────────────┴────────────┴───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┘ │
│ │ Decrypt │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ TREE ANNOUNCE (plaintext) │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x10 │ │
│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │
│ │ 9 │ timestamp │ 8 bytes │ u64 LE, Unix seconds │ │
│ │ 17 │ parent │ 32 bytes │ NodeId of selected parent │ │
│ │ 49 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │
│ │ 51 │ ancestry[0..n] │ 112 × n │ AncestryEntry array │ │
│ │ ... │ signature │ 64 bytes │ Schnorr sig over all above │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ANCESTRY ENTRY (112 bytes each) │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ node_id │ 32 bytes │ SHA-256(npub) of this node │ │
│ │ 32 │ sequence │ 8 bytes │ u64 LE, node's seq number │ │
│ │ 40 │ timestamp │ 8 bytes │ u64 LE, node's timestamp │ │
│ │ 48 │ signature │ 64 bytes │ Node's sig over its decl │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 8 + 32 + 2 + (depth × 112) + 64` bytes
| Tree Depth | Payload Size | With Link Overhead |
|------------|--------------|-------------------|
| 1 (root) | 227 bytes | 256 bytes |
| 3 | 451 bytes | 480 bytes |
| 5 | 675 bytes | 704 bytes |
| 10 | 1235 bytes | 1264 bytes |
**Concrete example** (node D at depth 3, ancestry = [D, P1, P2, Root]):
```text
PLAINTEXT BYTES (hex layout):
10 ← msg_type = TreeAnnounce
05 00 00 00 00 00 00 00 ← sequence = 5
C3 B2 A1 67 00 00 00 00 ← timestamp (Unix seconds)
[32 bytes P1's node_id] ← parent
04 00 ← ancestry_count = 4
ANCESTRY[0] - Self (D):
[32 bytes D's node_id]
05 00 00 00 00 00 00 00 ← D's sequence
C3 B2 A1 67 00 00 00 00 ← D's timestamp
[64 bytes D's signature]
ANCESTRY[1] - Parent (P1):
[32 bytes P1's node_id]
0A 00 00 00 00 00 00 00 ← P1's sequence
00 B0 A1 67 00 00 00 00 ← P1's timestamp
[64 bytes P1's signature]
ANCESTRY[2] - Grandparent (P2):
[32 bytes P2's node_id]
03 00 00 00 00 00 00 00 ← P2's sequence
00 A0 A1 67 00 00 00 00 ← P2's timestamp
[64 bytes P2's signature]
ANCESTRY[3] - Root:
[32 bytes Root's node_id]
01 00 00 00 00 00 00 00 ← Root's sequence
00 90 A1 67 00 00 00 00 ← Root's timestamp
[64 bytes Root's signature]
[64 bytes D's outer signature] ← signs entire message
Total payload: 1 + 8 + 8 + 32 + 2 + (4 × 112) + 64 = 563 bytes
```
### A.2 FilterAnnounce (0x11)
Propagates Bloom filter reachability information.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ FILTER ANNOUNCE (0x11) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x11 │ │
│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │
│ │ 9 │ ttl │ 1 byte │ Remaining propagation hops │ │
│ │ 10 │ hash_count │ 1 byte │ Number of hash functions (7) │ │
│ │ 11 │ filter_bits │ 4096 bytes│ Bloom filter bit array │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Total payload: 4107 bytes (fixed size) │
│ With link overhead: 4136 bytes │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ BLOOM FILTER STRUCTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ filter_bits[4096]: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Byte 0 │ Byte 1 │ ... │ Byte 4095 │ │
│ │ bits 0-7 │ bits 8-15 │ │ bits 32760-32767 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ To test membership of node_id: │
│ for i in 0..hash_count: │
│ bit_index = hash(node_id, i) % 32768 │
│ if !filter_bits[bit_index]: return false │
│ return true // "maybe present" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Concrete example**:
```text
PLAINTEXT BYTES:
11 ← msg_type = FilterAnnounce
2A 00 00 00 00 00 00 00 ← sequence = 42
02 ← ttl = 2 (will propagate 2 more hops)
07 ← hash_count = 7
[4096 bytes of filter bits] ← Bloom filter
Total: 4107 bytes
```
### A.3 LookupRequest (0x12)
Discovers tree coordinates for distant destinations.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ LOOKUP REQUEST (0x12) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x12 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, unique identifier │ │
│ │ 9 │ target │ 32 bytes │ NodeId being searched for │ │
│ │ 41 │ origin │ 32 bytes │ NodeId of requester │ │
│ │ 73 │ ttl │ 1 byte │ Remaining propagation hops │ │
│ │ 74 │ origin_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 76 │ origin_coords │ 32 × n │ Requester's ancestry │ │
│ │ ... │ visited_hash_cnt │ 1 byte │ Hash functions for visited │ │
│ │ ... │ visited_bits │ 256 bytes │ Compact bloom of visited nodes│ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 32 + 32 + 1 + 2 + (depth × 32) + 1 + 256` bytes
| Origin Depth | Payload Size |
|--------------|--------------|
| 3 | 429 bytes |
| 5 | 493 bytes |
| 10 | 653 bytes |
**Concrete example** (origin at depth 4):
```text
PLAINTEXT BYTES:
12 ← msg_type = LookupRequest
[8 bytes request_id] ← random unique ID
[32 bytes target node_id] ← who we're looking for
[32 bytes origin node_id] ← who's asking
08 ← ttl = 8
04 00 ← origin_coords_count = 4
[32 bytes] × 4 ← origin's ancestry (128 bytes)
07 ← visited hash_count = 7
[256 bytes visited bloom] ← nodes that have seen this request
Total: 1 + 8 + 32 + 32 + 1 + 2 + 128 + 1 + 256 = 461 bytes
```
### A.4 LookupResponse (0x13)
Returns target's coordinates to the requester.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ LOOKUP RESPONSE (0x13) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x13 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, echoes request │ │
│ │ 9 │ target │ 32 bytes │ NodeId that was found │ │
│ │ 41 │ target_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 43 │ target_coords │ 32 × n │ Target's ancestry to root │ │
│ │ ... │ proof │ 64 bytes │ Target's signature │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Proof signature covers: (request_id || target || target_coords) │
│ Prevents malicious nodes from claiming reachability for any target. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 32 + 2 + (depth × 32) + 64` bytes
| Target Depth | Payload Size |
|--------------|--------------|
| 3 | 203 bytes |
| 5 | 267 bytes |
| 10 | 427 bytes |
**Concrete example** (target at depth 5):
```text
PLAINTEXT BYTES:
13 ← msg_type = LookupResponse
[8 bytes request_id] ← echoed from request
[32 bytes target node_id] ← confirms who was found
05 00 ← target_coords_count = 5
[32 bytes] × 5 ← target's ancestry (160 bytes)
[64 bytes proof signature] ← target signs to prove existence
Total: 1 + 8 + 32 + 2 + 160 + 64 = 267 bytes
```
### A.5 Message Flow Example
Complete lookup flow showing packet nesting:
```text
Source S wants to reach distant destination D (not in local filters)
1. S creates LookupRequest, sends to peer P1:
UDP DATAGRAM
┌──────────────────────────────────────────────────────────────┐
│ LINK FRAME (S→P1 encrypted) │
│ ┌──────┬────────────┬─────────┬─────────────────────────────┐│
│ │ 0x00 │ P1_recv_idx│ counter │ ciphertext + tag ││
│ └──────┴────────────┴─────────┴─────────────────────────────┘│
│ │ │
│ ┌───────────┘ │
│ ▼ │
│ ┌──────┬───────────────────────────────────┐ │
│ │ 0x12 │ LookupRequest payload │ │
│ │ │ (target=D, origin=S, ttl=8, ...) │ │
│ └──────┴───────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
2. Request propagates through network, reaches D
3. D creates LookupResponse, routes back via greedy routing:
UDP DATAGRAM
┌──────────────────────────────────────────────────────────────┐
│ LINK FRAME (D→Pn encrypted) │
│ ┌──────┬────────────┬─────────┬─────────────────────────────┐│
│ │ 0x00 │ Pn_recv_idx│ counter │ ciphertext + tag ││
│ └──────┴────────────┴─────────┴─────────────────────────────┘│
│ │ │
│ ┌───────────┘ │
│ ▼ │
│ ┌──────┬───────────────────────────────────┐ │
│ │ 0x13 │ LookupResponse payload │ │
│ │ │ (target=D, coords=[D,P1,P2,Root]) │ │
│ └──────┴───────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
4. S receives response, caches D's coordinates, can now route directly
```
---
## References
- [spanning-tree-dynamics.md](spanning-tree-dynamics.md) - Tree protocol behavior

View File

@@ -293,20 +293,19 @@ No global routing tables. Each node makes purely local decisions.
---
## Part 4: Routing Session Establishment
## Part 4: Route Cache Management
> **Terminology note**: This section describes *routing sessions*—hop-by-hop
> cached state at intermediate routers. FIPS also has *crypto sessions*—end-to-end
> authenticated encryption between source and destination. See
> [fips-session-protocol.md](fips-session-protocol.md) §3 for crypto session details
> and §5 for route cache warming.
> **Wire formats**: For session layer message wire formats (SessionSetup,
> SessionAck, DataPacket, CoordsRequired, PathBroken), see
> [fips-session-protocol.md](fips-session-protocol.md) §8.
### Routing Session Purpose
### Route Cache Purpose
Establish cached coordinate state along a path so that subsequent data packets
can omit coordinates, minimizing per-packet overhead.
Intermediate routers cache coordinate mappings so that data packets can use
minimal headers (addresses only, no coordinates). This reduces per-packet
overhead from ~300 bytes to 38 bytes.
### Routing Session Lifecycle
### Cache Lifecycle
```text
┌─────────────────────────────────────────────────────────────────┐
@@ -318,74 +317,7 @@ can omit coordinates, minimizing per-packet overhead.
└─────────────────────────────────────────────────────────────────┘
```
### Routing Session Message Formats
```rust
/// Establishes cached state along path; optionally carries crypto handshake
struct SessionSetup {
src_addr: Ipv6Addr,
dest_addr: Ipv6Addr,
src_coords: Vec<NodeId>, // For return path caching
dest_coords: Vec<NodeId>, // For forward path routing
flags: SessionFlags,
// Crypto session establishment (see fips-session-protocol.md §6)
// Opaque to routers; only processed by destination
handshake_payload: Option<Vec<u8>>, // Noise IK message 1
}
struct SessionFlags {
request_ack: bool, // Ask destination to confirm
bidirectional: bool, // Set up both directions
}
/// Confirms session establishment; optionally carries crypto response
struct SessionAck {
src_addr: Ipv6Addr,
dest_addr: Ipv6Addr,
src_coords: Vec<NodeId>, // Acknowledger's coords (for return caching)
// Crypto session response (see fips-session-protocol.md §6)
handshake_payload: Option<Vec<u8>>, // Noise IK message 2
}
/// Data packet with optional coordinates
struct DataPacket {
flags: u8, // Bit 0: COORDS_PRESENT
hop_limit: u8,
payload_length: u16,
src_addr: Ipv6Addr, // 16 bytes
dest_addr: Ipv6Addr, // 16 bytes
// Optional: present only if COORDS_PRESENT flag is set
src_coords: Option<Vec<NodeId>>,
dest_coords: Option<Vec<NodeId>>,
payload: Vec<u8>,
}
/// Error when router cannot route (cache miss)
struct CoordsRequired {
dest_addr: Ipv6Addr,
reporter: NodeId, // Which router had the miss
}
```
### Data Packet Overhead
| Field | Size |
|-------|------|
| flags | 1 byte |
| hop_limit | 1 byte |
| payload_length | 2 bytes |
| src_addr | 16 bytes |
| dest_addr | 16 bytes |
| **Minimal header** | **36 bytes** |
Comparable to IPv6 (40 bytes). When COORDS_PRESENT flag is set, coordinates
add variable overhead based on tree depth (typically 200-400 bytes).
### Routing Session Setup Flow
### Session Setup Flow
```text
S R1 R2 D
@@ -460,7 +392,7 @@ impl Router {
}
```
### Cache Management
### Cache Data Structure
```rust
struct CoordCache {
@@ -485,7 +417,7 @@ by:
- SessionRefresh message (lightweight, just touches expiry)
- Data packet transit (optional: refresh on use)
### Handling Cache Eviction
### Cache Miss Recovery
When a router's cache entry is evicted mid-session:
@@ -501,7 +433,7 @@ When a router's cache entry is evicted mid-session:
The crypto session remains active throughout—only routing state is refreshed.
From application perspective: one packet delayed, transparent recovery.
### Sender Behavior
### Sender State Machine
```rust
impl Sender {

View File

@@ -495,3 +495,228 @@ After successful Noise IK handshake:
The first TreeAnnounce from a new peer may trigger parent reselection if that
peer offers a better path to root. See [fips-gossip-protocol.md](fips-gossip-protocol.md)
for TreeAnnounce and FilterAnnounce wire formats.
---
## 8. Session Layer Wire Format
Session layer messages are carried inside `SessionDatagram` (type 0x40) at the
link layer. The session datagram is encrypted hop-by-hop with link keys, but
the inner payload is encrypted end-to-end with session keys.
### 8.1 Message Type Codes
| Type Code | Message | Direction | Purpose |
|-----------|----------------|-----------|-----------------------------------|
| 0x00 | SessionSetup | S → D | Establish session + warm caches |
| 0x01 | SessionAck | D → S | Confirm session establishment |
| 0x10 | DataPacket | Both | Application data |
| 0x20 | CoordsRequired | R → S | Router cache miss |
| 0x21 | PathBroken | R → S | Greedy routing failed |
### 8.2 SessionSetup (0x00)
Establishes a crypto session and warms router coordinate caches along the path.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ SESSION SETUP PACKET │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x00 │
│ 1 │ flags │ 1 byte │ Bit 0: REQUEST_ACK │
│ │ │ │ Bit 1: BIDIRECTIONAL │
│ 2 │ src_addr │ 16 bytes │ Source fd::/8 address │
│ 18 │ dest_addr │ 16 bytes │ Destination fd::/8 address │
│ 34 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
│ 36 │ src_coords │ 32 × n │ NodeId array (self → root) │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE, number of dest coord entries│
│ ... │ dest_coords │ 32 × m │ NodeId array (dest → root) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg1 (82 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
```
**Example** (depth 3 source, depth 4 destination, with Noise handshake):
```text
┌──────┬───────┬──────────────────┬──────────────────┬───────┬─────────────┐
│ 0x00 │ 0x01 │ src_addr │ dest_addr │ 0x03 │ src_coords │
│ type │ flags │ 16 bytes │ 16 bytes │ count │ 3 × 32 bytes│
├──────┴───────┴──────────────────┴──────────────────┴───────┴─────────────┤
│ 0x04 │ dest_coords │ 0x52 │ handshake_payload │
│ count │ 4 × 32 bytes │ len=82│ 82 bytes (Noise IK msg1) │
└───────┴───────────────┴───────┴──────────────────────────────────────────┘
Total: 1 + 1 + 16 + 16 + 2 + 96 + 2 + 128 + 2 + 82 = 346 bytes
```
### 8.3 SessionAck (0x01)
Confirms session establishment and completes the Noise handshake.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ SESSION ACK PACKET │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x01 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ src_addr │ 16 bytes │ Acknowledger's address │
│ 18 │ dest_addr │ 16 bytes │ Original sender's address │
│ 34 │ src_coords_count │ 2 bytes │ u16 LE │
│ 36 │ src_coords │ 32 × n │ Acknowledger's coords (for caching) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg2 (33 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
```
### 8.4 DataPacket (0x10)
Carries encrypted application data (typically IPv6 payloads).
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA PACKET (Minimal Header) │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x10 │
│ 1 │ flags │ 1 byte │ Bit 0: COORDS_PRESENT │
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address │
│ 22 │ dest_addr │ 16 bytes │ Destination fd::/8 address │
│ 38 │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Minimal header: 38 bytes (comparable to IPv6's 40 bytes)
```
When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA PACKET (With Coordinates) │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x10 │
│ 1 │ flags │ 1 byte │ 0x01 (COORDS_PRESENT) │
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address │
│ 22 │ dest_addr │ 16 bytes │ Destination fd::/8 address │
│ 38 │ src_coords_count │ 2 bytes │ u16 LE │
│ 40 │ src_coords │ 32 × n │ Source coordinates │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE │
│ ... │ dest_coords │ 32 × m │ Destination coordinates │
│ ... │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
With depth-4 coords both directions: 38 + 2 + 128 + 2 + 128 = 298 bytes header
```
### 8.5 CoordsRequired (0x20)
Sent by an intermediate router when it cannot forward a DataPacket due to
coordinate cache miss.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ COORDS REQUIRED PACKET │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x20 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 16 bytes │ The address we couldn't route │
│ 18 │ reporter │ 32 bytes │ NodeId of reporting router │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Total: 50 bytes
```
### 8.6 PathBroken (0x21)
Sent when greedy routing fails (no peer is closer to destination).
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATH BROKEN PACKET │
├────────┬──────────────────┬───────────┬─────────────────────────────────────┤
│ Offset │ Field │ Size │ Description │
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x21 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 16 bytes │ The unreachable destination │
│ 18 │ reporter │ 32 bytes │ NodeId of reporting router │
│ 50 │ last_coords_count│ 2 bytes │ u16 LE │
│ 52 │ last_known_coords│ 32 × n │ Stale coords that failed │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
```
### 8.7 Full Packet Layout Example
A DataPacket from source S to destination D, transiting router R:
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ UDP DATAGRAM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ FIPS LINK LAYER (S→R encrypted) │ │
│ ├───────────┬──────────────┬────────────┬───────────────────────────────┤ │
│ │ 0x00 │ R's recv_idx │ counter │ ciphertext + tag │ │
│ │ 1 byte │ 4 bytes LE │ 8 bytes LE │ N + 16 bytes │ │
│ └───────────┴──────────────┴────────────┴───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┘ │
│ │ Decrypt with S↔R link keys │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ LINK MESSAGE (plaintext for R) │ │
│ ├───────────┬───────────────────────────────────────────────────────────┤ │
│ │ 0x40 │ SessionDatagram payload │ │
│ │ msg_type │ (routable by R, encrypted end-to-end) │ │
│ └───────────┴───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ SESSION LAYER (S↔D encrypted) │ │
│ ├───────────┬───────┬──────────┬──────────┬─────────────────────────────┤ │
│ │ 0x10 │ flags │ hop_limit│ pay_len │ src_addr │ dest_addr │ │
│ │ DataPacket│ 0x00 │ 64 │ 1400 │ 16 bytes │ 16 bytes │ │
│ ├───────────┴───────┴──────────┴──────────┴──────────────┴──────────────┤ │
│ │ │ │
│ │ ENCRYPTED PAYLOAD (S↔D session keys) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ IPv6 packet or application data │ │ │
│ │ │ (+ 16-byte AEAD tag) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Router R can see: dest_addr (for routing decision)
Router R cannot see: payload contents (encrypted with S↔D keys)
```
### 8.8 Encoding Rules
- All multi-byte integers are **little-endian**
- NodeId is 32 bytes (SHA-256 hash of npub)
- IPv6 addresses are 16 bytes (network byte order)
- Variable-length coordinate arrays use 2-byte u16 count prefix
```text
Vec<NodeId> encoding:
count: u16 (little-endian)
items: [u8; 32] × count
```

View File

@@ -803,7 +803,222 @@ protocol layers apply additional policy.
---
## Appendix A: Message Size Summary
## Appendix A: Detailed Packet Layouts
### A.1 Encrypted Frame (0x00)
Post-handshake data packets between authenticated peers.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ ENCRYPTED FRAME (0x00) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ discriminator │ 1 byte │ 0x00 │ │
│ │ 1 │ receiver_idx │ 4 bytes │ u32 LE, receiver's session idx│ │
│ │ 5 │ counter │ 8 bytes │ u64 LE, monotonic nonce │ │
│ │ 13 │ ciphertext │ N bytes │ ChaCha20 encrypted payload │ │
│ │ 13+N │ tag │ 16 bytes │ Poly1305 auth tag │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Total overhead: 29 bytes (1 + 4 + 8 + 16) │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ PLAINTEXT STRUCTURE │
│ (after decryption) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ Link message type (see below) │ │
│ │ 1 │ payload │ variable │ Message-specific payload │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Link message types: │
│ 0x10 = TreeAnnounce 0x12 = LookupRequest │
│ 0x11 = FilterAnnounce 0x13 = LookupResponse │
│ 0x40 = SessionDatagram │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Concrete example** (TreeAnnounce inside encrypted frame):
```text
WIRE BYTES (hex):
00 ← discriminator
78 56 34 12 ← receiver_idx = 0x12345678 (LE)
2A 00 00 00 00 00 00 00 ← counter = 42 (LE)
[N bytes ciphertext] ← encrypted link message
[16 bytes tag] ← Poly1305 authentication tag
DECRYPTED PLAINTEXT:
10 ← msg_type = TreeAnnounce
[TreeAnnounce payload] ← see fips-gossip-protocol.md
```
### A.2 Noise IK Message 1 (0x01)
Handshake initiation from connecting party (initiator → responder).
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ NOISE IK MESSAGE 1 (0x01) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ discriminator │ 1 byte │ 0x01 │ │
│ │ 1 │ sender_idx │ 4 bytes │ u32 LE, initiator's session idx│ │
│ │ 5 │ noise_msg1 │ 82 bytes │ Noise IK first message │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Total: 87 bytes │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ NOISE MSG1 BREAKDOWN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ ephemeral_pubkey │ 33 bytes │ Initiator's ephemeral pubkey │ │
│ │ │ │ │ (compressed secp256k1) │ │
│ │ 33 │ encrypted_static │ 33 bytes │ Initiator's static pubkey │ │
│ │ │ │ │ (encrypted with es key) │ │
│ │ 66 │ tag │ 16 bytes │ AEAD tag for encrypted_static │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Noise pattern: -> e, es, s, ss │
│ - e: ephemeral pubkey sent in clear │
│ - es: DH(ephemeral, responder_static) → mix into key │
│ - s: static pubkey encrypted with current key │
│ - ss: DH(static, responder_static) → mix into key │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Concrete example**:
```text
WIRE BYTES (hex):
01 ← discriminator
78 56 34 12 ← sender_idx = 0x12345678 (LE)
02 [32 bytes] ← ephemeral pubkey (compressed, 02/03 prefix)
[33 bytes] ← encrypted static pubkey
[16 bytes] ← AEAD tag
Total: 87 bytes
```
### A.3 Noise IK Message 2 (0x02)
Handshake response from responder (responder → initiator).
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ NOISE IK MESSAGE 2 (0x02) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ WIRE FORMAT │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ discriminator │ 1 byte │ 0x02 │ │
│ │ 1 │ sender_idx │ 4 bytes │ u32 LE, responder's session idx│ │
│ │ 5 │ receiver_idx │ 4 bytes │ u32 LE, echo of initiator's idx│ │
│ │ 9 │ noise_msg2 │ 33 bytes │ Noise IK second message │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Total: 42 bytes │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ NOISE MSG2 BREAKDOWN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────┬──────────────────┬───────────┬───────────────────────────────┐ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ ephemeral_pubkey │ 33 bytes │ Responder's ephemeral pubkey │ │
│ │ │ │ │ (compressed secp256k1) │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Noise pattern: <- e, ee, se │
│ - e: ephemeral pubkey sent in clear │
│ - ee: DH(responder_ephemeral, initiator_ephemeral) → mix into key │
│ - se: DH(responder_ephemeral, initiator_static) → mix into key │
│ │
│ After msg2, both parties derive identical session keys. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Concrete example**:
```text
WIRE BYTES (hex):
02 ← discriminator
01 EF CD AB ← sender_idx = 0xABCDEF01 (LE)
78 56 34 12 ← receiver_idx = 0x12345678 (LE, echoed)
03 [32 bytes] ← ephemeral pubkey (compressed, 02/03 prefix)
Total: 42 bytes
```
### A.4 Complete Handshake Flow
```text
Initiator (A) Responder (B)
───────────── ─────────────
generates sender_idx = 0x12345678
generates ephemeral keypair
┌──────────────────────────────────────────────────────┐
│ 0x01 | 0x12345678 | [82 bytes noise_msg1] │
└──────────────────────────────────────────────────────┘
──────────────────────────────►
validates msg1
learns A's static pubkey
generates sender_idx = 0xABCDEF01
generates ephemeral keypair
┌──────────────────────────────────────────────────────┐
│ 0x02 | 0xABCDEF01 | 0x12345678 | [33 bytes noise_msg2]│
└──────────────────────────────────────────────────────┘
◄──────────────────────────────
validates msg2
derives session keys
═══════════════════════ HANDSHAKE COMPLETE ═══════════════════════
A's view: B's view:
our_index = 0x12345678 our_index = 0xABCDEF01
their_index = 0xABCDEF01 their_index = 0x12345678
A sends to B: B sends to A:
receiver_idx = 0xABCDEF01 receiver_idx = 0x12345678
┌──────────────────────────────────────────────────────┐
│ 0x00 | 0xABCDEF01 | counter=0 | [ciphertext+tag] │
└──────────────────────────────────────────────────────┘
──────────────────────────────►
```
---
## Appendix B: Message Size Summary
| Packet Type | Size | Overhead |
|-------------|------|----------|