Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 13 分钟

eth_createAccessList:构建 EIP-2930 访问列表以降低 Gas

了解 EIP-2929 冷/热访问计费机制,如何使用 eth_createAccessList 构建 EIP-2930 访问列表,以及如何验证净 Gas 变化量。

TL;DR

eth_createAccessList 会针对当前状态模拟一次调用,并返回该调用会触及的地址和存储键,以及一个 gasUsed 数值。你可以将该列表附加到 type-1(EIP-2930)或 type-2(EIP-1559)交易中,这样每个存储槽的首次访问将按声明的访问列表价格计费,而不是按 EIP-2929 定义的更高冷访问价格计费。权衡是真实存在的:访问列表会为每个地址和每个存储键增加固有 Gas,再加上 calldata 字节,因此只有当被预热的存储槽被足够多次复用时,它才能带来收益。唯一可靠的判断方法是在同一区块下比较带列表和不带列表的 eth_estimateGas 结果。本指南涵盖请求/响应结构、可运行的 Node.js 示例、一张由你针对自己的端点填写的测量结果表,以及使访问列表成为小众优化而非默认选项的局限性。

EIP-2929 冷访问与热访问模型

在理解 EIP-2930 访问列表之前,你需要先了解创造这一机会的计费变更。EIP-2929 重新定义了状态访问成本,将每个地址和存储槽分为两种状态:冷(本交易中尚未触及)和热(已触及)。交易中对地址或存储槽的首次访问是冷访问,其 Gas 成本远高于后续的热访问。

这就是为什么预先声明将要访问的存储槽可以降低成本。如果一笔交易会多次触及同一个存储槽,或者触及一组原本会被 EVM 按冷价格收费的存储槽,通过访问列表预先预热它们,就能将这些首次访问转为更便宜的访问列表价格。该机制纯粹关乎顺序和声明:EVM 仍然执行相同的读写操作,但冷附加费只需按已知的较低费率支付一次。

实际结果是,访问列表是针对具有可预测、重复存储访问模式的合约的一种定向优化。对于简单的价值转移,它们毫无作用,因为普通转账不会触及任何能从中受益的合约存储槽。

  • 冷访问:交易中对地址或存储槽的首次触及,按较高的 EIP-2929 费率计费。
  • 热访问:同一交易中的任何后续触及,按较低费率计费。
  • 访问列表预先预热声明的地址和存储槽,使其首次触及按访问列表价格计费。
  • 收益大小取决于被预热的存储槽实际被复用的次数。

EIP-2930 访问列表究竟是什么

EIP-2930 引入了 type-1 交易和 accessList 字段:一个对象列表,每个对象包含一个 address 和一个 storageKeys 数组。在该列表中声明一个存储槽,就是告诉 EVM 从执行开始就将其视为热状态。type-2(EIP-1559)交易也带有 accessList 字段,因此你并非必须使用传统的 type-1 费用模型才能使用访问列表。

该字段在两种交易类型中都是可选的。省略或为空时,交易行为与 EIP-2930 之前完全相同。存在时,客户端需要为每个声明的地址和每个声明的存储键支付固有 Gas,再加上编码该列表的 calldata 成本。这种固有成本正是列表可能净亏损的原因:你预先为可能无法充分利用的热状态付费。

如果你想了解这些字段在网络上如何序列化,原始交易编码与 eth_getRawTransactionByHash 与 type-0/1/2 编码 中涵盖的属于同一系列。accessList 是 type-1 和 type-2 交易 RLP 载荷的一部分。

  • 结构:accessList 是 { address, storageKeys[] } 对象的数组。
  • type-1 交易是 EIP-2930 的原始载体;type-2 交易也可以包含该字段。
  • 固有成本:每个声明的地址、每个声明的存储键,加上 calldata 字节。
  • 空列表或省略列表是有效的,等同于 EIP-2930 之前的行为。

eth_createAccessList 请求与响应结构

eth_createAccessList 是一个模拟方法。你向它发送一个调用对象和一个区块标签,它会返回该调用所需的访问列表以及一个 gasUsed 估算值。以太坊 JSON-RPC 规范 记录了请求和响应的结构。调用对象接受 from、to、data 和 value,第二个参数是区块标签(例如 "latest" 或十六进制区块号)。

响应是一个包含两个字段的对象:accessList,即 { address, storageKeys[] } 条目的数组;以及 gasUsed,一个十六进制数量。accessList 正是你可以直接粘贴到交易 accessList 字段中的内容。gasUsed 反映的是模拟执行的结果,并不保证最终上链成本,因为状态可能在模拟和打包之间发生变化。

