附录 - 动态EVM Gas成本

  • wolflo
  • 发布于 2021-11-09 13:42
  • 阅读 18

本文详细介绍了以太坊交易的动态Gas成本,包括固有Gas、内存扩展、访问集、Gas退款等方面的计算方法和相关术语,深入探讨了 EIP-2929 和 EIP-3529 的影响。内容结构清晰,逻辑严谨,适合对以太坊交易机制有一定了解的读者。

附录 - 动态Gas成本

A0-0: 内在Gas

内在Gas是指在交易执行之前支付的Gas量。 也就是说,交易的发起者支付的Gas,这将始终是一个外部拥有的账号,在进行任何状态更新或执行任何代码之前支付。

Gas计算:

  • gas_cost = 21000: 基本成本
  • 如果 tx.to == null(合约创建交易):
    • gas_cost += 32000
  • gas_cost += 4 * bytes_zero: 每个零字节的内存数据为基础成本增加的Gas
  • gas_cost += 16 * bytes_nonzero: 每个非零字节的内存数据为基础成本增加的Gas

A0-1: 内存扩展

任何扩展正在使用的内存的操作都需支付额外的Gas成本。 这种内存扩展成本取决于现有内存的大小,如果该操作没有引用高于当前引用的内存地址,则成本为0。 引用是对内存的任何读、写或其他使用(例如在CALL中)。

术语:

  • new_mem_size: 问题操作之后引用的最高内存地址(以字节为单位)
  • new_mem_size_words = (new_mem_size + 31) // 32: 问题操作之后所需的(32字节)字数的数量
  • <code><em>C<sub>mem</sub>(machine_state)</em></code>: 给定机器状态的内存成本函数

