Alert Source Discuss
Standards Track: Core

EIP-2200: 用于净 Gas 计量的结构化定义

Authors Wei Tang (@sorpaas)
Created 2019-07-18

简述

这是一个实现净 gas 计量的 EIP。它是 EIP-1283EIP-1706 的组合版本,具有结构化的定义,以便与其他 gas 更改(如 EIP-1884)互操作。

摘要

此 EIP 为 SSTORE 操作码提供了净 gas 计量更改的结构化定义,从而为合约存储启用了新用法,并减少了与大多数实现方式不符的过度 gas 成本。

这是 EIP-1283EIP-1706 的组合。

动机

此 EIP 提出了一种在 SSTORE 上进行 gas 计量的方法,该方法使用大多数实现更普遍可用的信息,并尽可能减少实现结构的更改。

  • 存储槽的原始值。
  • 存储槽的当前值。
  • 退款计数器。

受益于此 EIP 的 gas 减少方案的用法包括:

  • 在同一调用帧内的后续存储写入操作。这包括重入锁、相同合约多重发送等。
  • 在子调用帧和父调用帧之间交换存储信息,其中此信息无需在事务之外持久存在。这包括子帧错误代码和消息传递等。

EIP-1283 的原始定义产生了一种新的重入攻击的危险,这种攻击针对现有合约,因为 Solidity 默认情况下会向简单的 transfer 调用授予 2300 gas 的“津贴”。如果 SSTORE 在低 gasleft 状态下不允许,则可以轻松缓解这种危险,而不会破坏向后兼容性和 EIP-1283 的原始意图。

此 EIP 还通过参数替换了 EIP-1283 的原始 gas 值定义,使其更结构化,并且更易于定义将来的更改。

规范

定义变量 SLOAD_GASSSTORE_SET_GASSSTORE_RESET_GASSSTORE_CLEARS_SCHEDULE。这些变量的新旧值是:

  • SLOAD_GAS: 从 200 更改为 800
  • SSTORE_SET_GAS: 20000,未更改。
  • SSTORE_RESET_GAS: 5000,未更改。
  • SSTORE_CLEARS_SCHEDULE: 15000,未更改。

使用这些变量更改 EIP-1283 的定义。结合 EIP-1283 和 EIP-1706 的新规范如下所示。原始值当前值新值的术语在 EIP-1283 中定义。

用以下逻辑替换 SSTORE 操作码 gas 成本计算(包括退款):

  • 如果 gasleft 小于或等于 gas 津贴,则当前调用帧将失败,并显示“out of gas”异常。
  • 如果 当前值 等于 新值(这是一个空操作),则扣除 SLOAD_GAS
  • 如果 当前值 不等于 新值
    • 如果 原始值 等于 当前值(此存储槽尚未被当前执行上下文更改)
      • 如果 原始值 为 0,则扣除 SSTORE_SET_GAS
      • 否则,扣除 SSTORE_RESET_GAS gas。如果 新值 为 0,则将 SSTORE_CLEARS_SCHEDULE gas 添加到退款计数器。
    • 如果 原始值 不等于 当前值(此存储槽已更改),则扣除 SLOAD_GAS gas。应用以下两个子句。
      • 如果 原始值 不为 0
        • 如果 当前值 为 0(也意味着 新值 不为 0),则从退款计数器中删除 SSTORE_CLEARS_SCHEDULE gas。
        • 如果 新值 为 0(也意味着 当前值 不为 0),则将 SSTORE_CLEARS_SCHEDULE gas 添加到退款计数器。
      • 如果 原始值 等于 新值(此存储槽已重置)
        • 如果 原始值 为 0,则将 SSTORE_SET_GAS - SLOAD_GAS 添加到退款计数器。
        • 否则,将 SSTORE_RESET_GAS - SLOAD_GAS gas 添加到退款计数器。

实现还应注意,使用上述定义,如果实现使用调用帧退款计数器,则计数器可能会变为负数。如果实现使用事务退款计数器,则计数器始终保持为正数。

理由

此 EIP 主要实现了瞬态存储尝试执行的操作(EIP-1087EIP-1153),而无需引入“脏映射”的概念或额外的存储结构。

  • 我们不会受到 EIP-1087 的优化限制。EIP-1087 需要为存储更改保留一个脏映射,并隐式地假设事务的存储更改在事务结束时提交到存储 trie。这对于某些实现来说效果很好,但对于其他实现则不然。在 EIP-658 之后,高效的存储缓存实现可能会使用内存 trie(不使用 RLP 编码/解码)或其他不可变数据结构来跟踪存储更改,并且仅在块的末尾提交更改。对于他们来说,可以知道存储的原始值和当前值,但如果不产生额外的内存或处理成本,则无法迭代所有存储更改。
  • 与当前方案相比,它永远不会花费更多的 gas。
  • 它涵盖了瞬态存储的所有用法。易于实现 EIP-1087 的客户端也易于实现此规范。其他一些客户端可能需要对此进行一些额外的重构。尽管如此,运行时不需要额外的内存或处理成本。