由于这是针对特定区块的模拟,结果仅在该区块有效。如果合约的存储布局或触及的存储槽发生变化,列表可能过时。请将返回的列表视为待测量的起点,而非永久性产物。

  • 参数:[callObject, blockTag],其中 callObject 支持 from、to、data、value。
  • 结果:{ accessList: [{ address, storageKeys[] }], gasUsed: hex }。
  • 返回的 accessList 可直接用作交易字段。
  • 有效性取决于模拟所针对的区块。

可运行的 Node.js 示例:创建、估算、比较

下面的示例使用基于 fetch 的原始 JSON-RPC,以便你看到确切的请求/响应结构。它针对一次合约调用调用 eth_createAccessList,打印 accessList 和 gasUsed,然后在同一区块下调用 eth_estimateGas 两次:一次不带列表,一次带列表。差值才是关键数字。

将 RPC_URL 替换为你的端点。OnFinality 通过 以太坊网络页面 提供以太坊 JSON-RPC;任何实现了该方法的节点都可用,但支持情况因提供商而异。该示例刻意不给出节省数值,因为结果取决于合约和区块。

const RPC_URL = "https://your-endpoint.example";

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function main() {
  const call = {
    from: "0xYourFromAddress",
    to: "0xContractAddress",
    data: "0xYourCalldata"
  };
  const blockTag = "latest";

  const created = await rpc("eth_createAccessList", [call, blockTag]);
  console.log("accessList:", JSON.stringify(created.accessList, null, 2));
  console.log("gasUsed (createAccessList):", created.gasUsed);

  const withoutList = await rpc("eth_estimateGas", [call, blockTag]);
  const withList = await rpc("eth_estimateGas", [
    { ...call, accessList: created.accessList },
    blockTag
  ]);

  const delta = BigInt(withList) - BigInt(withoutList);
  console.log("estimateGas without list:", withoutList);
  console.log("estimateGas with list:   ", withList);
  console.log("net delta (negative = saving):", delta.toString());
}

main().catch((e) => { console.error(e); process.exit(1); });

将结果嵌入 type-1 或 type-2 交易

一旦你有了 accessList,附加它就是机械操作。对于 type-1 交易,将 type 设为 0x1,并在 gasPrice、gasLimit、nonce、to、value 和 data 旁边包含 accessList 字段。对于 type-2 交易,将 type 设为 0x2,并在 maxFeePerGas 和 maxPriorityFeePerGas 旁边包含 accessList。两种情况下 accessList 字段完全相同。

如果你使用 ethers,accessList 会通过交易请求对象传递,库会处理序列化。如果你构建原始交易,accessList 会作为 type-1 或 type-2 载荷的一部分进行 RLP 编码。编码细节与 eth_getRawTransactionByHash 与 type-0/1/2 编码 中描述的属于同一系列。

重要的纪律是在广播前验证净效果。附加一个无法收回成本的列表会使交易严格变得更贵。始终在同一区块下比较估算值,如示例所示。

  • type-1:type 0x1、gasPrice、accessList。
  • type-2:type 0x2、maxFeePerGas、maxPriorityFeePerGas、accessList。
  • 两种类型的 accessList 字段结构相同。
  • 广播前用 eth_estimateGas 验证。

成本权衡:固有 Gas 与热状态

访问列表并非免费。你需要为每个声明的地址和每个声明的存储键支付固有 Gas,再加上编码列表的 calldata 成本。无论执行是否实际复用了被预热的存储槽,这笔成本都会产生。只有当被预热的存储槽被触及足够多次,使得避免的冷附加费超过固有成本时,节省才会实现。

这就是净节省可能为负的原因。一个声明了许多存储槽但只触及其中少数一次的列表会亏损 Gas。一个声明了一小组被反复触及的热存储槽的列表则可能盈利。盈亏平衡点取决于合约、calldata 和区块,因此必须测量而非假设。

关于估算周围的费用市场背景,基础费用和优先费用的机制在 使用 eth_feeHistory 估算 Gas 价格 中有所涵盖。访问列表改变的是执行 Gas 部分,而非费用市场部分,因此这两种分析是互补的。

  • 成本侧:每地址固有 Gas、每存储键固有 Gas、calldata 字节。
  • 收益侧:实际被复用的存储槽所避免的冷附加费。
  • 净节省可能为正、为零或为负,取决于复用情况。
  • 在同一区块下带列表和不带列表分别测量。

