Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

EIP-2929:状态访问操作码的Gas成本增加

EIP-2929:状态访问操作码的 Gas 成本增加

简要总结:

当操作员第一次在交易中使用 SLOADCALLBALANCEEXT*SELFDESTRUCT时,增加了这些操作的 gas 成本。

原文链接:https://eips.ethereum.org/EIPS/eip-2929

摘要

SLOAD(0x54)的 Gas 成本增加到 2100,并将*CALL操作码系列(0xf1、0xf2、0xf3、0xfa)、BALANCE(0x31)和EXT*操作码系列(0x3b、0x3c、0x3f)的 Gas 成本增加到 2600。免除(i)预编译,以及(ii)已在同一事务中访问地址和存储槽,从而降低了 Gas 成本。此外,还改革了 SSTORE计量和SELFDESTRUCT,以确保这些操作码中固有的“实际存储负载”定价正确。

动机

一般来说,操作码的 Gas 成本的主要功能是估计处理该操作码所需的时间,目标是让 Gas 限制与处理块所需的时间限制相对应。然而,存储访问操作码(SLOAD,以及*CALLBALANCEEXT*操作码)历来被低估。在 2016 年上海 Dos 攻击中,一旦修复了最严重的客户端错误,攻击者使用的更持久成功的策略之一就是简单地发送访问或调用大量账户的交易。

为了缓解这一问题,Gas 成本有所增加,但最近的数据表名,Gas 成本的增加还不够。引用: https://arxiv.org/pdf/1909.07220.pdf :

虽然这个问题本身看起来可能是良性的,但 EXTCODESIZE 会强制客户端在磁盘上搜索合约,从而导致 IO 大量事务。在我们的硬件上重播以太坊历史记录时,恶意交易的执行时间大约为 20 到 80 秒,而平均的交易只需几毫秒。

这项提议的 EIP 将这些操作码的成本增加约 3 倍,将最坏情况的处理时间减少到约 7-27 秒。改进数据库布局,重设计客户端以直接读取存储而不是通过 Merkle 树跳跃,将进一步减少这个时间。不过,这些技术可能需要很长时间才能完全推出,即使使用了这些技术,访问存储的 IO 开销仍将非常大。

该 EIP 的第二个好处是,它还执行使以太坊中的无状态见证大小可接受所需的大部分工作。 假设切换到二进制尝试,不包括代码大小的理论最大见证大小(因此是“大部分工作”而不是“全部”)将从(12500000 Gas limit)/(700 Gas per BALANCE)*(800 witness bytes per BALANCE) ~= 14.3M bytes12500000 / 2600 * 800 ~= 3.85M Bytes。 实施代码默克尔化时,代码访问的定价可能会发生变化。

在更远的将来,SNARK/STARK 见证人的情况也会有类似的好处。 Starkware 的最新数据表明,他们能够在消费者桌面上每秒证明 10000 个 Rescue 哈希值; 假设每个 Merkle 分支有 25 个哈希值,以及一个充满状态访问的块,目前这意味着见证人需要 12500000 / 700 * 25 / 10000 ~= 44.64 秒才能生成,但在此 EIP 之后将减少到 12500000 / 2500 * 25 / 10000 ~= 12.5 秒,这意味着一台台式计算机在任何条件下都能够按时生成见证人。 STARK 证明的未来收益可以用于 (i) 使用更昂贵但更强大的哈希函数,或 (ii) 进一步减少证明时间,减少延迟,从而改善依赖此类见证的无状态客户端的用户体验。

规范

参数

Constant 恒定值
FORK_BLOCK 12244000
COLD_SLOAD_COST 2100
COLD_ACCOUNT_ACCESS_COST 2600
WARM_STORAGE_READ_COST 100

对于block.number >= FORK_BLOCK的块,以下更改适用:

执行交易时,维持一组accessed_addresses:Set[Address]accessed_storage_keys:Set[Tuple[Address,Bytes]]

这些集合是交易上下文范围内的,与其他交易范围的结构(例如自销毁列表和全局退款计数器)的实现方式不同。特别是,如果范围回滚,访问列表应该进入该范围之前的状态。

当交易开始执行时:

  • accessed_storage_key是初始化为空的,并且
  • accessed_addresses被初始化包括
    • tx.sendertx.to(如果是部署合约,则为合约要部署到的地址)
    • 以及所有预编译的集合

存储读取变化

