本文详细介绍了以太坊交易的动态Gas成本,包括固有Gas、内存扩展、访问集、Gas退款等方面的计算方法和相关术语,深入探讨了 EIP-2929 和 EIP-3529 的影响。内容结构清晰,逻辑严谨,适合对以太坊交易机制有一定了解的读者。
内在Gas是指在交易执行之前支付的Gas量。 也就是说,交易的发起者支付的Gas,这将始终是一个外部拥有的账号,在进行任何状态更新或执行任何代码之前支付。
Gas计算:
gas_cost = 21000: 基本成本tx.to == null(合约创建交易):
gas_cost += 32000gas_cost += 4 * bytes_zero: 每个零字节的内存数据为基础成本增加的Gasgas_cost += 16 * bytes_nonzero: 每个非零字节的内存数据为基础成本增加的Gas任何扩展正在使用的内存的操作都需支付额外的Gas成本。
这种内存扩展成本取决于现有内存的大小,如果该操作没有引用高于当前引用的内存地址,则成本为0。
引用是对内存的任何读、写或其他使用(例如在CALL中)。
术语:
new_mem_size: 问题操作之后引用的最高内存地址(以字节为单位)new_mem_size_words = (new_mem_size + 31) // 32: 问题操作之后所需的(32字节)字数的数量Gas计算:
有用的备注:
RETURN, REVERT, MLOAD, MSTORE, MSTORE8。根据EIP-2929,维护两个交易范围的访问集合。 这些访问集合跟踪在当前交易中已触及的地址和存储槽。
touched_addresses : Set[Address]
tx.origin, tx.to*,和所有预编译touched_addresses 初始化时包括所创建合约的地址,而不是tx.to,后者是零地址。touched_storage_slots : Set[(Address, Bytes32)]
(address, storage_key){}以上访问集合与以下操作相关:
ADDRESS_TOUCHING_OPCODES := { EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, BALANCE, CALL, CALLCODE, DELEGATECALL, STATICCALL, SELFDESTRUCT }STORAGE_TOUCHING_OPCODES := { SLOAD, SSTORE }当一个地址是 ADDRESS_TOUCHING_OPCODES 之一的目标时,该地址会立即被添加到 touched_addresses 集合。
当一个存储槽是 STORAGE_TOUCHING_OPCODES 之一的目标时, (address, key) 对会立即被添加到 touched_storage_slots 集合。
重要备注:
*CALL 和 CREATE* 操作的 touched_addresses 集合的新增是在进入新的执行帧之前立即进行,因此在调用或合约创建中的任何失败都不会将失败的 *CALL 或 CREATE* 的目标地址移出 touched_addresses 集合。
一些棘手的边缘案例:
*CALL,如果调用因为超过最大调用深度或尝试发送超过当前地址的余额而失败,目标地址仍在 touched_addresses 集合中。CREATE*,如果合约创建因超过最大调用深度或尝试发送超过当前地址的余额而失败,所创建合约的地址不会保留在 touched_addresses 集合中。CREATE* 操作因尝试将合约部署到非空账户而失败,则所创建的合约地址保留在 touched_addresses 集合中。EIP-2930 引入了可选的访问列表,该列表可以作为交易的一部分包含。
此访问列表允许在交易执行开始之前添加元素到 touched_addresses 和 touched_storage_slots 访问集合。
为 touched_addresses 添加每个地址的成本为2400 Gas,为 touched_storage_slots 添加每个 (address, storage_key) 对的成本为 1900 Gas。
这种成本是在 内在Gas 的同一时间收取的。
重要备注:
touched_storage_slots 中添加 (ADDR, storage_key) 对而不将 ADDR 添加到 touched_addresses。原本旨在提供清除未使用状态的激励,整个交易执行过程中跟踪总 gas_refund。
根据EIP-3529,SSTORE 是唯一可能提供Gas退款的操作。
交易的最大退款限制为整个交易消耗的Gas的五分之一。
refund_amt := min(gas_refund, tx.gas_consumed // 5)消耗的Gas包括内在Gas,预填充访问集合的成本,以及任何代码执行的成本。
当交易结束时,交易消耗的Gas减去 refund_amt。
这实际上将 <code>refund_amt * tx.gasprice</code> 补偿给 tx.origin,但它还减少了交易对区块总消耗Gas的影响(在确定区块Gas限制时)。
由于Gas退款在交易结束时才被应用,因此非零退款不会影响交易是否导致 OUT_OF_GAS 异常。
#
术语:
byte_len_exponent: 指数中的字节数(指数是堆栈表示中的 b)Gas计算:
gas_cost = 10 + 50 * byte_len_exponent术语:
data_size: 要哈希的消息大小(以字节为单位,堆栈表示中的 len)data_size_words = (data_size + 31) // 32: 要哈希的消息中(32字节)字的数量mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1)Gas计算:
gas_cost = 30 + 6 * data_size_words + mem_expansion_cost以下适用于操作 CALLDATACOPY, CODECOPY, 和 RETURNDATACOPY (非 EXTCODECOPY)。
术语:
data_size: 要复制的数据大小(以字节为单位,堆栈表示中的 len)data_size_words = (data_size + 31) // 32: 要复制数据中(32字节)字的数量mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1)Gas计算:
gas_cost = 3 + 3 * data_size_words + mem_expansion_cost术语:
target_addr: 要复制代码的地址(堆栈表示中的 addr)access_cost: 访问热账户与冷账户的成本(见 A0-2)
access_cost = 100 如果 target_addr 在 touched_addresses 中(热访问)access_cost = 2600 如果 target_addr 不在 touched_addresses 中(冷访问)data_size: 要复制的数据大小(以字节为单位,堆栈表示中的 len)data_size_words = (data_size + 31) // 32: 要复制数据中的(32字节)字的数量mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1)Gas计算:
gas_cost = access_cost + 3 * data_size_words + mem_expansion_cost操作码 BALANCE, EXTCODESIZE, EXTCODEHASH 的定价函数基于对单一账户的访问。
有关EIP-2929和 touched_addresses 的详细信息,请参见 A0-2。
术语:
target_addr: 感兴趣的地址(堆栈表示中的 addr)Gas计算:
gas_cost = 100 如果 target_addr 在 touched_addresses 中(热访问)gas_cost = 2600 如果 target_addr 不在 touched_addresses 中(冷访问)请参见 A0-2 有关EIP-2929和 touched_storage_slots 的详细信息。
术语:
context_addr: 当前执行上下文的地址(即 ADDRESS 将在堆栈中放置的内容)target_storage_key: 要加载的32字节存储索引(堆栈表示中的 key)Gas计算:
gas_cost = 100 如果 (context_addr, target_storage_key) 在 touched_storage_slots 中(热访问)gas_cost = 2100 如果 (context_addr, target_storage_key) 不在 touched_storage_slots 中(冷访问)这变得复杂。 请参见EIP-2200,已在伊斯坦布尔硬叉中激活。
SSTORE 操作的成本依赖于现有值和要存储的值:
该成本还依赖于目标存储槽是否在同一交易中已经被访问。
请参见 A0-2 有关EIP-2929和 touched_storage_slots 的详细信息。
术语:
context_addr: 当前执行上下文的地址(即 ADDRESS 将在堆栈中放置的内容)target_storage_key: 要存储的32字节存储索引(堆栈表示中的 key)orig_val: 如果当前交易被回滚,存储槽的值current_val: 在问题 sstore 操作之前存储槽的值new_val: 在问题 sstore 操作之后存储槽的值Gas计算:
gas_cost = 0gas_refund = 0gas_left <= 2300:
throw OUT_OF_GAS_ERROR (无法使用 < 2300 Gas 执行 sstore 以保持向后兼容性)(context_addr, target_storage_key) 不在 touched_storage_slots 中(冷访问):
gas_cost += 2100new_val == current_val(无操作):
gas_cost += 100new_val != current_val:
current_val == orig_val(“干净槽”,当前执行上下文中尚未更新):
orig_val == 0(槽初始为零,目前仍为零,现在被更改为非零):
gas_cost += 20000orig_val != 0(槽初始非零,目前仍为相同非零值,现在被更改):
gas_cost += 2900并按如下方式更新退款……new_val == 0(要存储的值为0):
gas_refund += 4800current_val != orig_val(“脏槽”,当前执行上下文中已经更新):
gas_cost += 100并按如下方式更新退款……orig_val != 0(执行上下文开始时槽内有非零值):
current_val == 0(槽初始为非零,目前为零,现在被更改为非零):
gas_refund -= 4800new_val == 0(槽初始为非零,目前仍为非零,现在被更改为零):
gas_refund += 4800new_val == orig_val(槽重置为其最初值):
orig_val == 0(槽初始为零,目前为非零,现在被重置为零):
gas_refund += 19900orig_val != 0(槽初始为非零,目前为不同非零值,现在重置为原始非零值):
gas_refund += 2800请注意,对于 LOG* 操作,按数据字节支付Gas(而不是按字)。
术语:
num_topics: LOG* 操作的主题数。例如,LOG0 有 num_topics = 0,LOG4 有 num_topics = 4data_size: 要记录的数据大小(以字节为单位,堆栈表示中的 len)。mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1)Gas计算:
gas_cost = 375 + 375 * num_topics + 8 * data_size + mem_expansion_cost常用术语:
Gas计算:
gas_cost = 32000 + mem_expansion_cost + code_deposit_costCREATE2 由于需要对初始化代码进行哈希,相较于 CREATE 增加了额外的动态成本。
术语:
data_size: 初始化代码的大小(以字节为单位,堆栈表示中的 len)data_size_words = (data_size + 31) // 32: 初始化代码中的(32字节)字的数量Gas计算:
gas_cost = 32000 + 6 * data_size_words + mem_expansion_cost + code_deposit_cost除了 CREATE 和 CREATE2 操作的静态和动态成本外,存储返回的运行时代码还需收取每字节的成本。
与操作码的静态和动态成本不同,此成本在初始化代码执行停止后才会应用。
术语:
returned_code_size: 返回的运行时代码的长度Gas计算:
code_deposit_cost = 200 * returned_code_size与合约创建的代码存储步骤相关的备注:
根据EIP-3541,从合约创建返回的任何代码(即将成为已部署合约代码的内容),如果代码的第一个字节为 0xEF,则会导致整个合约创建异常终止。
CALL、CALLCODE、DELEGATECALL 和 STATICCALL 操作的Gas成本。
这些操作的Gas计算的一个重要部分是确定要与调用一起发送的Gas。
你很可能主要关心base_cost,并且可以忽略此额外计算,因为 gas_sent_with_call 在被调用合约的上下文中消耗,未使用的Gas会被退还。
如果不是,参见 gas_sent_with_call 部分。
类似于selfdestruct,如果 CALL 强制将账号添加到状态试图中,通过向之前为空的地址发送非零ET,额外的成本将会被收取。
此情况下“空”的定义见EIP-161(balance == nonce == code == 0x)。
常用术语:
call_value: 与调用一起发送的值(堆栈表示中的 val)target_addr: 调用的目标(堆栈表示中的 addr)access_cost: 访问热账户与冷账户的成本(见 A0-2)
access_cost = 100 如果 target_addr 在 touched_addresses 中(热访问)access_cost = 2600 如果 target_addr 不在 touched_addresses 中(冷访问)mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1)gas_sent_with_call: 实际上与调用一起发送的GasGas计算:
base_gas = access_cost + mem_expansion_costcall_value > 0(发送值与调用一起):
base_gas += 9000is_empty(target_addr)(强制在状态浏览中创建新账户):
base_gas += 25000下面计算 gas_sent_with_call :。
并且操作的最终成本为:
gas_cost = base_gas + gas_sent_with_callGas计算:
base_gas = access_cost + mem_expansion_costcall_value > 0(发送值与调用一起):
base_gas += 9000下面计算 gas_sent_with_call :。
并且操作的最终成本为:
gas_cost = base_gas + gas_sent_with_callGas计算:
base_gas = access_cost + mem_expansion_cost下面计算 gas_sent_with_call :。
并且操作的最终成本为:
gas_cost = base_gas + gas_sent_with_callGas计算:
base_gas = access_cost + mem_expansion_cost下面计算 gas_sent_with_call :。
并且操作的最终成本为:
gas_cost = base_gas + gas_sent_with_call除了操作的基本成本外, CALL、CALLCODE、DELEGATECALL 和 STATICCALL 还需要确定要与调用一起发送多少Gas。
这里的大部分复杂性来自于在EIP-150中进行的向后兼容更改。
下面是此计算使用的原因概述:
EIP-150将 CALL 操作的 base_cost 从40增加到700 Gas,但当时使用的大多数合约都通过每次调用发送 available_gas - 40。
因此,当 base_cost 增加时,这些合约突然尝试发送比他们剩余的Gas多的Gas(requested_gas > remaining_gas)。
为了避免破坏这些合约,如果 requested_gas 超过 remaining_gas,我们发送 all_but_one_64th的 remaining_gas,而不是尝试发送 requested_gas,这样将导致 OUT_OF_GAS_ERROR。
术语:
base_gas: 考虑到应与调用一起发送的Gas前的操作成本。
请参见 AA 以获得相关计算。available_gas: 当前执行上下文中,在操作码执行前剩余的Gasremaining_gas: 在扣除操作的 base_cost 后剩余的Gas,但在扣除 gas_sent_with_call 之前requested_gas: 请求与调用一起发送的Gas(堆栈表示中的 gas)all_but_one_64th: 所有但剩余Gas的下限(1/64)gas_sent_with_call: 实际上与调用一起发送的GasGas计算:
remaining_gas = available_gas - base_gasall_but_one_64th = remaining_gas - (remaining_gas // 64)gas_sent_with_call = min(requested_gas, all_but_one_64th)调用接收者未使用的 gas_sent_with_call 部分将在调用返回后退还给调用者。此外,如果 call_value > 0,将向调用中包含的Gas量添加2300的Gas津贴,但不包括在调用成本中。
SELFDESTRUCT 操作的Gas成本取决于该操作是否产生新的账户被添加到状态试图。
如果向之前为空的地址发送非零ET,将额外产生费用。
此情况下“空”的定义见EIP-161(balance == nonce == code == 0x)。
如果操作需要对接收地址进行冷访问,则该成本也会增加。
有关EIP-2929和 touched_addresses 的详细信息,请参见 A0-2。
术语:
target_addr: 自销毁合约的资金的接收者(堆栈表示中的 addr)context_addr: 当前执行上下文的地址(即 ADDRESS 将在堆栈中放置的内容)Gas计算:
gas_cost = 5000:基本成本balance(context_addr) > 0 && is_empty(target_addr) (向先前为空的地址发送资金):
gas_cost += 25000target_addr 不在 touched_addresses 中(冷访问):
gas_cost += 2600执行任何无效操作时,无论是指定的 INVALID 操作码还是简单地未定义的操作码,所有剩余Gas都会被消耗,状态会恢复到当前执行上下文开始前的状态。
- 原文链接: github.com/wolflo/evm-op...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!