# eth_getProof: Verify Blockchain State Without Trusting Your RPC Node

Yesterday we wrote about [what free RPC endpoints really give you](https://swiftnodes.io/blog/free-rpc-endpoints-real-limits), and the uncomfortable core of it is: every response from any RPC node — free or paid, yours or ours — is just something a server told you. `eth_getBalance` returns a number; you have no way to know it's true. For most apps that's fine. But there's a JSON-RPC method that changes the deal entirely: `eth_getProof` returns state *with a cryptographic receipt* — a Merkle proof you can verify yourself against a block hash. It's the primitive underneath [bridges](https://swiftnodes.io/blog/how-blockchain-bridges-work), light clients, and L2 withdrawals, and it works today on any Ethereum endpoint. Here's how it works and when to reach for it.

## What it returns

`eth_getProof` (standardized in EIP-1186) takes an address, a list of storage slots, and a block tag. We ran it live against the WETH contract, asking for storage slot `0x0`:

```bash
curl -X POST "https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getProof",
       "params":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
                 ["0x0000000000000000000000000000000000000000000000000000000000000000"],
                 "latest"]}'
```

The response has two halves. The **account proof**: the account's `nonce`, `balance` (~2.09M ETH held by WETH when we ran it), `codeHash`, and `storageHash`, plus `accountProof` — an array of trie nodes (nine of them, the first being the 532-byte state-trie root node). And a **storage proof** per requested slot: the slot's `value` plus its own node array (seven nodes for our query).

A nice detail from the live run: WETH's slot 0 value came back as

```plaintext
0x577261707065642045746865720000000000000000000000000000000000001a
```

— which is ASCII for **"Wrapped Ether"** with Solidity's short-string length byte at the end. The contract's `name` lives in slot 0, and we just received it with a cryptographic proof attached.

## How the proof actually proves anything

Ethereum's entire world state is a Merkle-Patricia trie whose root hash — the `stateRoot` — is committed in every block header. That's the trick:

1.  Take a block header you trust (more on that below). It contains `stateRoot`.
    
2.  The `accountProof` is the path of trie nodes from that root down to the account's leaf, keyed by `keccak256(address)`. Each node's hash appears inside its parent — so you can recompute hashes from the leaf up and check the top equals `stateRoot`. If any byte were forged, the hash chain breaks.
    
3.  The account leaf commits to `storageHash` — the root of that contract's own storage trie. Each `storageProof` walks from `storageHash` to the slot's value, keyed by `keccak256(slot)`, verified the same way.
    

So the node can't lie about a balance or a storage value without producing a hash collision. The only thing you have to trust is the block header — which is exactly the point: it reduces "trust this RPC server" to "trust this 32-byte block hash," and block hashes are much easier to source honestly:

*   **Cross-check multiple independent endpoints** for the block hash — forging state now requires colluding providers, not one bad node.
    
*   **A consensus light client** gives you headers verified against Ethereum's validator set — no RPC trust at all.
    
*   **On-chain sources**: every L2 exposes its view of recent L1 block hashes, which is precisely how canonical bridges verify L1 state — and how L2→L1 withdrawals prove, with a storage proof, that a withdrawal record exists in the L2's state.
    

## Using it from code

```ts
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http("https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY"),
});

const proof = await client.getProof({
  address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
  storageKeys: ["0x0000000000000000000000000000000000000000000000000000000000000000"],
  blockNumber: 23_500_000n, // pin to a specific block — proofs against "latest" go stale on the next block
});
```

Don't hand-roll the verification — trie traversal has enough edge cases (extension nodes, embedded short nodes, exclusion proofs) that you want a maintained implementation. `@ethereumjs/trie`'s `verifyProof`, or the verifier inside light-client libraries like Helios, take the header's `stateRoot`, the address, and the proof array and return the verified value.

## When to actually use it

*   **Reading state through endpoints you don't trust** — the free/public tier from yesterday's post. Fetch the proof from the cheap endpoint, verify against a header you got somewhere trustworthy.
    
*   **Cross-chain state** — reading L1 state from an L2 (or vice versa) without an oracle: on-chain block hash + storage proof = trust-minimized bridge read.
    
*   **High-stakes reads** — a payout system checking a balance before releasing funds can demand proof instead of taking an `eth_call`'s word for it.
    
*   **Historical facts** — "what was this slot at block N," provable, for audits and disputes. Note that proofs for old blocks need [archive state](https://swiftnodes.io/docs/api-reference#archive) — a full node can only prove recent state, the same limitation as [historical eth\_call](https://swiftnodes.io/blog/eth-call-historical-block).
    

## The gotchas

*   **You pass the raw slot, not its hash.** The node applies `keccak256(slot)` internally for the trie path. For mapping entries, compute the slot the same way you would for [state overrides](https://swiftnodes.io/blog/eth-call-state-overrides): `keccak256(abi.encode(mappingKey, mappingSlot))`.
    
*   **Pin the block.** A proof is against one block's `stateRoot`; verify it against *that* block's header, and remember ["latest" can differ between nodes](https://swiftnodes.io/blog/is-your-rpc-node-synced-stale-endpoint) — pass an explicit block number.
    
*   **A zero value with a valid proof is an exclusion proof** — cryptographic evidence the slot is *empty*, which is just as useful and just as verifiable.
    
*   **Proofs aren't small.** Nine account nodes plus seven storage nodes was ~5KB for our WETH query; deep tries and many slots multiply that. This is a precision tool, not a bulk-read replacement — for bulk, [batch normal calls](https://swiftnodes.io/blog/multicall3-cheat-sheet) and verify selectively.
    

`eth_getProof` works on Ethereum and EVM chains generally (support and trie formats vary on L2s — test your target). Try the WETH query above on our [Ethereum endpoint](https://swiftnodes.io/ethereum-rpc) — the [free tier](https://swiftnodes.io/) covers a lot of proofs, and flat-rate paid plans include the archive access that historical proofs need.

* * *

*Originally published on the* [*SwiftNodes blog*](https://swiftnodes.io/blog/eth-getproof-verify-state-merkle-proofs)*. SwiftNodes provides flat-rate multi-chain RPC endpoints — HTTP + WebSocket, 75+ chains, no per-request metering.* [*Grab a free key*](https://swiftnodes.io/)*.*
