In previous blog post, we introduced how to verify account balance using state proof. But we didn’t talk about how to get the proof and where to query the proof for a given account.
That is what this blog post is about.
In this blog post, I will introduce EIP1186, which is the standard for querying account proof or contract storage proof from a full node.
The use cases are, as a light client, who don’t have access to the entire blockchain data,
- You can ask for Merkle Proof for your Ethereum account balance and verify the correctness yourself.
- You can ask for Merkle Proof for your ERC20 token account balance. For instance, USDC account balance.
- You can ask for Merkle Proof for your ERC721 token ownership. For instance, to prove you are the owner of a CryptoKitties NFT.
You can query these Merkle Proof from untrusted full node, because you can verify the proof yourself easily. If the Merkle Proof is invalid, then it means the result of your Ethereum account or storage state that is sent along with the proof is invalid.
OK. Let’s start with the EIP standard — EIP1186.
EIP-1186
EIP-1186 defines a method eth_getProof to query for Merkle proof:
The eth_getProof method takes an address to query account state for and a block number. It returns an account object, which contains the account state data including balance, nonce, storageHash and codeHash. More importantly, it also return the accountProof for verifying the account state data.
eth_getProof also support for smart contract account, it provides an additional parameter (storage key) to query proof for the storage state. We won’t cover it in this blog post, but will cover in future post.
Example
Let’s walk through an example to query the proof for an Ethereum account, and verify the proof.
First, let’s pick an random Ethereum account:
In order to query the state proof, we need to find a full node that supports EIP-1186 standard.
There are a few services that produce APIs for the eth_getProof RPC method call, such as Infura and Alchemy.
In this example, I will just use Alchemy’s API to query the proof.
Again, the reason I pick Alchemy is not because I trust them more than other services. As a light client, we don’t need to trust the result from any full node, because we are able to verify the result ourselves. We will only accept the result if the proof can pass our validation.
Now let’s use Alchemy’s API to query the proof. We will need an API key for using Alchemy API. Once we have an API key, we can make the following HTTP request:
Among the parameters, the 0xB856af30B938B6f52e5BfF365675F358CD52F91B is the account address, and 0xE35B21 is the block number 14900001 in hex format.
This HTTP request returns the following JSON response:
The response shows the account balance is 0x4ef05b2fe9d8c8 , which is
0.02221932261997588 Ether at block 14900001. The nonce is 0x10 , which is 16.
The codeHash 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is the keccak256 hash of empty string, which means the account is a user account. User account has no code.
Since user account has no extra storage, the storage hash is a constant string: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421, which is keccak256 hash of an empty Merkle trie.
Now we’ve got the account state from the response, should we trust it? No, we don’t want to blindly trust that. We can verify it with the account proof that comes along with the account state data.
Next, let’s verify the account state with the account proof.
Get State Root Hash
The account proof is a list of hex string. They are essentially the encoded Merkle trie nodes.
To verify the proof, we need one additional piece of data. That is the state root hash of the world state trie at that particular block.
The state root trie hash is included in the block header of each block. And the data in the block header can be verified with its fields, including difficulty and nonce, which are known as the source of Proof of Work.
For simplicity, I will look up the state root hash from Etherscan, since it’s a different source than Alchemy where we query the account data state and its proof from.
Etherscan shows that the StateRootHash for this block is):
Now, with the proof from Alchemy and state root hash from Etherscan, we have everything needed for verifying the proof.
Verify Merkle Proof
In order to verify the Merkle proof with the state root hash, we need to use the Merkle Patricia Trie data structure.
I’ve previously introduced how Merkle Patricia Trie works, and implemented a simplified version. I also introduced How to verify the account state with the proof using the Merkle trie.
Here, we will continue using the simplified implementation of the Merkle trie to verify the proof that we queried from eth_getProof call.
First, let’s define the type for the eth_getProofcall’s response:
Next, let’s save the HTTP response into a JSON, and parse the data into a data structure:
Let’s create a trie, then we add the proof to the trie:
The proof is essentially the encoded trie nodes on the path from the root node of the trie to the leaf node that stores the account state data.
Now the trie is constructed, we call the VerifyProof method of the trie and pass in the account address and the state root hash.
The state root hash is the start point from which we traverse through the trie, and the account address is the actual path to traverse through the trie.
If we are able to reach a leaf node in the end, then the proof is valid.
If we didn’t reach a LeafNode, then the proof is invalid.
In the example, when we run the test case, it passes. So it means the account state is valid.
You can find the source code of the example here.
Summary
In this blog post, we introduced EIP-1186 — the standard of querying proof from full node for light client to verify the account state.
We walked through an example to verify the account state of an Ethereum account on mainnet.
But how to verify the storage state in a smart contract? For instance the USDC balance of an Ethereum account.
To understand that, we need to know how Ethereum structures the smart contract storage. I will cover it in my next blog post.
Stay tuned.
References
https://ethereum.github.io/yellowpaper/paper.pdf
https://eips.ethereum.org/EIPS/eip-1186
GitHub - vocdoni/storage-proofs-eth-go: Golang library and utility for extracting erc20 token…
A storage trie is where all of the contract data lives. Each Ethereum account has its own storage trie. A 256-bit hash…
github.com
More
If you are interested in learning more, check out my blog post series: