移除 SELFDESTRUCT 的 gas 返还,减少 SSTORE 的 gas 返还到一个较低水平,使得返还的量仍然相当大,但不会像现在高到可以被利用的地步。
移除 SELFDESTRUCT 的 gas 返还,减少 SSTORE 的 gas 返还到一个较低水平,使得返还的量仍然相当大,但不会像现在高到可以被利用的地步。
最初引入 SSTORE 和 SELFDESTRUCT 的 gas 返还是为了鼓励应用开发者写应用时能践行“良好的状态卫生”,清理不再需要的存储槽与合约。但是,结果证明这项技术带来的效率远低于预期,gas 返还还带来多项未预料到的有害后果:
常量 | 值 |
---|---|
FORK_BLOCK |
待决定 |
NEW_MAX_REFUND_QUOTIENT |
5 |
对于存在block.number >= FORK_BLOCK
的区块,需要作下列变更。
SELFDESTRUCT
的返还SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST
(EIP-2929 + EIP-2930 合力下的 4,800 gas) 取代 SSTORE_CLEARS_SCHEDULE
( EIP-2200 定义的)gas_used // NEW_MAX_REFUND_QUOTIENT
EIP-2200 引入了返还的三种情况:
SSTORE_CLEARS_SCHEDULE
(当前是15,000)的 gas 量到返还计算器SSTORE_SET_GAS - SLOAD_GAS
(当前是 19,900) 的 gas 量到返还计算器SSTORE_RESET_GAS - SLOAD_GAS
(当前是 4,900)的 gas 到返还计算器在这三种情况里,只有 (1) 会启动 gastoken 且允许区块在执行上消耗超过区块 gas limit 的 gas。(2) 不具有这个特点,因为要获得 19,900 的 gas 返还,同一个存储槽必须在之前从零改为非零,这需要消耗 20,000 gas。无法从一个存储槽获得 gas 并用它来编辑另一个存储槽,意味着它不能被用作 gas token。另外,获得返还需要恢复存储的写入和扩展,使得返还的 gas 不会增加客户端处理区块的工作量。(3) 是相似的:只有当同一个存储槽在之前已经消耗了 5,000 gas 的时候才能获得 4,900 gas 返还。
此 EIP 处理第一种情况。我们可以通过使用一个相似的“配对” 变元来确定在何种条件下 gastoken 是不可用的 (例如,你不可以在一个存储槽里获得比你的输入更多的 gas) ,将每一笔返还映射到同一笔交易的同一个存储槽的前一笔支出。当一个存储槽的原始值是非零值,如果它被改为 0 时,有两个可能性:
SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST
第一次读取和编辑存储槽的最小开销进行配对。SSTORE_CLEARS_SCHEDULE
的 gas 从返还中被移除的变元进行配对。对于第二次或之后的情况,SSTORE_CLEARS_SCHEDULE
的值是什么并不重要,因为那个 gas 大小的返还是与相同大小的清除返还相匹配的。这就只剩下第一种情况了。为了确保存储槽上消耗的 gas 总量为正,我们需要 SSTORE_CLEARS_SCHEDULE <= SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST
。因此,此 EIP 只把 SSTORE_CLEARS_SCHEDULE
减少到那两项开销的总和。
此 EIP 的另一个原因是,清除还未被读取的数据 (通常是“无用”数据) 是不会有净返还的,但清除被读取过的数据 (通常是“有用”数据) 还继续会有净返还。
返还当前仅在交易执行后应用,因此无法对执行中任何特定可用的调用框架造成影响。因此,清除它们将不会破坏任何代码的执行,尽管它将使得一些应用变得经济上不可行。
Gas token 会变得没有价值。DeFi 套利机器人今天经常不是使用已有的 gas token 方案就是一个定制的,以减少链上的开销,这得益于重写它们的代码以清除对那些不再有用的 gas 存储机制的调用。
然而,完全保留在new = original = 0 != current
里的返还,以及保留在其他 nonzero -> zero
情况里的一些返还能确保一些接收 (和值得) 更好的 gas 开销待遇的关键用例能持续获益。例如,zero -> nonzero -> zero
的存储设置模式保持只需消耗大约 100 gas。这些模式包括两个重要实例:
对之前关于移除返还的 EIP (EIP-3298 和 EIP-3403) 的批评是这些 EIP 完全消除了把一个值设为零的激励,相当于鼓励用户不要完全清除一个存储槽 (即使他们想这么做),哪怕他们想再次使用该存储槽的几率是最小的。
举一个例子,如果你有一个单位的 ERC20 代币,且你要送出或卖出你的所有余额,你可以只给出 0.999999 个单位,把剩余的留下。如果你想在未来重新放入更多该种代币到同一个账户,你仅需要为 SSTORE 支付 5,000 gas (2,100用于读取 + 2,900用于非零变为非零的设置) 而不是22,100 (20,000 用于零到非零值的设置)。今天,这部分的 gas 会被清除存储获得的 15,000 gas 返还所抵消,因此,如果你有超过 15000 / 17100 = 87.7%
的把握会再使用这个存储槽,你才会有动力这样做;按照 EIP 3298 或 EIP 3403 的设定,抵消激励这部分是不存在的,因此,如果你再次使用该存储槽的可能性是大于 0 的,设为非零值会更好。
对于剩下的 4,800 gas 返还,如果你觉得再次使用某个存储槽的几率大于 4800 / 17100 = 28.1%
,你才有保持该存储槽为非零的动力。这并不是完美的,但它可能高于一般人在清除了他们的全部余额后在同一个地址重新获得同一代币的几率。
gas 返还的上限是所消耗 gas 量的1/5,这意味着这种返还仅够用于增加处理一个区块所需的存储写入操作量最多为25%,限制了利用这个机制进行以存储写入为重点的拒绝服务攻击。
注意,“热”和“冷”存储槽之间是有区别的。这个表展示了 EIP-2929 下的值,假定所有变动过的存储槽都已经是“热”状态 (区别是一次性消耗 2,100 gas)。
代码 | 消耗了的 gas | 返还的gas | 原始的 | 第一次 | 第二次 | 第三次 | (返还后的)有效 gas |
---|---|---|---|---|---|---|---|
0x60006000556000600055 |
212 | 0 | 0 | 0 | 0 | 212 | |
0x60006000556001600055 |
20112 | 0 | 0 | 0 | 1 | 20112 | |
0x60016000556000600055 |
20112 | 19900 | 0 | 1 | 0 | 212 | |
0x60016000556002600055 |
20112 | 0 | 0 | 1 | 2 | 20112 | |
0x60016000556001600055 |
20112 | 0 | 0 | 1 | 1 | 20112 | |
0x60006000556000600055 |
3012 | 15000 | 1 | 0 | 0 | -11988 | |
0x60006000556001600055 |
3012 | 2800 | 1 | 0 | 1 | 212 | |
0x60006000556002600055 |
3012 | 0 | 1 | 0 | 2 | 3012 | |
0x60026000556000600055 |
3012 | 15000 | 1 | 2 | 0 | -11988 | |
0x60026000556003600055 |
3012 | 0 | 1 | 2 | 3 | 3012 | |
0x60026000556001600055 |
3012 | 2800 | 1 | 2 | 1 | 212 | |
0x60026000556002600055 |
3012 | 0 | 1 | 2 | 2 | 3012 | |
0x60016000556000600055 |
3012 | 15000 | 1 | 1 | 0 | -11988 | |
0x60016000556002600055 |
3012 | 0 | 1 | 1 | 2 | 3012 | |
0x60016000556001600055 |
212 | 0 | 1 | 1 | 1 | 212 | |
0x600160005560006000556001600055 |
40118 | 19900 | 0 | 1 | 0 | 1 | 20218 |
0x600060005560016000556000600055 |
5918 | 17800 | 1 | 0 | 1 | 0 | -11882 |
如果通过把 SSTORE_CLEARS_SCHEDULE 从 15,000 变为 4,800 (以及去除 selfdestruct 的返还) ,减少了部分的返还,下面是是一个对比表。
代码 | 消耗了的 gas | 返还的gas | 原始的 | 第一次 | 第二次 | 第三次 | (返还后的)有效gas |
---|---|---|---|---|---|---|---|
0x60006000556000600055 |
212 | 0 | 0 | 0 | 0 | 212 | |
0x60006000556001600055 |
20112 | 0 | 0 | 0 | 1 | 20112 | |
0x60016000556000600055 |
20112 | 19900 | 0 | 1 | 0 | 212 | |
0x60016000556002600055 |
20112 | 0 | 0 | 1 | 2 | 20112 | |
0x60016000556001600055 |
20112 | 0 | 0 | 1 | 1 | 20112 | |
0x60006000556000600055 |
3012 | 4800 | 1 | 0 | 0 | -1788 | |
0x60006000556001600055 |
3012 | 2800 | 1 | 0 | 1 | 212 | |
0x60006000556002600055 |
3012 | 0 | 1 | 0 | 2 | 3012 | |
0x60026000556000600055 |
3012 | 4800 | 1 | 2 | 0 | -1788 | |
0x60026000556003600055 |
3012 | 0 | 1 | 2 | 3 | 3012 | |
0x60026000556001600055 |
3012 | 2800 | 1 | 2 | 1 | 212 | |
0x60026000556002600055 |
3012 | 0 | 1 | 2 | 2 | 3012 | |
0x60016000556000600055 |
3012 | 4800 | 1 | 1 | 0 | -1788 | |
0x60016000556002600055 |
3012 | 0 | 1 | 1 | 2 | 3012 | |
0x60016000556001600055 |
212 | 0 | 1 | 1 | 1 | 212 | |
0x600160005560006000556001600055 |
40118 | 19900 | 0 | 1 | 0 | 1 | 20218 |
0x600060005560016000556000600055 |
5918 | 7600 | 1 | 0 | 1 | 0 | -1682 |
返还对于事务执行时不可见的,因此这不会对事务执行逻辑产生任何影响。
如果我们不计算后来重置回零的零到非零的 SSTORE,在一个区块里执行的最大 gas 消耗量受到 gas limit 的限制。不计算这些事可以的,因为如果这样的 SSTORE 被重置了,存储不会被扩展,客户端实际上不需要调整默克尔树;gas 消耗是可以返还的,但客户端对这些操作码的处理通常也会被取消。如果 new_value = original_value
,客户端应该保证不会进行存储写入;这是自以太坊创世以来的一次谨慎优化,但它现在变得更重要了。
ECN的翻译工作旨在为中国以太坊社区传递优质资讯和学习资源,文章版权归原作者所有,转载须注明原文出处以及ethereum.cn,若需长期转载,请联系eth@ecn.co进行授权。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!