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

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 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

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 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.