mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
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:
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
@@ -178,10 +178,10 @@ 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 |
|
||||
| **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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user