niko_trust/docs/API.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

7.8 KiB

HTTP API

Base path: /v1. Transport is JSON. Requests and responses are transport.Envelope objects (see docs/PROTOCOL.md).

Authentication

Read endpoints require a session token. Obtain one with the challenge/assert handshake (AuthAssertion bound to the server's audience):

POST /v1/pow/challenge {"purpose":"auth"} -> { "key", "difficulty", "ttl" }
POST /v1/auth/challenge ({"pow":{...}})   -> { "challenge": "<hex>" }
POST /v1/auth/assert  (envelope)          -> { "session_token", "identity", "scope" }

When -pow-auth-bits is enabled, /v1/auth/challenge requires a solved challenge first; see POW.md.

Send the token as Authorization: Bearer <token> or ?token=<token>. A session is valid for 30 minutes; each challenge is single-use and expires after 5 minutes. Scopes: read:claims, read:requests, read:responses, read:revocations, plus the generic read / *.

Rate limit: POST /v1/auth/challenge is capped at 30/min per IP (429).

Endpoints

Method Path Auth Query / body Purpose
POST /v1/objects none Envelope + optional pow Store a signed object (claim/request/...)
GET /v1/objects/{id} none Fetch one object by content id
GET /v1/objects?ids=a,b,c none ≤100 ids Batch fetch; missing ids are omitted
POST /v1/pow/challenge none {"purpose":"put"|"auth"} Issue a single-use PoW challenge key
GET /v1/claims session subject, limit,offset Claims about a subject
GET /v1/requests session recipient,limit,offset Approval requests to a recipient
GET /v1/responses session request,limit,offset Responses to a request
GET /v1/revocations session claim,limit,offset Revocations targeting a claim
GET /v1/config none { "audience": "...", "relay_pubkey": "..." }
GET /v1/metrics none Prometheus-style metrics
GET /v1/checkpoint/latest none Newest signed object-set commitment
GET /v1/checkpoint/{epoch} none A specific historical head
GET /v1/proof/object/{id} none Inclusion proof against the current root
GET /v1/proof/absent/{id} none Absence proof against the current root
POST /v1/gossip/checkpoint none peer head announcement Record another relay's signed head
GET /v1/peers/heads none Observed peer heads
GET /v1/ws session WebSocket control frames Live stream of newly stored envelopes
GET /v1/healthz none Liveness (200)
GET /v1/readyz none Readiness (200 / 503)

WebSocket streaming (GET /v1/ws)

Authenticate like any read endpoint (bearer token; browsers may use ?token=). The token's scopes gate channels exactly as the REST list endpoints do: read:claims, read:requests, read:responses, read:revocations, or the generic read/*.

After connecting, send JSON text frames:

{ "op": "subscribe", "channel": "claims", "key": "<subject address>" }
{ "op": "unsubscribe", "channel": "claims", "key": "..." }

Channels mirror the list endpoints: claims (by subject), requests (by recipient), responses (by request hash), revocations (by claim id). Every newly stored object on a subscribed channel arrives as:

{ "event": "object", "channel": "...", "key": "...",
  "object_id": "...", "envelope": { "tce": "...", "signature": "..." } }

The envelope is raw and unverified by design: verify it locally exactly as if you had fetched it yourself. Limits: ≤16 subscriptions per connection; a client that cannot keep up is disconnected rather than allowed to stall broadcasts; the session's 30-minute lifetime applies mid-stream.

Checkpoint and proof endpoints are specified in CHECKPOINT.md; the proof-of-work scheme in POW.md.

Storing objects (POST /v1/objects)

  • The relay does not verify the signature (see docs/TRUST-MODEL.md); verification is the verifier's job. Any well-formed envelope is stored.
  • Idempotent: replaying the same object returns the same object_id with 200 and creates no duplicate.
  • Body cap: the request body is limited to MaxClaimTCE*2 + 1024 bytes (~9 KiB). Larger bodies get 413.
  • Per-subject quota: a subject may hold at most 1000 claims. The 1001st distinct claim returns 422 (subject quota exceeded).
  • One response per request: a second ApprovalResponse for an already answered request returns 422 (request already answered).
  • Rate limit: 60/min per IP (429).

Request / response shape:

POST { "tce": "<base64>", "signature": "<base64>",
       "pow": { "key": "<hex>", "counter": <uint> } }
200 { "object_id": "<content-id>" }
422 { "error": "server: subject ... quota exceeded" }
413 { "error": "payload too large" }
429 { "error": "proof of work required" | "invalid proof of work" | "rate limited" }

The pow field is required when -pow-put-bits > 0 (the default); see POW.md for the scheme.

Listing endpoints (claims/requests/responses/revocations)

All require a session with the matching read:* scope and return a JSON object with a single list key (claims, requests, responses, revocations). Each entry is a raw Envelope ({ "tce": "<base64>", "signature": "<base64>" }); decode the TCE to read the issuer/subject/fields.

  • subject / recipient / request / claim is required; omitting it is 400.
  • Pagination: results are ordered lexicographically by content id.
    • limit (max items, 0 = no cap) and offset (skip) query params. Example: ?subject=trust1def&limit=10&offset=20.
    • after=<object_id> is the stable cursor: only ids greater than it are returned. Prefer it over offset — offset shifts when objects are inserted concurrently, a cursor does not.
  • Missing/invalid scope is 403; missing token is 401.

Health & metrics

  • GET /v1/healthz200 { "status": "ok" }.
  • GET /v1/readyz200 { "status": "ready" }, or 503 if not ready.
  • GET /v1/metricstext/plain Prometheus exposition with at least: trust_objects_stored, trust_objects_rejected, trust_challenges_issued, trust_assertions_ok, trust_assertions_failed, trust_sessions_active, trust_pow_ok, trust_pow_failed.

Status code summary

Code Meaning
200 OK / stored / fetched
400 Malformed request (bad envelope, missing param)
401 Missing/invalid auth token or assertion
403 Token present but insufficient scope
404 Unknown object id
413 Request body exceeds the size cap
422 Well-formed but rejected (quota / already answered)
429 Rate limit exceeded
503 Server not ready