Truncate NodeAddr from 32 bytes to 16 bytes (first 128 bits of SHA-256)

128-bit truncated SHA-256 provides adequate collision resistance
(2^64 birthday bound, 2^128 pre-image) while cutting per-packet
header overhead nearly in half, aligning with Yggdrasil's approach
and IPv6 address size.

Code changes:
- NodeAddr inner type [u8; 32] → [u8; 16], from_pubkey takes first 16 bytes
- DATA_HEADER_SIZE 68 → 36, signing_bytes capacity 80 → 48
- proof_bytes capacity 40 → 24
- Updated all make_node_addr test helpers and size assertion tests

Doc changes:
- All wire format tables and offset calculations updated
- AncestryEntry size 112 → 96 bytes
- Packet size examples recalculated throughout
This commit is contained in:
Johnathan Corgan
2026-02-06 14:32:15 +00:00
parent dc12b4a82e
commit 71f1db42e3
13 changed files with 143 additions and 141 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ Cryptographic identity using Nostr keys (secp256k1).
Identity
├── npub: PublicKey // public key (bech32: npub1...)
├── nsec: SecretKey // secret key (bech32: nsec1...)
├── node_addr: NodeAddr // SHA-256(pubkey), 32 bytes
├── node_addr: NodeAddr // SHA-256(pubkey) truncated, 16 bytes
└── address: FipsAddress // IPv6 ULA derived from node_addr (fd::/8)
```
+51 -51
View File
@@ -38,13 +38,13 @@ coordinates and distances.
TreeAnnounce {
sequence: u64, // Monotonic, increments on parent change
timestamp: u64, // Unix timestamp (seconds)
parent: NodeAddr, // 32 bytes, SHA-256(pubkey) of selected parent
parent: NodeAddr, // 16 bytes, truncated SHA-256(pubkey) of selected parent
ancestry: Vec<AncestryEntry>, // Path from self to root
signature: Signature, // 64 bytes, signs (sequence || timestamp || parent || ancestry)
}
AncestryEntry {
node_addr: NodeAddr, // 32 bytes
node_addr: NodeAddr, // 16 bytes
sequence: u64, // That node's sequence number
timestamp: u64, // That node's timestamp
signature: Signature, // That node's signature over its declaration
@@ -72,11 +72,11 @@ signed by the declaring node, allowing verification of the entire chain.
|-----------|------|
| sequence | 8 bytes |
| timestamp | 8 bytes |
| parent | 32 bytes |
| parent | 16 bytes |
| signature | 64 bytes |
| Per ancestry entry | 32 + 8 + 8 + 64 = 112 bytes |
| Per ancestry entry | 16 + 8 + 8 + 64 = 96 bytes |
For tree depth D: `112 + D * 112` bytes. At depth 10: ~1.2 KB.
For tree depth D: `96 + D * 96` bytes. At depth 10: ~1.1 KB.
### 2.4 Exchange Rules
@@ -248,8 +248,8 @@ local Bloom filters.
```text
LookupRequest {
request_id: u64, // Unique identifier for this request
target: NodeAddr, // 32 bytes, who we're looking for
origin: NodeAddr, // 32 bytes, who's asking
target: NodeAddr, // 16 bytes, who we're looking for
origin: NodeAddr, // 16 bytes, who's asking
origin_coords: Vec<NodeAddr>, // Origin's ancestry (for return path)
ttl: u8, // Remaining propagation hops
visited: CompactBloomFilter,// ~256 bytes, prevents loops
@@ -313,7 +313,7 @@ LookupResponse returns the target's coordinates to the requester.
```text
LookupResponse {
request_id: u64, // Echoes LookupRequest.request_id
target: NodeAddr, // 32 bytes, confirms who was found
target: NodeAddr, // 16 bytes, confirms who was found
target_coords: Vec<NodeAddr>, // Target's ancestry (the key payload)
proof: Signature, // 64 bytes, target signs to prove existence
}
@@ -384,7 +384,7 @@ handshake completion.
## 8. Encoding
All multi-byte integers are little-endian. NodeAddr is 32 bytes (SHA-256 hash).
All multi-byte integers are little-endian. NodeAddr is 16 bytes (truncated SHA-256 hash).
Signatures are 64 bytes (secp256k1 Schnorr).
Variable-length fields (ancestry, coordinates) are prefixed with a 2-byte
@@ -431,34 +431,34 @@ Propagates spanning tree state between directly connected peers.
│ │ 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 │ NodeAddr of selected parent │ │
│ │ 49 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │
│ │ 51 │ ancestry[0..n] │ 112 × n │ AncestryEntry array │ │
│ │ 17 │ parent │ 16 bytes │ NodeAddr of selected parent │ │
│ │ 33 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │
│ │ 35 │ ancestry[0..n] │ 96 × n │ AncestryEntry array │ │
│ │ ... │ signature │ 64 bytes │ Schnorr sig over all above │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ANCESTRY ENTRY (112 bytes each) │ │
│ │ ANCESTRY ENTRY (96 bytes each) │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ node_addr │ 32 bytes │ SHA-256(pubkey) 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 │ │
│ │ 0 │ node_addr │ 16 bytes │ Truncated SHA-256(pubkey) │ │
│ │ 16 │ sequence │ 8 bytes │ u64 LE, node's seq number │ │
│ │ 24 │ timestamp │ 8 bytes │ u64 LE, node's timestamp │ │
│ │ 32 │ signature │ 64 bytes │ Node's sig over its decl │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 8 + 32 + 2 + (depth × 112) + 64` bytes
**Size calculation**: `1 + 8 + 8 + 16 + 2 + (depth × 96) + 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 |
| 1 (root) | 195 bytes | 224 bytes |
| 3 | 387 bytes | 416 bytes |
| 5 | 579 bytes | 608 bytes |
| 10 | 1059 bytes | 1088 bytes |
**Concrete example** (node D at depth 3, ancestry = [D, P1, P2, Root]):
@@ -467,36 +467,36 @@ 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_addr] ← parent
[16 bytes P1's node_addr] ← parent
04 00 ← ancestry_count = 4
ANCESTRY[0] - Self (D):
[32 bytes D's node_addr]
[16 bytes D's node_addr]
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_addr]
[16 bytes P1's node_addr]
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_addr]
[16 bytes P2's node_addr]
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_addr]
[16 bytes Root's node_addr]
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
Total payload: 1 + 8 + 8 + 16 + 2 + (4 × 96) + 64 = 483 bytes
```
### A.2 FilterAnnounce (0x11)
@@ -585,11 +585,11 @@ Discovers tree coordinates for distant destinations.
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x12 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, unique identifier │ │
│ │ 9 │ target │ 32 bytes │ NodeAddr being searched for │ │
│ │ 41 │ origin │ 32 bytes │ NodeAddr 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 │ │
│ │ 9 │ target │ 16 bytes │ NodeAddr being searched for │ │
│ │ 25 │ origin │ 16 bytes │ NodeAddr of requester │ │
│ │ 41 │ ttl │ 1 byte │ Remaining propagation hops │ │
│ │ 42 │ origin_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 44 │ origin_coords │ 16 × n │ Requester's ancestry │ │
│ │ ... │ visited_hash_cnt │ 1 byte │ Hash functions for visited │ │
│ │ ... │ visited_bits │ 256 bytes │ Compact bloom of visited nodes│ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
@@ -597,13 +597,13 @@ Discovers tree coordinates for distant destinations.
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 32 + 32 + 1 + 2 + (depth × 32) + 1 + 256` bytes
**Size calculation**: `1 + 8 + 16 + 16 + 1 + 2 + (depth × 16) + 1 + 256` bytes
| Origin Depth | Payload Size |
|--------------|--------------|
| 3 | 429 bytes |
| 5 | 493 bytes |
| 10 | 653 bytes |
| 3 | 349 bytes |
| 5 | 381 bytes |
| 10 | 461 bytes |
**Concrete example** (origin at depth 4):
@@ -611,15 +611,15 @@ Discovers tree coordinates for distant destinations.
PLAINTEXT BYTES:
12 ← msg_type = LookupRequest
[8 bytes request_id] ← random unique ID
[32 bytes target node_addr] ← who we're looking for
[32 bytes origin node_addr] ← who's asking
[16 bytes target node_addr] ← who we're looking for
[16 bytes origin node_addr] ← who's asking
08 ← ttl = 8
04 00 ← origin_coords_count = 4
[32 bytes] × 4 ← origin's ancestry (128 bytes)
[16 bytes] × 4 ← origin's ancestry (64 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
Total: 1 + 8 + 16 + 16 + 1 + 2 + 64 + 1 + 256 = 365 bytes
```
### A.4 LookupResponse (0x13)
@@ -638,9 +638,9 @@ Returns target's coordinates to the requester.
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x13 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, echoes request │ │
│ │ 9 │ target │ 32 bytes │ NodeAddr that was found │ │
│ │ 41 │ target_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 43 │ target_coords │ 32 × n │ Target's ancestry to root │ │
│ │ 9 │ target │ 16 bytes │ NodeAddr that was found │ │
│ │ 25 │ target_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 27 │ target_coords │ 16 × n │ Target's ancestry to root │ │
│ │ ... │ proof │ 64 bytes │ Target's signature │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
@@ -650,13 +650,13 @@ Returns target's coordinates to the requester.
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 32 + 2 + (depth × 32) + 64` bytes
**Size calculation**: `1 + 8 + 16 + 2 + (depth × 16) + 64` bytes
| Target Depth | Payload Size |
|--------------|--------------|
| 3 | 203 bytes |
| 5 | 267 bytes |
| 10 | 427 bytes |
| 3 | 139 bytes |
| 5 | 171 bytes |
| 10 | 251 bytes |
**Concrete example** (target at depth 5):
@@ -664,12 +664,12 @@ Returns target's coordinates to the requester.
PLAINTEXT BYTES:
13 ← msg_type = LookupResponse
[8 bytes request_id] ← echoed from request
[32 bytes target node_addr] ← confirms who was found
[16 bytes target node_addr] ← confirms who was found
05 00 ← target_coords_count = 5
[32 bytes] × 5 ← target's ancestry (160 bytes)
[16 bytes] × 5 ← target's ancestry (80 bytes)
[64 bytes proof signature] ← target signs to prove existence
Total: 1 + 8 + 32 + 2 + 160 + 64 = 267 bytes
Total: 1 + 8 + 16 + 2 + 80 + 64 = 171 bytes
```
### A.5 Message Flow Example
+8 -8
View File
@@ -135,7 +135,7 @@ The pubkey is hashed to derive a node_addr used for routing:
```
pubkey (secp256k1 x-only, 32 bytes)
→ SHA-256
→ node_addr (32 bytes)
→ node_addr (16 bytes)
→ truncate with prefix
→ IPv6 address (128 bits, fd::/8)
```
@@ -177,13 +177,13 @@ session establishment.
FIPS uses several related but distinct identifiers at different protocol layers:
| Term | Layer | Visible To | Description |
|----------------------------|---------------------|----------------|-------------------------------------------------------|
| **FIPS address / pubkey** | Application/Session | Endpoints only | 32-byte secp256k1 public key - the endpoint identity |
| **npub** | (encoding) | Human readers | Bech32 encoding of pubkey for display/config |
| **node_addr** | Routing | Routing nodes | SHA-256(pubkey) - cannot be reversed to pubkey |
| **link_addr** | Transport | Direct peers | IP:port, MAC, .onion - transport-specific |
| **IPv6 address** | IPv6 shim | Applications | fd::/8 derived from node_addr - optional compatibility|
| Term | Layer | Visible To | Description |
|----------------------------|---------------------|----------------|----------------------------------------------------------------------|
| **FIPS address / pubkey** | Application/Session | Endpoints only | 32-byte secp256k1 public key - the endpoint identity |
| **npub** | (encoding) | Human readers | Bech32 encoding of pubkey for display/config |
| **node_addr** | Routing | Routing nodes | SHA-256(pubkey) truncated to 128 bits - cannot be reversed to pubkey |
| **link_addr** | Transport | Direct peers | IP:port, MAC, .onion - transport-specific |
| **IPv6 address** | IPv6 shim | Applications | fd::/8 derived from node_addr - optional compatibility |
**Privacy property**: The pubkey (FIPS address / Nostr identity) is never exposed to
intermediate routing nodes. They see only the node_addr, a one-way hash. An observer
+2 -2
View File
@@ -584,7 +584,7 @@ enum RouteState {
| LookupResponse | Return coordinates | ~400 bytes | Reply to discovery |
| SessionSetup | Warm router caches + crypto init | ~400-700 bytes | Before data transfer |
| SessionAck | Confirm session + crypto response | ~300-500 bytes | Session confirmation |
| DataPacket | Application data | 68 bytes + payload (minimal) | Bulk of traffic |
| DataPacket | Application data | 36 bytes + payload (minimal) | Bulk of traffic |
| DataPacket | With coordinates | ~300-500 bytes + payload | After CoordsRequired |
| CoordsRequired | Request coords in next packet | ~50 bytes | Cache miss recovery |
@@ -601,7 +601,7 @@ enum RouteState {
- **Bloom filter traffic**: Near zero (event-driven, no changes)
- **Discovery traffic**: Rare (warm caches)
- **Session traffic**: Rare (established sessions)
- **Data traffic**: Minimal overhead (68-byte header)
- **Data traffic**: Minimal overhead (36-byte header)
### Network Churn
+34 -34
View File
@@ -288,7 +288,7 @@ The crypto session handshake (SessionSetup/SessionAck) warms route caches at
intermediate routers as it transits. Each message carries the sender's
coordinates; routers extract and cache `(src_addr, dest_addr) → next_hop` for
both directions. After the handshake completes, data packets use minimal
68-byte headers and routers forward based on cached routes.
36-byte headers and routers forward based on cached routes.
### 5.2 Cache Miss Recovery
@@ -533,7 +533,7 @@ the inner payload is encrypted end-to-end with session keys.
| 0x21 | PathBroken | R → S | Greedy routing failed |
> **Address terminology**: The `src_addr` and `dest_addr` fields in session packet
> headers are node_addrs (32-byte SHA-256 hashes of pubkeys). These are visible to
> headers are node_addrs (16-byte truncated SHA-256 hashes of pubkeys). These are visible to
> intermediate routers for routing decisions. The actual FIPS addresses (pubkeys/npubs)
> are exchanged only during the Noise IK handshake and never appear in packet
> headers—routers cannot determine endpoint identities from the node_addrs they see.
@@ -551,12 +551,12 @@ Establishes a crypto session and warms router coordinate caches along the path.
│ 0 │ msg_type │ 1 byte │ 0x00 │
│ 1 │ flags │ 1 byte │ Bit 0: REQUEST_ACK │
│ │ │ │ Bit 1: BIDIRECTIONAL │
│ 2 │ src_addr │ 32 bytes │ Source node_addr │
34 │ dest_addr │ 32 bytes │ Destination node_addr │
66 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
│ 68 │ src_coords │ 32 × n │ NodeAddr array (self → root) │
│ 2 │ src_addr │ 16 bytes │ Source node_addr │
18 │ dest_addr │ 16 bytes │ Destination node_addr │
34 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
36 │ src_coords │ 16 × n │ NodeAddr array (self → root) │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE, number of dest coord entries│
│ ... │ dest_coords │ 32 × m │ NodeAddr array (dest → root) │
│ ... │ dest_coords │ 16 × m │ NodeAddr array (dest → root) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg1 (82 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -567,13 +567,13 @@ Establishes a crypto session and warms router coordinate caches along the path.
```text
┌──────┬───────┬──────────────────┬──────────────────┬───────┬─────────────┐
│ 0x00 │ 0x01 │ src_addr │ dest_addr │ 0x03 │ src_coords │
│ type │ flags │ 32 bytes │ 32 bytes │ count │ 3 × 32 bytes│
│ type │ flags │ 16 bytes │ 16 bytes │ count │ 3 × 16 bytes│
├──────┴───────┴──────────────────┴──────────────────┴───────┴─────────────┤
│ 0x04 │ dest_coords │ 0x52 │ handshake_payload │
│ count │ 4 × 32 bytes │ len=82│ 82 bytes (Noise IK msg1) │
│ count │ 4 × 16 bytes │ len=82│ 82 bytes (Noise IK msg1) │
└───────┴───────────────┴───────┴──────────────────────────────────────────┘
Total: 1 + 1 + 32 + 32 + 2 + 96 + 2 + 128 + 2 + 82 = 378 bytes
Total: 1 + 1 + 16 + 16 + 2 + 48 + 2 + 64 + 2 + 82 = 234 bytes
```
### 8.3 SessionAck (0x01)
@@ -588,10 +588,10 @@ Confirms session establishment and completes the Noise handshake.
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x01 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ src_addr │ 32 bytes │ Acknowledger's node_addr │
34 │ dest_addr │ 32 bytes │ Original sender's node_addr │
66 │ src_coords_count │ 2 bytes │ u16 LE │
│ 68 │ src_coords │ 32 × n │ Acknowledger's coords (for caching) │
│ 2 │ src_addr │ 16 bytes │ Acknowledger's node_addr │
18 │ dest_addr │ 16 bytes │ Original sender's node_addr │
34 │ src_coords_count │ 2 bytes │ u16 LE │
36 │ src_coords │ 16 × n │ Acknowledger's coords (for caching) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg2 (33 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -612,12 +612,12 @@ Carries encrypted application data (typically IPv6 payloads).
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 32 bytes │ Source node_addr │
38 │ dest_addr │ 32 bytes │ Destination node_addr │
70 │ payload │ variable │ Encrypted application data │
│ 6 │ src_addr │ 16 bytes │ Source node_addr │
22 │ dest_addr │ 16 bytes │ Destination node_addr │
38 │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Minimal header: 70 bytes
Minimal header: 38 bytes
```
When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
@@ -633,16 +633,16 @@ When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 32 bytes │ Source node_addr │
38 │ dest_addr │ 32 bytes │ Destination node_addr │
70 │ src_coords_count │ 2 bytes │ u16 LE │
72 │ src_coords │ 32 × n │ Source coordinates │
│ 6 │ src_addr │ 16 bytes │ Source node_addr │
22 │ dest_addr │ 16 bytes │ Destination node_addr │
38 │ src_coords_count │ 2 bytes │ u16 LE │
40 │ src_coords │ 16 × n │ Source coordinates │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE │
│ ... │ dest_coords │ 32 × m │ Destination coordinates │
│ ... │ dest_coords │ 16 × m │ Destination coordinates │
│ ... │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
With depth-4 coords both directions: 70 + 2 + 128 + 2 + 128 = 330 bytes header
With depth-4 coords both directions: 38 + 2 + 64 + 2 + 64 = 170 bytes header
```
### 8.5 CoordsRequired (0x20)
@@ -658,11 +658,11 @@ coordinate cache miss.
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x20 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 32 bytes │ The node_addr we couldn't route │
34 │ reporter │ 32 bytes │ NodeAddr of reporting router │
│ 2 │ dest_addr │ 16 bytes │ The node_addr we couldn't route │
18 │ reporter │ 16 bytes │ NodeAddr of reporting router │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Total: 66 bytes
Total: 34 bytes
```
### 8.6 PathBroken (0x21)
@@ -677,10 +677,10 @@ Sent when greedy routing fails (no peer is closer to destination).
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x21 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 32 bytes │ The unreachable node_addr │
34 │ reporter │ 32 bytes │ NodeAddr of reporting router │
66 │ last_coords_count│ 2 bytes │ u16 LE │
│ 68 │ last_known_coords│ 32 × n │ Stale coords that failed │
│ 2 │ dest_addr │ 16 bytes │ The unreachable node_addr │
18 │ reporter │ 16 bytes │ NodeAddr of reporting router │
34 │ last_coords_count│ 2 bytes │ u16 LE │
36 │ last_known_coords│ 16 × n │ Stale coords that failed │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
```
@@ -715,7 +715,7 @@ A DataPacket from source S to destination D, transiting router R:
│ │ SESSION LAYER (S↔D encrypted) │ │
│ ├───────────┬───────┬──────────┬──────────┬─────────────────────────────┤ │
│ │ 0x10 │ flags │ hop_limit│ pay_len │ src_addr │ dest_addr │ │
│ │ DataPacket│ 0x00 │ 64 │ 1400 │ 32 bytes │ 32 bytes │ │
│ │ DataPacket│ 0x00 │ 64 │ 1400 │ 16 bytes │ 16 bytes │ │
│ ├───────────┴───────┴──────────┴──────────┴──────────────┴──────────────┤ │
│ │ │ │
│ │ ENCRYPTED PAYLOAD (S↔D session keys) │ │
@@ -735,12 +735,12 @@ Router R cannot see: payload contents (encrypted with S↔D keys)
### 8.8 Encoding Rules
- All multi-byte integers are **little-endian**
- NodeAddr is 32 bytes (SHA-256 hash of npub)
- NodeAddr is 16 bytes (truncated 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<NodeAddr> encoding:
count: u16 (little-endian)
items: [u8; 32] × count
items: [u8; 16] × count
```
+1 -1
View File
@@ -470,7 +470,7 @@ mod tests {
use super::*;
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
+1 -1
View File
@@ -534,7 +534,7 @@ mod tests {
use super::*;
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
+20 -18
View File
@@ -33,7 +33,7 @@ pub enum IdentityError {
#[error("signature verification failed")]
SignatureVerificationFailed,
#[error("invalid node_addr length: expected 32, got {0}")]
#[error("invalid node_addr length: expected 16, got {0}")]
InvalidNodeAddrLength(usize),
#[error("invalid address length: expected 16, got {0}")]
@@ -64,42 +64,44 @@ pub enum IdentityError {
InvalidHex(#[from] hex::FromHexError),
}
/// 32-byte node identifier derived from SHA-256(pubkey).
/// 16-byte node identifier derived from truncated SHA-256(pubkey).
///
/// The node_addr is used in protocol messages and bloom filters. Hashing the
/// public key prevents grinding attacks that exploit secp256k1's algebraic
/// structure.
/// The node_addr is the first 16 bytes of SHA-256(pubkey), providing 128 bits
/// of collision resistance. Hashing the public key prevents grinding attacks
/// that exploit secp256k1's algebraic structure.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeAddr([u8; 32]);
pub struct NodeAddr([u8; 16]);
impl NodeAddr {
/// Create a NodeAddr from a 32-byte array.
pub fn from_bytes(bytes: [u8; 32]) -> Self {
/// Create a NodeAddr from a 16-byte array.
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
/// Create a NodeAddr from a slice.
pub fn from_slice(slice: &[u8]) -> Result<Self, IdentityError> {
if slice.len() != 32 {
if slice.len() != 16 {
return Err(IdentityError::InvalidNodeAddrLength(slice.len()));
}
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes.copy_from_slice(slice);
Ok(Self(bytes))
}
/// Derive a NodeAddr from an x-only public key (npub).
///
/// Computes SHA-256(pubkey) and takes the first 16 bytes.
pub fn from_pubkey(pubkey: &XOnlyPublicKey) -> Self {
let mut hasher = Sha256::new();
hasher.update(pubkey.serialize());
let hash = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&hash);
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&hash[..16]);
Self(bytes)
}
/// Return the raw bytes.
pub fn as_bytes(&self) -> &[u8; 32] {
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
@@ -547,8 +549,8 @@ mod tests {
fn test_identity_generation() {
let identity = Identity::generate();
// NodeAddr should be 32 bytes
assert_eq!(identity.node_addr().as_bytes().len(), 32);
// NodeAddr should be 16 bytes
assert_eq!(identity.node_addr().as_bytes().len(), 16);
// Address should start with 0xfd
assert_eq!(identity.address().as_bytes()[0], 0xfd);
@@ -663,15 +665,15 @@ mod tests {
#[test]
fn test_node_addr_from_slice() {
let bytes = [0u8; 32];
let bytes = [0u8; 16];
let node_addr = NodeAddr::from_slice(&bytes).unwrap();
assert_eq!(node_addr.as_bytes(), &bytes);
// Wrong length should fail
let short = [0u8; 16];
let short = [0u8; 8];
assert!(matches!(
NodeAddr::from_slice(&short),
Err(IdentityError::InvalidNodeAddrLength(16))
Err(IdentityError::InvalidNodeAddrLength(8))
));
}
+1 -1
View File
@@ -1608,7 +1608,7 @@ mod tests {
#[allow(dead_code)]
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
+1 -1
View File
@@ -488,7 +488,7 @@ mod tests {
}
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
+1 -1
View File
@@ -260,7 +260,7 @@ mod tests {
use crate::{Identity, PeerIdentity};
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
+13 -13
View File
@@ -31,8 +31,8 @@ use thiserror::Error;
pub const PROTOCOL_VERSION: u8 = 1;
/// Data packet header size in bytes (excluding payload).
/// flags(1) + hop_limit(1) + payload_length(2) + src_addr(32) + dest_addr(32) = 68
pub const DATA_HEADER_SIZE: usize = 68;
/// flags(1) + hop_limit(1) + payload_length(2) + src_addr(16) + dest_addr(16) = 36
pub const DATA_HEADER_SIZE: usize = 36;
// ============================================================================
// Link Layer Message Types (peer-to-peer, hop-by-hop)
@@ -479,9 +479,9 @@ impl LookupResponse {
/// Get the bytes that should be signed as proof.
///
/// Format: request_id (8) || target (32)
/// Format: request_id (8) || target (16)
pub fn proof_bytes(request_id: u64, target: &NodeAddr) -> Vec<u8> {
let mut bytes = Vec::with_capacity(40);
let mut bytes = Vec::with_capacity(24);
bytes.extend_from_slice(&request_id.to_le_bytes());
bytes.extend_from_slice(target.as_bytes());
bytes
@@ -716,12 +716,12 @@ impl DataFlags {
/// Minimal data packet with addresses only (no coordinates).
///
/// The 68-byte header contains:
/// The 36-byte header contains:
/// - flags (1 byte)
/// - hop_limit (1 byte)
/// - payload_length (2 bytes)
/// - src_addr (32 bytes)
/// - dest_addr (32 bytes)
/// - src_addr (16 bytes)
/// - dest_addr (16 bytes)
///
/// Routers use cached coordinates for routing decisions.
#[derive(Clone, Debug)]
@@ -852,7 +852,7 @@ mod tests {
use super::*;
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
@@ -971,9 +971,9 @@ mod tests {
fn test_data_packet_size() {
let packet = DataPacket::new(make_node_addr(1), make_node_addr(2), vec![0u8; 100]);
// 68 byte header + 100 byte payload
assert_eq!(packet.total_size(), 168);
assert_eq!(packet.header_size(), 68);
// 36 byte header + 100 byte payload
assert_eq!(packet.total_size(), 136);
assert_eq!(packet.header_size(), 36);
assert_eq!(packet.payload_len(), 100);
}
@@ -1086,9 +1086,9 @@ mod tests {
let target = make_node_addr(42);
let bytes = LookupResponse::proof_bytes(12345, &target);
assert_eq!(bytes.len(), 40); // 8 + 32
assert_eq!(bytes.len(), 24); // 8 + 16
assert_eq!(&bytes[0..8], &12345u64.to_le_bytes());
assert_eq!(&bytes[8..40], target.as_bytes());
assert_eq!(&bytes[8..24], target.as_bytes());
}
// ===== FilterAnnounce Tests =====
+9 -9
View File
@@ -144,9 +144,9 @@ impl ParentDeclaration {
/// Get the bytes that should be signed.
///
/// Format: node_addr (32) || parent_id (32) || sequence (8) || timestamp (8)
/// Format: node_addr (16) || parent_id (16) || sequence (8) || timestamp (8)
pub fn signing_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(80);
let mut bytes = Vec::with_capacity(48);
bytes.extend_from_slice(self.node_addr.as_bytes());
bytes.extend_from_slice(self.parent_id.as_bytes());
bytes.extend_from_slice(&self.sequence.to_le_bytes());
@@ -551,7 +551,7 @@ mod tests {
use super::*;
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
@@ -766,14 +766,14 @@ mod tests {
let decl = ParentDeclaration::new(node, parent, 100, 1234567890);
let bytes = decl.signing_bytes();
// Should be 80 bytes: 32 + 32 + 8 + 8
assert_eq!(bytes.len(), 80);
// Should be 48 bytes: 16 + 16 + 8 + 8
assert_eq!(bytes.len(), 48);
// Verify structure
assert_eq!(&bytes[0..32], node.as_bytes());
assert_eq!(&bytes[32..64], parent.as_bytes());
assert_eq!(&bytes[64..72], &100u64.to_le_bytes());
assert_eq!(&bytes[72..80], &1234567890u64.to_le_bytes());
assert_eq!(&bytes[0..16], node.as_bytes());
assert_eq!(&bytes[16..32], parent.as_bytes());
assert_eq!(&bytes[32..40], &100u64.to_le_bytes());
assert_eq!(&bytes[40..48], &1234567890u64.to_le_bytes());
}
#[test]