2021-04-18 21:03 | 出处: EthFans
“柏林” 硬分叉将在 4 月 15 日激活,该硬分叉所包含 EIP 中的两个(EIP-2929 和 EIP-2930)都会影响事务的 Gas 开销。本文会解释 “柏林” 激活之前,一些操作码的 Gas 消耗量是如何计算的,而 EIP-2929 对此有何影响,以及,2930 引入的访问清单(Access List)功能应如何使用。
这篇文章很长,你要是只想知道结论,看完这部分就可以把网页关掉了:
eth_createAccessList 来简化访问清单的生成。PUSH1 总是消耗 3 gas,而 MUL 消耗 5 gas,等等。有一些操作码的消耗量是可变的:举个例子,SHA3 操作码的开销由输入值的长度决定。SLOAD 和 SSTORE 操作码,因为这两个操作码受 “柏林” 影响最大。后面我们会再谈谈那些以地址为目标的操作,比如所有的 EXT* 类操作码和 CALL* 类操作码,因为它们的 Gas 开销也被改变了。SLOADSLOAD 开销的计算方式很简单:总是消耗 800 gas。所以,也没啥可展开的。SSTORESSTORE 操作码可能是最复杂的了。因为消耗多少取决于该存储项槽当前的值、要写入的新值、该存储项是否已经修改过。我们只会分析少数几种场景,了解个大概。如果你想了解更多,请阅读本文末尾所附的 EIP 链接。SSTORE 都消耗 800 gasSSTORE 是昂贵的,具体消耗多少 gas 则依赖于多个因素。CALL (调用)另一个合约,那么该合约的地址就会被标记为 “访问过的”。类似地,如果你 SLOAD 或者 SSTORE 过一些存储项槽 ,在该笔事务余下的执行过程里,这些槽也会被当成已经访问过的。到底用的哪个操作码是没有关系的,即使你只 SLOAD 过某个槽,接下来使用 SSTORE 时该槽也会被当成已访问过的。执行事务时,保持一个集合: accessed_addresses: Set[Address]以及accessed_storage_keys: Set[Tuple[Address, Bytes32]]
(address, storageKey) 已被访问过了。SLOADSLOAD 的 Gas 消耗量是固定的 800。但升级后,Gas 消耗量要看这个存储槽是否已经被访问过。还没访问过的,消耗量就是 2100 gas;访问过的,就是 100 gas。所以,如果某个存储项槽已经在 “已访问过的存储项键` 的集合里了,就可以省掉 2000 gas。SSTORESSTORE 都消耗 100 gasSSTORE 操作会节约 2100 gas(相比于从未访问过)。| 操作码 | “柏林” 前 | “柏林” 后 | |
|---|---|---|---|
| 未访问过 | 访问过 | ||
| SLOAD | 800 | 2100 | 100 |
| SSTORE from 0 to 1 | 20000 | 22100 | 20000 |
| SSTORE from 1 to 2 | 5000 | 5000 | 2900 |
| SLOAD + SSTORE* | 5800 | 5000 | 3000 |
| SSTORE* + SLOAD | 5800 | 5100 | 3000 |
| SSTORE 一个已经被写过的槽 | 800 | 100 | 100 |
| *从一个非零值改为另一个非零值,就像第三行所示的那样 |
SLOAD 需要耗费 2100 gas,但如果该存储槽被包含在了事务的 “访问清单” 中,则操作的消耗量机会降为 100 gas。A 发送了一条事务。我们编写了一条这样的访问清单:accessList: [{address: "<address of A>",storageKeys: ["0 x0000000000000000000000000000000000000000000000000000000000000000"]}]如果我们发送了一条带有这条访问清单的事务,而使用0 x0存储槽的第一个操作码就是SLOAD,则 Gas 消耗量会是 100 而非 2100,也就是减免了 2000 gas。但是,在访问列表中声明一个存储项键需要额外支付 1900 gas,所以我们只节约了 100 gas。(如果对该存储槽的第一个操作是SSTROE,我们在单个操作中就省下了 2100 gas,也就是总共省下了 200 gas,因为访问清单本身需要消耗 gas)。
"<address of A>")SLOAD 和 SSTORE 操作码,但 “柏林” 升级还改变了别的操作码。举个例子,CALL 操作码原来的 Gas 消耗量为固定的 700,但 2929 实施后,如果所调用的地址不在访问清单中,消耗量将提高到 2600;如果在,则降低为 100。而且,就像访问过的存储键一样,到底哪个操作码访问过那个地址并不重要(比如,如果用户最先调用的是 EXTCODESIZE,这一个操作的消耗量是 2600,但后续的调用,只要是对同一个地址的,无论是 EXTCODESIZE、CALL 还是 STATICCALL ,都只消耗 100 gas。accessList: [{ address: "<address of B>", storageKeys: [] }]我们首先需要为在这条事务的访问清单中加入这个地址支付 2400 gas,但对 B 使用的第一个操作码就只需要消耗 100 gas 而不是 2600 gas,这就剩下了 100 gas。如果 B 也需要使用其存储项,我们又知道它将使用哪个键,我们也可以把这些键包含在访问列表中,然后为每个键的操作省下 100 或 200 gas(取决于第一个操作码是SLOAD还是SSTORE)。
accessList: [{address: "<address of A>", storageKeys: []},{address: "<address of B>", storageKeys: []},]你当然可以这样做,但不值得,因为 EIP-2929 指明了你一开始调用的合约(也即是tx.to的目的地)必定会被包含在accessed_addresses列表中,所以你就是额外花了 2400 gas,什么好处都没得到。
accessList: [{address: "<address of A>",storageKeys: ["0 x0000000000000000000000000000000000000000000000000000000000000000"]}]这样做其实是浪费,除非你在里面加多几个存储项键。如果我们假设所有的存储项键的第一个操作都是SLOAD,那你要至少 24 个键,才能赚回来。
eth_createAccessList RPC 方法eth_createAccessList RPC 方法,你可以用它来生成访问清单,就像使用 eth_estimateGas 一样,只不过返回的不是 Gas 消耗量估计,而是形如这样的数据:{"accessList": [{"address": "0 xb0ee076d7779a6ce152283f009f4c32b5f88756c","storageKeys": ["0 x0000000000000000000000000000000000000000000000000000000000000000","0 x0000000000000000000000000000000000000000000000000000000000000001"]}],"gasUsed": "0 x8496"}也就是告诉你一笔事务将会用到的地址和存储项键的清单,以及,假定纳入这份访问清单 将耗用多少 gas。跟eth_estimateGas一样,这也是估计出来的,该笔事务真正上链时,会访问到哪些数据仍有可能改变。但是,再说一遍,这绝不意味着你只要使用了访问清单,所用的 Gas 就会比不用清单更少!
let gasEstimation = estimateGas(tx)let { accessList, gasUsed } = createAccessList(tx)if (gasUsed > gasEstimation) {accessList[tx.to]}tx.accessList = accessList;sendTransaction(tx)
缓解由 EIP-2929 带来的合约变砖风险,因为事务可以预先指定、预先支付自身尝试范文的账户和存储槽,因此,在实际的执行中,SLOAD 和 EXT* 操作码都只会消耗 100 gas:这个值低到既足以防止 2929 打破某些合约,也可以 “解封” 被 EIP-1884 封印的合约。