hraness

peer discovery in layers: mdns, bootstrap, dht, and when each fails

finding each other is the first consensus problem

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

Before two peers can exchange a single byte they have to find each other, and finding each other is harder than it looks. It is also the first place a peer-to-peer system quietly reintroduces a central authority (a bootstrap server, a DNS name, a rendezvous point) while claiming not to have one. Discovery deserves to be treated as what it is: the first consensus problem, because every peer has to agree on who is in the system before anything else can happen.

The standard answer is layered: mDNS on the local network, bootstrap nodes to get onto the wider one, a DHT for decentralized lookup once you are on. Valhalla uses almost none of that yet, and the reasons it doesn’t are the lesson.

the three layers and their failures

Each layer solves a different reachability problem and fails differently.

mDNS / local broadcast. Multicast DNS and its kin answer “who is on this LAN right now” without any infrastructure: peers announce themselves on a multicast group and hear each other. It is genuinely serverless, and it stops working the moment you leave the LAN. Corporate networks filter multicast. Home routers drop it. The public internet has no multicast at all. mDNS is the right answer for “two laptops on the same Wi-Fi” and nothing else; a system that leans on it has a discovery mechanism that works in the demo and nowhere else.

Bootstrap nodes. To join a wide-area overlay you need one address that already knows someone. Bootstrap nodes are that: a small set of known, long-lived peers whose only job is to hand you introductions. They fail in two directions. Operationally, they are infrastructure: someone runs them, they can go down, and a network whose only entry points are unreachable is a network you cannot join. Adversarially, a malicious bootstrap can hand you a view of the network it controls: only its own peers, an eclipse by introduction. The bootstrap doesn’t have to lie about anything else; it only has to choose which peers you learn about.

DHT lookup. A distributed hash table spreads the index across the peers themselves: no bootstrap dependency after entry, no server to kill. The failure mode is Sybil: the DHT’s correctness depends on the peers storing the records being honest, and in an open network an attacker can mint as many peer identities as the protocol allows cheaply. If “who holds the record for key K” is decided by the K closest node IDs, an adversary who controls those IDs controls the answer. The DHT removes the central server and replaces it with a population you have to trust in aggregate.

what valhalla actually does

The codebase is honest about where it stands: peer discovery on the public internet is not built, and the layers that exist are deliberately narrower than the standard stack.

The narrowest layer is explicit, and it is the only one vhalla-native has. Route::parse takes a multiaddr string and an expiry (both supplied out of band by the operator) and accepts only a literal IP + UDP + QUICv1 + p2p shape. No DNS, no relays, no p2p-circuit, no extra protocols, 256-byte cap on the string. The doc comment states the security posture: “the operator supplies the route out of band and the pinned peer key, not the address, is what authenticates the session.” The address is a hint. The Ed25519 peer ID embedded in it is the only thing that gets checked cryptographically, and even that is a dial pin, not evidence: transport_key extracts the key, requires it to be an inline Ed25519 identity (not a hash of one), and the handshake still has to bind observed keys against a pinned Pairing before anything is admitted.

The surrounding bounds tell you how experimental the layer is: MAX_CONNECTIONS = 4, one established connection per peer, a 5-second handshake deadline, a 10-second request deadline, a 60-second listener lifetime, loopback by default. An optional listen host can bind a LAN or private-overlay interface, but the README’s own walkthrough is two terminals and two explicitly exchanged keys. This is not a network you join; it is a wire you string between two machines you already trust.

The rooms nodes add one layer up: persistent_peers, a static list in the node config, plus Malachite’s DiscoveryConfig for the gossipsub overlay that carries consensus traffic. That is bootstrap-by-configuration: the validator set is private, the members are known, and “discovery” reduces to “stay connected to the peers you were configured with.” The one concession to real networks is in DiscoveryConfig: max_connections_per_ip is lifted to usize::MAX only when the listen address is loopback, because on loopback every peer shares an IP and the limit would refuse the whole validator set. Even the permissive case is gated on an actual check of the bind address.

The third layer is not peer discovery at all, and that is the point. vhalla-discovery is content discovery: bounded search over “already-admitted social history.” Query::parse accepts at most 256 bytes of input, 8 terms, 96 bytes per term, literal AND-matching plus quoted phrases: no regex, no escaping, no network. You cannot search for a peer you haven’t already admitted; the search space is the records your local policy already let in. Discovery, in this codebase, is what you do after admission, not the mechanism that gets you admitted.

discovery is not admission

The rule that ties it together is the one the whole workspace enforces: the output of any discovery mechanism is a hint, never authority. vhalla-native’s lib.rs states it directly: “Route advertisements are untrusted hints, never membership grants.” A route tells you where to dial; the pinned pairing decides whether the peer you reach is the peer you meant. A descriptor, an invitation, a listing: the discovery crate’s docs call them all “hints a local policy may consider; none self-authorize.”

This is the property the standard stack loses. mDNS, bootstrap nodes, and DHTs all return candidate addresses; the failure in each is treating the candidate as authenticated because the discovery mechanism returned it. Valhalla’s answer is to make the confusion unrepresentable: Route cannot be admitted, only dialed; a SignedEnvelope cannot be an AuthorizedEffect; the discovery record cannot be a member. The handshake is the only place a peer becomes real, and it happens after discovery, not because of it.

the honest gap

The fair criticism is that this is all a way of not solving the problem. Explicit route handoff is discovery by “the operator already knows.” Persistent peers is discovery by “the validator set is fixed.” Neither is a mechanism for two strangers to find each other on the open internet, and the workspace says so: public rooms, open membership, and browser reachability are all listed as unqualified. There is no mDNS implementation in the maintained crates (it appears only transitively in prototype lockfiles), no public DHT, no rendezvous. The data model already leaves room for it (vhalla-transport’s Path enum has a Relay variant alongside Direct), but the only relay that exists is InMemoryRelay, the deterministic test double the steel thread uses. The abstraction is in place; the mechanism that would justify it is not.

What the codebase does instead is refuse to skip ahead. The promotion gates require each layer to prove its own boundary before the next is built on it: signed chat between explicitly paired identities came before rooms, a private validator set before any public admission. Discovery will presumably be layered the same way when it is built: mDNS where it works, bootstrap where it is trusted, a DHT if the Sybil question is ever answered. Until then, the system’s discovery story is a two-line summary: the operator supplies the route, the pairing supplies the trust, and nothing that finds a peer gets to vouch for it.

sources

  • vhalla
  • crates/vhalla-native/src/lib.rs: Route::parse, literal-IP only, “untrusted hints, never membership grants”
  • crates/vhalla-rooms-node/src/unix.rs: persistent_peers and DiscoveryConfig in the node config
  • crates/vhalla-discovery/src/lib.rs and crates/vhalla-discovery/src/query.rs: content discovery over admitted records
  • crates/vhalla-session/src/invitation.rs: the invitation as non-self-authorizing descriptor
  • valhalla-promotion-gates.md: the qualification order