Verifying an Ethereum state proof comes down to one core action: take the Merkle path returned by eth_getProof, hash upward from the leaf node level by level, and check whether the final root matches the stateRoot you trust. The logic for verifying balance and storage is the same. The difference is that storage verification adds one more layer: you first prove the account exists and get its storageRoot, then use that root to verify a specific storage slot.

A leading global cryptocurrency platform,suitable for both beginners and experienced traders.
New user benefit: 20% off trading fees upon registration!!
First, get a trusted stateRoot
This is the precondition for the whole verification, and also the easiest place to make mistakes. The proof returned by eth_getProof does not itself contain the information that "this root is trusted." If you get both the stateRoot and the proof from the same RPC node, verification becomes meaningless—the node can forge both and make them pass your check.
There are several sources for a trusted stateRoot. If you run a full node, take it directly from your local block header. If you use a light client such as Helios, it verifies block headers through sync committee signatures, and the stateRoot it produces has been verified by the consensus layer. If you only have a normal RPC, at least fetch the stateRoot for the same block from two independent sources and compare them. This is not as strict as a light client, but it is more reliable than trusting a single node.
Fixing the block number is also important. If you fetch the proof using the latest tag, you are verifying the state at a certain moment, but the stateRoot corresponding to that moment may have already changed by the time you verify it. Specify a concrete block number or block hash, take the stateRoot from that block header, and the whole verification process will stay consistent.
Balance verification: how the account proof works
In the eth_getProof response, accountProof is an array of RLP-encoded nodes, starting from the root of the state trie and following the path of the account address down to the leaf node that contains that account.
The verification steps are mechanical. Take the first node of accountProof, RLP-decode it, and confirm that its hash matches the stateRoot you hold. If it is the root node, it is the root itself. Then follow the nibble path of the account address—after applying keccak256 to the address—to find the reference to the next node in the parent node. Decode that next node and verify that its hash matches the hash stored in the parent. Repeat this process until you reach the leaf node. The leaf node contains four fields: balance, nonce, storageRoot, and codeHash. Take the balance you care about, and at the same time RLP-encode and keccak256-hash the leaf node. The result should equal the hash referenced by the previous-level node.
If the account does not exist in the given block, accountProof may be an empty array, and balance and nonce will return zero. This itself is also a verifiable claim: it proves that the address does not exist in the state trie, rather than the node lying that the balance is zero.
Storage verification: two layers of proof linked together
Verifying a storage slot requires one more step than verifying a balance. Each element in the storageProof array contains key, value, and proof. You first need to complete the account proof and get the storageRoot from the account leaf. Then use that storageRoot as the root, use the key—the 32-byte storage slot position after keccak256 hashing—as the path, and perform the same level-by-level verification on the nodes in proof. The value in the final leaf is the value of that storage slot.
There is one detail: in Solidity, the storage position of a mapping is not simply keccak256(key). For mapping(address => uint256), the slot is calculated as keccak256(abi.encode(key, mappingSlot)), where mappingSlot is the position where that mapping is declared in the contract, counting from 0. If you do not know the contract's storage layout, you cannot construct the correct storageKeys parameter, and the proof returned by eth_getProof cannot be used to verify the value you care about. That is not a problem with the verification logic; it is a wrong input.
Use a library or write it yourself
Writing complete MPT verification logic by hand makes it easy to make mistakes in nibble handling, RLP decoding, and empty node handling. In practice, using an existing verification library is more reliable.
@merkleforge/ethereum provides verifyEthGetProof(stateRoot, response), which returns the verified account and storage values and throws an InvalidProof error when verification fails. It also provides a mappingSlot helper function to calculate the storage position of a mapping. The value of this kind of library is that it encapsulates the MPT edge cases—different handling for extension nodes, branch nodes, and leaf nodes—so you only need to make sure the stateRoot you pass in is trusted.
If you want to write it by hand in JavaScript, you need two dependencies: rlp and keccak256. OnFinality's tutorial provides a complete Node.js script. The core logic is to iterate through the accountProof array, verify the hash chain level by level using the address's nibble path, and finally compare the RLP-encoded hash of the leaf node with the stateRoot. This script is good for understanding the mechanism, but it is not recommended for production use—it lacks handling for malformed input, empty proofs, and extension node edge cases.
A practical issue that can easily get you stuck
If you encounter a MissingTrieNode error during verification, it usually does not mean your verification logic is wrong. It means the proof data is incomplete. The node array returned by eth_getProof should theoretically form a complete path from root to leaf, but some RPC implementations or proxies may truncate or omit nodes. Switch to another RPC endpoint and fetch the proof again, or check whether the library you are using applies extra filtering to proof nodes.
Another common error is a hash mismatch even though the path looks correct. Check whether the address or storage slot key was encoded correctly before hashing. The account address needs to be keccak256-hashed first and then used as a nibble path. The 32-byte storage slot key also needs to be keccak256-hashed. If you directly use the raw address or slot to traverse the trie, the path will not match.
What counts as done
The sign that verification is complete is this: starting from the stateRoot you trust, the hash chain of accountProof closes, and the balance in the leaf—or the storageRoot plus the value verified through storageProof—matches the value you see from other channels such as a block explorer or a contract call. If the hash chain does not close, the proof is invalid, no matter how reasonable the returned numbers look.
If your application needs to verify state continuously, wrap the verification logic into a function whose inputs are stateRoot, address, and an optional list of storage slots, and whose output is the verified value or an error. Run this function every time after calling eth_getProof, instead of directly trusting the balance field returned by the RPC.

A leading global cryptocurrency platform,suitable for both beginners and experienced traders.
New user benefit: 20% off trading fees upon registration!!
References
- Foresight News · A detailed look at Helios launched by a16z: fully trustless Ethereum access, page publication or update date: not indicated on the page; verification date: 2026-10-20.
- OnFinality · eth_getProof (EIP-1186): account and storage proofs for state verification, page publication or update date: not indicated on the page; verification date: 2026-10-20.
- OnFinality · EVM ETH GetProof State Account Storage Proofs, page publication or update date: not indicated on the page; verification date: 2026-10-20.
- NPM · @merkleforge/ethereum package page, page publication or update date: not indicated on the page; verification date: 2026-10-20.


