柏林奥运会后的合同天然气费用

硬叉将于4月15日激活,硬叉中包含的两个EIP(EIP-2929和EIP-2930)将影响交易的天然气成本。本文将解释一些操作码的气体消耗是如何在柏林激活之前计算的,EIP-292…

硬叉将于4月15日激活,硬叉中包含的两个EIP(EIP-2929和EIP-2930)将影响交易的天然气成本。本文将解释一些操作码的气体消耗是如何在柏林激活之前计算的,EIP-2929是如何影响的,以及2930引入的访问列表功能应该如何使用。

抽象的

这篇文章很长。如果只想知道结论,可以在看完这部分后关闭网页:

柏林硬叉改变了一些操作码的Gas开销。如果您在自己的应用程序中对某些操作可用的气体数量进行硬编码,这些操作可能会被卡住。如果出现这种情况,并且您的智能合同无法升级,用户需要使用“访问列表”功能来使用您的应用程序。访问库存功能可以略微降低燃气开销,但有时也会增加总燃气消耗量。Geth客户端引入了一种新的RPC方法eth_createAccessList,简化了访问列表的生成。“柏林”升级前的天然气支出

EVM执行的每个操作码都有相应的气体消耗量。大多数操作码的消耗是固定的:PUSH1总是消耗3个气,MUL消耗5个气,以此类推。一些操作码的消耗是可变的:例如,SHA3操作码的开销由输入值的长度决定。

我们先来了解一下SLOAD和SSTORE操作码,因为这两个操作码受柏林影响最大。后面我们会谈到以地址为目标的操作,比如所有的EXT*类操作码和CALL*类操作码,因为它们的Gas开销也发生了变化。

“柏林”之前的SLOAD

在EIP-2929实施之前,SLOAD开销的计算方法很简单:总是消耗800个气。所以,没什么好扩展的。

“柏林”之前的上海

说到燃气消耗量的计算,SSTORE操作码可能是最复杂的。因为消耗取决于存储项目插槽的当前值、要写入的新值以及存储项目是否已被修改。我们只分析几个场景,大致了解一下。如果你想知道更多,请阅读本文末尾的EIP链接。

如果存储项目的值从0更改为1(或任何非零值),则燃气消耗量为20000;如果存储项目的值从1更改为2(或任何非零值),则燃气消耗量为5000;如果存储项目的值从1更改为1(或任何非零值),则燃气消耗量为5000,但在交易执行后,您将获得燃气补贴。我们这里也不讨论气体返回机制,因为它不会受到柏林的影响。在一次交易中,如果存储项目不是第一次修改,那么后续每次800燃气的SSTORE消耗的细节在这里并不重要。重要的是SSTORE贵,消耗多少气取决于很多因素。

EIP-2929之后的天然气消耗量

EIP-2929改变了所有这些价值观。但在展开之前,我们先来谈谈EIP引入的一个重要概念:被访问的存储项的访问地址和存储键。

当在事务中使用存储项目的地址或密钥时,该地址(或密钥)将被视为在事务执行的剩余时间内被“访问”。例如,如果您在交易中调用另一个合同,该合同的地址将被标记为“已访问”。同样,如果您通过一些存储槽来执行SLOAD或SSTORE,这些存储槽将被视为在事务执行的剩余时间内被访问。使用哪个操作码并不重要,即使你只SLOAD一个槽,下次使用SSTORE的时候,这个槽也会被认为是访问过的。

注意:存储项目的密钥被“嵌入”在一些地址中,正如EIP所解释的:

当执行一个事务时,维护一个集合:access _ addresses : set[address]和access _ storage _ keys : set[tuple[address,bytes32]]

也就是说,当我们说一个存储槽已经被访问时,我们实际上指的是:(address,storageKey)已经被访问。

理解了这个概念之后,再来说一个新的燃气消耗量计算模型。

“柏林”后的SLOAD

SLOAD的油耗在升级前固定在800。但是,升级后,气体消耗量取决于存储插槽是否已被访问。没去过的话,消费2100气;参观的是百气。因此,如果一个存储项目槽已经在“被访问的存储项目键”集合中,则可以节省2000个气体。

“柏林”之后的上海

让我们一个一个来比较,EIP-2929实施后,上述例子会发生什么变化:

如果存储项目的值从0更改为1(或任何非零值),气体消耗量为20000。如果存储项目密钥未被访问,它将消耗22100气体。如果存储项目值从1更改为2(或任何非零值),则气体消耗量为5000。消耗5000气如果已经接入,消耗2900气。如果存储项目的值从1(或任何非零值)变为0,消耗将保持不变,气体返回机制将保持不变

在一笔事务中,如果存储项已不是第一次修改,则后续每一次 SSTORE 都消耗 100 gas由此可见,如果某个槽此前已访问过,则对它的第一次 SSTORE 操作会节约 2100 gas(相比于从未访问过)。

汇总一下

上面的文字实在啰嗦,我们就直接做一张表,把上面提到的值都汇总一下:

