Ethereum's decentralized architecture relies heavily on cryptographic proofs to ensure data integrity, especially for lightweight clients that cannot store the entire blockchain. One of the most powerful tools in this ecosystem is state proof verification, which allows users to validate the state of smart contracts without trusting a third party.
In a previous article, we explored how to verify Ethereum account balances using state proofs. Now, we’ll go deeper—focusing specifically on how data is stored within smart contracts and how to cryptographically verify individual storage values using Merkle proofs.
This guide walks you through the inner workings of Ethereum storage, explains how state variables are mapped to storage slots, and demonstrates how to query and verify their values using the eth_getProof RPC method.
How Smart Contracts Store Data
An Ethereum smart contract is essentially a special type of account. Like regular accounts, it has a balance and a nonce. But unlike external accounts, smart contracts also contain code and persistent storage.
This storage is implemented as a key-value store, where each value is 32 bytes long. To enable efficient and secure verification, Ethereum uses a Merkle Patricia Trie (MPT) structure to organize this data. This trie allows anyone to generate a compact proof of existence for any piece of stored data.
👉 Discover how blockchain verification powers trustless systems
Understanding Storage Slots
Smart contract state variables are not stored using their names (like owner or last_completed_migration). Instead, they are assigned to fixed positions called storage slots, indexed from 0.
The assignment is deterministic and happens at compile time, based solely on the order in which variables appear in the source code.
For example, consider Truffle’s default migration contract:
contract Migrations {
address public owner;
uint256 public last_completed_migration;
}Here:
owneris stored in slot 0last_completed_migrationis stored in slot 1
These positions are fixed and independent of variable names or types (as long as they fit within 32 bytes).
Querying Storage Values
To retrieve a value from a specific slot, Ethereum provides the JSON-RPC method eth_getStorageAt. It requires three parameters:
- Contract address
- Storage slot index
- Block identifier (e.g.,
"latest"or a specific block number)
Using the deployed contract at address 0xcca577ee56d30a444c73f8fc8d5ce34ed1c7da8b, we can query:
Fetching the Owner (Slot 0)
{
"jsonrpc": "2.0",
"method": "eth_getStorageAt",
"params": [
"0xcca577ee56d30a444c73f8fc8d5ce34ed1c7da8b",
"0x0",
"0xA8894B"
],
"id": 1
}Result:
0x000000000000000000000000de74da73d5102a796559933296c73e7d1c6f37fbNote the left-padding with zeros—each slot holds exactly 32 bytes.
Fetching Last Completed Migration (Slot 1)
{
"params": [
"0xcca577ee56d30a444c73f8fc8d5ce34ed1c7da8b",
"0x1",
"0xA8894B"
]
}Result:
0x0000000000000000000000000000000000000000000000000000000000000002Which decodes to the integer 2.
While these values come from a node, we don’t need to trust them blindly—because we can verify them cryptographically.
Behind the Scenes: The Storage Merkle Trie
To support verifiable queries, Ethereum builds a Merkle Patricia Trie over all storage slots. But before insertion, both keys and values undergo transformation:
- Key (slot index): Padded to 32 bytes, then hashed with keccak256
- Value: Encoded as a hex string and RLP-encoded
So for:
- Slot 0 → Key:
keccak256(32-byte-padded(“0”)) - Value: RLP(
owner_address)
And similarly for slot 1.
Once processed, these transformed key-value pairs are inserted into the trie. The resulting root hash is part of the contract’s account state and ultimately linked to the global state root in the block header.
You can reconstruct this trie locally and compute its root hash to compare against on-chain data.
Verifying Contract State with Proof
To verify the authenticity of storage data, use the eth_getProof RPC method:
{
"method": "eth_getProof",
"params": [
"0xcca577ee56d30a444c73f8fc8d5ce34ed1c7da8b",
["0x0"],
"0xA8894B"
],
"id": 1
}This returns:
- Account-level information (nonce, balance, code hash)
storageHash: Root of the storage triestorageProof: Path from root to leaf node for slot 0
We already computed the expected storageHash locally—it matches 0x7317ebbe7d6c43dd6944ed0e2c5f79762113cb75fa0bed7124377c0814737fb4. This confirms our local trie structure is correct.
But more importantly, we can now verify the specific proof for slot 0 against this known storageHash.
👉 Learn how cryptographic proofs secure decentralized applications
Verifying a Single Variable with Storage Proof
Yes—you can verify just one variable without downloading all contract data.
The returned storageProof contains an array of encoded trie nodes forming the path from root to the target leaf. By replaying this path and checking if it produces the expected value, you can confirm its validity.
Steps:
- Start from the
storageHash - Traverse down using each node in
storageProof - Confirm final value matches what was returned by
eth_getStorageAt
If successful, you’ve proven that at block 11045195, the owner of the Migration contract at 0x...da8b is indeed 0xde74...37fb.
This process enables light clients (like mobile wallets or off-chain services) to securely interact with Ethereum without storing terabytes of data.
Frequently Asked Questions
What is a storage slot in Ethereum?
A storage slot is a 32-byte position in a smart contract’s persistent storage. State variables are assigned to slots starting from index 0 based on declaration order.
Can two variables share the same slot?
Yes—if they are small enough (e.g., multiple uint128 variables), Solidity may pack them into a single slot to save space. However, each distinct state variable still has a deterministic location.
Why do we hash the slot index before storing it?
Hashing prevents predictable key patterns and ensures uniform distribution across the trie, improving security and performance. It also aligns with Ethereum’s use of keccak256 throughout its protocol.
How does eth_getProof help light clients?
It allows light clients to request minimal proof data instead of full state. They verify proofs against trusted block headers, enabling trustless access to any contract state.
Is it possible to verify dynamic data like arrays or mappings?
Yes—but their layout is more complex. Arrays use length at the slot and elements at derived positions; mappings use hashing to calculate locations. We’ll cover these in a follow-up article using real-world contracts like USDC.
Can I verify ERC-20 balances using this method?
Absolutely. An ERC-20 balance (e.g., USDC) is stored in a mapping inside the token contract. You can compute the correct storage slot using keccak256 hashing rules and verify it via eth_getProof.
👉 Explore tools that simplify blockchain verification workflows
Summary
Here’s what we’ve learned:
- Smart contracts store state variables in deterministic storage slots indexed from 0.
- Slots are not named—they’re positional and compiled based on variable order.
- Each slot holds 32 bytes and supports complex data via encoding rules.
- Ethereum uses a Merkle Patricia Trie to organize storage, enabling efficient proofs.
- With
eth_getProof, you can retrieve and verify cryptographic proofs for any storage value. - Light clients leverage these proofs to trustlessly validate state without full synchronization.
This mechanism underpins many advanced use cases: cross-chain bridges, Layer 2 rollups, fraud proofs, decentralized oracles, and more.
What’s Next?
So far, we’ve focused on simple, fixed-size variables. But real-world contracts often use dynamic data structures: arrays, mappings, strings, and nested objects.
In the next article, we’ll analyze the USDC contract to show:
- How token balances are stored in mappings
- How to compute storage keys for user balances
- How to verify ERC-20 balances with proofs
- And even how NFT metadata can be validated off-chain
Stay tuned for deep dives into dynamic storage layouts and practical verification techniques.
Core Keywords
Ethereum smart contract state, storage proof, Merkle Patricia Trie, eth_getProof, verify contract state, blockchain verification, cryptographic proof, light client validation