EIPs 项目详解:EIP-1087 SSTORE 净燃气计量(Net Gas Metering)机制与后续演进
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
导读
EIP-1087(Net gas metering for SSTORE operations)是 Ethereum Improvement Proposal 仓库中针对 EVMSSTORE指令燃气计费提出的一项核心改进提案,目标是消除合约存储多次写入场景下的"过度收费"问题,并为重入锁、跨调用上下文传递等此前因成本过高而不可行的存储使用模式打开空间。本文以 EIPS/eip-1087.md 为骨架,结合仓库中 EIP-1283、EIP-2200、EIP-1706、EIP-1153 等后续提案与测试用例,完整还原其动机、规范、成本复算、测试向量与最终演进脉络,帮助读者理解现代以太坊存储燃气模型的设计源头。
背景:传统SSTORE计费方式的痛点
在 EIP-1087 提出之前,EVM 中SSTORE(0x55)指令的燃气收费规则为:
- 将存储槽从 0 写为非 0:扣除20,000 gas;
- 其他任何值变更:扣除5,000 gas;
- 将存储槽从非 0 写回 0:交易结束时给予10,000 gas 退款。
该提案的作者 Nick Johnson 在 Motivation 中指出:当一笔交易只对某个存储值做单次更新时,上述成本与节点实际消耗的资源基本匹配;但当同一交易内出现多次更新序列时,收费便显得过高。原文给出了三个典型例子:
- 写回原值场景:空存储合约先将 slot 0 设为 1,再设回 0,会被收取
20000 + 5000 - 10000 = 15000gas——尽管这组操作实际上不需要任何磁盘写入。 - 多次自增场景:空存储合约将 slot 0 自增 5 次,会被收取
20000 + 5 * 5000 = 45000gas——尽管这组操作所需的磁盘活动与单次写入(20000 gas)相当。 - 余额转移场景:账户 A 转给 B、再转给 C,且所有账户期初、期末余额均非 0,需要
5000 * 4 = 20000gas。
这种"按操作次数线性收费、不区分是否产生净变化"的模型,直接抑制了以"交易结束时存储无净变化"为特征的用例:例如防止重入的互斥锁(mutex)、在多次合约调用之间传递的上下文信息等。作者特别提到,approveAndCall这类"单笔交易内先授权再调用"的操作,若没有成本优化,将要求代币标准为兼容而专门改造合约。
核心规范:基于"脏映射(dirty map)"的净计量
EIP-1087 的 Specification 对 EVM 提出如下修改,核心是引入一张按交易维度的脏映射来跟踪本交易内被修改过的所有合约存储槽:
- 脏映射(dirty map):为每笔交易维护,记录当前交易中所有合约里被修改过的存储槽;其作用域与存储更新保持一致——即在后续 revert 的调用帧内对脏映射的修改不会被保留。
- 写入当前值:当向存储槽写入其已包含的值时,扣除200 gas。
- 首次变更:当存储槽值首次变更时,将该槽标记为脏;若该槽此前为 0,扣除20,000 gas;否则扣除5,000 gas。
- 重复写入脏槽:当已处于脏映射中的存储槽再次被写入时,扣除200 gas。
- 交易结束时结算退款:对脏映射中的每个槽:
- 若槽在交易前为 0、交易结束时仍为 0:退款19,800 gas;
- 若槽在交易前非 0、且值未发生变化:退款4,800 gas;
- 若槽在交易前非 0、交易结束时为 0:退款15,000 gas。
关键设计在于:首次变更承担全部"冷启动"成本,后续重复写入只收 200 gas 的廉价操作费,而"最终回到初始状态"的槽则通过退款机制把多收的费用返还。这样,交易的最终燃气消耗只与该槽的净状态变化挂钩,而非操作次数。
改进效果:三个动机示例的复算
按新规则复算 Motivation 中的三个例子:
- 写回原值场景:
20000 + 200 - 19800 = 400gas,从 15000 大幅降至 400; - 多次自增场景:
20000 + 5 * 200 = 21000gas,从 45000 降至 21000; - 余额转移场景:
5000 * 3 + 200 - 4800 = 10400gas,从 20000 降至 10400。
可以验证:只对存储槽做单次变更的交易,成本保持原样;而多次变更的合约将看到显著的成本下降。
设计取舍(Rationale):为何不用更简单的方案
提案作者认为,上述机制是"在收费不合理场景下降低存储燃气成本的最简单方式",并在 Rationale 中逐一驳斥了三种备选方案:
- 备选一:统一收取 200 gas + 交易末尾追加收费。对所有
SSTORE统一收 200 gas,交易结束时再对新增/变更值追加 19800 / 4800 gas。该方案更简单、甚至无需脏映射,但把一大块燃气消耗推迟到交易末尾结算,会复杂化调试、削弱合约限制被调用方燃气消耗的能力,并给 EVM 引入全新机制。 - 备选二:为存储退款维护独立退款计数器。可避免"退款被限制为消耗燃气的一半"这一问题的干扰(本提案并不依赖该限制),但需要额外跟踪,增加复杂度。
- 备选三:每次写回初始值即时退款。会引入"即时退款"新机制、复杂化合约间调用的燃气记账,甚至可能导致负执行成本的合约调用。
从仓库中后续提案的表述看,这一设计取舍被 EIP-1283、EIP-2200 继承并形式化:两者均明确指出"对于绝对燃气消耗(实际燃气减去退款),本 EIP 在所有场景下与 EIP-1087 等价",见 EIPS/eip-1283.md 与 EIPS/eip-2200.md 的 Rationale 部分。
向后兼容性与硬分叉要求
EIP-1087 明确要求硬分叉方可实施。由于新规则下任何合约都不会出现燃气成本上升、多数合约反而成本下降,提案预期合约层不存在向后兼容问题。这一点与后续 EIP-1283、EIP-2200 的 Backwards Compatibility 表述一致——后者还补充说明"执行SSTORE从未在低于 5000 gas 的情况下成为可能,因此不引入与主网的不兼容"。
测试用例:十二组验收向量
原文档提供了一组覆盖各类组合的测试用例,是理解新计费规则最直接的验收依据(以下全部继承自 EIPS/eip-1087.md 的 Test Cases 章节):
| # | 场景 | 燃气/退款 |
|---|---|---|
| 1 | 向值为 0 的槽写入非 0 值 x | 20k gas,无退款 |
| 2 | 向值为 x 的槽写入 y(x≠y 且 x≠0) | 5k gas,无退款 |
| 3 | 向值为 x 的槽写入 0(x≠0) | 5k gas,10k 退款 |
| 4 | 向已为 0 的槽写入 0 | 200 gas,无退款 |
| 5 | 向已为 x 的槽写入 x(x≠0) | 200 gas,无退款 |
| 6 | 向值为 0 的槽依次写 x、y(x≠y) | 20200 gas,无退款 |
| 7 | 向值为 0 的槽依次写 x、y、z(x≠y≠z 且 x≠0) | 5200 gas,无退款 |
| 8 | 向值为 0 的槽依次写 x、0(x≠0) | 20200 gas,19800 退款 |
| 9 | 向值为 y 的槽依次写 x、y(x≠y≠0) | 5200 gas,4800 退款 |
| 10 | 向值为 0 的槽写 x、0,随后 revert 所在栈帧 | 20200 gas,无退款 |
| 11 | 向值为 y 的槽写 x、y,随后 revert 所在栈帧 | 5200 gas,无退款 |
| 12 | 嵌套帧中:向值为 0 的槽写 x、返回,再写 0 | 20200 gas,19800 退款 |
注意第 10、11 条直接验证了脏映射的作用域规则——被 revert 的帧内产生的写入不会保留脏标记,因此退款也不生效;第 12 条则验证跨调用帧场景下脏映射与退款依然正确工作。
源码级佐证:从 EIP-1087 到最终采纳方案的演进
EIP-1087 在仓库中状态为Stagnant,并未被直接采纳;但其思想经由 EIP-1283、EIP-2200 两个 Final 状态的提案最终落地。仓库中的演进脉络如下:
- EIP-1283:Net gas metering for SSTORE without dirty maps:由 Wei Tang 提出的替代方案,自述"作为 EIP-1087 的替代,对采用不同存储变更缓存优化策略的实现更友好"。它放弃了脏映射,改用三个各实现普遍可得的信息——存储槽原始值(original value)、当前值(current value)与退款计数器——并将计费归纳为 No-op / Fresh / Dirty 三种状态,配套给出了 17 条含字节码(如
0x60016000556000600055)的测试向量与附录数学证明(归纳法证明"最终值为 0 时收费200*N"、"最终值为非 0 时收费20000 + 200*(N-1)"等性质)。其 Rationale 也明确指出了 EIP-1087 方案的实现局限:脏映射隐式假设交易内存储变更在交易结束时才提交到存储 trie,这对采用内存 trie 或不可变数据结构做存储缓存的客户端不友好——它们能得知原始值与当前值,却无法在无额外内存/处理开销的前提下遍历全部存储变更。 - EIP-2200:Structured Definitions for Net Gas Metering:EIP-1283 与 EIP-1706 的合并版本,将数值参数化为
SLOAD_GAS、SSTORE_SET_GAS、SSTORE_RESET_GAS、SSTORE_CLEARS_SCHEDULE四个变量,并加入"gasleft 低于 2300 stipend 时禁止SSTORE"的安全条款——该条款正是为修复 EIP-1283 引发的新型重入攻击(Solidity 默认向 transfer/send 授予 2300 gas stipend)而引入的。在测试用例中可以看到参数化的效果:SLOAD_GAS从 200 改为 800 后,同名字节码测试的 Used Gas 相应平移(如0x60016000556000600055从 20212 变为 20812)。 - EIP-1153:Transient storage opcodes:EIP-1087 动机中提到的"互斥锁、跨帧上下文传递"等用例的最终归宿。它引入
TLOAD/TSTORE两个新指令,用交易级临时存储直接解决帧间通信问题,并在 Rationale 中说明 EIP-3529 将退款上限压缩至交易燃气 20% 后,单纯依赖SSTORE退款已不足以支撑这些用例。
下述状态转移图来自 EIP-1283 的 assets/eip-1283/state.png,以状态节点(current=orig=0、current!=orig、current=orig!=0、current=0)与箭头上的燃气/退款标注,直观展示了净燃气计量下存储槽在不同原始值/当前值组合之间的迁移成本——它与 EIP-1087 的脏映射方案在"绝对燃气消耗"上等价,可作为理解本提案结算逻辑的可视化辅助:
实现状态与总结
原文档的 Implementation 章节标注为 TBD(待定),这与其 Stagnant 状态相符——EIP-1087 更多承担了"提出净计量思想、明确问题域"的开创性角色,而具体实现最终由无脏映射的 EIP-1283 / EIP-2200 完成并被客户端采纳。对读者而言,理解本提案的价值在于:
- 掌握存储燃气模型的核心矛盾:按操作次数线性收费与"节点实际磁盘工作只取决于净变化"之间的错配;
- 理解净计量的通用框架:首次变更承担全量成本、重复写入廉价化、回到初始状态触发退款,这一框架在 EIP-1283 的三态模型(No-op/Fresh/Dirty)与 EIP-2200 的参数化定义中一脉相承;
- 读懂测试向量的验收逻辑:退款与脏标记的作用域规则(revert 帧不保留)是所有后续实现必须满足的兼容性底线。
如需深入,可继续阅读 EIPS/eip-1283.md(含完整状态转移表与附录证明)、EIPS/eip-2200.md(参数化定义与 gasleft 条款)以及 EIPS/eip-1153.md(这些存储用例的最终落地方案)。
版权说明:本提案原文版权已按 LICENSE.md 中声明的 CC0 协议放弃,本文仅作技术解读。
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考