- Category
- Operations
- Reading time
- 11 minutes
- Published
How to verify EVM state with eth_getProof
An RPC response can tell you an account balance or a contract storage value. eth_getProof can also return the Merkle proof needed to check that value against the state root committed by a particular block. That distinction matters when an application must verify state instead of trusting one endpoint's decoded answer.
The method is easy to call and easy to misuse. A proof is only as trustworthy as the block header used as its root. The caller must know the correct storage slot, preserve the selected block identity, decode Ethereum's trie values correctly, and distinguish an unavailable historical proof from a zero value.
This guide covers the complete process: choose a block, request account and storage proofs, verify both layers, handle absence proofs and reorganizations, and qualify an endpoint for the historical range your product needs.
What is the short answer?
Use eth_getProof when you need cryptographic evidence for an account or contract storage value at one EVM block. First obtain a block header whose hash and stateRoot you trust. Then request the account and any required storage keys at that exact block. Verify accountProof against the block's stateRoot, decode the account leaf, and verify each storageProof against the decoded storageHash.
The proof does not decide whether the block itself is canonical, finalized, or trusted. If the same untrusted endpoint supplies both the header and the proof, verification shows that the two responses are internally consistent. It does not independently establish that the header belongs to the chain your application intends to trust.
Do not treat eth_getProof as a more detailed eth_getStorageAt. The caller still needs the correct raw storage slot, a proof verifier that implements Ethereum's trie and encoding rules, and a policy for block identity, finality, historical availability, and errors.
What does eth_getProof return?
The current Ethereum Execution API definition accepts an account address, an array of storage keys, and a block reference. It returns the account address, balance, nonce, code hash, storage root, an account proof, and one storage-proof entry for each requested key.
The EIP-1186 specification describes the two proof layers. accountProof is a sequence of RLP-encoded trie nodes along the path from the block's state root to the account. The account leaf commits to the account's nonce, balance, storage root, and code hash. Each storageProof starts from that account's storage root and follows the path for one storage key to its value or to evidence that the key is absent.
The returned balance, nonce, code hash, storage hash, and storage values are convenient decoded fields. A verifier should not trust those fields separately from the proof. It should derive or validate them from the verified leaf data and reject any mismatch.
| Field | Verified against | What it describes |
|---|---|---|
| accountProof | The selected block's stateRoot | The account leaf, or proof that the account is absent |
| balance and nonce | The verified account leaf | The account values at the selected block |
| codeHash | The verified account leaf | A commitment to the account's bytecode, not the bytecode itself |
| storageHash | The verified account leaf | The root used to verify this account's storage proofs |
| storageProof | The verified storageHash | One requested slot value, or evidence that the slot is absent |
What makes the proof trustworthy?
A Merkle proof needs a trusted root. For eth_getProof, that root is the stateRoot in the selected block header. The Ethereum Merkle Patricia trie documentation explains how a root commits to the paths and values below it. Changing an account or storage value changes the hashes on its path and therefore changes the root.
Decide how your application trusts the header before requesting the proof. A light client can verify the chain's consensus and obtain a trusted header. Another system may consume a finalized header from an independently verified source. Comparing several ordinary RPC endpoints can detect disagreement, but agreement alone is not the same as consensus verification.
Store the chain ID, block number, block hash, parent hash, state root, and the source and time of the trust decision. The proof should be processed only against that exact state root. Never fetch a proof at latest and then verify it against a header fetched later, because the head can advance or reorganize between the two requests.
How do you request a proof for one exact block?
Resolve the intended block first. For a durable check, choose an explicit block according to the product's finality policy and store its hash and state root. Then call eth_getProof with the address, the exact 32-byte storage keys, and that block reference. A request with no storage keys still returns the account proof.
A minimal request shape is:
{"jsonrpc":"2.0","method":"eth_getProof","params":["0xACCOUNT",["0xSTORAGE_KEY"],"0xBLOCK_NUMBER"],"id":1}
Prefer a block hash where the endpoint supports it. EIP-1898 extends default-block methods, including eth_getProof, with a block-hash reference and an optional requireCanonical flag. Support and accepted parameter shape still need testing on the exact chain and endpoint. If hash addressing is unavailable, use a fixed block number, compare the returned header hash before and after the proof call, and reject the operation if the identity changed.
Named tags answer different questions. latest is fresh but movable, safe and finalized depend on the network's exposed consensus semantics, and pending describes a local candidate state. Do not use a moving tag for evidence that must remain reproducible.
How do you verify the account proof?
Start from the trusted block stateRoot. The account-trie path is derived from the account address according to Ethereum's state-trie rules. Feed the root, path, and RLP-encoded nodes in accountProof into a well-tested Ethereum proof verifier. Avoid writing a partial decoder that only handles the branch shapes observed in one fixture.
A successful inclusion proof yields the account leaf. Decode its nonce, balance, storage root, and code hash, then compare them with the corresponding result fields. Reject extra, reordered, malformed, or trailing proof data according to the verifier library's rules. A proof that hashes correctly but decodes to different account fields is not a valid response for the requested account.
The proof authenticates codeHash, not the contract bytecode. To verify code, fetch the bytecode for the same block, compute its Keccak-256 hash, and compare it with the verified codeHash. Keep that read pinned to the same block identity. A normal balance, nonce, or code response without this comparison remains an endpoint assertion rather than a proof-checked value.
How do you verify a contract storage value?
Verify the account layer first, because its leaf supplies the authenticated storageHash. For each requested slot, derive the storage-trie path from the full 32-byte slot key, then verify that entry's proof against the authenticated storage root. Decode the leaf value using Ethereum's storage-trie encoding and compare it with the returned quantity.
Do not use the contract address or an ABI function selector as the storage key. The input is the raw storage slot before the trie applies its path hashing. A simple state variable may occupy a fixed slot, but packed variables share a slot and require bit extraction. Mappings, dynamic arrays, strings, bytes, structs, inheritance, and upgradeable proxy layouts require additional slot derivation.
Use compiler-produced storage-layout metadata when available. The Solidity storage layout documentation defines packing and the Keccak-based locations used by mappings and dynamic arrays. Record the compiler version, contract code hash, storage-layout artifact, slot formula, key encoding, and expected value type with the proof. A valid proof for the wrong slot is still the wrong application answer.
Can eth_getProof prove that an account or slot is absent?
Yes. A Merkle Patricia proof can establish inclusion or non-inclusion. EIP-1186 requires enough matching trie nodes to show where the requested path stops when an account or storage value does not exist. The verifier must distinguish a valid absence proof from a truncated, malformed, or unavailable proof.
Application meaning comes after cryptographic verification. An absent storage leaf usually reads as zero at the EVM level, but that does not prove that a contract variable was intentionally initialized to zero. A missing account also has protocol-specific empty-account semantics that should be handled by the verifier library, not inferred from an empty array or a provider error.
Keep three outcomes separate: a verified non-inclusion proof, a verified inclusion proof whose decoded value is zero, and no valid proof. Collapsing all three into 0x0 hides retention failures and malformed responses.
Why can a historical proof fail when an old balance works?
Historical state values and historical proof material are separate retention requirements. An endpoint may be able to reconstruct or read an old balance while lacking the historical trie nodes needed to produce the Merkle path for that block. Blocks, receipts, logs, historical flat state, and proof nodes can all have different availability boundaries.
This is why an archive label does not prove that eth_getProof works at every historical block. Current execution-client archive documentation gives a concrete example: historical state retention and historical trie-node retention can be configured separately, and proof support depends on retaining the required trie nodes. Treat that as an implementation example, not a portable promise for every client or EVM network.
Test the exact oldest block, account, storage key, and block-reference form your product requires. Record a pruned-history error, unsupported method, unknown block, and malformed proof as different failures. Retrying a smaller range cannot repair missing proof nodes because eth_getProof is a single-state request, not a range scan.
How should proofs handle reorganizations and endpoint changes?
Bind every proof artifact to its chain ID, block hash, and state root. If a recent block becomes non-canonical, the proof can remain cryptographically valid for that old block while no longer describing the canonical state. Whether that evidence is still useful depends on the application. An audit tool may retain it as fork history, while a settlement workflow may require a canonical or finalized block.
Keep the selected block fixed across retries and endpoint changes. Another endpoint may not know the block hash, may have pruned its proof nodes, or may return a different block for the same height after a reorganization. Those are explicit capability or canonicality outcomes. Do not silently substitute latest, another height, or an unverified header.
For a production qualification, test normal operation and a controlled endpoint change with the same block hash and slots. The proof must verify against the original trusted state root. Availability after a switch matters, but identical verification output matters more than HTTP success.
How do you qualify an RPC endpoint for eth_getProof?
Build fixtures whose correct account and storage results are known independently. Include a current account, a contract with simple and derived slots, an absent slot, an absent account, an old block near the required history boundary, and a block that is no longer canonical in a test environment. Save the trusted headers and expected decoded leaves.
Run these checks through the exact production route:
- Verify
eth_chainId, then resolve and store the target block hash and state root. - Request an account-only proof with an empty storage-key array.
- Request one and several storage keys, including a verified zero or absent slot.
- Verify every proof locally and compare the decoded account fields and values.
- Fetch contract code at the same block and compare its hash when code identity matters.
- Repeat at the oldest required block and just beyond the advertised boundary.
- Test block number, raw block hash, and EIP-1898 object forms that the application intends to use.
- Capture unknown-block, non-canonical, pruned-history, unsupported-method, timeout, and malformed-result behavior.
- Repeat during a controlled endpoint change without changing the trusted block.
- Measure response bytes, verification time, request latency, concurrency, and retry amplification with the real number of slots.
HTTP 200 and matching decoded values are not enough. The acceptance condition is that every proof verifies against the intended trusted root and every unsupported case fails clearly without substituting another state.
eth_getProof production checklist
- Define which account fields and storage variables need proof, and why ordinary RPC reads are insufficient.
- Establish a trusted source for the block hash and state root.
- Pin the request and every supporting read to one block identity.
- Derive storage keys from compiler layout and contract-specific formulas.
- Verify the account proof before using its storage root.
- Verify each storage proof and decode trie values with a maintained Ethereum library.
- Compare decoded nonce, balance, storage root, code hash, keys, and values with the response.
- Fetch and hash bytecode separately when code identity matters.
- Treat inclusion, verified absence, and unavailable proof as different outcomes.
- Store the chain ID, block number, block hash, state root, proof nodes, slot derivation, and verifier version.
- Test historical trie-node availability separately from historical state reads.
- Keep the block fixed across retries and endpoint changes.
- Recheck canonicality or finality according to the application's risk policy.
- Bound proof size, slot count, concurrency, verification time, retries, and total deadline.
- Re-run fixtures after network, client, endpoint, contract, compiler, or storage-layout changes.
The durable rule is simple: the proof authenticates a value only relative to one trusted state root. Preserve that root, the block identity, and the derivation inputs together.
Frequently asked questions
- Is eth_getProof the same as eth_getStorageAt?
No. eth_getStorageAt returns a slot value asserted by the endpoint. eth_getProof returns the account and storage trie nodes needed to verify a slot value against the selected block's state root.
- Does eth_getProof verify contract bytecode?
It verifies the account's code hash. To verify bytecode, fetch the code at the same block, hash it with Keccak-256, and compare that hash with the codeHash decoded from the verified account leaf.
- Does an archive endpoint always support historical eth_getProof?
No. Historical state values and the trie nodes required for Merkle proofs can have different retention boundaries. Test the exact account, storage key, block, and proof verification result at the oldest depth you require.
- Can eth_getProof prove that a storage slot is zero?
It can prove either an included value that decodes to zero or non-inclusion of the slot. Keep those verified outcomes separate from an error or unavailable proof, even if the EVM-level read would produce zero in both valid cases.