关于 SSTORE gas 成本和退款,请参阅附录,以获取此 EIP 满足的属性的证明。

  • 对于 使用的绝对 gas(即,实际 使用的 gas 减去 退款),对于所有情况,此 EIP 等效于 EIP-1087。
  • 对于一种特殊情况,其中存储槽被更改,重置为其原始值,然后再次更改,与 EIP-1087 相比,EIP-1283 会将更多的 gas 转移到退款计数器。

检查 EIP-1087 的 Motivation 中提供的示例(SLOAD_GAS200):

  • 如果具有空存储的合约将槽 0 设置为 1,然后返回到 0,则将收取 20000 + 200 - 19800 = 400 gas。
  • 具有空存储的合约将槽 0 递增 5 次,将收取 20000 + 5 * 200 = 21000 gas。
  • 从帐户 A 到帐户 B 的余额转移,然后从 B 到 C 的转移,所有帐户都具有非零的起始和结束余额,这将花费 5000 * 3 + 200 - 4800 = 10400 gas。

为了保持现有合约的隐式重入保护不变,如果剩余 gas 低于 Solidity 中赋予“transfer”/“send”的 gas 津贴,则不应允许事务修改状态。以下是其他提议的补救措施和反对实施的理由:

  • 删除 EIP-1283 并放弃修改 SSTORE 成本
    • EIP-1283 是一项重要的更新
    • 它已被接受并在测试网络和客户端中实现。
  • 添加一个新的调用上下文,该上下文允许 LOG 操作码,但不允许更改状态。
    • 除了现有的 regular/staticcall 之外,还添加了另一种调用类型
  • 将脏槽的 SSTORE 成本提高到 >=2300 gas
    • 使净 gas 计量的用处大大降低。
  • 降低 gas 津贴
    • 使津贴几乎毫无用处。
  • 将写入脏槽的成本提高回 5000 gas,但将 4800 gas 添加到退款计数器
    • 仍然没有明确的固定值。
    • 要求调用方提供更多的 gas,只是为了获得退款
  • 添加合约元数据,指定每个合约的 EVM 版本,并且仅将 SSTORE 更改应用于使用新版本部署的合约。

向后兼容性

此 EIP 需要进行硬分叉才能实现。预计不会增加 gas 成本,并且许多合约会看到 gas 减少。

使用少于 5000 gas 执行 SSTORE 从未成为可能,因此它不会对以太坊主网引入不兼容性。Gas 估算应考虑此要求。

测试用例

代码 使用的 Gas 退款 原始 1st 2nd 3rd
0x60006000556000600055 1612 0 0 0 0  
0x60006000556001600055 20812 0 0 0 1  
0x60016000556000600055 20812 19200 0 1 0  
0x60016000556002600055 20812 0 0 1 2  
0x60016000556001600055 20812 0 0 1 1  
0x60006000556000600055 5812 15000 1 0 0  
0x60006000556001600055 5812 4200 1 0 1  
0x60006000556002600055 5812 0 1 0 2  
0x60026000556000600055 5812 15000 1 2 0  
0x60026000556003600055 5812 0 1 2 3  
0x60026000556001600055 5812 4200 1 2 1  
0x60026000556002600055 5812 0 1 2 2  
0x60016000556000600055 5812 15000 1 1 0  
0x60016000556002600055 5812 0 1 1 2  
0x60016000556001600055 1612 0 1 1 1  
0x600160005560006000556001600055 40818 19200 0 1 0 1
0x600060005560016000556000600055 10818 19200 1 0 1 0

实施

待添加。

附录:证明

因为 存储槽的原始值 定义为在 当前事务 发生回滚时的值,所以很容易看出调用帧不会干扰 SSTORE gas 计算。因此,尽管下面的证明是在没有调用帧的情况下讨论的,但它适用于所有有调用帧的情况。我们将分别讨论 原始值 为零和不为零的情况,并使用归纳法来证明 SSTORE gas 成本的一些属性。

最终值 是事务结束时特定存储槽的值。使用的绝对 gas使用的 gas 减去 退款 的绝对值。我们使用 N 来表示存储槽上 SSTORE 操作的总数。对于下面讨论的状态,请参阅 解释 部分中的 状态转换

