dig-node-control-interface 0.18.0

Canonical client <-> dig-node CONTROL interface contract: the method catalog for controlling/querying a running dig-node (config, status, peers, subscriptions, cache, wallet), transport-agnostic. SSOT so client and node can't drift.
Documentation

dig-node-control-interface

The canonical client ⇄ dig-node CONTROL interface contract. One ecosystem definition of the management/query surface a client (the CLI dign, the browser extension, dig-app, hub) uses to control and query a running dig-node — status, config, cache, hosted/pinned stores, §21 sync, the peer network, subscriptions, the auto-update beacon, live log level, and control-token pairing — so the client side and the node side can never silently drift. Transport-agnostic: rides the dig-ipc-protocol local session, or loopback-mTLS + a signed control token over HTTP/WebSocket for clients that aren't on the local pipe.

  • License: Apache-2.0 OR MIT
  • Spec: SPEC.md (normative) — this README is the at-a-glance interface reference.
[dependencies]
dig-node-control-interface = "0.2"

Usage

Client side — build a typed request, carry it over your transport, parse the typed result:

use dig_node_control_interface::{
    params::SetCapParams,
    traits::{build_request, parse_response},
    envelope::JsonRpcResponse,
};

let call = SetCapParams { cap_bytes: 128 * 1024 * 1024 };
let request = build_request(1.into(), &call);          // → control.cache.setCap envelope
// ... send `request` over dig-ipc / loopback-mTLS, receive `response` ...
# let response = JsonRpcResponse::success(1.into(), serde_json::json!({ "cap_bytes": 134217728 }));
let result = parse_response::<SetCapParams>(response)?; // → SetCapResult { cap_bytes }
# Ok::<(), dig_node_control_interface::ControlError>(())

Node side — implement ControlHandler (one typed method per control method); the provided dispatch routes a raw request to the right method and builds the response.

Authorization

Every control.* method is token-gated unless the tables below mark it : present the node's local control token as the X-Dig-Control-Token header (preferred) or a params._control_token field. Open today are pairing.request / pairing.poll (a token-less client uses them to obtain a scoped token after local operator approval), the wallet CHAIN READS (they need only public chain data — an address or a coin id, never a key), and control.peerCounts (three integers about this node's own connectivity). The three control.pairing.* admin methods require the MASTER token specifically.

UNAUTHORIZED from an OPEN method means the node predates it — upgrade the node. From a GATED method (control.wallet.broadcast / .arrivals / .watch / .unwatch / .watched) it means what it says — find the token. See SPEC.md §4.2.

Full interface reference

Every control method, for LLM/agent lookup without reading code. Auth: T = requires the control token, M = requires the MASTER token, = open. Route: how the node resolves it — own (service shell), del (engine), open (bootstrap).

Status / config / log

Method Auth Route Params Result
control.status T own {running, service, version, commit, protocol, uptime_secs, addr, upstream, cache:{cap_bytes,used_bytes,dir,shared}, hosted_store_count, cached_capsule_count, pinned_store_count, sync:{available}}
control.config.get T own {addr, port, upstream, upstream_override:string|null, cache_dir, cache_shared, config_path, sync_available}
control.config.setUpstream T own {upstream:string} {upstream, requires_restart:true}
control.log.setLevel T own {filter:string} (an EnvFilter directive) {filter}

Cache

Method Auth Route Params Result
control.cache.get T own {cap_bytes, used_bytes, dir, shared}
control.cache.setCap T own {cap_bytes:u64} {cap_bytes} (floored at 64 MiB)
control.cache.clear T own {cleared:true}

Hosted / pinned stores

Method Auth Route Params Result
control.hostedStores.list T own {stores:[{store_id, pinned, capsule_count, total_bytes, capsules:[{capsule,root,size_bytes,last_used_unix_ms}]}]}
control.hostedStores.pin T own {store:"storeId[:root]"} {store_id, root:string|null, pinned:true, fetch:{status,…}}
control.hostedStores.unpin T own {store} {store_id, unpinned:bool, evicted_capsules:u64}
control.hostedStores.status T own {store} {store_id, pinned, capsule_count, total_bytes, capsules}

§21 whole-store sync

Method Auth Route Params Result
control.sync.status T own {available, method, pinned_total, pinned_synced, whole_store_trigger_supported}
control.sync.trigger T own {store:"storeId:root"} {store_id, root, status:"synced", size_bytes, served_root}

