以太坊状态证明怎么验?余额、存储与可信区块

 / 
4

验证以太坊状态证明,核心动作只有一个:拿eth_getProof返回的 Merkle 路径,从叶子节点逐级向上哈希,看最终算出的根是否等于你信任的那个stateRoot。余额和存储的验证逻辑相同,区别在于存储验证多了一层:先证明账户存在并拿到storageRoot,再用这个根去验证具体的存储槽。

欧易OKX交易所
全球领先的加密货币平台,适合新手与进阶交易者
新手福利:注册即享20% 交易手续费减免!

先拿到一个可信的 stateRoot

这是整个验证的前提,也是最容易搞错的地方。eth_getProof返回的证明本身不包含"这个根是可信的"这一信息。如果你从同一个 RPC 节点同时获取stateRoot和证明,验证就失去了意义——节点可以伪造两者并让你通过。

可信stateRoot的来源有几种。如果你运行全节点,直接从本地区块头取。如果用轻客户端(如 Helios),它通过同步委员会签名验证区块头,产出的stateRoot是共识层验证过的。如果你只有普通 RPC,至少要从两个独立来源获取同一区块的stateRoot并比对,虽然这不如轻客户端严格,但比单一节点可靠。

把区块号固定下来也很重要。用latest标签获取证明,你验证的是某个时刻的状态,但那个时刻对应的stateRoot可能在你验证时已经变了。指定一个具体的区块号(或区块哈希),从该区块头取stateRoot,整个验证过程才是一致的。

余额验证:账户证明的走法

eth_getProof的响应里,accountProof是一个 RLP 编码的节点数组,从状态树的根开始,沿着账户地址的路径一直到包含该账户的叶子节点。

验证的步骤是机械的。取accountProof的第一个节点,RLP 解码,确认它包含的哈希与你手中的stateRoot一致(如果是根节点,它本身就是根)。然后沿着账户地址(先做 keccak256 哈希)的 nibble 路径,找到下一个节点在父节点中的引用,解码并验证它的哈希与父节点中存储的哈希匹配。重复这个过程直到叶子节点。叶子节点里包含balance、nonce、storageRoot、codeHash四个字段,取出你关心的balance,同时对叶子节点做 RLP 编码和 keccak256 哈希,结果应该等于上一级节点中引用的哈希。

如果账户在给定区块不存在,accountProof可能是空数组,balance和nonce返回零。这本身也是一个可验证的声明:证明该地址在状态树中不存在,而不是节点在撒谎说余额为零。

存储验证:两层证明串联

存储槽的验证比余额多一步。storageProof数组里每个元素包含key、value和proof。你首先需要完成账户证明,从账户叶子中拿到storageRoot。然后以storageRoot为根,用key(32 字节的存储槽位置,做 keccak256 哈希)作为路径,对proof中的节点执行同样的逐级验证。最终叶子中的值就是该存储槽的值。

有一个细节:Solidity 里mapping的存储位置不是简单的keccak256(key)。对于mapping(address => uint256),槽位计算是keccak256(abi.encode(key, mappingSlot)),其中mappingSlot是该 mapping 在合约中声明的位置(从 0 开始计数)。如果你不知道合约的存储布局,就无法构造正确的storageKeys参数,eth_getProof返回的证明也无法验证你关心的那个值。这不是验证逻辑的问题,是输入错了。

用库还是手写

手写完整的 MPT 验证逻辑容易在 nibble 处理、RLP 解码、空节点处理这些细节上出错。实际使用中,用现成的验证库更可靠。

@merkleforge/ethereum提供了verifyEthGetProof(stateRoot, response),返回验证后的账户和存储值,验证失败时抛出InvalidProof错误。它同时提供mappingSlot辅助函数来计算 mapping 的存储位置。这类库的价值在于把 MPT 的边界情况(扩展节点、分支节点、叶子节点的不同处理)封装好了,你只需要确保传入的stateRoot是可信的。

如果要用 JavaScript 手写,需要rlp和keccak256两个依赖。OnFinality 的教程提供了一个完整的 Node.js 脚本,核心逻辑是遍历accountProof数组,用地址的 nibble 路径逐级验证哈希链,最后将叶子节点的 RLP 编码哈希与stateRoot比对。这个脚本适合理解机制,但不建议直接用于生产——缺少对畸形输入、空证明、扩展节点边界情况的处理。

一个容易卡住的实践问题

如果你在验证时遇到MissingTrieNode错误,通常不是验证逻辑写错了,而是证明数据不完整。eth_getProof返回的节点数组理论上应该构成从根到叶子的完整路径,但某些 RPC 实现或代理可能截断或遗漏节点。换一个 RPC 端点重新获取证明,或者检查你使用的库是否对证明节点做了额外的过滤。

另一个常见错误是哈希不匹配但路径看起来正确。检查地址或存储槽 key 在哈希前是否做了正确的编码。账户地址需要先keccak256再作为 nibble 路径,存储槽的 32 字节 key 同样需要keccak256。如果你直接拿原始地址或槽位去遍历,路径会对不上。

完成的标准

验证完成的标志是:从你信任的stateRoot出发,accountProof的哈希链闭合,叶子中的balance(或storageRoot加上storageProof验证后的value)与你从其他渠道(如区块浏览器、合约调用)看到的值一致。如果哈希链不闭合,证明无效,不管返回的数值看起来多合理。

如果你的应用需要持续验证状态,把验证逻辑封装成一个函数,输入是stateRoot、地址、可选的存储槽列表,输出是验证后的值或错误。每次调用eth_getProof后都跑一遍这个函数,而不是直接信任 RPC 返回的balance字段。

欧易OKX交易所
全球领先的加密货币平台,适合新手与进阶交易者
新手福利:注册即享20% 交易手续费减免!

参考资料

  1. Foresight News·详解 a16z 推出的 Helios:完全无需信任的以太坊访问,页面发布或更新日期:页面未标明更新日期;核查日期:2026-10-20。
  2. OnFinality·eth_getProof (EIP-1186):用于状态验证的账户和存储证明,页面发布或更新日期:页面未标明更新日期;核查日期:2026-10-20。
  3. OnFinality·EVM ETH GetProof State Account Storage Proofs,页面发布或更新日期:页面未标明更新日期;核查日期:2026-10-20。
  4. NPM·@merkleforge/ethereum 包页面,页面发布或更新日期:页面未标明更新日期;核查日期:2026-10-20。