一笔最简的 ETH 转账固定消耗 21000 gas(黄皮书附录 G 的 Gtransaction);一笔 ERC-20 转账的消耗取决于合约实现与槽位冷热状态,通常落在数万 gas 这个量级,这是量级估计而非实测值。这些数字由协议规定,只有硬分叉才会改动;而同样一笔交易实际支付多少 ETH,可以在几小时内变化十倍以上。前者是计量单位,后者是价格。把这两个概念分开,费用讨论、扩容叙事与执行中止的行为才谈得清楚。
Gas 机制同时承担三件事:把计算与状态操作折算成统一单位,用一个价格市场分配有限的区块空间,并在资源耗尽时给出确定的回滚边界。下面按这三条线展开:固有成本与动态成本各包含什么,冷热访问如何计价,交易在无效、REVERT 与 out-of-gas 三种中止下分别退回什么,EIP-1559 之后 base fee、priority fee 与执行层 gas 的边界在哪里,以及为什么 Gas 让 DoS 变得可计价,却无法让定价永远正确。
Gas 是计算与存储的计量单位,与币价是两回事
Gas 是抽象的计价单位,对应执行一条指令、读写一次状态、扩展一个内存字所消耗的抽象资源量。它的数值由协议常量给定,例如加法 3 gas(Gverylow)、柏林升级(区块 12,244,000,2021 年 4 月 15 日)之后 SLOAD 热访问 100 gas、SSTORE 从零写非零 20000 gas。这些常量只有通过硬分叉才会变化,与市场供需无关。
Gas 的价格以 wei 计价,常用单位是 gwei,1 gwei 等于 10⁹ wei。一笔交易的费用等于 gas_used 乘以 effective_gas_price,前者由执行过程决定,后者由费用市场与交易签名里的出价共同决定。这个乘法关系是后面全部讨论的基础。
分清这两层有几个直接好处。讨论“降 gas”时,先确认目标到底是降低 gas_used 还是降低 gas price:前者要靠减少操作、优化存储布局或修改 EVM 计价规则,后者取决于当下需求与费用市场的形态,EIP-1559 让后者更可预测,但没有改变任何一条指令的价格。讨论“手续费归谁”时,base fee 部分被协议销毁,不存在收款方,只有 priority fee 进入验证者账户。讨论“交易上限”时,gas_limit 是容量上限而非费用上限,费用上限是 max_fee_per_gas,两者是不同的字段。
固有成本:执行开始前就已扣掉的部分
每笔交易要先付固有成本(intrinsic gas),它在 EVM 执行第一条指令之前就被扣掉。黄皮书式 (64) 把它记为 g₀:21000 的固定底价(Gtransaction),加上数据字段每个零字节 4 gas、每个非零字节 16 gas(非零字节价由 EIP-2028 在伊斯坦布尔升级,区块 9,069,000,2019 年 12 月 8 日,从 68 降到 16),若为合约创建交易再加 32000(Gtxcreate);EIP-2930(柏林升级,区块 12,244,000,2021 年 4 月 15 日)之后,access list 中的每个地址 2400 gas、每个存储键 1900 gas;EIP-3860(上海升级,区块 17,034,870,2023 年 4 月 12 日)之后,合约创建的 initcode 每 32 字节字再收 2 gas。
21000 的含义容易被误读成“转账的手续费”。准确的说法是,它表示“有一笔交易”的底价,与转账金额无关。最简 ETH 转账的 calldata 为空,数据部分不产生额外成本,所以正好是 21000。
EIP-2929(柏林升级,区块 12,244,000,2021 年 4 月 15 日)还规定了交易开始时的热集合:发送方地址、接收方地址(创建交易则是被创建的地址)以及所有预编译合约地址已经在 accessed_addresses 中,对它们不收取冷访问费用。这是为什么一笔转账调向普通地址时,接收方的账户访问不会额外收费。
固有成本在执行前扣除,因此有三条行为边界。gas_limit 低于固有成本的交易直接无效,不会被包含进区块,也不扣任何 gas。执行中途因 out-of-gas 中止时,固有成本不会退回。access list 的 2400 与 1900(柏林之后的价格)属于预付费:遍历一遍将要访问的地址与槽位,把它们提前置为热,但如果实际执行没有访问,这笔钱不退。
动态成本:内存扩展与冷热访问如何累加
执行阶段的成本由静态部分与动态部分相加。静态部分是操作码的固定价,黄皮书附录 G 的费率表用几个档位表示:Gverylow 3 gas(ADD、SUB、MLOAD、MSTORE、PUSH 系列等)、Glow 5、Gmid 8、Ghigh 10、Gbase 2、Gjumpdest 1。
动态部分取决于操作数规模与状态访问冷热。内存扩展按 C_mem(a) = 3a + ⌊a² / 512⌋ 计价(黄皮书式 (328),Gmemory = 3),a 是字数,实际扣费为扩展前后之差,这项在 704 字节(22 个字)以内近似线性,之后二次项开始生效。复制类操作在基础价上按字数加收 3 gas/字(Gcopy);KECCAK256 收 30 gas(Gkeccak256)加 6 gas/字(Gkeccak256word);LOG 收 375 gas(Glog)加每个 topic 375 gas(Glogtopic),再加 8 gas/字节(Glogdata);EXP 收 10 gas(Gexp)加 50 gas/指数字节(Gexpbyte)。
冷热访问由 EIP-2929(柏林升级)定义。首次访问某个 (地址, 槽位) 对是冷访问,SLOAD 收 2100 gas;同一交易内再次访问是热访问,收 100 gas。账户类访问的冷价是 2600 gas,覆盖 CALL、CALLCODE、DELEGATECALL、STATICCALL、BALANCE 与 EXTCODESIZE、EXTCODECOPY、EXTCODEHASH。这套集合是交易作用域的,作用域回滚时集合一并回滚。
| 操作(柏林升级之后口径) | 冷访问 | 热访问 |
|---|---|---|
| SLOAD | 2100 | 100 |
| CALL 系列、BALANCE、EXT* | 2600 | 100 |
| SSTORE 的附加冷访问费 | 2100 | 不加收 |
冷热机制的直接来源是两次 DoS 攻击留下的教训。EIP-150(Tangerine Whistle,区块 2,463,000,2016 年 10 月 18 日)在 2016 年 9 月与 10 月的攻击后,已经把 SLOAD 从 50 gas 抬到 200、CALL 从 40 抬到 700、SELFDESTRUCT 从 0 抬到 5000。EIP-2929 的动机引用了 2019 年论文 arXiv:1909.07220 中的一次测量:在作者那套硬件上重放以太坊历史时,同一批恶意交易要跑 20 到 80 秒,而普通交易只需几毫秒,说明状态读取类操作码仍然定价过低。这是一次性测量,只代表 2019 年的硬件与负载,不能当作持续跟踪的口径。
执行中止的三种结局:无效交易、REVERT 与 out-of-gas
交易可能在三个不同阶段结束,回滚范围与费用处理各不相同。
第一种是无效交易,包括 nonce 不匹配、签名错误、余额不足以覆盖 gas 预付款、gas_limit 低于固有成本等情况。这类交易根本不会进入区块,不消耗 gas;它属于链下拒绝,与执行中止是两回事。
第二种是 REVERT,由 revert 语句、require 失败或内联汇编的 revert 触发。当前帧的状态变更全部回滚,剩余 gas 退回给上一层,已经消耗的部分照常支付;退款计数器也回滚到进入该帧之前。REVERT 是“有理由的失败”,可以把错误数据回传,适合需要让调用方区分失败原因的场景。
第三种是异常中止(exceptional halt),包括 out-of-gas、非法操作码、栈溢出、静态调用中写状态、initcode 超限等。状态同样回滚,但当前帧剩余的 gas 全部销毁。若异常发生在顶层帧,交易的 gas_used 等于 gas_limit,费用按满额扣除,没有退回的部分。这就是“OOG 之后 gas 全部消耗”的确切含义:当前帧的剩余 gas 归零,与整条链的余额无关。
// REVERT:状态回滚,剩余 gas 退回,已消耗的照付
function guarded(uint256 x) external pure returns (uint256) {
require(x != 0, "zero");
return 1e18 / x;
}
// 异常中止:无限循环触发 out-of-gas,当前帧剩余 gas 全部销毁
function oog() external pure {
while (true) {}
}
子调用的失败是否牵连父帧,由 EIP-150(Tangerine Whistle)引入的“保留 1/64”规则决定。调用子帧时,父帧最多只能转发自己剩余 gas 的 63/64(即 gas - gas // 64),至少保留 1/64;CREATE 与 CREATE2 同样只提供 63/64。因此子帧 OOG 不会连带父帧 OOG,父帧可以用保留的 gas 捕获失败并继续执行。这条规则在 2016 年替换了原先的调用深度限制,把“深度炸弹”从一类结构性攻击降级为纯 gas 问题。
// 最多向子调用转发 gasleft() 的 63/64,父帧保留 1/64
(bool ok, ) = target.call{gas: gasleft()}("");
带 value 的 CALL 还有两个附加项(黄皮书附录 G):非零转账收 Gcallvalue 9000 gas,若目标账户不存在再收 Gnewaccount 25000 gas;被调用方会得到 2300 gas 的调用补贴(Gcallstipend),用于最简单的接收逻辑。EIP-2200(伊斯坦布尔升级,区块 9,069,000,2019 年 12 月 8 日)规定 gasleft 小于等于 2300 时 SSTORE 直接以 OOG 失败,正是为了防止这段补贴路径被用来写入状态。
退款(refund)在交易执行结束后结算,执行期间不可见。EIP-3529(伦敦升级,区块 12,965,000,2021 年 8 月 5 日)把非零改写为零的退款从 15000 降到 4800,取消了 SELFDESTRUCT 退款,并把单笔交易的总退款上限压到 gas_used // 5。由于退款在执行期间不可用,合约不能依赖它来垫付交易中途需要的 gas。
EIP-1559:base fee、tip 与执行层的边界
EIP-1559(伦敦升级,区块 12,965,000,2021 年 8 月 5 日)把交易费用拆成两段。base fee 由协议按父区块的 gas_used 与目标值(gas_limit 的一半)计算,单块最多上下调整 12.5%,且被销毁。priority fee(tip)是用户给验证者的额外单价,用于争取更早打包。交易签名里给出 max_fee_per_gas 与 max_priority_fee_per_gas 两个上限,实际生效单价是:priority fee 取 max_priority_fee_per_gas 与 (max_fee_per_gas - base fee) 的较小值,effective_gas_price 等于 priority fee 加 base fee。未使用的 gas 按这个单价退回。
执行层的边界也要说清。GASPRICE 操作码在 EIP-1559 之后返回的是 effective_gas_price,也就是发送方实付单价;验证者实得的部分无法从执行环境里直接读到。这也意味着任何依赖 GASPRICE 判断“矿工收入”的旧逻辑都不再成立。
两段费用回答的是不同问题:base fee 决定进入区块的最低门槛,tip 决定同一区块内的优先级。区块空间总量没有变化,当持续用量超过目标值时,base fee 会逐块上升把需求挤出去,这是排队机制,与产能扩张无关。把 EIP-1559 当作降低 gas_used 的方案是常见误读,它只改变价格形成方式。
还有一层容易混淆的边界:blob 费用不属于执行层 gas。EIP-4844(坎昆升级,区块 19,426,587,2024 年 3 月 13 日)为 blob 数据引入了独立的基础费与计价单位(blob gas),EIP-7516 增加了返回 blob base fee 的操作码。Rollup 的数据发布成本讨论的是 blob 市场,与合约执行消耗的 gas 是两套账本。
DoS 的“可计价”不等于定价正确
Gas 的核心安全属性是把资源占用折算成成本。要让一个区块被垃圾交易占满,攻击者必须支付与该区块全部 gas 对应的费用,这是“可计价”的含义。相比不收费的网络,攻击的边际成本从零变成与占用成正比。
但这只在定价与真实成本相符时才有效。2016 年 9 月与 10 月的 DoS 攻击利用的正是被低估的状态读取与调用类操作码,直接催生了 Tangerine Whistle 硬分叉;此后 EIP-1884(伊斯坦布尔升级,区块 9,069,000,2019 年 12 月 8 日)把 SLOAD 从 200 抬到 800,BALANCE 与 EXTCODEHASH 从 400 抬到 700,EIP-2929 又把状态访问拆成冷热两档并大幅提高冷访问价,理由仍是同样的模式可以把单块处理时间拉到几十秒。EIP-3860 管的是 initcode 的跳转目标分析,此前这段工作量完全没有计量。计价规则与实现成本之间存在结构性滞后:客户端的数据库布局与执行引擎持续优化,gas 常量却要等下一次硬分叉才会重估。
退款机制提供了反向的例证。它本来鼓励合约清理不再使用的状态,实际却催生了 GasToken 这类把状态槽位当电池、在低费率时囤积 gas、在高费率时释放的方案,造成状态膨胀与区块 gas 使用量的额外方差。EIP-3529 削减与封顶退款,就是为了关掉这条通道。
机制失效的条件有几处需要单独指出。定价是治理产物,常量通常按各实现的最坏用例挑选(EIP-3860 就写明其 2 gas/字来自不同实现的最坏情况基准),而不同客户端在同一批操作上的耗时不同,同一 gas 价格对应的真实资源成本因此并不一致。这是一个定性判断;EIP-2929 为抬价给出的定量证据是 2019 年在某套测试硬件上的一次性测量,不能当作持续口径。排序市场的价值并不完全由 tip 承载,MEV 拍卖与客户端策略都会影响打包顺序,tip 高只是更可能进入下一块的激励之一。区块 gas 上限本身就是吞吐与验证成本的权衡,抬高上限会同时抬高全节点的硬件门槛。
这一节的落点在于说明它是一套需要定期校准的机制:“gas 定价可靠”与“gas 定价不可靠”都是过强的结论。EIP-150、EIP-1884、EIP-2929、EIP-3860 都是校准动作。至于下一次校准由什么触发,只能给推断而不是结论:四次校准的直接诱因都是被公开观测到的资源放大攻击,据此本文推断下一次大概率仍是同类事件。这个推断没有公开依据,也不排除因状态增长、区块容量等其他考量提前重估。
边界与不确定的地方
引用的口径是黄皮书附录 G 费率表与式 (64)、式 (328)(下载版本的页脚标注为 Shanghai 版本),以及 EIP-150、EIP-1884、EIP-2028、EIP-2200、EIP-1559、EIP-2929、EIP-2930、EIP-3529、EIP-3860 的规范文本。具体数字随分叉变化,正文标出的分叉与区块高度是:伊斯坦布尔(2019 年 12 月 8 日,区块 9,069,000)、柏林(2021 年 4 月 15 日,区块 12,244,000)、伦敦(2021 年 8 月 5 日,区块 12,965,000)、上海(2023 年 4 月 12 日,区块 17,034,870)、坎昆(2024 年 3 月 13 日,区块 19,426,587)。在 EVM 兼容链上讨论 gas 之前,需要先确认该链启用了哪些分叉,计价常量可能与以太坊主网不同。Tangerine Whistle 的区块 2,463,000 与 2016 年 10 月 18 日同样来自 ethereum.org 的升级历史。
关于“定价滞后”的严重程度,只能给出旁证,给不出量化结论:公开材料里没有一份持续跟踪“协议 gas 常量与实际执行耗时比值”的时间序列,因此无法判断滞后是在扩大还是收敛。这一点应作为不确定项保留,不用它反推“正在恶化”或“已经收敛”之类的结论。
另外,费用市场的行为受 MEV 与客户端策略影响,没有展开排序市场的机制细节,也没有把 blob 费用市场与执行层 gas 放在同一框架里比较,两者在 EIP-4844 之后是分开的计价体系。引用这些数字做成本模型时,需要先确认口径与版本。
资料来源
- EIP-1559: Fee market change for ETH 1.0 chain,https://eips.ethereum.org/EIPS/eip-1559
- EIP-2929: Gas cost increases for state access opcodes,https://eips.ethereum.org/EIPS/eip-2929
- EIP-3529: Reduction in refunds,https://eips.ethereum.org/EIPS/eip-3529
- EIP-150: Gas cost changes for IO-heavy operations,https://eips.ethereum.org/EIPS/eip-150
- EIP-1884: Repricing for trie-size-dependent opcodes,https://eips.ethereum.org/EIPS/eip-1884
- EIP-2028: Transaction data gas cost reduction,https://eips.ethereum.org/EIPS/eip-2028
- EIP-2200: Structured Definitions for Net Gas Metering,https://eips.ethereum.org/EIPS/eip-2200
- EIP-2930: Optional access lists,https://eips.ethereum.org/EIPS/eip-2930
- EIP-3860: Limit and meter initcode,https://eips.ethereum.org/EIPS/eip-3860
- EIP-4844: Shard Blob Transactions,https://eips.ethereum.org/EIPS/eip-4844
- EIP-7516: BLOBBASEFEE opcode,https://eips.ethereum.org/EIPS/eip-7516
- Ethereum Yellow Paper,附录 G 费率表、式 (64) 固有成本、式 (328) 内存计价函数,https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org 开发者文档:Gas 与费用,https://ethereum.org/en/developers/docs/gas/
- ethereum.org 操作码参考,https://ethereum.org/en/developers/docs/evm/opcodes/
- ethereum.org 网络升级历史(各分叉区块高度与日期),https://ethereum.org/en/history/
- arXiv:1909.07220,EIP-2929 引用的状态访问耗时测量,https://arxiv.org/abs/1909.07220
延伸阅读
本篇属于 EVM 基础系列,前置篇目为 0.5《三类存储别搞混:Memory、Storage 与 Transient Storage》。