Auto-update beacon (proxied to dig-updater)

Method Auth Route Params Result
control.updater.status T own (proxied beacon status)
control.updater.setChannel T own {channel:"nightly"|"stable"} (proxied)
control.updater.pause T own {until?:u64} (unix secs; omit = indefinite) (proxied)
control.updater.resume T own (proxied)
control.updater.checkNow T own (proxied)

Pairing (control-token lifecycle)

Method Auth Route Params Result
control.pairing.list M own (pending requests + issued tokens)
control.pairing.approve M own {pairing_id:string} {approved:true, client_name, token_id}
control.pairing.revoke M own {token_id:string} {revoked:bool, token_id}
pairing.request open {client_name:string} {pairing_id, pairing_code, expires_ms}
pairing.poll open {pairing_id:string} {status, token?} (token present once, after approval)

Peers (delegated to the engine)

Method Auth Route Params Result
control.peerStatus T del (peer-pool + relay-reservation snapshot, incl. per-peer peers[]); its relay.peer_count counts THE RELAY's peers, not this node's
control.peerCounts del {dig_peer_count:u32|null, chia_peer_count:u32|null, known_dig_peer_count:u32|null} — DIG content/gossip peers CONNECTED, CHIA full-node peers, and DIG peers this node KNOWS OF (its own address book — a local lower bound, never the network size); 0 is observed, null unobservable
control.peers.connect T del {peer:string} (address or peer_id) {connected:true, peer_id}
control.peers.disconnect T del {peer:string} (peer_id) {disconnected:true, peer_id}
control.chiaPeers.add M own {ip:string} (bare IP literal, canonical) {added:true, ip, port, corroboration_bypassed, notice:string} — trusting a peer means believing it WITHOUT corroboration, so it takes the MASTER token: the entry outlives the token that wrote it and control.pairing.revoke does not remove it
control.chiaPeers.list T own none {peers:[{ip,port,peak_height:u32|null,user_managed,banned}]}peak_height is that peer's CLAIM; null unobservable, never 0. The only enumeration of the banned set
control.chiaPeers.remove M own {ip:string, ban?:bool} {outcome:"removed"|"no_such_peer", ip, banned} — the ONLY un-trust remedy, so it reports when it matched nothing rather than always succeeding

Wallet chain transport (delegated to the engine)

The caller-addressed reads and the chain-position reads are OPEN; the arrival cursor and the push are token-gated. The node never signs — it reads chain state and pushes bytes somebody else signed.

Method Auth Route Params Result
control.wallet.balance del {address:string, asset:"xch"|"dig"} {balance, pending, source, synced, peak_height}
control.wallet.coins del {address:string, asset:"xch"|"dig"} {coins:[WalletCoinRecord], source, synced, peak_height}
control.wallet.coinById del {coin_id:string} (64-hex, 0x accepted) {coin:WalletCoinRecord|null, source, synced, peak_height}; coin:null = the chain holds no such coin
control.wallet.coinSpend del {coin_id:string} (64-hex, 0x accepted) {spend:{coin,puzzle_reveal,solution}|null, source, synced, peak_height}; spend:null = the chain holds no spend of that coin (unspent, or unknown) — NEVER "could not answer", which is an error. puzzle_reveal MUST tree-hash to the coin's puzzle_hash
control.wallet.coinsByParent del {parent_coin_id:string, after_coin_id?:string, limit?:u32} (ids 64-hex, 0x accepted; limit 1..=1000, default 100, REFUSED out of range — never clamped) {coins:[WalletCoinRecord], complete:bool, cursor:string|null, source, synced, peak_height}; ONE PAGE of the parent's DIRECT children, ascending by coin_id, ONE hop, never a recursive walk. complete:false = TRUNCATED, resume from cursor — never infer completeness from the page length. coins:[] = no known children, never an unreachable chain
control.wallet.arrivals T del {after_seq:u64=0, limit?:u32} {arrivals:[WalletArrivalRecord], cursor, latest}; TOKEN-GATED: the caller supplies only a cursor, so the answer names this node's own watched puzzle hashes. CONFIRMED incoming funds only, never the wallet's own change. Resume from cursor, never latest
control.wallet.peak del {peak_height:u32|null, synced:bool}; synced = catch-up COMPLETED, weaker than syncStatus's
control.wallet.syncStatus del {phase:"not_started"|"syncing"|"synced"|"no_wallet_enrolled"|"wallet_not_unlocked", peak_height:u32|null, chia_peer_count:u32|null, watched_addresses:u32|null, subscription_peer_count:u32|null, chia_peer_peak_height:u32|null}; synced also requires a LIVE Chia peer. no_wallet_enrolled = nothing to watch and that is correct; wallet_not_unlocked = a wallet EXISTS and nothing is watching it — NEVER render it as settled. subscription_peer_count is the replica's 0-or-1 supervisor-fed peer, NOT chia_peer_count and MUST NOT be summed with it; chia_peer_peak_height is the height this node's own Chia peers announced, distinct from peak_height. Both are optional, absent on older nodes. An UNKNOWN token parses to Unrecognized(token), never an error
control.wallet.broadcast T del {signed_bundle_hex:string} {accepted, transaction_id, rejection}; accepted = mempool admission, NOT confirmation
control.wallet.watch T del {public_keys:[string]} (each a 48-byte G1 key, lowercase 96-hex, 0x accepted; ANY malformed key refuses the WHOLE request) {added:u32, watched:u32}; enrol PUBLIC keys for the node to follow. IDEMPOTENT — re-enrolling reports added:0 and succeeds. Keys, never puzzle hashes: the node derives the addresses with the same derivation it uses for its own custody, so there is ONE mapping. Persisted across restarts
control.wallet.unwatch T del {public_keys:[string]} {removed:u32, watched:u32}; the following actually STOPS. A key that was never enrolled reports removed:0 and is not an error
control.wallet.watched T del {public_keys:[string]}; the enrolled keys only, never the node's own custody keys. TOKEN-GATED although it is a read — the caller supplies nothing, so the answer is this node's OWN key set