当地址是操作码(EXTCODESIZE(0x3B)、EXTCODECOPY(0x31)、EXTCODEHASH(0x3F)和BALANCE(0x31))的目标或者是操作码(CALL(0xF1)、CALLCODE(0xF2)、DELEGATECALL(0xF4)、STATICCALL(0xFA))的目标时,gas 成本计算如下:

  • 如果目标不在accessed_addresses中,则收取COLD_ACCOUNT_ACCESS_COST gas,并将该地址添加到accessed_addressed
  • 否则,消耗WARM_STORAGE_READ_COST gas

在所有情况下,都会收取 Gas 成本,并在调用操作码时更新映射。当调用 CREATE或者CREATE2操作码时,立即(即在检查以确保这个地址是未被使用的地址之前)将正在创建的地址添加到accessed_addresses,但CREATECREATE2的 Gas 成本保持不变。澄清:如果CREATECREATE2操作后续失败,例如在执行 initcode 期间或没有足够的 Gas 来将代码存储在状态中,则合约本身的地址仍保留在 access_addresses 中(但在内部范围内进行的任何添加都将被还原)

对于SLOAD,如果(addres, storage_key)对(其中address是正在读取起存储的合约的地址)尚未在accessed_storage_keys中,则收取COLD_SLOAD_COST gas,并将其添加到addressed_storage_keys中。如果该对已经在addressed_storage_key中,则收取WARM_STORAGE_READ_COST gas。

注意:对于呼叫变体,100/2600 成本立即应用(与此 EIP 之前如何收取 700 完全一样),即:在计算可用于进入呼叫的 63/64 之前。

注 2:目前无法在“冷帐户”上执行“冷加载读/写”,因为为了读/写slot,执行必须已经在account(帐户)内部。 因此,从本 EIP 开始,冷账户上的冷存储读/写行为尚未定义。 任何未来提议添加“远程读/写”的 EIP 都需要定义该更改的定价行为。

SSTORE 的变化

调用 SSTORE 时,检查(address, storage_key)对是否在accessed_storage_keys中,如果不在,则收取额外的COLD_SLOAD_COST gas,并将该对添加到accessed_storage_keys。另外,修改 EIP-2200 中定义的参数如下:

参数 旧值 新值
SLOAD_GAS 800 = WARM_STORAGE_READ_COST
SSTORE_RESET_GAS 5000 5000 - COLD_SLOAD_COST

EIP 2200中定义的其他参数不变。 注意:EIP 2200 中的多个地方使用了常量 SLOAD_GAS,例如 SSTORE_SET_GAS - SLOAD_GAS。 使用复合定义的实现也必须确保更新这些定义。

自毁变化

如果SELFDESTRUCT的ETH接收者不在accessed_addressed中(无论发送的金额是否非0),则在现有的 gas 成本之上收取额外的COLD_ACCOUNT_ACCESS_COST,并将 ETH 接收者添加到集合中。

注意:如果接收者已经处于热状态,SELFDESTRUCT 不会收取 WARM_STORAGE_READ_COST,这与其他调用变体的工作方式不同。 其背后的原因是为了保持较小的更改,SELFDESTRUCT 已经花费了 5K,并且如果调用多次则无操作。

基本原理

操作码成本与每字节见证数据的收费

改变 Gas 成本以反映见证人大小的自然替代途径是按见证人数据的字节收费。 然而,这将需要更长的时间来实施,从而阻碍了提供短期安全救济的目标。 此外,忠实地遵循这一路径将导致涉及合约代码的交易产生极高的 Gas 成本,因为需要对所有 24576 个合约代码字节进行收费; 这对开发商来说将是一个难以接受的沉重负担。 最好等待代码 Merklization 开始尝试正确考虑访问各个代码块的 Gas 成本; 从短期 DoS 防护的角度来看,从磁盘访问 24 kB 并不比从磁盘访问 32 字节昂贵多少,因此不必担心代码大小。

添加 accessed_addresses/accessed_storage_keys集合

添加已访问的帐户和存储槽的集合是为了避免对可缓存的内容进行不必要的收费(并且在所有高性能实现中都已缓存)。 此外,它消除了当前不良的现状,即不必要地无法负担进行自调用或调用预编译的费用,并实现了合约破坏缓解措施,其中包括预取一些存储密钥,允许未来的执行仍然需要预期的 Gas 量。

SSTORE Gas 成本变化

需要对 SSTORE 进行更改,以避免 DoS 攻击的可能性,这种攻击会“戳”随机选择的零存储槽,将其从 0 更改为 0,成本为 800 Gas,但需要实际的存储负载。 SSTORE_RESET_GAS 减少可确保 SSTORE 的总成本(现在需要支付 COLD_SLOAD_COST)保持不变。 另外,请注意,先执行 SLOAD 后执行 SSTORE 的应用程序(例如 storage_variable += x)实际上会变得更便宜!

