EIP-2200: 用于净 Gas 计量的结构化定义
Authors | Wei Tang (@sorpaas) |
---|---|
Created | 2019-07-18 |
简述
这是一个实现净 gas 计量的 EIP。它是 EIP-1283 和 EIP-1706 的组合版本,具有结构化的定义,以便与其他 gas 更改(如 EIP-1884)互操作。
摘要
此 EIP 为 SSTORE
操作码提供了净 gas 计量更改的结构化定义,从而为合约存储启用了新用法,并减少了与大多数实现方式不符的过度 gas 成本。
动机
此 EIP 提出了一种在 SSTORE
上进行 gas 计量的方法,该方法使用大多数实现更普遍可用的信息,并尽可能减少实现结构的更改。
- 存储槽的原始值。
- 存储槽的当前值。
- 退款计数器。
受益于此 EIP 的 gas 减少方案的用法包括:
- 在同一调用帧内的后续存储写入操作。这包括重入锁、相同合约多重发送等。
- 在子调用帧和父调用帧之间交换存储信息,其中此信息无需在事务之外持久存在。这包括子帧错误代码和消息传递等。
EIP-1283 的原始定义产生了一种新的重入攻击的危险,这种攻击针对现有合约,因为 Solidity 默认情况下会向简单的 transfer 调用授予 2300 gas 的“津贴”。如果 SSTORE
在低 gasleft 状态下不允许,则可以轻松缓解这种危险,而不会破坏向后兼容性和 EIP-1283 的原始意图。
此 EIP 还通过参数替换了 EIP-1283 的原始 gas 值定义,使其更结构化,并且更易于定义将来的更改。
规范
定义变量 SLOAD_GAS
、SSTORE_SET_GAS
、SSTORE_RESET_GAS
和 SSTORE_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 添加到退款计数器。
- 如果 原始值 为 0,则扣除
- 如果 原始值 不等于 当前值(此存储槽已更改),则扣除
SLOAD_GAS
gas。应用以下两个子句。- 如果 原始值 不为 0
- 如果 当前值 为 0(也意味着 新值 不为 0),则从退款计数器中删除
SSTORE_CLEARS_SCHEDULE
gas。 - 如果 新值 为 0(也意味着 当前值 不为 0),则将
SSTORE_CLEARS_SCHEDULE
gas 添加到退款计数器。
- 如果 当前值 为 0(也意味着 新值 不为 0),则从退款计数器中删除
- 如果 原始值 等于 新值(此存储槽已重置)
- 如果 原始值 为 0,则将
SSTORE_SET_GAS - SLOAD_GAS
添加到退款计数器。 - 否则,将
SSTORE_RESET_GAS - SLOAD_GAS
gas 添加到退款计数器。
- 如果 原始值 为 0,则将
- 如果 原始值 不为 0
- 如果 原始值 等于 当前值(此存储槽尚未被当前执行上下文更改)
实现还应注意,使用上述定义,如果实现使用调用帧退款计数器,则计数器可能会变为负数。如果实现使用事务退款计数器,则计数器始终保持为正数。
理由
此 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 的 Motivation 中提供的示例(SLOAD_GAS
为 200
):
- 如果具有空存储的合约将槽 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_GAS
为 200
。但是,请注意,无论 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.