dig-profile bodies (delegated to the engine)

Method Auth Route Params Result
control.profile.putBody T del {store_id:string, root:string, body_b64:string} {stored:true, store_id, root, body_bytes}; the node INDEPENDENTLY resolves root on chain and REFUSES any body whose recomputed root is not the confirmed one — root is a claim to be checked, never a fact to be trusted, and dig-app is a caller like any other. Decoded bodies above MAX_BODY_BYTES (4 MiB) are refused as INVALID_PARAMS
control.profile.getBody T del {store_id:string, root:string} {store_id, root, body_b64:string|null, body_bytes}; body_b64:null means this node holds no body at that root and NEVER that the body could not be read, which is an error. The answer is at the root that was ASKED for, never a newer one

Subscriptions (delegated to the engine)

Method Auth Route Params Result
control.subscribe T del {store_id:string, kind?:"capsule"|"profile"} {subscribed:true, added:bool, store_id, kind}; kind is OPTIONAL on the wire and ABSENT means capsule — the meaning every untagged row in an existing subscriptions.json already carries
control.unsubscribe T del {store_id:string} {subscribed:false, removed:bool, store_id}
control.listSubscriptions T del {subscriptions:[string], count}

Error codes

Every control error is {code:int, message:string, data:{code:SYMBOL, origin:string}}. Branch on data.code (the stable symbol), never the message.

Code Symbol Origin Meaning
-32700 PARSE_ERROR shell request body was not valid JSON
-32600 INVALID_REQUEST shell not a single JSON-RPC object
-32601 METHOD_NOT_FOUND boundary control method is not resolved
-32602 INVALID_PARAMS node missing/malformed params
-32000 DISPATCH_FAILED shell the node failed to dispatch a well-formed call
-32030 UNAUTHORIZED shell control.* called without a valid token
-32031 NOT_SUPPORTED shell control op unsupported on this build
-32032 CONTROL_ERROR shell control op failed at runtime

Boundary

  • dig-rpc-protocol — node ⇄ node peer wire.
  • dig-ipc-protocol — app ⇄ node local session/signing envelope (the transport this catalog rides over, for local clients).
  • dig-node-control-interface (this crate) — the control METHOD CATALOG carried inside that authenticated channel, or over loopback-mTLS for remote-transport clients.

See SPEC.md for the normative contract and the superproject SYSTEM.md for the wider cross-repo map.

Consumers

dig-node (server-side dispatch — implements ControlHandler), the CLI dign, dig-chrome-extension, dig-app, hub — all consuming this one control contract per epic #1110 (dig-node adoption + the dig-rpc Control-tier removal land in later tasks of the cascade).