以太坊漏油的 Gas 罐:揭秘 13 个代价高昂的 Gas 模型矛盾之处

本文为机器翻译
展示原文

以太坊中的 13 个 Gas 模型不一致问题

这篇文章总结了以太坊 gas 模型中的 13 个不一致之处。
所有符号均来自以太坊黄皮书(特别是附录 G.费用表)。


1. 创建账户时,外部交易不会产生新账户费用( G_ { newaccount } G newaccount 内部交易会产生

例子
假设你想让合约C将ETH转移到一个尚不存在的全新账户A

  • 如果C直接转账,内部新建账户路径收取25,000 gas ( G_ { newaccount } G newaccount )
  • 替代方案:首先从 EOA 向A发送一笔外部交易,费用为 1 wei。这笔交易的Gas 成本为 21,000(标准基数)。之后, A就存在了。现在CA转账不再产生新账户费用(节省约 4,000 Gas )。

结论
如果你依靠合约来创建新账户,则需要支付 25,000 gas。
但是如果你先发送一个外部交易(21,000 gas),然后让合约发送资金,你实际上可以节省~4,000 gas

原因
外部和内部创建路径使用不同的收费挂钩;只有合约调用才会触发明确的新账户费用。

2. 预编译调用有时会跳过交易输入字节费用

例子
调用预编译合约(例如ECRECOVER )可能会跳过交易输入字节的计费。通常情况下,每个输入字节的Gas 费用为 4(零)16(非零) 。以下是两个真实的交易示例:
* 1. 交易0x6b01使用 24,276 gas 调用ECRECOVER ,其中 276 gas 用于输入字节。
* 2. 交易0x1fb0使用 24,000 gas 调用ECRECOVER ,其中 0 gas 用于输入字节。
如果你阅读执行客户端的源代码,你会发现第二个事务也可以执行ECRECOVER ,而无需对输入字节进行计费。客户端会用零数据填充输入以匹配预期的大小。

补充说明
我想你足够聪明,会注意到我提到的两笔交易都是外部交易。
那么他们为什么要调用预编译合约呢?

我猜部分原因是一些浏览器错误地将预编译地址(0x01-0x0A)标记为“刻录地址”,这进一步使用户感到困惑(请参见下面的快照)。

burn_address
burn_address 539×182 39.1 KB

此外,在这些特殊地址(0x01–0x0A)中部署预编译地址是一个失败的设计。
有时,人们只是想直接呼叫这些特殊地址。

原因
预编译合约的地址设计不良以及区块浏览器的误导,导致混乱和错误标记。

3. 即使从未访问过,访问列表条目也会收费(即G_{accesslistaddress} G a c c e s s l i s t a d r e s s G_{accessliststorage} G a c c e s s l i s t s t o r a g e

例子
EIP-2930 引入了访问列表,允许交易指定其想要访问的地址和存储槽。然而,交易可以将地址和存储槽添加到其访问列表中,但永远不会触及它们。
例如,事务0x0dd0c设置了访问列表,但由于地址原因从未访问指定的插槽。

原因
该协议收取包含费用以简化执行,无论条目是否被使用。如果你相信你的用户能够提供正确的输入,那你还不如相信泰勒·斯威夫特是你的妻子。

4. 自转移仍需转移气体

例子
账户 A 将ETH发送给自己。
余额没有变化,但费用仍然包含价值转移所需的9,000 gasG_{ callvalue } G callvalue 根据@vbuterin这篇帖子

两次账户写入(一次余额编辑 CALL 通常需要花费 9000 gas)

为什么一个账户写入操作仍然需要花费9000 gas呢?其实,如果你看过执行客户端的源码,就会发现,当 from 地址和 to 地址相同时,客户端不会做任何操作。

当交易为自我转账或使用CALLCODE转移价值时,可能会发生上述情况。

原因
无论转移是否为无操作,都会触发执行费用。

5. 调用数据与合约字节码磁盘定价不匹配

例子

  • Tx calldata: 16 gas/字节(非零)4 gas/字节(零)
  • 合约字节码: 200 gas/字节
    两者都占用磁盘空间,但定价却不一致。这让我很困惑,因为交易调用数据比合约字节码更便宜,因为它应该考虑实际的磁盘使用量和网络开销。

原因
Gas 计划将“调用数据”和“代码存放”分开,而不将它们与实际磁盘使用情况对齐。

6. 撤销的交易将按写入磁盘的方式收费

例子
恢复的交易会修改内存中的状态,但不会保留任何更改,但仍会收取写入费用,以下 gas 费用会受到影响:

  • 25,000 gas 账户 G_ { newaccount } G newaccount
  • 9,000 gas 价值转移 G_ { callvalue } G callvalue
  • 2,100 gas (冷 G_ { coldslot } G
  • 200 gas 代码存款 G_ { codedeposit } G代码存款

实际内存成本约为100 gas

原因
执行期间会收取 Gas 费用;之后的回滚会取消状态更改,但不会取消费用。具体实现会谨慎收费,以防止 DoS 攻击。

7. 单笔交易中的多笔ETH转账被误认为是冷转账

例子
假设合约在单笔交易中多次将ETH发送到不同的账户。

  • 第一次转账正确产生了G_{callvalue} G call value 9,000 gas 写入账户余额
  • 同一交易中后续向其他账户的转账应收取热访问费( 100 gas + 4,500 gas ),但有时仍按冷访问费计费( 9,000 gas )。

原因
对于单笔交易中的多次价值转移,冷/暖访问簿记并不会持续更新。

8. 矿工/验证者奖励或提现写入不收取费用

例子
协议级余额更新(例如奖励、提款)会修改磁盘上的状态,但消耗0 gas

原因
系统级簿记绕过了 gas 会计挂钩。

9. SSTORE 首次磁盘读取不收费(根据 EIP-2200)

例子
执行SSTORE操作码时,它首先从磁盘(合约存储)读取当前值,然后再决定是否写入新值。根据EIP-2200规定,如果存储的值与现有值匹配,则不会进行磁盘写入,并且仅收取少量 Gas 费用。但是,初始磁盘读取本身不收取任何 Gas 费用——协议仅在值发生变化时对后续写入收取费用。

原因
EIP-2200 的逻辑侧重于对状态变化收费,但忽略了对总是首先发生的磁盘读取的收费。这意味着对存储槽的首次访问是免费的,即使是冷读。

10. 存储读取优化减少了 I/O,但 gas 保持不变

例子
以太坊客户端已采用扁平存储/快照优化(例如,Geth 的 快照加速结构),将状态组织为扁平键值存储,并允许直接磁盘读取,从而绕过了传统 Merkle-Patricia Trie (MPT) 所需的中间节点。此优化显著减少了冷存储读取的磁盘 I/O。例如,Geth 和其他客户端现在使用 SAS 或类似结构,但冷访问的 Gas 费用(2,600 / 2,100 / 2,400 / 1,900)保持不变。

原因
冷访问的 Gas 常数最初是针对 MPT 进行校准的,因为 MPT 需要遍历多个 trie 节点,因此磁盘读取成本更高。使用 SAS 后,实际磁盘资源消耗要低得多,但协议尚未更新相应的 Gas 费用。

减轻
当客户端切换到 SAS 或类似的优化存储后端时,重新校准气体常数以反映减少的磁盘 I/O。

11. SLOAD 与 MLOAD 定价不匹配

例子

  • SLOAD (温暖)→ 100 气体
  • MLOAD3 气体
    两者都是内存读取,但是价格差别很大。

原因
状态和内存操作之间的遗留区别;优化模糊了实际成本差距。

12. 内部交易有时无需 Gas 即可更新账户

例子
当磁盘中的账户更新不收取 Gas 费用时。具体来说,这个问题出现在用户向合约 A 发送外部交易,合约 A 又对合约 B 进行内部调用的场景中。如果合约 B 修改了其存储中的某个 slot,则必须在磁盘上更新合约 B 账户中对应的存储根。然而,由于账户 B 的此次更新不收取 Gas 费用,导致不一致。

原因
出现此漏洞的原因是,合约 B 的存储 trie 修改不会因更新其账户状态而产生额外的 gas 费用。这是因为即使执行了磁盘写入操作,协议也不会对由内部交易触发的账户状态更新收取费用。

13. EXT* 操作码定价太粗略

例子
EXTCODESIZE可能读取比BALANCE更多数据,但两者都收取相同的冷账户费用( 2,600 gas )。

原因
操作码定价桶很粗略,并且忽略了可变的工作。

结束语

这个问题来自我的论文如下,我通过此链接分享它。

何哲、李哲、罗建、罗锋、段建、李建…… & 张晓燕 (2025年2月)。Auspex:揭示区块链交易费机制的不一致性漏洞。刊于第23届USENIX文件与存储技术会议论文集。

如果您能引用我的论文,我将非常高兴。

所有这些都凸显了全面审查和调整以太坊协议内 Gas 定价机制的必要性。通过解决这些不一致问题,我们可以确保一个更高效、更公平的 Gas 市场,准确反映各种操作的底层资源成本。


来源
免责声明:以上内容仅为作者观点,不代表Followin的任何立场,不构成与Followin相关的任何投资建议。
喜欢
收藏
评论