1

The state trie stores account state that contains (address => rlp[nonce, balance, stateRoot, codeHash]) nodes. When I'm creating an MPTrie proof (aka EIP 1186) over that node I'm providing a path of sibling nodes that lead to our account's node like

trie.findPath(keccak256(toBuffer(address)))

and use that result as a proof that the account node (identified by its address, not its content) has been included in the block.stateRoot. That's fine.

However, this proof isn't guaranteeing anything about the state node's value (e.g. its balance), since its content is not part of the node's key. So if I'm providing a stateTree proof & its current node value a verifier cannot prove that the value is correct, only that the node is part of the state trie. I'm not sure what I'm missing here since Vitalik himself once outlined that these proofs are perfectly suitable for proving balances (or contract state roots for that matter):

https://blog.ethereum.org/2015/11/15/merkling-in-ethereum/

This allows for a highly advanced light client protocol that allows light clients to easily make and get verifiable answers to many kinds of queries:

  • Has this transaction been included in a particular block?
  • Tell me all instances of an event of type X emitted by this address in the past 30 days
  • What is the current balance of my account?
  • Does this account exist?
  • Pretend to run this transaction on this contract. What would the output be?

The first is handled by the transaction tree; the third and fourth are handled by the state tree, and the second by the receipt tree. The first four are fairly straightforward to compute; the server simply finds the object, fetches the Merkle branch (the list of hashes going up from the object to the tree root) and replies back to the light client with the branch.

2 Answers 2

1

You are correct, the key->value interface of the trie itself does not commit the balance, but there is an underlaying (also key->value based) DB that does.

When you lookup a key in the trie, it internally does many lookups in the underlaying DB. In this DB every single entry is a key->value pair such that the key is the keccak256 of the value.

See the Ethereum wiki for exact specification of the tree, but the proof works because each entry in the path contains a hash to the next entry's value all the way to the final leaf value (the account)

0

And as usual, the answer is far closer when you ask your question publicly first. The proof actually contains the leaf node with the value you want to prove as its last stack entry. Here's a sample for the Sepolia network. If I want you to prove the balance of my account 0xe127a39da6ea2d7b1979372ae973a20bab08a80a at block height 1432400 to you, I can provide you this stack of MPT nodes (you can use eth_getProof for that):

[
  '0xf90211a078400dd6bd6f6cc13cdde5b707bdb6b9802fbd11809cba8aab2ee72f6920bbf3a0d88879eb6f43ea2d9c07538601e041aa46d64c87df68296a552ad12f8e5bebc0a044d40ac93c1bce308feda36a7712a38d9508087f04e612b1411bc57f80fa77a8a0b92c5fc0dcbc4a7982563c3667b940117770965e5f447578e4bde4fefcbb6d5aa0bc555b313ac90177e117dc10917581dad616770625e0956feb0df75a4be2ce63a0bd19008ae54cf1af36e59d7c92c553693dd468a0ef86c89401d01fc84aa4b751a0ee549b7ab8dd9a565ee6507bb46b9033e57b3a07541a4085763207911633367ca05652bd80f5b970a09c2090c38b13afa1772f3d97422c9a16548acdf629c35113a0a2e78f2d0da2d7218535b94bd69c4fad4425ff3bb22304d722185cf99dac2596a0a9470e771ae9cec5535b057948e68c4c38d8b8c1c2d8ed180d8f5244c63e422ea0726d10a2a5c85b156105414e75591611d896b8aaebd7fc92535845445a8baa81a0fdfb5aa2c35136eab67c3539d3ba34648bb8542b57e745976d52bca263ab19d7a01eafa4a1f59d28009a03b24d9a11c878faa78fe7ef3ecc4ef8891a77fea3fa35a02c980b0a841d5ec1978ae63adf7ba2727ac125f792f545cb0146d8df46adbdbfa052b97e4692fd69b4bebc6ceca258032319b07aca2ee62b504285031670fbc073a02c415e41b6bf838a8b742123fda110703ffdedea8834bb5113ae26f81887668e80',
  '0xf90211a098e0b7071d2b8d890cca479dba9f5b697d639d70d51f0312c5b8a27afc31a23da060759f93f482f66b233480a4cd060372567752b9dae507045b266600ab9b290da0c35bb5b2aae7eb8bc75a9de125daab9caa7d6d39e403153c9b34e51dcde46f78a097a4c1ba8ac42378481754eac9306b62d3d176602098820cb87e3c7f4091734da074b2785ca9199d0c68e2fa6dd01dcc24791887616adf8043cda56651cff25e87a00937963d57bc119144ebeff9bb99dcbef11aed563fd41818aaf0de89d15364b3a000ba5def8acd7c1c2af85ef49bc5d144fa1ceef93e81e9e64ad6457ab92cb569a069cd4b3ac6b03b1928f50840f81086ff0c819d1cbd445db0e5c306cfce9c0728a0a78339d0b9405da5a7943a6ac97b9585767a6f9f0e481b5e197d8fd820ac1cfaa019e1319ade8f1c2254dcef0b68483240044bc6bee2ea8197de895eb427068b1aa0ef36884df92b5769e521c6c0feaead503a8d70d11f493b3e6c8170ac917ced92a079c1ffdaa2a8d4ddacac77f47b43f1553c634300d23a8003574ec90874547cbaa0de4f130f1fd52baf5229e3871394b6f0562c64576039c6c38d06c695b7d87b07a09668798efa8c1e9b860d9f4a49b65db83c22fefd92f703a5875b87c7ae11bdf4a087ca8f145dbc9ad7f88468dbec0627d77ced63a508b4d7a5d28439e89c6be8e4a04fdb75be4fce4d76e41578f0bbcfdc2ded74d59855c9f3afc73c3659b9e28aa980',
  '0xf90211a058d99773cf489bd86bac2c6452c20cc7056f3505d7f96d83480cf85336d0da85a07dc4216585f73a213fb1828076cf7d93d8ce95e1981a5c8eb11d7166af8433eba0f85f31a95a1f83516719ae596266afdc529aa4ff9af8f0f6c1d7da6751f07d70a0f1371490b8ef31ce814673e9ecf0f498f2ec9eedcd554e93162d5c0e99e77d69a01d9709211cc089eba15fe3c052539203d11235b219e1c72e0172c9bec1d317bea09e907ad084233149719b5c6f8b4777a617b41744a0c3e9f6ef002970465a0656a044f3abc34546455ba2ed3d64ead41c734d5f56c240f8f8adfd971179538ca0efa08403716832691c8867b1366421b703925ff28e45d6f4360f898b70461e29d4eda0d8f4711ffe7bc79040b2f397523786a67d72814e8af9b65c9e03c318bb0eba48a0db84d2a268e67d6e45af7cd18a92fab1df496180e9ba8f0f0b620ab6344457cba0d01707094316206d689d80ed2684cda3aadd72ee2d4efbe1437bb194aa0adc87a0355e0e203341ac995bf2d90937f1ce807a13624e9d3831476f3205838b84f191a083961a9b498b791ee8e6a110a87aca2f2cea629df0827f4fbd168567d7246207a05d312338764151dc1936485637367ad9d9c440b4b0dde368dec4dbd5aaf51f4ea076b77567690221701e2edfff06f1c84a25b3181e4c8aa3bed2b3e3797a883564a0975b63f1be5615a4a68ec7f313abc0784def11a352975c40e886e03df95f764e80',
  '0xf9013180a0944968a4a8c7ba1e91cbb2414471a67f16c74e2ef6ac87447af0402520aed9c88080a03f3e1fed1008e242b89ce5b56106a091f946826c539f23ecee74c5b36b5d38cca041b58c41b5435e10c9b9f6b1f1bdf3d5e0294dfcfbf59b7a8c80475249e1d9e6a0d0b42862dcb175e47bf17614bd251b17cdab2710ba13175fd9b8c17afdc0893ba0ff4dc7ee613a7805802e50d39c3c8d8b35db63ceda7371502c5d119eca0a7b1780a0cd328766d44f930edc3a32ae5fd2a791f0de4dbabe014c4be77fc63b4b427310a0c20a6a1812f18d0c446408f90b09d0b592a07c45d06e780b49c6ad68e7c92bc5a084688c931305a75246f9d527a07483cff6e9ffbe4746a22ca65e07d3b3c6045f8080a0515f82b6e34b4713edead2652f266fdcea3729d64217113db564e918a9b1ed408080',
  '0xf8518080a06b91f274ef06ab455966416f6bb3779519bf72c68e85ec4b61ff8b99aa73a1818080a0937ee4540dc495eaa3cd61e12a4f6158a6be147d71ac3955a50ecd080e9732698080808080808080808080',
  '0xf8709e3e0484bbc22108bc77412e65255ce0387dc7c6a8d1917625ddb60ccbd98fb84ff84d028901f3d52b3c4f92e45da056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'
]

