# dig-rpc-types — normative specification
**Status:** normative. This is the authoritative contract for the DIG-node
JSON-RPC interface. An independent reimplementation of a DIG node's RPC surface
MUST match the shapes, codes, names, and tiers defined here byte-for-byte. It
cross-references [`SYSTEM.md`](../../../SYSTEM.md) (cross-repo interaction map)
and the docs.dig.net protocol pages (the published, user-facing protocol spec);
the three MUST agree.
`dig-rpc-types` is the single source of truth both DIG node implementations —
the digstore `dig-node` crate and the standalone `dig-node` binary — depend on.
It defines types only: no I/O, no async, no server logic, no crypto.
---
## 1. JSON-RPC 2.0 envelope
The wire is strict [JSON-RPC 2.0](https://www.jsonrpc.org/specification).
### 1.1 Request
```json
- `jsonrpc` MUST be the literal string `"2.0"`. Any other value is rejected on
decode.
- `id` is numeric, string, or `null` (notifications; DIG RPC does not use them).
A response MUST echo the request `id` unchanged.
- `params` MAY be absent for methods that take none.
### 1.2 Response
```json
{ "jsonrpc": "2.0", "id": <id>, "result": <value> }
{ "jsonrpc": "2.0", "id": <id>, "error": <RpcError> }
```
Exactly one of `result` or `error` is present — never both, never neither. For
any well-formed request body the HTTP status is `200`; the error is carried in
the JSON envelope.
---
## 2. Error taxonomy
### 2.1 The error object
Every error is the uniform envelope:
```json
{
"code": <int>,
"message": <string>,
"data": { "code": <UPPER_SNAKE_CASE>, "origin": <origin>, "redirect"?: <RedirectInfo>, ...extra }
}
```
- `code` is the numeric wire code (§2.2). It is a **published contract** and
never changes once assigned.
- `data.code` is the stable `UPPER_SNAKE_CASE` machine identifier; it mirrors
`code` one-to-one and is the branch key an agent keys on.
- `data.origin` is one of `node`, `peer`, `upstream`, `onion`, `control` — the
subsystem the failure arose in.
- `data.redirect` is present ONLY on `-32008` (§2.3).
- Additional method-specific fields MAY be flattened onto `data`.
There is ONE constructor helper (`RpcError::new` / `of` / `code_only`); every
error, whether minted on a node's read path or a control gate, carries
`data.code` + `data.origin`.
### 2.2 Code set
| `-32700` | `PARSE_ERROR` | node | request body is not valid JSON |
| `-32600` | `INVALID_REQUEST` | node | not a valid Request object |
| `-32601` | `METHOD_NOT_FOUND` | node | method not implemented, OR a non-allowlisted method called on the peer surface |
| `-32602` | `INVALID_PARAMS` | node | missing/malformed params |
| `-32603` | `INTERNAL_ERROR` | node | well-formed call failed (network profile) |
| `-32000` | `SERVER_ERROR` | node | generic server error (config write, file I/O, chain read) |
| `-32004` | `RESOURCE_UNAVAILABLE` | node | resource not available at the requested root (genuine infra miss; distinct from a content miss, which is a decoy and never an error) |
| `-32005` | `ROOT_NOT_ANCHORED` | node | requested/served root ≠ chain-anchored root, chain unreachable, or no confirmed generation (read-path pin failing closed) |
| `-32006` | `PEER_UNREACHABLE` | peer | no NAT-traversal strategy reached the named peer |
| `-32007` | `RANGE_NOT_SATISFIABLE` | node | `offset ≥ total_length` or the range is otherwise unsatisfiable |
| `-32008` | `CONTENT_REDIRECT` | node | content held elsewhere — `data.redirect` names the holders (§2.3) |
| `-32010` | `UPSTREAM_ERROR` | upstream | an upstream/proxy fetch failed |
| `-32011` | `STAGE_INVALID_INPUT` | node | `dig.stage`: dir unreadable / walk budget exceeded |
| `-32012` | `STAGE_NO_FILES` | node | `dig.stage`: no files to compile |
| `-32013` | `STAGE_OVER_CAP` | node | `dig.stage`: input exceeds the store cap |
| `-32014` | `STAGE_COMPILE_FAILED` | node | `dig.stage`: compile / IO failure |
| `-32020` | `ONION_CIRCUIT_UNAVAILABLE` | onion | a `mode:"privacy"` read could not be served privately (fails closed) |
| `-32021` | `PRIVACY_REQUIRES_LOCAL_NODE` | onion | privacy mode requires the caller be a local originator |
| `-32022` | `ONION_HOPS_OUT_OF_RANGE` | onion | requested hop count outside `[2, 5]` |
| `-32030` | `UNAUTHORIZED` | control | control-plane call not authorized |
| `-32031` | `NOT_SUPPORTED` | control | control-plane method not supported here |
| `-32032` | `CONTROL_ERROR` | control | control-plane runtime error |
**Collision resolution (normative):** the onion codes `-32020/-32021/-32022`
are the published normative contract (docs.dig.net) and KEEP their numbers. The
control-plane errors — previously squatting those values — are renumbered to
`-32030/-32031/-32032`. This renumbering MUST land before the onion feature
ships.
`-32010 UPSTREAM_ERROR` is the dedicated code for an upstream/proxy fetch
failure. A node MAY currently fold upstream faults into `-32000`; new writers
SHOULD emit `-32010` so a client can distinguish an upstream fault from a local
one.
### 2.3 The redirect payload (`-32008`)
`error.data.redirect`:
```json
{
"content": { "store_id": 64hex, "root"?: 64hex, "retrieval_key"?: 64hex },
"providers": [ { "peer_id": 64hex, "addresses": [ { "host", "port", "kind" } ] } ],
"redirect_depth": <uint>,
"max_redirects": <uint = 4>
}
```
The caller re-requests the same `content` against a peer in `providers`, echoing
`redirect_depth` in its `params` so the hop budget stays monotone; it stops when
`redirect_depth` reaches `max_redirects`. A node never redirects to itself.
---
## 3. Access tiers
Every method has one PRIMARY tier:
- **`public-read`** — anonymous, self-verified content + discovery reads over
plain HTTPS (browser) or mTLS.
- **`peer`** — the mTLS peer surface between DIG nodes; `peer_id = SHA-256(TLS
SPKI DER)`.
- **`control`** — loopback / in-process only; NEVER reachable over the peer
surface.
### 3.1 The peer-surface allowlist
The peer surface is an **allowlist**, not a denylist. The peer client-cert
verifier authenticates a `peer_id`; it does NOT authorize. A method not on the
allowlist answers `-32601` over the peer surface. The allowlist is exactly:
```
dig.getContent dig.getNetworkInfo dig.getPeers dig.announce
dig.getAvailability dig.listInventory dig.fetchRange
dig.getAnchoredRoot dig.getCollection dig.listCollectionItems
```
The three chain-anchored reads (`getAnchoredRoot`, `getCollection`,
`listCollectionItems`) are PRIMARY `public-read` yet ALSO peer-reachable.
Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are NEVER
peer-reachable.
---
## 4. Method catalogue
Names are stable. Params/results are defined field-for-field in the crate's
`types` module; the following is the authoritative summary. Optional fields are
marked `?`.
### 4.1 public-read
- **`dig.getContent`** `{ store_id, retrieval_key, root?, offset?, mode?, redirect_depth? }`
→ a [content chunk](#5-the-content-chunk-object).
- **`dig.getCapsule`** (alias **`dig.getModule`**), **`dig.getManifest`**,
**`dig.getMetadata`**, **`dig.listCapsules`**, **`dig.getProof`**,
**`dig.getProofStatus`** — the read/discovery subset (see docs.dig.net dig-rpc
spec; the network profile carries the extra chunk fields in §5).
- **`dig.getAnchoredRoot`** `{ store_id }` → `{ store_id, root }`.
- **`dig.getCollection`** `{ launcher_ids[≤10000], did? }` →
`{ did?, declared_did?, item_count, resolved_count, royalty_basis_points? }`.
- **`dig.listCollectionItems`** `{ launcher_ids[≤10000], offset?, limit?≤200 }` →
`{ items[], offset, limit, total, next_offset? }`.
- **`dig.health`** → `{ status, version?, network_id?, methods[] }`.
- **`dig.methods`** → `{ methods[] }`.
### 4.2 peer
- **`dig.getNetworkInfo`** → `{ peer_id?, network_id, listen_addr, reflexive_addr?, candidate_addresses[], reachability, relay:{url,reserved} }`.
- **`dig.getPeers`** → `{ peers[] }` (each a `{ peer_id, addresses[] }`).
- **`dig.announce`** `{ peer_id, addresses[] }` → `{ accepted, known_peers }`.
- **`dig.getAvailability`** `{ items[≤512] }` → `{ items[] }` (each answer:
`{ available, roots?, total_length?, chunk_count?, complete?, providers? }`).
- **`dig.listInventory`** `{ store_id?, limit? }` →
`{ store_id, roots[] }` (with `store_id`) OR `{ stores[] }` (without).
- **`dig.fetchRange`** `{ store_id, root, retrieval_key, offset?, length, capsule?, redirect_depth? }`
→ a [range frame](#6-the-range-frame). Capsule mode is not yet served
(`-32004`).
### 4.3 control (loopback / in-process only)
- **`dig.stage`** `{ dir, store_id?, salt?, metadata? }` →
`{ capsule, store_id, root, module_path, size, content_address?, files[], ephemeral? }`.
- **`cache.getConfig`** → `{ cap_bytes, used_bytes, cache_dir, shared }`.
- **`cache.setCapBytes`** `{ cap_bytes }` → `{ cap_bytes }` (floored at 64 MiB).
- **`cache.clear`** → `{}`.
- **`cache.listCached`** → `{ cached: [ { capsule, store_id, root, size_bytes, last_used_unix_ms } ] }`.
- **`cache.removeCached`** `{ store_id, root }` → `{ removed }`.
- **`cache.fetchAndCache`** `{ store_id, root }` →
`{ status: "cached"|"already_cached"|"failed", size_bytes?, served_root?, message? }`.
- **`control.peerStatus`** → `{ running, peer_id?, network_id, relay:{url,reserved}, connected_peers, last_error? }`.
- **`rpc.discover`** → the OpenRPC 1.2.6 document (§7).
The canonical cache-path field name is `cache_dir` everywhere.
---
## 5. The content-chunk object
One type serves both profiles. `dig.getContent` (node profile) populates:
```
ciphertext (b64), root (64hex), complete (bool),
next_offset? (uint, iff not complete),
inclusion_proof? (b64, first window only), chunk_lens? (uint[], first window only),
The **network profile** (`rpc.dig.net`) additionally populates
`total_length`, `length`, `offset`, and `program_hash`. Those four fields are
network-profile-only; the node profile omits them.
`inclusion_proof` and `chunk_lens` appear on the FIRST window only (`offset == 0`).
---
## 6. The range frame
`dig.fetchRange` returns one frame:
```
offset (uint), length (uint), bytes (b64), complete (bool)
```
The FIRST frame (`offset == 0`) additionally carries:
`total_length`, `chunk_lens`, `chunk_index`, `inclusion_proof`, `root`.
---
## 7. OpenRPC discovery
`rpc.discover` returns an [OpenRPC 1.2.6](https://spec.open-rpc.org/) document
GENERATED from this crate's method / tier / error tables, so discovery can never
drift from the contract. Each method carries the `x-dig-tier` and
`x-dig-peer-reachable` extensions; `components.x-dig-errors` lists every code
with its numeric + machine forms; `x-dig-peer-allowlist` lists the peer-surface
methods. `dig.health` / `dig.methods` are thin summaries over the same table.
---
## 8. Shape-dispatched peer frames
The DHT and PEX wires travel over the mTLS peer transport but are dispatched on
a `type` discriminator, not a JSON-RPC `method` field.
- **DHT** (`find_node`, `find_providers`, `add_provider`, `ping`) — Kademlia
content location; requests carry `type` + `node_id` (+ `target_id` /
`content_key` / `provider`); responses carry `closer[]` / `providers[]` /
`stored` / `node_id`.
- **PEX** (`pex_handshake`, `pex_snapshot`, `pex_delta`, `pex_error`) — peer
exchange; snapshot ≤ 200 peers, delta ≤ 50 added / ≤ 50 removed.
---
## 9. Conformance
The crate ships conformance vectors (`tests/conformance.rs`) taken field-for-field
from the canonical node's emitted JSON. Both node implementations test against
these vectors; a change that breaks a vector is a wire-breaking change and MUST
bump [`INTERFACE_VERSION`](src/lib.rs).
## 10. Stability
- `ErrorCode` and `Method` are `#[non_exhaustive]`; adding a code/method is
additive.
- Numeric codes, machine codes, and method names never change once assigned.
- New optional fields are additive; removing/reshaping a field is a wire break
(major bump).