下面我们在所有参数都不变的情况下进行证明,这意味着 SLOAD_GAS200。但是,请注意,无论 SLOAD_GAS 如何更改,该证明仍然适用。

原始值为零

原始值 为 0 时,我们要证明:

  • 情况 I: 如果 最终值 最终仍然为 0,则我们要收取 200 * N gas,因为不需要磁盘写入。
  • 情况 II: 如果 最终值 最终为一个非零值,则我们要收取 20000 + 200 * (N-1) gas,因为它需要将此槽写入磁盘。

基本情况

我们总是从状态 A 开始。第一个 SSTORE 可以:

  • 转到状态 A:扣除 200 gas。我们满足 情况 I,因为 200 * N == 200 * 1
  • 转到状态 B:扣除 20000 gas。我们满足 情况 II,因为 20000 + 200 * (N-1) == 20000 + 200 * 0

归纳步骤

  • 从 A 到 A。先前的 gas 成本为 200 * (N-1)。当前的 gas 成本为 200 + 200 * (N-1)。它满足 情况 I
  • 从 A 到 B。先前的 gas 成本为 200 * (N-1)。当前的 gas 成本为 20000 + 200 * (N-1)。它满足 情况 II
  • 从 B 到 B。先前的 gas 成本为 20000 + 200 * (N-2)。当前的 gas 成本为 200 + 20000 + 200 * (N-2)。它满足 情况 II
  • 从 B 到 A。先前的 gas 成本为 20000 + 200 * (N-2)。当前的 gas 成本为 200 - 19800 + 20000 + 200 * (N-2)。它满足 情况 I

原始值不为零

原始值 不为 0 时,我们要证明:

  • 情况 I: 如果 最终值 最终保持不变,则我们要收取 200 * N gas,因为不需要磁盘写入。
  • 情况 II: 如果 最终值 最终为零,则我们要收取 5000 - 15000 + 200 * (N-1) gas。请注意,15000 是实际定义中的退款。
  • 情况 III: 如果 最终值 最终为已更改的非零值,则我们要收取 5000 + 200 * (N-1) gas。

基本情况

我们总是从状态 X 开始。第一个 SSTORE 可以:

  • 转到状态 X:扣除 200 gas。我们满足 情况 I,因为 200 * N == 200 * 1
  • 转到状态 Y:扣除 5000 gas。我们满足 情况 III,因为 5000 + 200 * (N-1) == 5000 + 200 * 0
  • 转到状态 Z:使用的绝对 gas 为 5000 - 15000,其中 15000 是退款。我们满足 情况 II,因为 5000 - 15000 + 200 * (N-1) == 5000 - 15000 + 200 * 0

归纳步骤

  • 从 X 到 X。先前的 gas 成本为 200 * (N-1)。当前的 gas 成本为 200 + 200 * (N-1)。它满足 情况 I
  • 从 X 到 Y。先前的 gas 成本为 200 * (N-1)。当前的 gas 成本为 5000 + 200 * (N-1)。它满足 情况 III
  • 从 X 到 Z。先前的 gas 成本为 200 * (N-1)。当前的绝对 gas 成本为 5000 - 15000 + 200 * (N-1)。它满足 情况 II
  • 从 Y 到 X。先前的 gas 成本为 5000 + 200 * (N-2)。当前的绝对 gas 成本为 200 - 4800 + 5000 + 200 * (N-2)。它满足 情况 I
  • 从 Y 到 Y。先前的 gas 成本为 5000 + 200 * (N-2)。当前的 gas 成本为 200 + 5000 + 200 * (N-2)。它满足 情况 III
  • 从 Y 到 Z。先前的 gas 成本为 5000 + 200 * (N-2)。当前的绝对 gas 成本为 200 - 15000 + 5000 + 200 * (N-2)。它满足 情况 II
  • 从 Z 到 X。先前的 gas 成本为 5000 - 15000 + 200 * (N-2)。当前的绝对 gas 成本为 200 + 10200 + 5000 - 15000 + 200 * (N-2)。它满足 情况 I
  • 从 Z 到 Y。先前的 gas 成本为 5000 - 15000 + 200 * (N-2)。当前的绝对 gas 成本为 200 + 15000 + 5000 - 15000 + 200 * (N-2)。它满足 情况 III
  • 从 Z 到 Z。先前的 gas 成本为 5000 - 15000 + 200 * (N-2)。当前的绝对 gas 成本为 200 + 5000 - 15000 + 200 * (N-2)。它满足 情况 II

版权

版权及相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Wei Tang (@sorpaas), "EIP-2200: 用于净 Gas 计量的结构化定义," Ethereum Improvement Proposals, no. 2200, July 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2200.