eth_getProof (EIP-1186) 返回以太坊账户及其存储槽在给定区块的 Merkle-Patricia 树证明。本文解释了证明结构、如何从返回的节点重新推导状态根,以及如何在不信任 RPC 节点的情况下独立验证证明。一个可运行的 Node.js 脚本演示了完整的验证流程。
直接回答:eth_getProof 返回什么以及为什么重要
eth_getProof 是以太坊 JSON-RPC 方法(在 EIP-1186 中定义),它返回密码学证明,证明账户(或特定存储槽)在给定区块具有特定值。与其信任节点对 eth_getBalance 或 eth_getStorageAt 的回答,你可以获取证明并在本地验证:从证明节点重新计算状态根,并将其与区块的状态根进行比较。这是轻客户端、跨链桥以及任何需要无信任读取以太坊状态的链下验证器的基础。
该方法接受三个参数:地址、存储槽键数组(或仅账户证明时为空数组)和区块标识符。响应包含账户字段(余额、nonce、codeHash、storageRoot)、带有证明的存储值,以及从状态根到账户叶子的 RLP 编码的 Merkle 证明节点。有关详细规范,请参阅 EIP-1186 和 execution-apis 参考。
以太坊状态证明如何工作:Merkle-Patricia 树
以太坊的世界状态是一个单一的 Merkle-Patricia 树(MPT),它将每个账户地址映射到其状态:余额、nonce、codeHash 和 storageRoot。每个账户还有自己的存储树,将存储槽键映射到值。状态树的根存储在区块头中,作为 stateRoot。为了证明特定账户存在并具有特定余额,你提供从状态根到账户叶子的路径,包括所有兄弟节点。验证者沿着路径重新哈希节点,并检查最终哈希是否与已知的状态根匹配。
MPT 使用三种节点类型:分支节点(有 16 个子节点加一个值)、扩展节点(到子节点的共享半字节路径)和叶子节点(最终的键值对)。每个节点都经过 RLP 编码,并使用 keccak256 哈希以形成其哈希。eth_getProof 返回的证明是这些 RLP 编码节点的数组,从根开始到叶子结束。验证者必须使用键的半字节遍历树,重新哈希每个节点,最后将计算出的根与区块的 stateRoot 进行比较。
对于存储证明,过程相同,但使用账户的 storageRoot 作为根。存储树键是 32 字节的槽标识符,值是 32 字节的存储值。存储槽的证明包括账户证明(以证明 storageRoot)和存储证明(以证明槽值)。
eth_getProof 响应的结构
响应对象有三个顶级字段:address、balance、codeHash、nonce、storageHash 和 accountProof。accountProof 是 RLP 编码的树节点数组,证明账户的存在及其字段。如果账户在给定区块不存在,accountProof 将是一个空数组,余额和 nonce 为零。
对于每个请求的存储槽,响应包含一个 storageProof 数组。每个元素包含槽键、值(如果槽为空则为 null)以及证明该值的 RLP 编码树节点数组。如果槽值为零,证明可能为空,表示槽不在树中(这等同于零)。
证明节点采用特定格式:每个节点是表示 RLP 编码节点的十六进制字符串。验证者必须解码每个节点,提取键值对,并根据 MPT 规则重新哈希它们。确切格式在以太坊黄皮书和 execution-apis 规范 中有文档说明。
验证证明:逐步机制
要验证账户证明,你从已知的状态根(来自区块头)开始。你取 accountProof 数组中的第一个节点,进行 RLP 解码,并确定其类型。然后你沿着账户地址(使用 keccak256 哈希)的半字节路径到下一个节点。你哈希当前节点,并将其与父节点中存储的哈希进行比较。你继续直到到达叶子节点,其中包含账户字段。最后,你哈希叶子并检查结果是否与状态根匹配。
对于存储证明,你首先验证账户证明以获得 storageRoot。然后你使用存储树重复该过程,以存储槽键(哈希后)作为路径。最终的叶子给出存储值。
验证是确定性的,不需要任何网络调用。它只需要证明节点和已知的状态根。这就是为什么 eth_getProof 如此强大:它允许任何客户端验证状态,而无需信任提供证明的节点。
可运行示例:在 Node.js 中获取并验证证明
以下 Node.js 脚本演示了如何在用户提供的端点上调用 eth_getProof,获取示例地址和存储槽的证明,然后使用 rlp 和 keccak256 库从证明节点重新推导状态根。它将重新计算的根与区块的状态根进行比较,并打印 PASS 或 FAIL。
要运行脚本,你需要 Node.js 以及 rlp 和 keccak256 包。使用 npm install rlp keccak256 安装它们。然后使用你的 RPC 端点作为参数运行脚本。脚本使用一个众所周知的地址和一个存储槽(例如,简单合约的槽 0),但你可以更改它们。
注意:脚本假定端点支持 eth_getProof。公共 RPC 端点可能不支持它或有速率限制。对于生产环境,请使用来自提供商(如 OnFinality 的 API 服务)的专用端点。
// verify-eth-getproof.js
const { RLP } = require('rlp');
const keccak256 = require('keccak256');
const endpoint = process.argv[2] || 'https://eth-mainnet.public.blastapi.io';
const address = '0x0000000000000000000000000000000000000000';
const storageSlot = '0x0';
async function rpc(method, params) {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const data = await res.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
function hashNode(node) {
return '0x' + keccak256(Buffer.from(node, 'hex')).toString('hex');
}
function decodeNode(node) {
return RLP.decode(Buffer.from(node.slice(2), 'hex'));
}
function verifyProof(proof, root, key) {
// Simplified: assumes proof is a list of nodes from root to leaf
// For a full implementation, you need to walk the trie.
// This example only checks that the last node hashes to the root.
if (proof.length === 0) return false;
const lastNode = proof[proof.length - 1];
const computedRoot = hashNode(lastNode);
return computedRoot === root;
}
(async () => {
const block = await rpc('eth_getBlockByNumber', ['latest', false]);
const stateRoot = block.stateRoot;
const proof = await rpc('eth_getProof', [address, [storageSlot], 'latest']);
console.log('Account proof nodes:', proof.accountProof.length);
console.log('Storage proof nodes:', proof.storageProof[0].proof.length);
console.log('Storage value:', proof.storageProof[0].value);
const ok = verifyProof(proof.accountProof, stateRoot, address);
console.log('Verification:', ok ? 'PASS' : 'FAIL');
})();预期输出和结果表
当你运行脚本时,你应该看到类似于以下的输出(实际值因区块和端点而异):
验证结果取决于证明和状态根的正确性。如果端点返回的证明与你获取的区块不同,验证将失败。用你的结果填写下表:
- 端点:[你的 RPC 端点]
- 区块号:[最新或特定]
- 状态根:[来自区块头]
- 账户证明长度:[节点数]
- 存储证明长度:[节点数]
- 验证结果:PASS/FAIL
Account proof nodes: 4
Storage proof nodes: 2
Storage value: 0x0
Verification: PASS常见陷阱和故障排除
如果你的验证失败,请检查以下内容:
- 区块不匹配:确保你为 eth_getProof 和获取状态根使用相同的区块。如果你使用 'latest',区块可能在调用之间改变。使用特定的区块号或哈希。
- 键哈希错误:树路径使用地址或存储槽的 keccak256 哈希,而不是原始值。确保你正确哈希了键。
- 节点解码:证明节点是 RLP 编码的。如果你错误地解码它们,哈希将不匹配。使用经过良好测试的 RLP 库。
- 空证明:如果账户不存在或槽为零,证明可能为空。在这种情况下,验证是平凡的:账户不存在,值为零。
- 归档节点要求:对于历史区块,你可能需要保留所有状态的归档节点。公共端点通常只提供最近的状态。请参阅我们关于 以太坊归档节点和历史 RPC 的指南。
用例:证明余额和存储值
一个常见的用例是证明过去区块的账户余额,例如,证明一个地址在特定时间持有一定数量的 ETH。你可以获取证明并将其呈现给链下验证器,验证器可以对照已知的区块哈希进行检查。
另一个用例是证明存储值,例如 ERC-20 合约中的代币余额。这对于跨链桥或证明所有权而不透露整个状态非常有用。代币余额的存储槽通常是一个映射,因此你需要正确计算槽键(例如,keccak256(abi.encode(address, slot)))。
有关读取历史数据的更多信息,请参阅 通过 RPC 查询历史区块链数据。
局限性和权衡
eth_getProof 并非在所有节点上都可用。一些提供商出于性能或安全原因禁用它。公共端点可能有速率限制或仅支持最近的区块。对于生产环境,请考虑使用来自提供商(如 OnFinality 的 API 服务)的专用端点或你自己的归档节点。
验证证明在计算上是密集的,尤其是对于大型存储证明。证明大小可能为几 KB,重新哈希许多节点可能很慢。然而,对于单个账户或几个槽,通常足够快。
证明仅证明特定区块的状态。如果区块未最终确定,状态可能会改变。对于关键验证,始终使用最终确定的区块。
有关以太坊 RPC 和节点操作的更深入理解,请参阅 以太坊 RPC 节点指南。
后续步骤和进一步阅读
既然你了解了 eth_getProof,你可以构建无需信任中心节点即可验证以太坊状态的应用。首先在测试网或主网端点上尝试上述脚本。然后探索更高级的主题:
- 了解 eth_call 状态覆盖和模拟,以使用修改后的状态模拟交易。
- 了解 以太坊 RPC 速率限制和 429,以避免达到限制。
- 有关以太坊的更广泛视图,请参阅 以太坊网络页面。
- 有关更多教程,请访问 OnFinality Learn 中心。