niko_trust/docs/POW.md
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

4.1 KiB

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, 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": "<hex>", "counter": <uint> }.

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/powSum, 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.