# Proof-of-work admission control This document specifies the relay's anti-abuse mechanism: a BLAKE3 proof-of-work that anonymous clients must pay before the relay does work for them. It is transport-layer only. It never enters TCE bytes, never affects a signature, and is invisible to verifiers: PROTOCOL.md §9 keeps server-side data out of signed statements, and this mechanism is server-side data. Companion documents: [API.md](API.md), [TRUST-MODEL.md](TRUST-MODEL.md). Frozen vectors: `testdata/vectors/pow_vectors.json`, generated by `tools/reference/pow_reference.py` and reproduced byte-for-byte by the Go test suite. --- ## 1. Why The relay is an open bulletin board. Two cheap defenses already exist — body caps and per-IP rate limits — but a botnet behind many IPs defeats IP limits, and honest low-volume clients are exactly the traffic worth keeping. A proof of work taxes submission per attempt, not per address: spam stops being free, while a real service posting a claim pays well under a second of hashing. ## 2. Scheme ``` sum = BLAKE3_keyed(key, DOMAIN || target || counter_be) valid ⟺ leading_zero_bits(sum) ≥ difficulty DOMAIN = "trust.n1ko.dev/pow/1" key = 32 bytes from the relay's CSPRNG, issued once target = object content ID for storage; 32 zero bytes for authentication counter = unsigned 32-bit integer, big-endian on the wire ``` Properties: - **Verification is one hash call.** The asymmetry between solver and checker is total. - **The keyed mode matters.** The hash input includes a fresh server-chosen key, so solutions cannot be precomputed, pooled across relays, or reused after a challenge is consumed. - **The target binds storage proofs to one submission.** A captured but unspent challenge only helps an attacker store the exact object its victim was going to store. - **Single use.** A challenge key is deleted when first presented, whether or not the proof verifies. Replay of a captured `{key, counter}` pair fails. - **Difficulty is in leading zero bits**, capped at **30**: solving ranges over a uint32 counter, and beyond 30 bits the counter space no longer guarantees a solution exists. ## 3. Endpoints and flow ### Storage (`POST /v1/objects`) 1. Client builds and signs the object locally; `object_id = SHA-256(tce)`. 2. `POST /v1/pow/challenge` with body `{"purpose": "put"}` → `{ "key", "difficulty", "ttl" }`. 3. Client solves for `(key, target=object_id, difficulty)` → `counter`. 4. `POST /v1/objects` with the envelope plus `"pow": { "key": "", "counter": }`. ### Authentication (`POST /v1/auth/challenge`) Identical, with `{"purpose": "auth"}` and target = 32 zero bytes; the solved proof accompanies the request body as `"pow"`. Binding to nothing beyond the fresh key is intentional here: single-use consumption carries the protection. ### Defaults | Tier | Flag | Default | |---|---|---| | storage | `-pow-put-bits` | 22 (~4M hashes; well under a second on desktop, seconds on a phone) | | authentication | `-pow-auth-bits` | 18 | `0` disables a tier. Values above 30 are clamped. Challenge issuance itself is additionally capped at 30/min per IP. ## 4. Failure modes | Situation | Response | |---|---| | tier enabled, no `pow` field | `429 proof of work required` | | malformed key hex | `429 malformed proof of work` | | unknown / expired / already-spent key | `429 unknown or expired challenge` | | hash misses the target | `429 invalid proof of work` | | challenge endpoint over rate limit | `429 rate limited` | A spent-but-invalid challenge is still consumed: guessing counters must not get free retries against one key. ## 5. Reference implementations - Go: `internal/pow` — `Sum`, `Verify`, `Solve`, `LeadingZeroBits`. - Python reference (independent, implements the single-block BLAKE3 it needs from scratch): `tools/reference/pow_reference.py`. - Frozen vectors: 6 accept cases (including the zero-target authentication binding, up to difficulty 16) with minimal recorded counters, plus rejects (off-by-one counter, wrong key) and configuration rejects (difficulty > 30). Any implementation agreeing with both files agrees with the specification.