Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ MUST NOT re-declare method names. Each entry carries a `served` class and `requi

| `served` | Meaning |
|---|---|
| `local` | Resolved by the node library (`handle_rpc`). |
| `local` | Resolved by the node library (`handle_rpc`). `requires_auth: false` except the HTTP-token-gated methods named below, which are `requires_auth: true`. |
| `passthrough` | Read path returns `-32601`; relayed verbatim to the upstream WHEN one is configured (§5.4), else returned to the caller as `-32601`. |
| `shell` | Answered by this service itself (`rpc.discover`). |
| `control` | The gated control plane (§7); always `requires_auth: true`. |
Expand All @@ -1056,8 +1056,11 @@ For the current node library (§2.2) the catalogue is:
`dig.getCollection`, `dig.listCollectionItems`, the L7 peer surface (`dig.getNetworkInfo`,
`dig.getPeers`, `dig.announce`, `dig.getAvailability`, `dig.listInventory`, `dig.fetchRange`),
all `cache.*` (`cache.getConfig`, `cache.setCapBytes`, `cache.clear`, `cache.listCached`,
`cache.removeCached`, `cache.fetchAndCache`, `cache.pushCapsule` — §5.5.3), and the chat subsystem
`chat.send` / `chat.poll` (§5.5.2).
`cache.removeCached`, `cache.fetchAndCache`, `cache.pushCapsule` — §5.5.3), the chat subsystem
`chat.send` / `chat.poll` (§5.5.2), and the reward reads `dig.getRewardProverStatus`,
`dig.listRewardDistributors`, `dig.getPayeeRewardClaimStatus`, `dig.getRewardDistributor`,
`dig.listRewardDistributorCommitments` (dig_ecosystem#3352 / #3351 / #3355 — see the
`requires_auth` clause below).
- **passthrough**: `dig.listCapsules` (needs a chain generation walk this node does not perform)
and `dig.getProofStatus` (polls an execution-proof JOB this node does not run — inventing a
status would be the fabrication the anti-fabrication rule forbids: an absent attestation is
Expand All @@ -1077,8 +1080,18 @@ Param/result schemas for the `dig.*`/`cache.*` methods are owned by the digstore
published on docs.dig.net (Protocol → the L7 read/RPC pages); this repo's OpenRPC document is a
method + error **discovery** catalogue with intentionally permissive schemas.

Every non-`control.*` method MUST have `requires_auth: false`; every `control.*` method MUST have
`served: "control"` and `requires_auth: true`.
Every `control.*` method MUST have `served: "control"` and `requires_auth: true`. A non-`control.*`
method MUST have `requires_auth: false` UNLESS the HTTP surface token-gates it — today the
holder-/holdings-revealing `cache.fetchAndCache` / `cache.pushCapsule` / `cache.listCached` (§14.3,
#2108), the node-identity chat pair `chat.send` / `chat.poll` (#1946), and the NODE-LOCAL reward reads
`dig.getRewardProverStatus` / `dig.listRewardDistributors` / `dig.getPayeeRewardClaimStatus`
(dig_ecosystem#3352: each volunteers this node's own prover inventory, funded-distributor set or
payee claim state, failing §7.2's WHO-NAMES-THE-SUBJECT test). Those keep their `served` class and
carry `requires_auth: true`. `requires_auth` is the COMPILED statement of the HTTP token gate: the set
of catalogued methods with `requires_auth: true` MUST equal the set `server.rs` refuses `-32030
UNAUTHORIZED` without a master or paired token (`requires_http_token`), and a test pins the equality.
The two chain-keyed reward reads `dig.getRewardDistributor` / `dig.listRewardDistributorCommitments`
are OPEN (dig_ecosystem#3351) and rate-bounded per source (§10, `-32034`).

#### 5.5.0. `dig.getContent` — the window envelope (#2071)

Expand Down Expand Up @@ -1469,8 +1482,9 @@ Two layers, both REQUIRED:
mismatched credential is answered `UNAUTHORIZED` (`-32030`, §10). Token comparison MUST be
constant-time (`ct_eq`) so verification cannot be probed via a timing oracle.

Exactly the `control.` method prefix is gated (`is_control_method`); unknown `control.*` methods
still pass the auth gate first, then yield `METHOD_NOT_FOUND`. The pairing-administration methods
The `control.` method prefix is token-gated as a class (`is_control_method`); the HTTP surface
additionally token-gates the non-`control.*` methods §5.5 enumerates (`requires_http_token`);
unknown `control.*` methods still pass the auth gate first, then yield `METHOD_NOT_FOUND`. The pairing-administration methods
(`control.pairing.list`/`approve`/`revoke`, §7.11) require the MASTER token specifically — a paired
token is NOT accepted for them. The exceptions are the wallet CHAIN READS — `control.wallet.balance`, `control.wallet.coins`,
`control.wallet.coinById`, `control.wallet.coinSpend`, `control.wallet.coinsByParent`,
Expand Down Expand Up @@ -3315,6 +3329,7 @@ method runs, and it MUST NOT be conflated with the wallet's own `-32043` egress
| -32031 | `NOT_SUPPORTED` | shell | A control operation this build/pin cannot perform (e.g. §21 sync without an identity). |
| -32032 | `CONTROL_ERROR` | shell | A control operation failed at runtime (distinct from bad input / absent capability). |
| -32033 | `CONTROL_INGRESS_LIMITED` | shell | An OPEN, token-less `control.*` read was refused AT INGRESS, before the request reached the dispatcher and before any DB work was done for it: this SOURCE's request bound is exhausted. The open reads present no credential, so without this bound an unauthenticated caller can drive unbounded SQLite work (`.coinById`/`.coinSpend` each run up to two lookups plus an LRU `UPDATE`) simply by asking repeatedly. The bound is PER SOURCE — one flooding source MUST NOT refuse another — and the node's OWN loopback operator is EXEMPT, so this code is only ever seen by a non-loopback caller (i.e. under `DIG_NODE_ALLOW_REMOTE=1`). It MUST stay DISTINCT from `-32043 WALLET_RATE_LIMITED`: that bound is on chain EGRESS and protects the third-party oracle, this one is on REQUESTS and protects this process. They fire for different reasons and have different remedies, so collapsing them would leave a caller unable to tell which bound it hit. Back off and retry. |
| -32034 | `REWARD_INGRESS_LIMITED` | shell | An OPEN reward chain read (`dig.getRewardDistributor` / `dig.listRewardDistributorCommitments`) was refused AT INGRESS: this SOURCE's request bound is exhausted. Each call is one upstream chain read for any caller-supplied launcher_id, so without this bound an anonymous caller drives unbounded upstream work. The bound is PER SOURCE (`RequestorId` — never the launcher id, which the caller controls); the loopback operator is EXEMPT, so only a `DIG_NODE_ALLOW_REMOTE=1` caller ever sees it. Distinct from -32033 (control-read ingress) and -32043 (wallet chain egress). Back off and retry. |
| -32040 | `WALLET_NO_CHAIN_SOURCE` | node | a wallet chain read (`control.wallet.balance`/`.coins`/`.coinById`/`.coinSpend`/`.coinsByParent`/`.peak`) or `control.wallet.broadcast` had NO live chain source able to answer an arbitrary (non-wallet) address. Distinct from a truthful `0`. A read the node can answer WITHOUT a chain source MUST NOT be refused with this code: the replica fast path and the node own chain-read cache both answer from bytes already in hand, so on `.coinById`/`.coinSpend` liveness is consulted only on a cache MISS. Refusing a cached answer because a third party is momentarily unreachable gives availability away for nothing on exactly the rows a lineage walk re-reads (a spent coin record is immutable), and the refusal then cascades into the retries that exhaust the `-32043` bound. The refusal MUST stay for a miss, and `.coinSpend` MUST treat a PARTIAL cache hit (spend cached, coin record not) as a miss, because the heights come from the record. |
| -32041 | `WALLET_NOT_SYNCED` | node | `control.wallet.balance` of the wallet's OWN address while the local DB is still syncing and no live fallback is attached (nothing can answer yet). |
| -32042 | `WALLET_READ_FAILED` | node | `control.wallet.balance`/`.coins`/`.coinById`/`.coinSpend`/`.coinsByParent`/`.peak` failed at the underlying DB / chain-source layer. On `.coinById` this INCLUDES a chain source that answered with a record for a DIFFERENT coin than the id asked for: a coin id is self-certifying (`SHA256(parent ‖ puzzle_hash ‖ amount)`), so a substituted record is a failed READ -- never that coin's record, and never `coin: null`. On `.coinSpend` it likewise INCLUDES a source that answered with another coin's spend, a puzzle reveal that does not tree-hash to the spent coin's own `puzzle_hash` (or will not parse), and a spend the coin record contradicts (no record, or a record calling the coin unspent) -- each fails CLOSED rather than being served unverified. On `.coinsByParent` it INCLUDES a source that returned a child naming a different parent, which fails the WHOLE page rather than being silently filtered (a filtered page is a lineage with an invisible hole). Distinct from `WALLET_NO_CHAIN_SOURCE` and `WALLET_NOT_SYNCED`. |
Expand Down
29 changes: 20 additions & 9 deletions crates/dig-node-core/src/seams/dig_rpc/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,10 +907,11 @@ impl RpcDispatch for Node {
"count": set.len()}});
}
// dig.getRewardProverStatus (dig_ecosystem#3269, dig-rewards-coin SPEC.md
// §2.3/§2.4) — CONTROL plane: loopback admin / in-process FFI ONLY (the token tier of
// this NODE-LOCAL read is dig_ecosystem#3352's decision, not #3351's), NEVER over the
// mTLS peer surface (absent from `is_peer_reachable_method`;
// `reward_methods_tier_guard.rs` fails closed on that). Reads the node's live
// §2.3/§2.4) — `Tier::Control` = local dispatch only, never the mTLS peer surface
// (`reward_methods_tier_guard.rs`); token-GATED on `POST /` at
// `server.rs::is_node_local_reward_read` (master or paired token, `-32030`;
// dig_ecosystem#3352) because it volunteers node-local state; the in-process FFI path
// stays open like `cache.*`. Reads the node's live
// `reward_prover_statuses` registry (empty until dig_ecosystem#3265 spawns a prover
// loop) — a REAL read of a real, currently-empty registry, so
// `{"statuses":{"outcome":"consulted","observed_at":N,"items":[]}}` means "this node
Expand Down Expand Up @@ -1013,7 +1014,11 @@ impl RpcDispatch for Node {
// dig-node-service `tests/server.rs`. Chain-derived state ONLY — never the local prover
// loop's self-reported state (see `GetRewardProverStatus` above for that). Goes entirely
// through `rewards::port::RewardsChainPort`: this crate never calls `dig-rewards-coin`
// itself (dig_ecosystem#3269 unit 0).
// itself (dig_ecosystem#3269 unit 0). Token-LESS does not mean UNBOUNDED: staying open
// per dig_ecosystem#3351 above, `POST /` also rate-bounds this read PER SOURCE at
// ingress (`-32034 REWARD_INGRESS_LIMITED`, dig_ecosystem#3355,
// `server.rs::is_open_reward_chain_read`) — a caller-supplied `launcher_id` is never
// the limiter's key, only the source is.
Some(Method::GetRewardDistributor) => {
let params = req.get("params").cloned().unwrap_or(json!({}));
let launcher_id = match parse_launcher_id_arg(&params) {
Expand Down Expand Up @@ -1095,8 +1100,11 @@ impl RpcDispatch for Node {
};
return json!({"jsonrpc":"2.0","id":id,"result": result});
}
// dig.listRewardDistributors (dig_ecosystem#3269 unit 2, SPEC §2.6) — CONTROL plane,
// same guard shape as the other reward handlers above. Two independently-consulted
// dig.listRewardDistributors (dig_ecosystem#3269 unit 2, SPEC §2.6) — `Tier::Control`
// = local dispatch only, never the mTLS peer surface (`reward_methods_tier_guard.rs`);
// token-GATED on `POST /` at `server.rs::is_node_local_reward_read` (master or paired
// token, `-32030`; dig_ecosystem#3352) because it volunteers node-local state; the
// in-process FFI path stays open like `cache.*`. Two independently-consulted
// halves (`funded` / `claimable`), each a `Half<RewardDistributorRef>` — SPEC §12.5
// clause 6's "reassuring zero" rule applies to EACH half separately.
//
Expand Down Expand Up @@ -1177,8 +1185,11 @@ impl RpcDispatch for Node {
return json!({"jsonrpc":"2.0","id":id,"result": result});
}
// dig.getPayeeRewardClaimStatus (dig_ecosystem#3268/#3269 unit 3, SPEC §12.5) —
// CONTROL plane: loopback admin / in-process FFI ONLY, absent from
// `is_peer_reachable_method` (`reward_methods_tier_guard.rs` fails closed on that).
// `Tier::Control` = local dispatch only, never the mTLS peer surface
// (`reward_methods_tier_guard.rs`); token-GATED on `POST /` at
// `server.rs::is_node_local_reward_read` (master or paired token, `-32030`;
// dig_ecosystem#3352) because it volunteers node-local state; the in-process FFI path
// stays open like `cache.*`.
// Dispatched through `Method::from_name(..)` like every other reward method — never
// the string pre-match above the enum, which bypasses this tier guard entirely
// (dig_ecosystem#3261: a reward RPC reachable by a peer is a money hole).
Expand Down
Loading
Loading