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 添加到退款计数器。
- 如果原始值不为 0
- 如果原始值等于当前值(此存储槽未被当前执行上下文更改)
退款计数器的工作方式与以前相同——它限制为 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.