diff --git a/docs/design/fips-architecture.md b/docs/design/fips-architecture.md index a889df6..963517c 100644 --- a/docs/design/fips-architecture.md +++ b/docs/design/fips-architecture.md @@ -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) ``` diff --git a/docs/design/fips-gossip-protocol.md b/docs/design/fips-gossip-protocol.md index 9957c73..59aeb4e 100644 --- a/docs/design/fips-gossip-protocol.md +++ b/docs/design/fips-gossip-protocol.md @@ -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, // 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, // 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, // 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 diff --git a/docs/design/fips-intro.md b/docs/design/fips-intro.md index 46ae0b5..b738428 100644 --- a/docs/design/fips-intro.md +++ b/docs/design/fips-intro.md @@ -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 diff --git a/docs/design/fips-routing.md b/docs/design/fips-routing.md index 2d8e480..cd2e408 100644 --- a/docs/design/fips-routing.md +++ b/docs/design/fips-routing.md @@ -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 diff --git a/docs/design/fips-session-protocol.md b/docs/design/fips-session-protocol.md index f3ac417..938e839 100644 --- a/docs/design/fips-session-protocol.md +++ b/docs/design/fips-session-protocol.md @@ -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 encoding: count: u16 (little-endian) - items: [u8; 32] × count + items: [u8; 16] × count ``` diff --git a/src/bloom.rs b/src/bloom.rs index db5fe89..8ee9f7e 100644 --- a/src/bloom.rs +++ b/src/bloom.rs @@ -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) } diff --git a/src/cache.rs b/src/cache.rs index 95a7ecc..86c4cab 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -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) } diff --git a/src/identity.rs b/src/identity.rs index 72c9f74..7454497 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -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 { - 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)) )); } diff --git a/src/node.rs b/src/node.rs index 8067c89..82c0d9e 100644 --- a/src/node.rs +++ b/src/node.rs @@ -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) } diff --git a/src/peer/active.rs b/src/peer/active.rs index 0ea0bc4..b610bf2 100644 --- a/src/peer/active.rs +++ b/src/peer/active.rs @@ -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) } diff --git a/src/peer/mod.rs b/src/peer/mod.rs index d7f78f4..9819a13 100644 --- a/src/peer/mod.rs +++ b/src/peer/mod.rs @@ -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) } diff --git a/src/protocol.rs b/src/protocol.rs index e7d2e15..7549b9c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -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 { - 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 ===== diff --git a/src/tree.rs b/src/tree.rs index 7c5d47a..072d22d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -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 { - 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]