Skip to main content
Status: Draft Type: Standards Track Category: Core (RPC) Created: 2026-05-11 Requires: CIP-4 (State Commitment — QMDB / MMR proofs) Required by: CIP-15 v2.r2 §6 (Gateway routes-table fetch), CIP-15 gateway-implementation r2 §2.2 (Phase 1 routes resolver), CIP-19 §10.1 (MCP tools/list derivation) Companions: CIP-14 v2.r2 §5 (read_handler RPC — complementary, distinct purpose)

1. Abstract

This CIP specifies GET_STATE — a verifiable single-key state-read RPC exposed by Cowboy full nodes (Runner-side re-serving is anticipated by the §6.1 Gateway flow but is not yet implemented — only node RPC exposes /state/* today). Given an actor_address and a storage key, it returns the current value plus a QMDB/MMR operation-inclusion proof against the node’s unified state_root at a known block height. Clients (Gateways, off-chain indexers, light clients) verify the proof locally before trusting the value. GET_STATE is not a handler-invocation RPC — that role belongs to CIP-14 v2.r2 §5 read_handler. GET_STATE is a raw, deterministic, proof-attached KV read. It is the building block CIP-15 v2.r2 needs to fetch a target actor’s __cowboy/routes table without invoking the actor (no PVM cycles), and that CIP-19 §10.1 needs to derive tools/list deterministically from on-chain routes. The current RPC layer (node/rpc/src/rpc.rs:370-384) exposes /actor/{address} and /actors/{address}/storage for unverified state reads. GET_STATE adds the Merkle-proof attachment that closes the trust model: a Gateway no longer has to trust a single Runner — it verifies the response against the unified state_root from a recent block.

2. Motivation

CIP-15 v2.r2 Gateway implementation depends on a per-actor Routes table stored at the actor’s KV key __cowboy/routes. The Gateway fetches this table on every block of activity, caches it, and resolves incoming HTTP requests against it. Without a verifiable read:
  1. Single point of trust. A Gateway fetching from one Runner has no way to detect a malicious or stale response. The Runner could return a forged routes table redirecting traffic.
  2. Cache invalidation theory only. CIP-15 v2.r2 §6 says “poll manifest_root every MANIFEST_POLL_INTERVAL blocks.” But polling for state_root changes requires a verifiable read in the first place — otherwise the polled value is itself a trust assumption.
  3. CIP-19 tools/list consequence. CIP-19 §10.1 step 1 reads the routes table identically. The same trust gap applies.
The current /actor/{address} and /actors/{address}/storage endpoints (node/rpc/src/rpc.rs:370-384) return raw KV values with no proof. CIP-15 gateway-implementation r2 §2.2 explicitly flags this as the single hardest blocker for Phase 1 shipping. The fix is a small, well-scoped addition: take the existing KV-read path, attach the QMDB/MMR operation proof against the unified state root (already maintained per CIP-4), and expose the bundle through a new RPC endpoint.

3. Design Goals

  • Verifiable. Response carries everything a client needs to reconstruct the actor’s state-root commitment for the read leaf, with no further round-trip.
  • Cheap on the server. The proof is a single QMDB/MMR operation proof; the node maintains the authenticated state anyway (CIP-4) so generation is microseconds, not milliseconds.
  • Stateless on the client. No subscription, no session, no pagination. One KV → one response.
  • Reusable. Applicable to any system actor or user actor; not specialized to Gateway routes.
  • Compatible with light clients. A future Cowboy light client (per CIP-25 §1.4 native-light-client backend) consumes the same proofs.

4. Non-goals

  • Handler invocation. That is CIP-14 v2.r2 §5 read_handler.
  • Streaming subscriptions. A future CIP may add SUBSCRIBE_STATE (push notifications when a key changes); v1 here is pull-only.
  • Multi-key proofs. v1 returns one proof per call. Batch reads with a combined proof are a future optimization.
  • Cross-actor proofs. v1 proves inclusion of one (actor_address, key, value) operation against the unified state root. Cross-actor relations require multiple GET_STATE calls.
  • Full archival history. v1 serves the latest committed block plus a single retained snapshot height (GET /state/at/{height}/..., §5.1). Arbitrary-depth historical reads across all pruned heights (needed by CIP-25 §1.4 cross-chain backends) remain out of scope.

5. RPC Endpoint

5.1 HTTP route

Path parameters:
  • actor_address — 20-byte hex-encoded address with 0x prefix (e.g. 0x0000...000D)
  • key_hex — hex-encoded raw KV key bytes with 0x prefix; same encoding as used by the actor when writing via state_set
Query parameters:
  • prove: boolean, default true. If false, behaves identically to the existing /actors/{address}/storage lookup — value only, no proof. Provided for parity with cheap unverified reads.
A height-pinned variant GET /state/at/{height}/actor/{actor_address}/key/{key_hex} (node/rpc/src/handlers/proof.rs::get_state_at) returns the same envelope proven against the snapshot root at {height}; only the latest retained snapshot height is queryable (other heights → 404).

5.2 Response

Fields:
  • value — raw bytes of the KV value, hex-encoded with 0x prefix. null if the key does not exist (see absent below).
  • state_root — the node’s global unified state root at block_height (the single QMDB authenticated keyspace covering accounts, actors, and all actor-storage slots). Cowboy has no per-actor storage trie; the proof authenticates the (actor_address, key) operation directly against this one root. Equals the block header’s state root at block_height.
  • block_height — the block height at which the proof was generated; always the latest committed block at the time of RPC handling.
  • block_hash — block-header hash for cross-verification with the node’s block-explorer view.
  • proof — the QMDB/MMR operation-inclusion proof (SerializableStateProof), the implementation-pinned proof form. It authenticates the codec-encoded state operation (the MMR leaf) against the operations-MMR root, which is then compared to state_root. It is not an MPT Merkle path. Sub-fields:
    • proof.proof_version — wire-format version (currently 1).
    • proof.loc — the operation’s location (leaf index) in the QMDB operations MMR.
    • proof.chunk — the activity-bitmap chunk (hex, 32 bytes) covering loc.
    • proof.mmr_leaves — the MMR leaf count the proof is taken against.
    • proof.inactive_peaks — number of inactive peaks committed by the proof, required to recompute the root under commonware’s BackwardFold peak-bagging policy.
    • proof.digests — the MMR range-proof digests (hex, each 32 bytes Blake3).
    • proof.partial_chunk_digest — partial chunk digest from the status-bitmap tail (hex); null when absent.
    • proof.ops_root — the operations-MMR root (hex, 32 bytes Blake3).
  • encoded_operation — hex of the codec-encoded MMR leaf (Operation::Update{key, value, next_key}) that proof authenticates. Supplied so a client verifies without re-deriving commonware’s wire codec. Present only for a present key (an absent key ships its leaf inside exclusion_proof).
  • absenttrue iff the key does not exist at block_height. When absent, proof is omitted and a separate exclusion_proof object is returned instead (the two are mutually exclusive). exclusion_proof is a tagged (kind) union:
    • kind: "span" — the absent key falls strictly between two adjacent active keys: { op_proof, span_key, span_value, span_next_key, encoded_operation }, where op_proof is a SerializableStateProof authenticating the bounding operation and encoded_operation is the codec-encoded bounding Operation::Update leaf.
    • kind: "empty" — the keyspace is empty through the latest commit: { op_proof, commit_value?, encoded_operation } (bounding Operation::CommitFloor). A client verifies exclusion by MMR-checking op_proof over encoded_operation against state_root (same primitive as inclusion) and confirming the absent key lies in the proven span.

5.3 Proof verification (client-side)

There is a single unified state root — no separate per-actor storage-trie root exists (§5.2), so the earlier draft’s “second proof against the block’s account trie” step is moot: one MMR operation proof against state_root fully authenticates the (actor_address, key) read. The client only needs to trust the block’s state_root out-of-band (which a Gateway already maintains for CIP-14 v2.r2 cache invalidation, and compares with block_hash).

5.4 Error responses

All error responses carry a structured JSON body { error_code, message, ... } (Cowboy ApiError / ErrorCode), not a bare status. System / precompile actors (addresses 0x010x10) are exempt from the “actor does not exist” 404 and return 200 + an exclusion proof for absent keys. The “absent” case (key not in actor storage) is NOT a 404 — it returns 200 with value: null and absent: true plus a valid exclusion proof.

6. Use Cases

6.1 CIP-15 v2.r2 Gateway routes fetch (primary motivator)

Per cip-15-gateway-implementation.md r2 §2.3, the Gateway’s poll loop calls:

6.2 CIP-19 tools/list derivation

The MCP tools/list generator (CIP-19 §10.1 step 1) uses the same call to fetch the routes table; the rest of §10.1 is independent of how the table was fetched.

6.3 Off-chain indexer audit

Indexers building cross-actor views (token balance trackers, governance vote tallies) issue GET_STATE calls against the canonical balance / vote keys and verify each proof before incorporating values into their derived state. Eliminates the “indexer trusts a single node” failure mode.

6.4 Light client support (future)

A Cowboy light client (per CIP-25 §1.4 “native light client” backend on a destination chain) consumes GET_STATE responses verbatim. The destination-chain verifier code is just the §5.3 procedure compiled into the destination’s VM.

7. Implementation Sketch (non-normative)

The node side requires:
  1. One new HTTP route in node/rpc/src/rpc.rs (after the existing /actors/{address}/storage handler at rpc.rs:378): GET /state/{actor_address}/{key_hex}. (This route + handler now live at rpc.rs:716 / node/rpc/src/handlers/proof.rs.)
  2. One new method on the existing storage interface that returns (value, proof) instead of just value. The QMDB store Cowboy uses for state already maintains the operation-proof primitives — exposing them is a few-line API surface addition.
  3. One new struct in node/types/src/rpc.rs (or equivalent) for the response envelope.
Estimated implementation size: < 200 lines including tests. No new on-chain state, no new consensus, no new storage layout. Existing primitives:
  • Unified QMDB state maintained per node/storage/src/blockchain_storage.rs (per CIP-4).
  • Block header / state-root view via node/chain/src/engine.rs.
  • HTTP route boilerplate per other endpoints in node/rpc/src/rpc.rs.
The implementation is intentionally orthogonal to any other in-flight work; it lands as a Phase 0 deliverable ahead of CIP-15 v2.r2 / CIP-19 activation.

8. Relationship to Other CIPs


9. Security Considerations

  • Trust model. A Gateway / indexer / light client verifies the Merkle proof locally before trusting the value. A malicious node returning a forged (value, proof) either trips proof verification (proof doesn’t reconstruct to the claimed state_root) or returns a state_root that doesn’t match the block header the client has from a separate trusted source (e.g. another node, or its own block subscription).
  • Stale reads. Responses always reflect the latest committed block at RPC handling time. Clients that need monotonic reads should compare block_height across successive calls and reject regression.
  • DoS surface. Proof generation is cheap (a single QMDB/MMR operation proof), but a flood of GET_STATE calls against large actor states could pressure a node. Mitigation: /state/* has a dedicated per-IP rate limiter (state_read_limiter, env STATE_READ_RATE_PER_SEC) in addition to the node’s configurable global RPC limiter (RPC_GLOBAL_RATE_LIMIT_PER_SEC), both in node/rpc/src/rpc.rs. Oversized keys are rejected pre-DB (MAX_ACTOR_KV_KEY_BYTES), and stale-node reads are refused (503) rather than served.
  • Absent-key fingerprinting. Returning an exclusion proof reveals “this key does not exist” deterministically. This is the same information already exposed by /actors/{address}/storage; no new leak.
  • Key encoding edge cases. Implementations MUST decode key_hex strictly (reject non-hex chars, odd length, etc.) to prevent ambiguity between, e.g., 0x__cowboy/routes (literal nine-byte string) and a typoed escape sequence.

10. Backwards Compatibility

Fully additive. New HTTP route; no existing RPC, on-chain state, or PVM syscall is modified. Clients that don’t speak GET_STATE continue to use /actor/{address} and /actors/{address}/storage unchanged. If GET_STATE lands in node code, the cip-15-gateway-implementation companion’s §9 open-question item 1 is resolved.

11. Future Work

  • Full archival history. Reads at arbitrary pruned heights — v1 retains only the latest snapshot for the height-pinned read (§5.1); needed by CIP-25 §1.4 native light client backend.
  • Batch reads. POST /state/batch with multiple (actor, key) pairs returning combined proofs.
  • Subscribe API. WebSocket-based push when a watched key changes — useful for Gateway cache invalidation tighter than CIP-15’s MANIFEST_POLL_INTERVAL.
  • Bundled account-trie proof. Resolve §5.3 limitation by returning both the actor’s storage proof AND the account-trie proof for actor_address.state_root in one response.
  • CIP-25 cross-chain extension. A cross-chain L1 anchor (CIP-25 §1) can carry GET_STATE-style proofs over the L1 mailbox, allowing destination-chain L3 apps to verify Cowboy state directly.

12. References

  • node/rpc/src/rpc.rs:370-384 — existing unverified state endpoints; /state/* GET_STATE routes at rpc.rs:716node/rpc/src/handlers/proof.rs::get_state
  • node/storage/src/blockchain_storage.rs — unified QMDB state + proof generation
  • CIP-4 — QMDB / MMR state-proof primitives
  • CIP-14 v2.r2 §5 — read_handler (complementary RPC)
  • CIP-15 v2.r2 §6 + cip-15-gateway-implementation.md r2 §2.2 — primary use case
  • CIP-19 §10.1 — secondary use case
  • CIP-25 §1.4 — native light client future use case