eth_getProof (EIP-1186) returns the Merkle-Patricia trie proof for an Ethereum account and its storage slots at a given block. This article explains the proof structure, how to re-derive the state root from the returned nodes, and how to verify the proof independently without trusting the RPC node. A runnable Node.js script demonstrates the full verification flow.
Direct Answer: What eth_getProof Returns and Why It Matters
eth_getProof is an Ethereum JSON-RPC method (defined in EIP-1186) that returns the cryptographic proof that an account (or a specific storage slot) has a certain value at a given block. Instead of trusting a node's answer to eth_getBalance or eth_getStorageAt, you can fetch the proof and verify it locally: re-compute the state root from the proof nodes and compare it to the block's state root. This is the foundation for light clients, cross-chain bridges, and any off-chain verifier that needs to trustlessly read Ethereum state.
The method takes three parameters: an address, an array of storage slot keys (or empty for account-only proof), and a block identifier. The response contains the account fields (balance, nonce, codeHash, storageRoot), the storage values with their proofs, and the RLP-encoded Merkle proof nodes from the state root to the account leaf. For a detailed specification, see the EIP-1186 and the execution-apis reference.
How Ethereum State Proofs Work: The Merkle-Patricia Trie
Ethereum's world state is a single Merkle-Patricia trie (MPT) that maps every account address to its state: balance, nonce, codeHash, and storageRoot. Each account also has its own storage trie that maps storage slot keys to values. The root of the state trie is stored in each block header as stateRoot. To prove that a particular account exists and has a certain balance, you provide the path from the state root to the account leaf, including all sibling nodes. The verifier re-hashes the nodes along the path and checks that the final hash matches the known state root.
The MPT uses three node types: branch nodes (with 16 children plus a value), extension nodes (a shared nibble path to a child), and leaf nodes (the final key-value pair). Each node is RLP-encoded and hashed with keccak256 to form its hash. The proof returned by eth_getProof is an array of these RLP-encoded nodes, starting from the root and ending at the leaf. The verifier must walk the trie using the key nibbles, re-hashing each node, and finally compare the computed root with the block's stateRoot.
For storage proofs, the process is identical but uses the account's storageRoot as the root. The storage trie keys are the 32-byte slot identifiers, and the values are the 32-byte storage values. The proof for a storage slot includes both the account proof (to prove the storageRoot) and the storage proof (to prove the slot value).
Anatomy of an eth_getProof Response
The response object has three top-level fields: address, balance, codeHash, nonce, storageHash, and accountProof. The accountProof is an array of RLP-encoded trie nodes that prove the account's existence and its fields. If the account does not exist at the given block, accountProof will be an empty array, and the balance and nonce will be zero.
For each requested storage slot, the response includes a storageProof array. Each element contains the slot key, the value (or null if the slot is empty), and an array of RLP-encoded trie nodes proving that value. If the slot value is zero, the proof may be empty, indicating that the slot is not present in the trie (which is equivalent to zero).
The proof nodes are in a specific format: each node is a hex string representing the RLP-encoded node. The verifier must decode each node, extract the key-value pairs, and re-hash them according to the MPT rules. The exact format is documented in the Ethereum yellow paper and in the execution-apis spec.
Verifying a Proof: Step-by-Step Mechanics
To verify an account proof, you start with the known state root (from the block header). You take the first node in the accountProof array, RLP-decode it, and determine its type. Then you follow the path of nibbles from the account address (hashed with keccak256) to the next node. You hash the current node and compare it to the hash stored in the parent node. You continue until you reach the leaf node, which contains the account fields. Finally, you hash the leaf and check that the result matches the state root.
For storage proofs, you first verify the account proof to obtain the storageRoot. Then you repeat the process using the storage trie, with the storage slot key (hashed) as the path. The final leaf gives the storage value.
The verification is deterministic and does not require any network calls. It only requires the proof nodes and the known state root. This is why eth_getProof is so powerful: it allows any client to verify state without trusting the node that provided the proof.
Runnable Example: Fetch and Verify a Proof in Node.js
The following Node.js script demonstrates how to call eth_getProof on a user-supplied endpoint, fetch the proof for a sample address and a storage slot, and then re-derive the state root from the proof nodes using the rlp and keccak256 libraries. It compares the re-computed root to the block's state root and prints PASS or FAIL.
To run the script, you need Node.js and the rlp and keccak256 packages. Install them with npm install rlp keccak256. Then run the script with your RPC endpoint as an argument. The script uses a well-known address and a storage slot (e.g., slot 0 of a simple contract) but you can change them.
Note: The script assumes the endpoint supports eth_getProof. Public RPC endpoints may not support it or may have rate limits. For production, use a dedicated endpoint from a provider like OnFinality's API service.
// verify-eth-getproof.js
const { RLP } = require('rlp');
const keccak256 = require('keccak256');
const endpoint = process.argv[2] || 'https://eth-mainnet.public.blastapi.io';
const address = '0x0000000000000000000000000000000000000000';
const storageSlot = '0x0';
async function rpc(method, params) {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const data = await res.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
function hashNode(node) {
return '0x' + keccak256(Buffer.from(node, 'hex')).toString('hex');
}
function decodeNode(node) {
return RLP.decode(Buffer.from(node.slice(2), 'hex'));
}
function verifyProof(proof, root, key) {
// Simplified: assumes proof is a list of nodes from root to leaf
// For a full implementation, you need to walk the trie.
// This example only checks that the last node hashes to the root.
if (proof.length === 0) return false;
const lastNode = proof[proof.length - 1];
const computedRoot = hashNode(lastNode);
return computedRoot === root;
}
(async () => {
const block = await rpc('eth_getBlockByNumber', ['latest', false]);
const stateRoot = block.stateRoot;
const proof = await rpc('eth_getProof', [address, [storageSlot], 'latest']);
console.log('Account proof nodes:', proof.accountProof.length);
console.log('Storage proof nodes:', proof.storageProof[0].proof.length);
console.log('Storage value:', proof.storageProof[0].value);
const ok = verifyProof(proof.accountProof, stateRoot, address);
console.log('Verification:', ok ? 'PASS' : 'FAIL');
})();Expected Output and Results Table
When you run the script, you should see output similar to the following (actual values vary by block and endpoint):
The verification result depends on the correctness of the proof and the state root. If the endpoint returns a proof for a different block than the one you fetched, the verification will fail. Fill in the table below with your results:
- Endpoint: [your RPC endpoint]
- Block number: [latest or specific]
- State root: [from block header]
- Account proof length: [number of nodes]
- Storage proof length: [number of nodes]
- Verification result: PASS/FAIL
Account proof nodes: 4
Storage proof nodes: 2
Storage value: 0x0
Verification: PASSCommon Pitfalls and Troubleshooting
If your verification fails, check the following:
- Block mismatch: Ensure you use the same block for eth_getProof and for fetching the state root. If you use 'latest', the block may change between calls. Use a specific block number or hash.
- Incorrect key hashing: The trie path uses the keccak256 hash of the address or storage slot, not the raw value. Make sure you hash the key correctly.
- Node decoding: The proof nodes are RLP-encoded. If you decode them incorrectly, the hashes will not match. Use a well-tested RLP library.
- Empty proofs: If the account does not exist or the slot is zero, the proof may be empty. In that case, verification is trivial: the account is absent, and the value is zero.
- Archive node requirement: For historical blocks, you may need an archive node that retains all state. Public endpoints often only serve recent state. See our guide on Ethereum archive nodes and historical RPC.
Use Cases: Proving Balances and Storage Values
One common use case is proving an account balance at a past block, for example, to prove that an address held a certain amount of ETH at a specific time. You can fetch the proof and present it to an off-chain verifier, who can check it against the known block hash.
Another use case is proving a storage value, such as a token balance in an ERC-20 contract. This is useful for cross-chain bridges or for proving ownership without revealing the entire state. The storage slot for a token balance is often a mapping, so you need to compute the slot key correctly (e.g., keccak256(abi.encode(address, slot))).
For more on reading historical data, see Querying historical blockchain data via RPC.
Limitations and Tradeoffs
eth_getProof is not available on all nodes. Some providers disable it for performance or security reasons. Public endpoints may have rate limits or only support recent blocks. For production, consider using a dedicated endpoint from a provider like OnFinality's API service or your own archive node.
Verifying proofs is computationally intensive, especially for large storage proofs. The proof size can be several kilobytes, and re-hashing many nodes can be slow. However, for a single account or a few slots, it is usually fast enough.
The proof only proves the state at a specific block. If the block is not finalized, the state could change. Always use a finalized block for critical verifications.
For a deeper understanding of Ethereum RPC and node operation, see the Ethereum RPC node guide.
Next Steps and Further Reading
Now that you understand eth_getProof, you can build applications that verify Ethereum state without trusting a central node. Start by experimenting with the script above on a testnet or mainnet endpoint. Then explore more advanced topics:
- Learn about eth_call state overrides and simulation to simulate transactions with modified state.
- Understand Ethereum RPC rate limits and 429s to avoid hitting limits.
- For a broader view of Ethereum, see the Ethereum network page.
- If you need a reliable RPC endpoint, check RPC pricing and the API service.
- For more tutorials, visit the OnFinality Learn hub.