dig-rpc-types 0.2.1

Canonical DIG-node JSON-RPC interface: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server logic.
Documentation
# 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": "2.0", "id": <id>, "method": <string>, "params": <object|absent> }
```

- `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

| Code | Machine code | Origin | Meaning |
|---|---|---|---|
| `-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),
source? ("local"|"remote" — node profile additive tag)
```

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).