Fix secp256k1 parity in Noise IK, add disconnect protocol, cross-connection handling, timeout cleanup

Noise IK parity fix:
- Pre-message hash normalizes responder static key to even parity (0x02)
  so initiator and responder hash chains match regardless of actual parity
- ECDH uses shared_secret_point() + SHA-256(x-only) instead of
  SharedSecret::new() which includes a parity-dependent version byte
- Fixes handshake failure for ~50% of keys when initiator has only npub

Graceful disconnect protocol (link message 0x50):
- DisconnectReason enum with 8 reason codes
- Disconnect struct with encode/decode
- send_encrypted_link_message() reusable helper
- handle_disconnect() with immediate peer removal
- send_disconnect_to_all_peers() called during Node::stop()

Cross-connection fix in handle_msg1():
- addr_to_link check now distinguishes inbound duplicates (reject) from
  outbound links (cross-connection, allow and resolve via tie-breaker)
- remove_link() only clears addr_to_link if entry maps to same link_id
- Link cleanup and addr_to_link restoration in cross-connection branches

Handshake timeout cleanup:
- RX loop uses tokio::select! with 1-second interval tick
- check_timeouts() scans for stale (>30s) and failed connections
- cleanup_stale_connection() removes all associated state

Tests: 279 passing (4 new: cross-connection, stale cleanup, failed
cleanup, odd-parity handshake)
This commit is contained in:
Johnathan Corgan
2026-02-10 21:25:26 +00:00
parent e6f63678ba
commit 4445c46066
11 changed files with 975 additions and 39 deletions
+4 -3
View File
@@ -465,10 +465,11 @@ Exchanged between directly connected peers, encrypted with link session keys:
| Type | Name | Purpose |
|------|----------------|--------------------------------------------|
| 0x10 | TreeAnnounce | Spanning tree state (parent, ancestry) |
| 0x11 | FilterAnnounce | Bloom filter reachability update |
| 0x12 | LookupRequest | Query for node's tree coordinates |
| 0x13 | LookupResponse | Response with coordinates and proof |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x30 | LookupRequest | Query for node's tree coordinates |
| 0x31 | LookupResponse | Response with coordinates and proof |
| 0x40 | SessionDatagram| Carries end-to-end encrypted payloads |
| 0x50 | Disconnect | Orderly disconnect notification |
### Session Layer Messages
+10
View File
@@ -411,6 +411,16 @@ Secp256k1 and SHA-256 are already used for Nostr identities, and
ChaCha20-Poly1305 matches NIP-44 encryption. Lightning's BOLT 8 provides a
proven reference for adapting Noise Protocol to secp256k1.
**secp256k1 parity normalization**: Nostr npubs encode x-only public keys
(32 bytes, no y-coordinate parity). The Noise IK pre-message mixes the
responder's static key as a 33-byte compressed key, and the default
secp256k1 ECDH hash includes a parity-dependent version byte. Since the
initiator may only have the x-only key, both operations are normalized to be
parity-independent: the pre-message hash uses even parity (`0x02` prefix),
and ECDH hashes only the x-coordinate of the result point. This ensures
handshakes succeed regardless of the responder's actual key parity. See
`secp256k1-parity-fix.md` for detailed analysis.
### 6.4 Handshake Integration with SessionSetup
The Noise handshake messages embed in SessionSetup/SessionAck:
+2 -2
View File
@@ -232,9 +232,9 @@ PeerSlot::Connecting(PeerConnection)
PeerSlot::Active(ActivePeer)
Link failure / explicit disconnect
Disconnect message (0x50) / link failure / timeout
[removed from peers map]
[removed from peers map, index freed, link cleaned up]
```
**PeerConnection** contains:
+50 -3
View File
@@ -156,6 +156,53 @@ Indices SHOULD be:
4. **Rotated on rekey**: When a session rekeys, allocate new indices to prevent
cross-session correlation.
### 2.6 Link Control Messages
Link control messages are sent inside encrypted frames (discriminator 0x00) and
use the 0x500x5F message type range. The first defined control message is
Disconnect (0x50).
#### Disconnect (0x50)
Orderly disconnect notification sent before closing a peer link:
```text
ENCRYPTED FRAME (discriminator 0x00):
[receiver_idx][counter][ENCRYPTED_PAYLOAD + tag]
DECRYPTED PLAINTEXT:
┌──────────┬──────────┐
│ 0x50 │ reason │
│ 1 byte │ 1 byte │
└──────────┴──────────┘
Total plaintext: 2 bytes
```
**Reason codes:**
| Code | Name | Description |
|------|---------------------|------------------------------------------|
| 0x00 | Shutdown | Normal operator-requested stop |
| 0x01 | Restart | Restarting, may reconnect soon |
| 0x02 | ProtocolError | Protocol error encountered |
| 0x03 | TransportFailure | Transport failure |
| 0x04 | ResourceExhaustion | Memory or connection limit |
| 0x05 | SecurityViolation | Authentication or policy violation |
| 0x06 | ConfigurationChange | Peer removed from configuration |
| 0x07 | Timeout | Keepalive or stale detection timeout |
| 0xFF | Other | Unspecified reason |
**Semantics:**
- **Best-effort delivery**: If the transport is broken, the message won't arrive.
Timeout-based detection (stale peer, keepalive failure) remains the fallback.
- **Receiver action**: Immediately remove the peer from the peer table, free the
session index, remove the link, and clean up address mappings. If the departed
peer was a tree parent, trigger parent reselection.
- **Shutdown sequence**: On node shutdown, Disconnect is sent to all active peers
*before* transports are stopped.
---
## 3. Packet Dispatch
@@ -846,9 +893,9 @@ Post-handshake data packets between authenticated peers.
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Link message types: │
│ 0x10 = TreeAnnounce 0x12 = LookupRequest │
│ 0x11 = FilterAnnounce 0x13 = LookupResponse │
│ 0x40 = SessionDatagram
│ 0x10 = TreeAnnounce 0x30 = LookupRequest │
│ 0x20 = FilterAnnounce 0x31 = LookupResponse │
│ 0x40 = SessionDatagram 0x50 = Disconnect
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```