# Checkpoints, proofs and light nodes This document specifies the relay's signed commitment to its own object set, the proof endpoints built on it, and the light node that consumes both. It is the mechanism that turns the INV-1 caveat ("a relay can withhold anything") from an act of faith into a detectable event. Companion documents: [TRUST-MODEL.md](TRUST-MODEL.md), [API.md](API.md), [PROTOCOL.md](PROTOCOL.md). --- ## 1. The idea A relay stores a set of content-addressed objects. The sparse Merkle root of that set (`internal/smt`) is a **function of the set alone**: two relays that store the same objects compute the same root regardless of insertion order or restart history. A relay signs `{epoch, size, root, prev, created_at}` every interval or every N new objects; the `prev` field chains each head to the hash of its predecessor, making per-relay history tamper-evident without any global consensus. Decentralization falls out of gossiping heads: mirrors that disagree about what a relay stores are detectable by comparing what independent peers claim under the same relay key. No token, no mining, no block cadence — approval flows keep their 60-second lifetime, and phones can participate as light nodes instead of miners. ## 2. What the relay key may and may not do INV-1 amendment: the relay holds exactly one key. Its entire power is to describe its own storage — signing checkpoints and nothing else. Statement signing still lives exclusively in `internal/identity/signer`, which server code cannot import (enforced by `TestServerDoesNotImportSigner`), and the checkpoint package never touches protocol objects. Both properties are source-scanned by `TestNoSigningOutsideSigner`. The seed persists in `/relay_key.seed` (0600). In-memory mode generates an ephemeral key per process; heads are then valid only while the process lives. ## 3. Trie and proofs Hash domain (SHA-256): ``` leaf = H(0x00 || object_id) branch = H(0x01 || uvarint(prefixLen) || prefixBits || left || right) empty = H(0x02) ``` The branch prefix is inside the hash so trie shape is committed. Branches split exactly where their keys first differ, so shape is canonical. - **Inclusion proof** (`GET /v1/proof/object/{id}`): sibling path from the leaf to the root. Verifies only for the exact object id. - **Absence proof** (`GET /v1/proof/absent/{id}`): witnesses the spot where the id would live — either the neighbouring leaf reached by walking the id's bits, or the branch whose prefix the id leaves mid-way. This is what lets a consumer prove "nothing else about this subject exists" rather than trusting a relay's silence. Proofs verify against a root the consumer obtained independently (from a quorum of peers), never against a root the serving relay just made up. ## 4. Endpoints | Method | Path | Purpose | |--------|---------------------------|------------------------------------------------| | GET | `/v1/checkpoint/latest` | newest signed head `{bytes, signature, id, public_key, checkpoint}` | | GET | `/v1/checkpoint/{epoch}` | a specific historical head | | GET | `/v1/proof/object/{id}` | inclusion proof against the current root | | GET | `/v1/proof/absent/{id}` | absence proof against the current root | | POST | `/v1/gossip/checkpoint` | announce a peer's signed head | | GET | `/v1/peers/heads` | observed peer heads | `bytes` is authoritative (base64); the decoded `checkpoint` view is a convenience, mirroring how envelopes work. Gossip accepts a head only if the signature verifies under the announcing key and the id matches the bytes; same-key/same-epoch/different-bytes is recorded as divergence (`trust_gossip_divergence`) and exposed, never silently resolved. Configuration flags: `-ckpt-interval` (default 60s), `-ckpt-every` (default 128 new objects). ## 5. Light nodes `cmd/lightnode` follows heads from `-peers`, requires `-quorum` distinct relay keys to agree on one root (TOFU pinning unless `-pin-keys` given), then serves the read API locally: - every served object was proven present in the agreed set before caching; - every absence claim was proven against the agreed root; - storage is the working set plus heads — no history; - cached objects keep serving after all peers disappear. Run it against two or more independent relays holding the same data: ``` lightnode -peers https://a.example,https://b.example \ -quorum 2 -cache-dir /var/lib/lightnode -addr :8090 ``` The node makes no authorization decisions; consumers verify statements locally as before (INV-5). Trust your own verification stack, not the mirror.