可复现的测量:一张由你填写的测量结果表

由于访问列表的节省因合约和区块而异,诚实的做法是制作一张针对你自己端点填写的测量表。对你关心的每次调用运行上面的 Node.js 示例,记录数值并计算净差值。不要照抄博客文章中的数字,包括本文;数字取决于会变化的状态。

同一行中的两次估算使用相同的区块标签,以确保比较公平。如果你想检查稳定性,在稍后的区块重复该行并记录差值是否变化。差值在不同区块间正负翻转,说明该优化对该调用并不稳健。

  • 调用:你测试的合约调用的简短标签。
  • 链 ID:你运行所针对的网络。
  • eth_createAccessList 的 gasUsed:模拟执行 Gas。
  • 不带列表的 eth_estimateGas:基线估算值。
  • 带列表的 eth_estimateGas:附加返回的 accessList 后的估算值。
  • 净差值:带列表减去不带列表;负值表示节省。
  • 决策:附加列表、跳过列表,或在稍后的区块重新测量。

局限性与访问列表无帮助的情况

第一个局限是方法支持。eth_createAccessList 要求节点实现它,支持情况有文档记录 / 因提供商而异。如果你的端点返回方法未找到错误,则无法在不切换端点的情况下使用此工作流。以太坊 RPC 节点指南(RPC Assistant) 是检查节点暴露哪些方法的有用参考。

第二个局限是过时性。结果仅在模拟所针对的区块有效,因为随着新区块到来以及其他交易改变存储,状态和冷/热状态会漂移。在区块 N 生成的列表在区块 N+1 可能次优或错误。

第三个局限是适用范围。访问列表很少对简单转账有帮助,因为普通价值转移不会触及任何能从预热中受益的合约存储槽。在 L2 上它们也不是普遍盈利的,因为费用模型不同,节省可能无法迁移。最后,估算本身是模拟;如果执行路径不同,上链成本可能不同。

  • 方法支持:有文档记录 / 因提供商而异。
  • 过时性:仅在模拟的区块有效。
  • 适用范围:很少对简单转账有帮助。
  • L2 费用模型不同;节省可能无法迁移。
  • 模拟不保证上链成本。

故障排除:方法未找到、空列表、负节省

方法未找到是最常见的失败。它意味着节点未实现 eth_createAccessList。检查提供商的方法列表;如果缺失,你就无法通过这种方式生成列表。一些提供商仅在归档或启用调试的端点上暴露它,因此同一提供商在不同套餐下可能表现不同。

始终为空的 accessList 通常意味着该调用未触及该方法认为值得声明的合约存储槽,或者该调用回滚或过于简单。验证调用对象是否正确(to、data、from),以及区块标签是否有效。如果调用回滚,模拟可能返回空列表而非错误。

负节省意味着列表的固有成本超过了避免的冷附加费。这是合理的结果,不是 bug。将列表缩减为仅包含实际被复用的存储槽,或完全跳过列表。在决定前,在同一区块重新测量以确认差值稳定。

  • 方法未找到:节点未实现该方法;查看提供商文档。
  • 空列表:调用可能过于简单、回滚或未触及相关存储槽。
  • 负节省:列表成本高于节省;精简或跳过。
  • 决定前始终在同一区块重新测量。

后续步骤:估算、追踪与选择端点

访问列表是控制 Gas 的若干杠杆之一。估算本身在 eth_estimateGas:Gas 限制与滑点 中有所涵盖,其中解释了如何设置留有裕度的 Gas 限制。费用市场方面在 使用 eth_feeHistory 估算 Gas 价格 中有所涵盖。两者结合,为你提供交易成本的完整图景。

如果你需要了解为什么一次调用会触及它所触及的存储槽,追踪是下一个工具。使用 trace 和 debug 进行以太坊交易追踪 展示了如何在操作码级别检查执行,这在访问列表行为异常时很有用。

关于端点选择,以太坊网络页面 列出了网络,RPC 定价 和 API 服务 描述了访问权限的配置方式。OnFinality Learn 中心 汇集了本系列的其余指南。在生产中采用访问列表之前,先针对你自己的端点运行测量表。

  • 估算留有裕度的 Gas 限制:eth_estimateGas 指南。
  • 了解费用市场百分位:eth_feeHistory 指南。
  • 检查执行:trace 和 debug 指南。
  • 选择端点:网络页面、定价和 API 服务。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始