Gas计算:

  • <code>gas_cost = <em>C<sub>mem</sub>(new_state)</em> - <em>C<sub>mem</sub>(old_state)</em></code>
  • <code>gas_cost = (new_mem_size_words ^ 2 // 512) + (3 * new_mem_size_words) - <em>C<sub>mem</sub>(old_state)</em></code>

有用的备注:

  • 以下操作码除了其静态Gas成本外,还会产生内存扩展成本: RETURN, REVERT, MLOAD, MSTORE, MSTORE8
  • 引用零长度范围不需要将内存扩展到范围的开始。
  • 内存成本函数在使用724字节的内存时线性,在这时,额外的内存费用要高得多。

A0-2: 访问集合

根据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 集合。

  • <code>touched addresses = touched_addresses ∪ { target_address }</code>

当一个存储槽是 STORAGE_TOUCHING_OPCODES 之一的目标时, (address, key) 对会立即被添加到 touched_storage_slots 集合。

  • <code>touched_storage_slots = touched_storage_slots ∪ { (current_address, target_storage_key) }</code>

重要备注:

  • 向这些集合中添加重复元素是无操作(no-op)。高性能实现将使用更复杂的添加逻辑的映射。
  • 如果一个执行帧回滚,访问集合将返回到进入该帧之前的状态。
  • *CALLCREATE* 操作的 touched_addresses 集合的新增是在进入新的执行帧之前立即进行,因此在调用或合约创建中的任何失败都不会将失败的 *CALLCREATE* 的目标地址移出 touched_addresses 集合。 一些棘手的边缘案例:
    • 对于 *CALL,如果调用因为超过最大调用深度或尝试发送超过当前地址的余额而失败,目标地址仍在 touched_addresses 集合中。
    • 对于 CREATE*,如果合约创建因超过最大调用深度或尝试发送超过当前地址的余额而失败,所创建合约的地址不会保留在 touched_addresses 集合中。
    • 如果 CREATE* 操作因尝试将合约部署到非空账户而失败,则所创建的合约地址保留在 touched_addresses 集合中。
预填充访问集合

EIP-2930 引入了可选的访问列表,该列表可以作为交易的一部分包含。 此访问列表允许在交易执行开始之前添加元素到 touched_addressestouched_storage_slots 访问集合。 为 touched_addresses 添加每个地址的成本为2400 Gas,为 touched_storage_slots 添加每个 (address, storage_key) 对的成本为 1900 Gas。 这种成本是在 内在Gas 的同一时间收取的。

重要备注:

  • 不可在 touched_storage_slots 中添加 (ADDR, storage_key) 对而不将 ADDR 添加到 touched_addresses
  • 访问列表可能包含重复项。 向一个访问集合添加重复项是无操作,但添加的成本仍会被收取。
  • 为交易提供访问列表可能会带来适度的每个唯一访问的折扣,但这并不总是如此。 更多完整讨论见 @fvictorio/gas-costs-after-berlin

A0-3: Gas退款

原本旨在提供清除未使用状态的激励,整个交易执行过程中跟踪总 gas_refund。 根据EIP-3529SSTORE 是唯一可能提供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 异常。

#

A1: EXP

术语:

  • byte_len_exponent: 指数中的字节数(指数是堆栈表示中的 b

Gas计算:

  • gas_cost = 10 + 50 * byte_len_exponent

A2: SHA3

术语:

  • 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

A3: *COPY 操作

以下适用于操作 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

A4: EXTCODECOPY

术语:

  • 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

A5: BALANCE, EXTCODESIZE, EXTCODEHASH

操作码 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 中(冷访问)

A6: SLOAD

请参见 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 中(冷访问)

A7: SSTORE

这变得复杂。 请参见EIP-2200,已在伊斯坦布尔硬叉中激活。

SSTORE 操作的成本依赖于现有值和要存储的值:

  1. 零值与非零值 - 存储非零值的成本高于存储零。
  2. 存储槽的当前值与要存储的值 - 更改存储槽的值的成本高于不更改。
  3. “脏”槽与“干净”槽 - 更改在当前执行上下文中尚未被更改的槽的成本高于更改已经被更改的槽的成本。

该成本还依赖于目标存储槽是否在同一交易中已经被访问。 请参见 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 = 0
  • gas_refund = 0
  • 如果 gas_left &lt;= 2300
    • throw OUT_OF_GAS_ERROR (无法使用 < 2300 Gas 执行 sstore 以保持向后兼容性)
  • 如果 (context_addr, target_storage_key) 不在 touched_storage_slots 中(冷访问):
    • gas_cost += 2100
  • 如果 new_val == current_val(无操作):
    • gas_cost += 100
  • 否则 new_val != current_val
    • 如果 current_val == orig_val(“干净槽”,当前执行上下文中尚未更新):
      • 如果 orig_val == 0(槽初始为零,目前仍为零,现在被更改为非零):
        • gas_cost += 20000
      • 否则 orig_val != 0(槽初始非零,目前仍为相同非零值,现在被更改):
        • gas_cost += 2900并按如下方式更新退款……
        • 如果 new_val == 0(要存储的值为0):
          • gas_refund += 4800
    • 否则 current_val != orig_val(“脏槽”,当前执行上下文中已经更新):
      • gas_cost += 100并按如下方式更新退款……
      • 如果 orig_val != 0(执行上下文开始时槽内有非零值):
        • 如果 current_val == 0(槽初始为非零,目前为零,现在被更改为非零):
          • gas_refund -= 4800
        • 否则如果 new_val == 0(槽初始为非零,目前仍为非零,现在被更改为零):
          • gas_refund += 4800
      • 如果 new_val == orig_val(槽重置为其最初值):
        • 如果 orig_val == 0(槽初始为零,目前为非零,现在被重置为零):
          • gas_refund += 19900
        • 否则 orig_val != 0(槽初始为非零,目前为不同非零值,现在重置为原始非零值):
          • gas_refund += 2800

A8: LOG* 操作

请注意,对于 LOG* 操作,按数据字节支付Gas(而不是按字)。

术语:

  • num_topics: LOG* 操作的主题数。例如,LOG0 有 num_topics = 0,LOG4 有 num_topics = 4
  • data_size: 要记录的数据大小(以字节为单位,堆栈表示中的 len)。
  • mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1

Gas计算:

  • gas_cost = 375 + 375 * num_topics + 8 * data_size + mem_expansion_cost

A9: CREATE* 操作

常用术语:

  • mem_expansion_cost: 任何所需内存扩展的成本(见 A0-1
  • code_deposit_cost: 存储已部署代码所产生的每字节成本(见 A9-F)。

A9-1: CREATE

Gas计算:

  • gas_cost = 32000 + mem_expansion_cost + code_deposit_cost

A9-2: CREATE2

CREATE2 由于需要对初始化代码进行哈希,相较于 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

A9-F: 代码存储成本

除了 CREATECREATE2 操作的静态和动态成本外,存储返回的运行时代码还需收取每字节的成本。 与操作码的静态和动态成本不同,此成本在初始化代码执行停止后才会应用。

术语:

  • returned_code_size: 返回的运行时代码的长度

Gas计算:

  • code_deposit_cost = 200 * returned_code_size

与合约创建的代码存储步骤相关的备注: 根据EIP-3541,从合约创建返回的任何代码(即成为已部署合约代码的内容),如果代码的第一个字节为 0xEF,则会导致整个合约创建异常终止。

AA: CALL* 操作

CALLCALLCODEDELEGATECALLSTATICCALL 操作的Gas成本。 这些操作的Gas计算的一个重要部分是确定要与调用一起发送的Gas。 你很可能主要关心base_cost,并且可以忽略此额外计算,因为 gas_sent_with_call 在被调用合约的上下文中消耗,未使用的Gas会被退还。 如果不是,参见 gas_sent_with_call 部分

类似于selfdestruct,如果 CALL 强制将账号添加到状态试图中,通过向之前为空的地址发送非零ET,额外的成本将会被收取。 此情况下“空”的定义见EIP-161balance == 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: 实际上与调用一起发送的Gas

AA-1: CALL

Gas计算:

  • base_gas = access_cost + mem_expansion_cost
  • 如果 call_value > 0(发送值与调用一起):
    • base_gas += 9000
    • 如果 is_empty(target_addr)(强制在状态浏览中创建新账户):
      • base_gas += 25000

下面计算 gas_sent_with_call

并且操作的最终成本为:

  • gas_cost = base_gas + gas_sent_with_call

AA-2: CALLCODE

Gas计算:

  • base_gas = access_cost + mem_expansion_cost
  • 如果 call_value > 0(发送值与调用一起):
    • base_gas += 9000

下面计算 gas_sent_with_call

并且操作的最终成本为:

  • gas_cost = base_gas + gas_sent_with_call

AA-3: DELEGATECALL

Gas计算:

  • base_gas = access_cost + mem_expansion_cost

下面计算 gas_sent_with_call

并且操作的最终成本为:

  • gas_cost = base_gas + gas_sent_with_call

AA-4: STATICCALL

Gas计算:

  • base_gas = access_cost + mem_expansion_cost

下面计算 gas_sent_with_call

并且操作的最终成本为:

  • gas_cost = base_gas + gas_sent_with_call

AA-F: 与CALL操作一起发送的Gas

除了操作的基本成本外, CALLCALLCODEDELEGATECALLSTATICCALL 还需要确定要与调用一起发送多少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_64thremaining_gas,而不是尝试发送 requested_gas,这样将导致 OUT_OF_GAS_ERROR

术语:

  • base_gas: 考虑到应与调用一起发送的Gas前的操作成本。 请参见 AA 以获得相关计算。
  • available_gas: 当前执行上下文中,在操作码执行前剩余的Gas
  • remaining_gas: 在扣除操作的 base_cost 后剩余的Gas,但在扣除 gas_sent_with_call 之前
  • requested_gas: 请求与调用一起发送的Gas(堆栈表示中的 gas
  • all_but_one_64th: 所有但剩余Gas的下限(1/64)
  • gas_sent_with_call: 实际上与调用一起发送的Gas

Gas计算:

  • remaining_gas = available_gas - base_gas
  • all_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津贴,但不包括在调用成本中。

AB: SELFDESTRUCT

SELFDESTRUCT 操作的Gas成本取决于该操作是否产生新的账户被添加到状态试图。 如果向之前为空的地址发送非零ET,将额外产生费用。 此情况下“空”的定义见EIP-161balance == 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 += 25000
  • 如果 target_addr 不在 touched_addresses 中(冷访问):
    • gas_cost += 2600

AF: INVALID

执行任何无效操作时,无论是指定的 INVALID 操作码还是简单地未定义的操作码,所有剩余Gas都会被消耗,状态会恢复到当前执行上下文开始前的状态。

  • 原文链接: github.com/wolflo/evm-op...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
wolflo
wolflo
江湖只有他的大名,没有他的介绍。