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 specifiesGET_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-actorRoutes 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:
- 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.
- Cache invalidation theory only. CIP-15 v2.r2 §6 says “poll
manifest_rooteveryMANIFEST_POLL_INTERVALblocks.” But polling forstate_rootchanges requires a verifiable read in the first place — otherwise the polled value is itself a trust assumption. - CIP-19
tools/listconsequence. CIP-19 §10.1 step 1 reads the routes table identically. The same trust gap applies.
/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 multipleGET_STATEcalls. - 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
actor_address— 20-byte hex-encoded address with0xprefix (e.g.0x0000...000D)key_hex— hex-encoded raw KV key bytes with0xprefix; same encoding as used by the actor when writing viastate_set
prove: boolean, defaulttrue. Iffalse, behaves identically to the existing/actors/{address}/storagelookup — value only, no proof. Provided for parity with cheap unverified reads.
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
value— raw bytes of the KV value, hex-encoded with0xprefix.nullif the key does not exist (seeabsentbelow).state_root— the node’s global unified state root atblock_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 atblock_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 tostate_root. It is not an MPT Merkle path. Sub-fields:proof.proof_version— wire-format version (currently1).proof.loc— the operation’s location (leaf index) in the QMDB operations MMR.proof.chunk— the activity-bitmap chunk (hex, 32 bytes) coveringloc.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’sBackwardFoldpeak-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);nullwhen 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}) thatproofauthenticates. Supplied so a client verifies without re-deriving commonware’s wire codec. Present only for a present key (an absent key ships its leaf insideexclusion_proof).absent—trueiff the key does not exist atblock_height. When absent,proofis omitted and a separateexclusion_proofobject is returned instead (the two are mutually exclusive).exclusion_proofis 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 }, whereop_proofis aSerializableStateProofauthenticating the bounding operation andencoded_operationis the codec-encoded boundingOperation::Updateleaf.kind: "empty"— the keyspace is empty through the latest commit:{ op_proof, commit_value?, encoded_operation }(boundingOperation::CommitFloor). A client verifies exclusion by MMR-checkingop_proofoverencoded_operationagainststate_root(same primitive as inclusion) and confirming the absent key lies in the proven span.
5.3 Proof verification (client-side)
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
{ error_code, message, ... } (Cowboy ApiError / ErrorCode), not a bare status. System / precompile actors (addresses 0x01–0x10) 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)
Percip-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) issueGET_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) consumesGET_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:- One new HTTP route in
node/rpc/src/rpc.rs(after the existing/actors/{address}/storagehandler atrpc.rs:378):GET /state/{actor_address}/{key_hex}. (This route + handler now live atrpc.rs:716/node/rpc/src/handlers/proof.rs.) - One new method on the existing storage interface that returns
(value, proof)instead of justvalue. The QMDB store Cowboy uses for state already maintains the operation-proof primitives — exposing them is a few-line API surface addition. - One new struct in
node/types/src/rpc.rs(or equivalent) for the response envelope.
- 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.
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 claimedstate_root) or returns astate_rootthat 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_heightacross successive calls and reject regression. - DoS surface. Proof generation is cheap (a single QMDB/MMR operation proof), but a flood of
GET_STATEcalls against large actor states could pressure a node. Mitigation:/state/*has a dedicated per-IP rate limiter (state_read_limiter, envSTATE_READ_RATE_PER_SEC) in addition to the node’s configurable global RPC limiter (RPC_GLOBAL_RATE_LIMIT_PER_SEC), both innode/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_hexstrictly (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 speakGET_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/batchwith 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_rootin 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 atrpc.rs:716→node/rpc/src/handlers/proof.rs::get_statenode/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.mdr2 §2.2 — primary use case - CIP-19 §10.1 — secondary use case
- CIP-25 §1.4 — native light client future use case