注意看最后一行:此时已不再需要区分它到底有没有被访问过,因为,如果此前已写入,则必定已被访问过。

EIP-2930:可选 “访问清单” 的事务类型

另一个 “柏林” 升级包含的 EIP 是 2930。该 EIP 加入了一种新的类型的事务,可以在事务的负载中包含一个 “访问清单”,意思是,你可以在事务执行前就声明哪些地址和存储槽应被认为是 “访问过的”。举个例子,对一个未访问过的槽执行 SLOAD 需要耗费 2100 gas,但如果该存储槽被包含在了事务的 “访问清单” 中,则操作的消耗量机会降为 100 gas。

但如果只要地址和槽被当成 “已访问过的” 就可以降低操作的 Gas 消耗量;而访问清单可以把地址和槽标记为 “已访问过的”;那岂不是说我们可以把这些东西都放在访问清单中,来获得 Gas 消耗量的减免?真棒,天赐 Gas!

额,并不完全如此,因为你每添加一个地址或存储项键,都要支付额外的 Gas。

举个例子。假如我们要向合约 A 发送了一条事务。我们编写了一条这样的访问清单:

这是不是说,每次使用访问清单我们都能节省 gas 呢?很遗憾,也不是,因为在访问清单中填入地址也需要支付 gas。(也就是我们示例中的 "<address of A>")

访问过的地址

迄今为止,我们只讨论了 SLOAD 和 SSTORE 操作码,但 “柏林” 升级还改变了别的操作码。举个例子,CALL 操作码原来的 Gas 消耗量为固定的 700,但 2929 实施后,如果所调用的地址不在访问清单中,消耗量将提高到 2600;如果在,则降低为 100。而且,就像访问过的存储键一样,到底哪个操作码访问过那个地址并不重要(比如,如果用户最先调用的是 EXTCODESIZE,这一个操作的消耗量是 2600,但后续的调用,只要是对同一个地址的,无论是 EXTCODESIZE、CALL 还是 STATICCALL ,都只消耗 100 gas。

那个这个设计对带有访问清单的事务有何影响?假设我们向合约 A 发送一条交易,而合约 A 调用了合约 B,而我们在访问清单中写入这样的内容:

我们首先需要为在这条事务的访问清单中加入这个地址支付 2400 gas,但对 B 使用的第一个操作码就只需要消耗 100 gas 而不是 2600 gas,这就剩下了 100 gas。如果 B 也需要使用其存储项,我们又知道它将使用哪个键,我们也可以把这些键包含在访问列表中,然后为每个键的操作省下 100 或 200 gas(取决于第一个操作码是 SLOAD 还是 SSTORE)。

但为啥我们要加多一个合约来举例子?我们不是可以这样写吗?

你当然可以这样做,但不值得,因为 EIP-2929 指明了你一开始调用的合约(也即是 tx.to 的目的地)必定会被包含在 accessed_addresses 列表中,所以你就是额外花了 2400 gas,什么好处都没得到。

所以,回头看我们上面举的例子:

这样做其实是浪费,除非你在里面加多几个存储项键。如果我们假设所有的存储项键的第一个操作都是 SLOAD,那你要至少 24 个键,才能赚回来。

而且,如你所见,自己一五一十地分析这些因素、手动生成访问清单,显然是极其繁琐而令人崩溃的事。好在,还有更好的办法。

eth_createAccessList RPC 方法

Geth 客户端(从 1.10.2)开始将包含一个新的 eth_createAccessList RPC 方法,你可以用它来生成访问清单,就像使用 eth_estimateGas 一样,只不过返回的不是 Gas 消耗量估计,而是形如这样的数据:

我估计随着时间推移,我们会越来越知道怎么利用这个功能,但我个人估计,方法的伪代码形式会像这样

防止合约变砖

值得提醒,访问清单功能的主要目的不是节省 Gas。如该 EIP 自身所述:

缓解由 EIP-2929 带来的合约变砖风险,因为事务可以预先指定、预先支付自身尝试范文的账户和存储槽,因此,在实际的执行中,SLOAD 和 EXT* 操作码都只会消耗 100 gas:这个值低到既足以防止 2929 打破某些合约,也可以 “解封” 被 EIP-1884 封印的合约。

原本,只要一个合约预设了执行的 Gas 开销,操作码的 Gas 消耗量变动就有可能导致它变砖。比如,如果一个合约预设另一个合约的 someFunction 只会用到 34500 gas,因此总是用 someOtherContract.someFunction{gas: 34500}() 调用那个合约,这个合约就有可能变砖。但只要你在事务中添加合适的访问清单,这个合约就还能工作。

自己验证

如果你想自己测试一下,克隆这个仓库,这里面有很多例子,可以使用 Hardhat 和 Geth 客户端来运行。请仔细阅读 README。

为您推荐

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

联系我们

联系我们

        暂没开通电话      

在线咨询: QQ交谈

       

邮箱: 暂没开通邮箱

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注我们
返回顶部