# 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//swarm: ``` 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` registry tracking non-expired LAN peers. **`KademliaDht`** wraps `libp2p::kad::Behaviour` 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 }, 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.