仅最小程度地更改 SSTORE 会计

SSTORE Gas 成本继续使用 Wei Tang 的原始/当前/新方法,而不是重新设计为使用脏映射,因为 Wei Tang 的方法正确地考虑了更改存储的实际成本,只关心当前值与最终值而不关心 中间值

根据该提案,平均应用程序的 Gas 消耗量将如何增加?

从见证人规模进行粗略分析

我们可以看看 Alexey Akhunov 早期关于平均情况块数据的工作。 总之,平均区块的见证大小约为 1000 kB,其中约 750 kB 是 Merkle 证明而不是代码。 假设每个 Merkle 分支保守为 2000 字节,这意味着每个块约有 375 次访问(SLOAD 具有类似的气体增加与字节的比率,因此无需单独分析它们)。

来自 Etherscan 的每日交易每日区块数据显示每个区块约 160 笔交易(参考日期:7 月 1 日),这意味着这些访问的很大一部分只是 tx.sendertx.to,它们不包括在 Gas 成本增加中, 但由于地址重复,可能少于 320。

因此,这意味着每个区块有约 50-375 次收费访问,每次访问的 Gas 成本增加 1900;50 * 1900 = 95000375 * 1900 = 712500,这意味着 Gas 限制需要提高约 1-6% 来补偿。 然而,这种分析可能会在任一方向上进一步复杂化,因为(i)在多个交易中访问账户/存储密钥,这将在见证人中出现一次,但在天然气成本增加中出现两次,以及(ii)账户/存储密钥被多次访问 同一笔交易中的次数,这导致天然气成本降低。

向后兼容性

这些天然气成本的增加可能会破坏依赖固定天然气成本的合同; 请参阅安全注意事项部分,了解为什么我们预计总风险较低以及如何在需要时进一步降低风险的详细信息和论据。

测试用例:

参考原文章

安全考虑

与任何增加 Gas 成本的 EIP 一样,在三种可能的情况下可能会导致应用程序崩溃:

  1. 修复了合约中子调用的 Gas 限制
  2. 依赖于消耗接近完全 Gas 限制的合约调用的应用程序
  3. ETH 转账调用给被叫方的 2300 基础限制

这些风险之前已在早期天然气成本增加 EIP-1884 的背景下进行过研究。 请参阅 Martin Swende 的早期报告Hubert Ritzdorf 的分析,重点关注 (1) 和 (3)。 (2) 得到的分析较少,但人们可能会认为这种情况不太可能发生,因为应用程序很少在交易中使用接近全部的 Gas 限制,而且最近 Gas 限制从 1000 万提高到 1250 万。 EIP-1884在实践中确实导致了少数合同因此破裂

有两种方法可以看待这些风险。 首先,我们可以注意到,迄今为止,开发人员已经收到了多年的警告; 存储访问操作码的 Gas 成本增加已经被讨论了很长时间,并就此类变化的可能性向主要 dapp 开发人员发表了多种声明。 EIP-1884 本身敲响了重要的警钟。 因此,我们可以说这次的风险将明显低于 EIP-1884。

合约损坏缓解措施

查看风险的第二种方法是探索缓解措施。 首先,accessed_addresses accessed_storage_keys 映射的存在(存在于本EIP 中,不存在于EIP-1884 中)已经使某些情况变得可恢复:在任何情况下,合约A 需要将资金发送到某个地址B,该地址接受 资金来自任何来源,但会留下依赖于存储的日志,可以通过首先向 B 发送单独的调用将其拉入缓存,然后调用 A 来恢复,因为知道 A 触发的 B 的执行每次只会收取 100 Gas SLOAD。 这一事实并不能解决所有情况,但它确实可以显着降低风险。

但有一些方法可以进一步扩展这种模式的可用性。 一种可能性是添加 POKE 预编译,它将地址和存储密钥作为输入,并允许交易尝试通过预戳将访问的所有存储槽来“拯救”卡住的合约。 即使该地址只接受来自合约的交易,这种方法也有效,并且在当前气体限制的许多其他情况下也有效。 唯一不起作用的情况是交易调用必须从 EOA 直接进入特定合约,然后再子调用另一个合约。

另一种选择是 EIP-2930,它与 POKE 具有类似的效果,但更通用:它也适用于 EOA -> 合约 -> 合约案例,并且通常应该适用于所有已知的由于 Gas 成本增加而导致损坏的案例。 此选项更为复杂,尽管它可以说是用于其他用例(再生、帐户抽象、SSA 所有需求访问列表)的访问列表的垫脚石。