Layer 7 · The DIG Node peer network
Canonical references:
dig-gossip(the peer transport, discovery, and gossip layer — TLS-WebSocket peers,peer_id = SHA-256(TLS SPKI DER), address manager, introducer + peer-exchange),dig-relay(the rendezvous / hole-punch coordinator / circuit relay serving theRelayMessagewire),dig-nat(theconnect(peer)NAT-traversal ladder),dig-dht(the Kademlia DHT with provider records that locate which peers hold content), anddig-constants(DIG_RELAY_URL). This layer is how DIG Nodes find and reach each other; the dig RPC is what they speak once connected.
This is the normative anchor for DIG Node ↔ Node communication. Every peer-facing crate — dig-nat, dig-relay, dig-gossip, dig-dht, and dig-node — conforms to the contracts below. Where a statement is a wire contract it is fixed at the field/byte level; a conforming reimplementation must reproduce it exactly.
The thesis: authenticated peers, direct when possible, relay only as a last resort
- Every peer link is mutually authenticated. All node↔node traffic runs over mutual-TLS (mTLS). A peer's identity is the hash of its TLS public key; there is no unauthenticated peer channel.
- Direct paths are preferred. A node tries, in a fixed order, to open a direct connection (already reachable → UPnP → NAT-PMP → PCP → relay-coordinated hole-punch) before it ever relays. When any direct path succeeds, no relay is used.
- The relay is the last resort, never the trust anchor.
relay.dig.netbridges bytes only when every direct strategy fails. It forwards opaque, end-to-end-authenticated payloads by peer id and can read none of them.
The relay has four distinct roles
relay.dig.net is not a single service — it fills four separate roles, three of which are low-bandwidth signalling. Keeping them distinct matters because only the fourth carries a peer's data stream:
| # | Role | Bandwidth | What the relay does | Where |
|---|---|---|---|---|
| 1 | STUN server | tiny | Answers a Binding request so a node learns its public reflexive IP:port (RFC 5389). | §3 |
| 2 | Introducer | small | Registers a node's presence and returns known-peer lists for rendezvous/discovery. | §4a |
| 3 | Hole-punch signalling | small | Brokers a hole punch between two NAT'd peers — relays their candidate-address exchange and coordinates the simultaneous-open timing — after which the peers connect directly. The relay carries only the coordination messages, never the data. | §5 |
| 4 | Relayed (TURN-like) transport | full | Proxies all of a peer connection's data when no direct path exists. High-bandwidth, last resort only. | §6 |
Roles 1–3 are how the relay helps two peers connect directly; role 4 is the only one where the peer's stream flows through the relay. A node always prefers role 3 (broker a direct link) over role 4 (proxy the whole stream), because role 3 costs the relay almost nothing while role 4 consumes real bandwidth. Whatever the tier, once connected the link is mTLS with peer_id = SHA-256(SPKI) (§1) — the relay is never the trust anchor.
0 · The two RPC tiers — mTLS peer/control vs anonymous public read
A DIG Node's RPC is served on two distinct tiers with two distinct authentication models, because two different callers need it: other nodes (which present a client certificate) and browsers/agents (which cannot present a client certificate but must still be able to READ content). One dig-node process serves both; the tier a method lives on is a frozen contract every serve-layer and every RPC client conforms to.
| PEER / CONTROL tier | PUBLIC READ tier | |
|---|---|---|
| Purpose | node↔node: discovery, DHT, PEX, availability-for-sync, PUSH/WRITE, config/control | browser/agent content retrieval only |
| Auth | mutual TLS — client cert REQUIRED; peer_id = SHA-256(TLS SPKI DER) (§1); write routes additionally per-request BLS-signed (§21.9) | none — anonymous, no client cert, no bearer for reads |
| Transport | dig-nat mTLS mux (peer RPC + DHT + PEX streams) on the P2P port DIG_PEER_PORT (default 9444); the §21 authenticated HTTPS write routes | plain HTTPS JSON-RPC, CORS-enabled, on the public read listener (network-wide: rpc.dig.net) |
| Browser-reachable | No — an anonymous caller cannot open it | Yes — this is the browser/agent read path |
| Mutation / identity | write, peer, config, control all live here | read-only — no mutation, no peer/config method reachable |
| Integrity trust | mTLS peer identity | client-side self-verification (merkle inclusion proof + chain-anchored root pin) — the server is untrusted (Verification & provenance) |
| Miss behavior | method-specific error | decoy-on-miss, never 404 (blind host) |
The boundary invariant
Two rules, jointly, are the contract:
- No peer / write / config / control method is reachable without mTLS. Every method that mutates state, exchanges peer information, moves the DHT/PEX, or reconfigures the node is served only on the mTLS peer/control tier. On the anonymous public-read listener these methods do not exist — an anonymous caller that names one receives
-32601(method not found), the same as any unimplemented method. A node MUST NOT honor a write/peer/control method on a connection that did not complete the mTLS handshake. - Content read requires no mTLS. The read methods —
dig.getContentand the read side ofdig.getProof/dig.getCapsule/dig.getManifest/dig.getMetadata, plus the §21 GET routes (content/proof/roots/descriptor) — are served anonymously so a browser works. They are read-only: they never mutate, never reveal peer/config state, and never accept a write.
Which tier every method sits on is enumerated in §7a.
Why this is secure
Splitting authentication by tier does not weaken the network, because the public tier is read-only and client-verified:
- The read server is already untrusted. Every read is verified client-side against the CHIP-0035 chain-anchored root (
dig.getContentstreaming contract) — a merkle inclusion proof under the on-chain root plus per-chunk AES-256-GCM-SIV authentication. A malicious or anonymous read server can only serve bytes that either verify (correct content) or fail the proof/tag (detected, discarded). Anonymity of the reader changes nothing: the content is public ciphertext, keyed byretrieval_key = SHA-256(URN), and the AES key is derived client-side and never sent. There is nothing to authenticate a reader for. - A miss is indistinguishable. A content miss returns a deterministic decoy, byte-shaped like a hit, never a 404 — so anonymous access leaks no presence/absence oracle.
- Everything that could harm the network stays mTLS-gated. Advancing a root (PUSH/WRITE), announcing a provider record, injecting peers, or reconfiguring the node all require the client certificate (and, for writes, a per-request BLS signature). An attacker with no certificate can read public ciphertext and nothing more — exactly the capability a browser needs and no more.
- The two never blur. Because the anonymous listener simply does not implement the peer/write/control methods, there is no "forgot to check auth" path: an unauthenticated write is not a rejected request, it is a nonexistent method.
How one process serves both
A dig-node runs two listeners:
- The mTLS peer/control listener binds the P2P port (
DIG_PEER_PORT, default 9444) with rustlsCERT_REQUIRED; it carries the dig-nat mux (peer RPC, DHT, PEX). The §21 authenticated write/push routes (transport & push) are the control tier's HTTPS face (per-request BLS auth, middleware order auth-THEN-rate-limit). - The public read listener serves the anonymous JSON-RPC read subset over plain HTTPS with
Access-Control-Allow-Origin: *, no credentials,OPTIONS→ 204. On the public network this listener isrpc.dig.net. A dig-node run purely for a local consumer keeps this listener on loopback (127.0.0.1, default read port 9778); a node that wants to serve the wider network exposes the read listener on its public interface (TLS-fronted, asrpc.dig.netis).
The recommended endpoint split (design-first call): the public read tier is the same JSON-RPC endpoint shape as the network profile — one POST endpoint speaking JSON-RPC 2.0 — but the anonymous serve layer answers only the read subset; the peer/write/control methods return -32601 there. This reuses the existing node-profile / dig.methods gating (an agent already gates on dig.methods rather than assuming one uniform surface) instead of inventing a parallel protocol, and it is exactly what a browser already speaks to rpc.dig.net. The peer/control tier is not a JSON-RPC-over-HTTPS surface at all — it is the dig-nat mTLS mux — so it cannot be reached by an anonymous HTTP client even by accident.
rpc.dig.net is dual-mode: the public read tier plus an mTLS gatewayrpc.dig.net (the dig RPC network profile) is the anonymous, CORS-enabled, decoy-on-miss, client-verified public read tier — the tier a browser has always used, and this section states the invariant that keeps the peer/write/control surface off it. It is also the public mTLS gateway: node-class clients, and anonymous readers holding an ephemeral certificate, reach the very same hostname over the mTLS peer/control transport instead. §0a is the two-front contract.
Browser specifics — fetch + verify without mTLS
A browser (or an agent with no client certificate) reads content like this, needing no mTLS at any step:
- CORS. The public read listener answers a cross-origin
POSTwithAccess-Control-Allow-Origin: *and no credentials (Access-Control-Allow-Credentialsis never set — the content is public ciphertext, there is nothing to send credentials for), and answers theOPTIONSpreflight with204. So a page on any origin canfetch()the read endpoint directly. - Read.
POSTJSON-RPCdig.getContent(windowed byoffset/length) to the public read endpoint; reassemble the ciphertext bytotal_length(streaming contract). No client certificate, no bearer token. - Verify — the server is untrusted. Verify the first window's
inclusion_proofagainst the caller-supplied chain-anchored root (the CHIP-0035 singleton's current on-chainmetadata.root_hash, resolved independently of the serving node), then split bychunk_lensand AES-256-GCM-SIV-open each chunk with the client-derived key. A wrong/forged byte fails the proof or the authentication tag and is rejected. This is the same read-crypto every DIG read client runs; the anonymity of the transport does not relax it.
Because the read tier is CORS-* + anonymous, the exact same fetch works from a web page, a service worker, the extension, the SDK, or a headless agent — none of which can present a client certificate.
0a · rpc.dig.net is dual-mode — the public gateway serves both tiers
rpc.dig.net is one public, network-wide hostname, but it is not one listener — it is the public gateway, answering on two independently-authenticated fronts so a node-class client and a browser both reach it correctly:
| mTLS gateway front | Public-read front | |
|---|---|---|
| Who dials it | node-class clients — any DIG-identity-key holder: the dig-store CLI, dig-sdk, a standalone dig-node, DIG Browser in standalone-node mode — plus anonymous readers using an ephemeral certificate | browsers, the extension's fetch path, headless agents with no client certificate |
| Auth | mTLS, CERT_REQUIRED — the same handshake as the peer/control tier | none — plain HTTPS, CORS-* |
| Identity carried | the caller's peer_id derived from its presented cert (§1) — persistent for a node-class client's own identity-derived cert, disposable for an ephemeral read cert | none |
| Methods reachable | the full PEER/CONTROL surface and the PUBLIC-READ methods, all over the mTLS channel | the PUBLIC-READ surface only |
| Write authorization | §21.9 signed-request, checked against the caller's identity | none — this front carries no write route |
A node-class client never uses the public-read front — not even for reads. It resolves its endpoint through the fixed three-tier ladder — an explicit override wins outright, then dig.local, then localhost, then rpc.dig.net as the final fallback (see Point a consumer at your node and the dig-store CLI's ladder) — and at every tier of that ladder, including rpc.dig.net, it dials over mTLS with the client certificate derived from its DIG identity key (§1), layering §21.9 over the channel for any guarded call. This is a hard rule, not an optimization: a node-class client downgraded to the anonymous front would lose its stable peer_id (never recognized as a returning peer for allowlisting, PEX, or reputation), lose access to the PEER/CONTROL methods a full read path may need (DHT lookups, availability-for-sync, multi-source dig.fetchRange), and lose the channel-binding described next.
Anonymous reads over the mTLS front — the ephemeral client certificate
An anonymous reader is not required to use the plain-HTTPS public-read front — it MAY instead dial the mTLS gateway front for channel-bound request integrity, at the cost of running a TLS client-cert handshake. The mTLS listener requires a certificate (CERT_REQUIRED), not a known one — peer identity is verified by the peer_id hash, not a CA (§1) — so an anonymous caller satisfies the handshake with an ephemeral, self-signed client certificate:
- Generate a throwaway keypair and a self-signed leaf, scoped to the TLS session (or a short-lived batch of sessions) about to be opened. No registration and no persisted identity — the certificate exists only to complete the handshake.
- Complete the mTLS handshake with it. The gateway derives a
peer_idfrom it exactly as it would for any peer (§1); thispeer_idis disposable and carries no reputation, no allowlisting, and no authorization — it is a transport artifact, not a claimed identity. - Issue the read (
dig.getContentor another PUBLIC-READ method) inside that authenticated channel, optionally carrying the same §21.9-shaped signed-request headers a node-class client would send. - The TLS session binds the request. A signed request that travels inside a channel authenticated by that specific ephemeral certificate cannot be replayed over a different TLS session while still inside its 300-second freshness window — the channel itself becomes part of what a verifier can require to match. This is strictly additive integrity over the plain-HTTPS front, which binds a request to nothing.
The read itself is governed by the same rules regardless of which front carried it: read-only, no mutation, client-side self-verification against the chain-anchored root, decoy-on-miss (§7a). Reaching the mTLS front never grants an anonymous caller a PEER/CONTROL method — the boundary invariant is enforced by what the request names, not by which front carried it; an ephemeral-cert caller naming a peer/write/config method gets the same -32601 a plain-HTTPS caller would.
The rpc.dig.net mTLS front described in this section is a normative design contract, not yet a live endpoint — today rpc.dig.net serves only the plain-HTTPS public-read front, and existing node-class clients still reach it that way for reads. This is a rollout transition, not a standing exception to the rule above: an implementation MUST NOT hard-break (crash, refuse to run, or treat the gateway as unreachable) merely because the mTLS front does not exist yet. It gates on the front's presence — a capability probe or a handshake attempt — and, only while that probe fails, continues reaching rpc.dig.net over the plain-HTTPS front for reads, never for peer/write/config methods (which simply are not reachable there, §7a). Once the mTLS front is deployed, every node-class client switches to it at the rpc.dig.net tier, closing this transition.
1 · Peer identity + mTLS
A peer is identified by the SHA-256 of the DER-encoded SubjectPublicKeyInfo of the certificate it presents in the TLS handshake:
peer_id = SHA-256( SubjectPublicKeyInfo DER ) // 32 bytes
peer_idis aBytes32— 32 raw bytes, rendered as 64 lower-case hex on any text surface (dig-gossiptypes/peer.rs:pub type PeerId = Bytes32,peer_id_from_tls_spki_der).- The hashed input is the full
SubjectPublicKeyInfoASN.1 sequence (algorithm identifier + subject public-key bit string) lifted from the peer's X.509 leaf certificate — not the bare public-key bit string. Both sides recover the other'speer_idfrom the certificate exchanged during the handshake, so identity is bound to key material: impersonation requires the private key.
mTLS is mandatory
All peer-to-peer connections use mutual TLS over a WebSocket (wss://). This is a hard requirement — plaintext and server-only TLS are never accepted for a peer link.
- Both endpoints present a certificate. The dialing peer presents its cert to the listener; the listener presents its cert to the dialer. Each derives the other's
peer_idfrom the presented cert. - Self-signed node certificates are expected. Peer identity is verified by the
peer_idhash, not by a certificate authority — the CA chain is not the trust root here, the key hash is. A node generates its certificate on first run and reuses it thereafter. - The listener requires a client certificate (
CERT_REQUIRED). A peer that presents no certificate, or whose TLS handshake fails, is dropped — there is no fallback to a weaker transport. - After the TLS handshake, peers exchange a
Handshakecarrying thenetwork_id(the network genesis challenge as lower-case hex), the protocol version, the node's declared listen port, and its node type + capabilities. Anetwork_idmismatch, or a protocol version below the minimum-compatible floor, ends the connection. TheHandshakeis the Chia-streamable message (big-endian) — this layer speaks the Chia peer protocol, not a bespoke framing. - Unauthenticated peer traffic is rejected. A message received before a completed mTLS handshake +
Handshakeexchange is not processed. - The mainnet genesis challenge is not yet public, so a stock
dig-nodeuses an all-zero placeholder fornetwork_idand skips peer bring-up entirely (the read path — serving/fetching capsules — is unaffected). A developer or private deployment that wants the peer network up today can set theDIG_NETWORK_GENESISenvironment variable to a 64-character hex value (32 bytes); any valid non-zero value brings the gossip peer pool up under that id. See Configure dig-node.
A node's link to the relay is a standard server-authenticated wss:// connection (the relay presents a TLS certificate; the node does not present one to the relay). Peer↔peer identity is never delegated to the relay — end-to-end payloads carried over the relay remain authenticated by the peer protocol itself, so a relay cannot forge a peer.
Identity rotation
A node MAY rotate its network identity (regenerate its certificate, hence its peer_id) on an interval to reduce long-term linkability. Rotation is a network-layer concern only — it is independent of any consensus/validator identity and does not disturb address-book entries, which are keyed by IP:port, not by peer_id.
2 · Connection establishment — the NAT-traversal ladder
A node reaches a peer through a single abstract operation — connect(peer) — which attempts the strategies below in order and returns the first that yields a working, mTLS-authenticated link. This ordered ladder is the contract dig-nat implements; every strategy above "relayed" produces a direct peer link (no relay in the data path).
| # | Strategy | What it is | Relay's role | Result |
|---|---|---|---|---|
| a | DIRECT | The peer is already reachable — publicly routable, or a port is forwarded to it. Dial its advertised address. | none | Direct link |
| b | UPnP / IGD | Ask the local gateway (Internet Gateway Device) to map an external port to the node via UPnP, then advertise the mapped address. | none | Direct link |
| c | NAT-PMP | Request a port mapping from the gateway via NAT Port Mapping Protocol. | none | Direct link |
| d | PCP | Request a mapping via the Port Control Protocol (RFC 6887), NAT-PMP's successor. | none | Direct link |
| e | RELAY-COORDINATED HOLE PUNCH | Neither peer is directly reachable: the relay signals only — it relays the candidate-address exchange and coordinates a simultaneous open so both sides punch through their NATs. The data stream then flows peer-to-peer (see §5). | signalling only (low bandwidth) | Direct link |
| f | RELAYED / TURN transport | Every direct strategy failed: the relay proxies all of the connection's data as an untrusted bridge (see §6). | carries the data (high bandwidth) | Relayed link |
Rules:
- Attempt in order; stop at the first success. A node does not skip ahead to the relay while an earlier, cheaper strategy can still yield a direct path.
- Prefer hole-punch signalling (e) over full relaying (f). Both involve the relay, but they are not the same tier: in (e) the relay only brokers the introduction and the stream goes peer-to-peer, whereas in (f) the relay carries every byte. A node falls to (f) only when the hole punch of (e) fails — this saves relay bandwidth, since a brokered direct link costs the relay almost nothing. A successful (e) is a direct link (strategy result "Direct"), authenticated by the same mTLS
peer_idas every other tier. - Strategies (b)–(d) run once at startup / on address change, not per connection: they establish inbound reachability so future dials land as (a) DIRECT for peers dialing this node. A node that obtains a stable external mapping via UPnP/NAT-PMP/PCP advertises it as a candidate address so peers dial it directly.
- Candidate addresses. A node advertises the set of addresses at which it may be reachable — its configured/observed listen address, any UPnP/NAT-PMP/PCP-mapped external address, and its STUN-derived reflexive address (§3). Peers dial candidates most-direct-first and IPv6-first (§2a).
- Reflexive discovery precedes hole-punch. Before requesting a hole-punch a node learns its public reflexive
IP:portvia STUN (§3) and supplies it as theexternal_addrin the coordination exchange.
2a · Address family — IPv6-first, IPv4-fallback
All peer communication is IPv6-first, with IPv4 used only as a fallback. This applies wherever a node binds, advertises, or dials a peer address.
- Bind — dual-stack. A node's peer listener binds the IPv6 unspecified address
[::]as a dual-stack socket, so the single socket accepts both native IPv6 connections and IPv4 connections (via IPv4-mapped-IPv6) on the same port. It does not bind an IPv4-only wildcard. - Advertise — IPv6 first. A node advertises its real, directly-dialable candidate addresses ordered IPv6-first: a routable (global-unicast) IPv6 address precedes the IPv4 fallback. The wildcard bind address is never advertised (it is not dialable). A node with no routable address advertises no direct candidate and is reached via the relay-coordinated tiers (§2e/f).
- Dial — happy-eyeballs, IPv6 preferred. When dialing a peer with several candidate addresses, a node tries the peer's IPv6 candidate(s) first and falls back to IPv4 only when IPv6 fails or times out. The full candidate list is preserved through the dial path so every family is tried in IPv6-first order — not collapsed to one address.
IPv4 remains a fully supported fallback (many networks are still IPv4-only); it is the fallback, never the default, wherever IPv6 is possible.