chronogrid/docs/ARCHITECTURE/ADR/0009-net-transport-discover...

144 lines
6.1 KiB
Markdown

# ADR-0009: chronogrid_net — Transport & Discovery Layer (Task 2.3)
**Status:** Accepted
**Date:** 2026-08-22
**Task:** 2.3 — Implement chronogrid_net: Transport & Discovery Layer
**Author:** Autonomous Agent
---
## Context
Task 2.3 required implementing the libp2p transport stack and peer discovery layer
inside `crates/chronogrid_net/`, building on the `NodeIdentity` and `SwarmKey`
primitives established in Task 2.2 (ADR-0008).
The implementation must satisfy:
- QUIC over UDP as the primary transport (low-latency, multiplexed).
- TCP + Noise XK as a fallback transport for UDP-blocked environments.
- SwarmKey-based cluster authentication to prevent unauthorized nodes from joining.
- Zero-configuration LAN peer discovery via mDNS.
- WAN peer discovery via Kademlia DHT with bootstrap node support.
- A unified `PeerEvent` enum consumed by `SwarmManager` (Task 2.5).
---
## Decision
### New modules added to `crates/chronogrid_net/`
| Module | Responsibility |
|-----------------|-----------------------------------------------------------------------|
| `peer_event` | `PeerEvent` enum — unified peer lifecycle events for all consumers |
| `transport` | `TransportConfig`, `build_transport()`, `dalek_to_libp2p_keypair()` |
| `discovery` | `MdnsDiscovery` (LAN) and `KademliaDht` (WAN) discovery wrappers |
### Transport Architecture
**Primary transport (QUIC):** Configured via `SwarmBuilder::with_quic()` in
`SwarmManager` (Task 2.5). QUIC provides built-in TLS 1.3-equivalent security,
stream multiplexing, and connection migration — no separate Noise layer needed.
**Fallback transport (TCP + Noise + Yamux):** `build_transport()` constructs a
`Boxed<(PeerId, StreamMuxerBox)>` using:
```
TCP (tokio) → Noise XX (Ed25519) → Yamux multiplexer
```
The Noise XX pattern is used for the TCP fallback (not XK) because XK requires
the initiator to know the responder's static key in advance, which is not
guaranteed in a dynamic peer mesh. XX provides mutual authentication with
forward secrecy and is the libp2p standard.
**SwarmKey authentication:** The 256-bit pre-shared `SwarmKey` is enforced at
the application layer via the `identify` protocol. The key fingerprint
(`SHA-256(key)[0..8]` as 16 hex chars) is embedded in the `UserAgent` string
exchanged during identification:
```
chronogrid/<version>/swarm:<fingerprint>
```
Peers with a mismatched fingerprint are disconnected by `SwarmManager` after
the identify handshake completes. This approach is compatible with libp2p 0.54's
`SwarmBuilder` API, which does not expose a raw private-network transport layer.
**Keypair conversion:** `dalek_to_libp2p_keypair()` extracts the 32-byte seed
from the `ed25519-dalek` `SigningKey` and re-imports it into `libp2p::identity::ed25519::Keypair`.
Both libraries use the same Ed25519 curve parameters, ensuring the derived
`PeerId` is stable and consistent with the `NodeId` (SHA-256 of the public key).
### Discovery Architecture
**`MdnsDiscovery`** wraps `libp2p::mdns::tokio::Behaviour` with:
- A 5-minute TTL for peer advertisements.
- A 5-second re-query interval.
- IPv4-only (IPv6 disabled for simplicity; can be enabled in Task 2.5 config).
- Deduplication: `PeerDiscovered` is only emitted for genuinely new peers.
- An `active_peers: HashSet<PeerId>` registry tracking non-expired LAN peers.
**`KademliaDht`** wraps `libp2p::kad::Behaviour<MemoryStore>` with:
- An in-memory routing store (sufficient for small personal device clusters).
- 60-second replication interval and 5-minute publication interval.
- `add_bootstrap_peer()` / `bootstrap()` for initial DHT seeding.
- `RoutingUpdated` events (new peers) → `PeerDiscovered`.
- `UnroutablePeer` events → `PeerExpired`.
### PeerEvent enum
```rust
pub enum PeerEvent {
PeerDiscovered { peer_id: PeerId, addrs: Vec<Multiaddr> },
PeerExpired { peer_id: PeerId },
PeerConnected { peer_id: PeerId },
PeerDisconnected { peer_id: PeerId },
}
```
`PeerConnected` and `PeerDisconnected` are emitted by `SwarmManager` (Task 2.5)
from the libp2p `Swarm` connection lifecycle events. This module defines the
enum; Task 2.5 completes the emission.
### libp2p feature flags added
```toml
libp2p = { version = "0.54", features = [
"noise", "quic", "tcp", "yamux",
"mdns", "gossipsub", "kad", "identify", "relay",
"tokio", "macros",
] }
```
`tcp` and `yamux` were not included in the initial Task 2.2 dependency
declaration and were added during Task 2.3 implementation.
---
## Test Coverage
65 unit tests pass across all modules:
| Module | Tests | Coverage highlights |
|---------------|-------|------------------------------------------------------------------|
| `peer_event` | 10 | All 4 variants: accessors, display, clone, is_discovery/connected |
| `transport` | 12 | Config, user-agent, fingerprint extraction, keypair conversion, transport construction |
| `discovery` | 13 | mDNS: discovered/expired/dedup/multi-peer; Kademlia: bootstrap, routing, unroutable |
| `identity` | 9 | (from Task 2.2, unchanged) |
| `swarm_key` | 16 | (from Task 2.2, unchanged) |
**Note on Kademlia `RoutingUpdated` tests:** `KadEvent::RoutingUpdated` uses
`libp2p::kad::Addresses` and `libp2p::kad::Distance` which are not constructable
from outside the `libp2p-kad` crate (private internal types). The routing-table
tracking logic is tested via a `#[cfg(test)]` helper `insert_peer_for_test()`.
Full end-to-end `RoutingUpdated` coverage will be provided by the integration
test in Task 2.5 where a real swarm runs.
---
## Consequences
- `SwarmManager` (Task 2.5) can now import `MdnsDiscovery`, `KademliaDht`,
`TransportConfig`, `build_transport`, and `PeerEvent` directly.
- The `identify` protocol must be included in the Task 2.5 composite behaviour
to enable SwarmKey fingerprint validation.
- The `PeerConnected` / `PeerDisconnected` variants of `PeerEvent` are defined
but not yet emitted — Task 2.5 completes this.
- No breaking changes to existing `identity` or `swarm_key` modules.