6.1 KiB
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
PeerEventenum consumed bySwarmManager(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:
PeerDiscoveredis 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.RoutingUpdatedevents (new peers) →PeerDiscovered.UnroutablePeerevents →PeerExpired.
PeerEvent enum
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
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 importMdnsDiscovery,KademliaDht,TransportConfig,build_transport, andPeerEventdirectly.- The
identifyprotocol must be included in the Task 2.5 composite behaviour to enable SwarmKey fingerprint validation. - The
PeerConnected/PeerDisconnectedvariants ofPeerEventare defined but not yet emitted — Task 2.5 completes this. - No breaking changes to existing
identityorswarm_keymodules.