you need to run a very light node (or trust some light node) that just tracks the blockchain headers. Ask it for the headers of block 1432400:

{
  hash: '0xc8dbbd057b1c9eaee6dde6ffce6ff78ff744383f49f8d94e91b64b5e35d9effe',
  mixHash: '0x90462af88f38a49c7d43d7d269bb859be8fae98c406eabbb6fd0c5936022f057',
  nonce: '0xdb57853cf8cfb7bf',
  number: 1432400,
  parentHash: '0x421eac5daca03ab86b98418ac369f54ea35d2a6baa92a573fc6c2a282c1d36b9',
  receiptsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421',
  sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
  size: 543,
  stateRoot: '0xb159ecc463dbee96f50fcf7d1e3d2b23315271512762bafcecdfd1924dc8cea7',
  timestamp: 1656877567,
  totalDifficulty: '13654894464040916',
  transactions: [],
  transactionsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421',
  uncles: []
}

now that you have the block's verified stateRoot you can:

  const { toBuffer, bufferToHex, keccak256, rlp, Account, Address } = require('ethereumjs-util');
  const { SecureTrie } = require('merkle-patricia-tree');

  const accountProof = [...]   //the proof from above
  const address = "0xe127a39da6ea2d7b1979372ae973a20bab08a80a"

  //create a trie out of the provided proof
  const proofBufs = accountProof.map(p => toBuffer(p));
  const proofTrie = await SecureTrie.fromProof(proofBufs);
  const accountNodeRaw = await proofTrie.get(keccak256(toBuffer(address)));
  const account = Account.fromRlpSerializedAccount(accountNodeRaw);
  console.log("proven value", account);

  const valid = bufferToHex(proofTrie.root) == block.stateRoot;
  //or: await proofTrie.checkRoot(toBuffer(block.stateRoot));
  console.log("proof is valid", valid);

->

proven value Account {
  nonce: <BN: 2>,
  balance: <BN: 1f3d52b3c4f92e45d>,
  stateRoot: <Buffer 56 e8 1f 17 1b cc 55 a6 ff 83 45 e6 92 c0 f8 6e 5b 48 e0 1b 99 6c ad c0 01 62 2f b5 e3 63 b4 21>,
  codeHash: <Buffer c5 d2 46 01 86 f7 23 3c 92 7e 7d b2 dc c7 03 c0 e5 00 b6 53 ca 82 27 3b 7b fa d8 04 5d 85 a4 70>
}
proof is valid true

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.