Alert Source Discuss
Standards Track: Core

EIP-1283: 用于无脏映射的 SSTORE 的净 Gas 计量

Authors Wei Tang (@sorpaas)
Created 2018-08-01

摘要

该 EIP 提出了 SSTORE 操作码的净 gas 计量变更,从而为合约存储启用 新的用法,并减少了 gas 成本过高的情况 因为它与大多数实现的工作方式不符。

这可以作为 EIP-1087 的替代方案,因为它试图 对使用不同存储变更缓存优化策略的实现更加友好。

动机

该 EIP 提出了一种在 SSTORE 上进行 gas 计量的方法(作为替代方案 对于 EIP-1087 和 EIP-1153),使用对大多数实现更普遍可用的信息,并尽可能减少 实现结构的变更。

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

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

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

规范

术语定义如下:

  • 存储槽的原始值:如果存储发生,这是存储的值 在当前交易中发生回滚。
  • 存储槽的当前值:这是存储的值 在 SSTORE 操作发生之前。
  • 存储槽的新值:这是存储的值 在 SSTORE 操作发生之后。

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

  • 如果当前值等于新值(这是一个空操作),则扣除 200 gas。
  • 如果当前值不等于新值
    • 如果原始值等于当前值(此存储槽未被当前执行上下文更改)
      • 如果原始值为 0,则扣除 20000 gas。
      • 否则,扣除 5000 gas。如果新值为 0,则将 15000 gas 添加到退款计数器。
    • 如果原始值不等于当前值(此存储槽是脏的),则扣除 200 gas。应用以下两个 子句。
      • 如果原始值不为 0
        • 如果当前值为 0(也意味着新值不为 0),则从退款计数器中删除 15000 gas。我们可以证明 退款计数器永远不会低于 0。
        • 如果新值为 0(也意味着当前值不为 0),则将 15000 gas 添加到退款计数器。
      • 如果原始值等于新值(此存储槽已 重置)
        • 如果原始值为 0,则将 19800 gas 添加到退款计数器。
        • 否则,将 4800 gas 添加到退款计数器。

退款计数器的工作方式与以前相同——它限制为 gas 消耗量的一半。在交易级别,退款计数器永远不会低于 零。但是,根据 实现细节,有一些重要的注意事项:

  • 如果实现使用“交易级别”退款计数器(退款 在每个调用帧中进行检查),则退款计数器 继续为无符号。
  • 如果实现使用“执行帧级别”退款计数器 (在每个调用帧中创建一个新的退款计数器,然后合并 返回到父级,当调用帧完成时),则退款 计数器需要更改为有符号——在内部调用中,子退款 可以低于零。

解释

SSTORE 的新 gas 成本方案分为三种不同的 类型:

  • 空操作:虚拟机不需要执行任何操作。这是 如果当前值等于新值,则为这种情况。
  • 新鲜:此存储槽尚未更改,或者已重置 为其原始值。这是当前值不 等于新值,并且原始值等于当前值的情况。
  • :此存储槽已被更改。这是 如果当前值不等于新值,并且原始 值不等于当前值,则为这种情况。

我们可以看到,以上三种类型涵盖了所有可能的 原始值当前值新值的变化。

空操作是一个简单的操作。下面我们只考虑 新鲜的情况。

特定存储槽上的所有初始(非空操作SSTORE都以 新鲜开始。之后,如果 值已更改,则它将变为。当从新鲜时,我们收取与当前方案相同的 gas 成本。可以通过 SSTORE 操作码将 存储槽重置回 新鲜。这将触发 退款。

当存储槽保持在 时,我们收取 200 gas。在这种 情况下,我们还需要跟踪 R_SCLEAR 退款——如果我们 已经发出退款但它不再适用(当前值为 0),然后从退款计数器中删除此退款。如果我们没有 发出退款但它现在适用(新值为 0),则添加此 退款到退款计数器。在上述情况下,不可能未发出退款 但我们删除了退款,因为所有存储槽都以 新鲜 状态 开始。

状态转换

下面是一个图表(作者 @Arachnid) 显示了 gas 成本的可能状态转换。我们忽略 空操作 状态,因为它很简单:

状态转换

下面是上述图表的表格版本。垂直显示了正在设置的新 值,水平显示了原始值当前值的状态。

原始值为 0 时:

  A (current=orig=0) B (current!=orig)
~0 B; 20k gas B; 200 gas
0 A; 200 gas A; 200 gas, 19.8k refund

原始值不为 0 时:

  X (current=orig!=0) Y (current!=orig) Z (current=0)
orig X; 200 gas X; 200 gas, 4.8k refund X; 200 gas, -10.2k refund
~orig, ~0 Y; 5k gas Y; 200 gas Y; 200 gas, -15k refund
0 Z; 5k gas, 15k refund Z; 200 gas, 15k refund Z; 200 gas

理由

该 EIP 主要实现了瞬态存储尝试做的事情 (EIP-1087 和 EIP-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 的动机中提供的示例:

  • 如果存储为空的合约将槽 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。

向后兼容性

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

测试用例

下面我们提供 17 个测试用例。其中 15 个覆盖了连续的两个 SSTORE 操作,基于 由 @chfast 的工作。两个附加的 具有三个 SSTORE 操作的案例用于测试当 插槽被重置然后再次设置的情况。

代码 Used Gas Refund Original 1st 2nd 3rd
0x60006000556000600055 412 0 0 0 0  
0x60006000556001600055 20212 0 0 1 2  
0x60016000556000600055 20212 19800 0 1 0  
0x60016000556002600055 20212 0 0 1 2  
0x60016000556001600055 20212 0 0 1 1  
0x60006000556000600055 5212 15000 1 0 0  
0x60006000556001600055 5212 4800 1 0 1  
0x60006000556002600055 5212 0 1 0 2  
0x60026000556000600055 5212 15000 1 2 0  
0x60026000556003600055 5212 0 1 2 3  
0x60026000556001600055 5212 4800 1 2 1  
0x60026000556002600055 5212 0 1 2 2  
0x60016000556000600055 5212 15000 1 1 0  
0x60016000556002600055 5212 0 1 1 2  
0x60016000556001600055 412 0 1 1 1  
0x600160005560006000556001600055 40218 19800 0 1 0 1
0x600060005560016000556000600055 10218 19800 1 0 1 0

附录:证明

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

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

原始值为零

原始值为 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-1283: 用于无脏映射的 SSTORE 的净 Gas 计量," Ethereum Improvement Proposals, no. 1283, August 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1283.