Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-4762: 无状态性 gas 成本变更

更改 gas 计划,以反映创建见证的成本,要求客户端更新其数据库布局以匹配。

Authors Guillaume Ballet (@gballet), Vitalik Buterin (@vbuterin), Dankrad Feist (@dankrad), Ignacio Hagopian (@jsign), Tanishq Jasoria (@tanishqjasoria), Gajinder Singh (@g11tech)
Created 2022-02-03
Discussion Link https://ethereum-magicians.org/t/eip-4762-statelessness-gas-cost-changes/8714

摘要

这个 EIP 引入了 gas 计划的变更,以反映创建见证的成本。它要求客户端更新其数据库布局以匹配,从而避免潜在的 DoS 攻击。

动机

将 Verkle 树引入以太坊需要根本性的改变,作为准备,本 EIP 针对的是紧挨着 verkle 树分叉之前的分叉,目的是激励 Dapp 开发者采用新的存储模型,并有充足的时间进行调整。它还激励客户端开发者在 verkle 分叉之前迁移他们的数据库格式。

规范

辅助函数

def get_storage_slot_tree_keys(storage_key: int) -> [int, int]:
    if storage_key < (CODE_OFFSET - HEADER_STORAGE_OFFSET):
        pos = HEADER_STORAGE_OFFSET + storage_key
    else:
        pos = MAIN_STORAGE_OFFSET + storage_key
    return (
        pos // 256,
        pos % 256
    )

访问事件

每当读取状态时,都会发生一个或多个 (address, sub_key, leaf_key) 形式的访问事件,确定正在访问的数据。我们将访问事件定义如下:

账户头部的访问事件

当:

  1. 一个非预编译合约,且不是系统合约,是 *CALLCALLCODESELFDESTRUCTEXTCODESIZEEXTCODECOPY 操作码的目标,
  2. 一个非预编译合约,且不是系统合约,是一个合约创建的目标地址,该地址的 initcode 开始执行,
  3. 任何地址是 BALANCE 操作码的目标
  4. 一个 已部署的 合约调用 CODECOPY

处理这个访问事件:

(address, 0, BASIC_DATA_LEAF_KEY)

注意:一个没有值的 SELFDESTRUCT*CALLCALLCODE,以预编译合约或系统合约为目标,不会导致 BASIC_DATA_LEAF_KEY 被添加到见证中。

如果一个 *CALLCALLCODESELFDESTRUCT 是有值的(即,它传输非零的 wei),无论 callee 是否为预编译合约或系统合约,处理这个额外的访问事件:

(caller, 0, BASIC_DATA_LEAF_KEY)

注意:当检查 callee 的存在性时,存在性检查是通过验证在相应的 stem 处存在一个扩展和后缀树来完成的,并且不依赖于 CODEHASH_LEAF_KEY

当调用 EXTCODEHASH 时,处理这个访问事件:

(address, 0, CODEHASH_LEAF_KEY)

注意,预编译合约和系统合约被排除在外,因为它们的哈希值对于客户端是已知的。

当创建一个合约时,处理这些访问事件:

(contract_address, 0, BASIC_DATA_LEAF_KEY)
(contract_address, 0, CODEHASH_LEAF_KEY)

存储的访问事件

带有给定地址和键的 SLOADSSTORE 操作码处理一个 (address, tree_key, sub_key) 形式的访问事件

(address, tree_key, sub_key)

其中 tree_keysub_key 被计算为 tree_key, sub_key = get_storage_slot_tree_keys(address, key)

代码的访问事件

在以下条件下,“访问代码块 chunk_id” 被理解为 (address, (chunk_id + 128) // 256, (chunk_id + 128) % 256) 形式的访问事件

  • 在 EVM 执行的每一步,当且仅当 PC < len(code) 时(其中 PC 是当前程序计数器),访问 callee 的代码块 PC // CHUNK_SIZE。特别要注意以下极端情况:
    • JUMP(或肯定评估的 JUMPI)的目标被认为是已访问的,即使该目标不是 jumpdest 或位于 pushdata 内部
    • 如果 jump 条件为 false,则 JUMPI 的目标不被认为是已访问的。
    • 如果执行到达 jump 操作码,但没有足够的 gas 来支付执行 JUMP 操作码的 gas 成本(包括如果 JUMP 是尚未访问的代码块中的第一个操作码,则包括代码块访问成本),则 jump 的目标不被认为是已访问的
    • 如果 jump 的目标超出了代码范围 (destination >= len(code)),则不认为该目标被访问
    • 如果代码通过越过代码末尾来停止执行,则不认为 PC = len(code) 被访问
  • 如果 EVM 执行的当前步骤是 PUSH{n},则访问 callee 的所有代码块 (PC // CHUNK_SIZE) <= chunk_index <= ((PC + n) // CHUNK_SIZE)
  • 如果非零读取大小的 CODECOPYEXTCODECOPY 读取了字节 x...y(包括),则访问已访问合约的所有代码块 (x // CHUNK_SIZE) <= chunk_index <= (min(y, code_size - 1) // CHUNK_SIZE)
    • 示例 1:对于 CODECOPY,起始位置为 100,读取大小为 50,code_size = 200,则 x = 100y = 149
    • 示例 2:对于 CODECOPY,起始位置为 600,读取大小为 0,则不访问任何代码块
    • 示例 3:对于 CODECOPY,起始位置为 1500,读取大小为 2000,code_size = 3100,则 x = 1500y = 3099
  • CODESIZEEXTCODESIZEEXTCODEHASH 不访问任何代码块。 当创建一个合约时,访问代码块 0 ... (len(code)+30)//31

写入事件

我们将 写入事件 定义如下。请注意,当发生写入时,也会发生访问事件(因此下面的定义应该是访问事件定义的子集)。写入事件的形式为 (address, sub_key, leaf_key),确定要写入的数据。

账户头部的写入事件

当发生具有给定发送者和接收者的非零余额发送 CALLCALLCODESELFDESTRUCT 时,处理这些写入事件:

(caller, 0, BASIC_DATA_LEAF_KEY)
(callee, 0, BASIC_DATA_LEAF_KEY)

如果 callee_address 处不存在账户,也处理:

(callee, 0, CODEHASH_LEAF_KEY)

当初始化合约创建时,处理这些写入事件:

(contract_address, 0, BASIC_DATA_LEAF_KEY)
(contract_address, 0, CODEHASH_LEAF_KEY)

存储的写入事件

带有给定 addresskeySSTORE 操作码处理一个 (address, tree_key, sub_key) 形式的写入事件

(address, tree_key, sub_key)

其中 tree_keysub_key 被计算为 tree_key, sub_key = get_storage_slot_tree_keys(address, key)

代码的写入事件

当创建一个合约时,处理这些写入事件:

(
    address,
    (CODE_OFFSET + i) // VERKLE_NODE_WIDTH,
    (CODE_OFFSET + i) % VERKLE_NODE_WIDTH
)

对于 i0 ... (len(code)+30)//31 中。

注意:由于直到这个 EIP 之前代码都不存在访问列表,因此请注意,代码访问不收取预热成本。

交易

访问事件

对于一个交易,创建这些访问事件:

(tx.origin, 0, BASIC_DATA_LEAF_KEY)
(tx.origin, 0, CODEHASH_LEAF_KEY)
(tx.target, 0, BASIC_DATA_LEAF_KEY)
(tx.target, 0, CODEHASH_LEAF_KEY)

写入事件

(tx.origin, 0, BASIC_DATA_LEAF_KEY)

如果 value 非零:

(tx.target, 0, BASIC_DATA_LEAF_KEY)

见证 gas 成本

移除以下 gas 成本:

  • 如果 CALL 发送非零值,则增加 CALL 的 gas 成本
  • EIP-2200 SSTORE gas 成本,除了 SLOAD_GAS
  • 每字节合约代码成本 200
  • 所有与发送非零值的 CALLCODE 相关的成本

降低 gas 成本:

  • CREATE/CREATE2 降至 1000
常量
WITNESS_BRANCH_COST 1900
WITNESS_CHUNK_COST 200
SUBTREE_EDIT_COST 3000
CHUNK_EDIT_COST 500
CHUNK_FILL_COST 6200

执行一个交易时,维护四个集合:

  • accessed_subtrees: Set[Tuple[address, int]]
  • accessed_leaves: Set[Tuple[address, int, int]]
  • edited_subtrees: Set[Tuple[address, int]]
  • edited_leaves: Set[Tuple[address, int, int]]

当发生 (address, sub_key, leaf_key)访问事件时,执行以下检查:

  • 执行以下步骤,除非事件是一个 交易访问事件
  • 如果 (address, sub_key) 不在 accessed_subtrees 中,则收取 WITNESS_BRANCH_COST gas,并将该元组添加到 accessed_subtrees
  • 如果 leaf_key 不是 None(address, sub_key, leaf_key) 不在 accessed_leaves 中,则收取 WITNESS_CHUNK_COST gas,并将其添加到 accessed_leaves

当发生 (address, sub_key, leaf_key)写入事件时,执行以下检查:

  • 如果事件是 交易写入事件,则跳过以下步骤。
  • 如果 (address, sub_key) 不在 edited_subtrees 中,则收取 SUBTREE_EDIT_COST gas,并将该元组添加到 edited_subtrees
  • 如果 leaf_key 不是 None(address, sub_key, leaf_key) 不在 edited_leaves 中,则收取 CHUNK_EDIT_COST gas,并将其添加到 edited_leaves
    • 此外,如果 (address, sub_key, leaf_key) 处没有存储值(即,状态在该位置保存 None),则收取 CHUNK_FILL_COST

请注意,树键不能再被清空:只能将值 0...2**256-1 写入树键,并且 0 与 None 不同。一旦树键从 None 更改为非 None,它就永远不能返回到 None

请注意,只有在有足够的 gas 来支付与其相关的事件成本时,才应将值添加到见证中。如果没有足够的 gas 来支付事件成本,则应消耗所有剩余的 gas。

CREATE**CALL 在嵌套执行之前保留 1/64 的 gas。为了使此收费的行为与访问列表的分叉前行为相匹配:

  • 当执行 CALLCALLCODEDELEGATECALLSTATICCALL 时,在收取见证成本 之后 检查此最小 1/64 gas 保留
  • 当执行 CREATECREATE2 时,在收取见证成本 之前 扣除此 1/64 的 gas

区块级别操作

以下任何一项:

  • 在系统调用期间访问的预编译账户、系统合约账户和系统合约的插槽,
  • 矿工账户
  • 提款账户

在交易开始时都不是预热的。

系统合约

当(且仅当)调用系统合约时,无论是

  • 通过系统调用 还是
  • 为了解析预编译/操作码

系统合约的 代码块账户头部 访问不应出现在见证中,因为这些应该在客户端中是已知/缓存的。但是,任何其他访问和所有写入都应出现在见证中。

此外,需要为 预编译/操作码解析 收取相应的见证费用,但在 系统调用 中不收取。

账户抽象

TODO:仍在等待 7702 和 3074 之间的最终决定

理由

Gas 改革

存储和代码读取的 Gas 成本已进行改革,以更紧密地反映新的 Verkle 树设计下的 Gas 成本。WITNESS_CHUNK_COST 设置为每个代码块收取 6.25 gas,WITNESS_BRANCH_COST 设置为平均每个分支收取约 13.2 gas (假设 144 字节的分支长度),在最坏的情况下,如果攻击者故意计算密钥以最大化证明长度,则每个字节收取约 2.5 gas。

与柏林的 gas 成本的主要区别在于:

  • 每 31 字节的代码块收取 200 gas。据估计,这将使平均 gas 使用量增加约 6-12%(表明在 350 gas/块级别上,gas 使用量增加 10-20%)。
  • 访问相邻存储槽 (key1 // 256 == key2 // 256) 的成本从 2100 降低到 200,适用于该组中的第一个槽之后的所有槽,
  • 访问存储槽 0…63 的成本从 2100 降低到 200,包括第一个存储槽。这可能会显著提高许多现有合约的性能,这些合约使用这些存储槽来存储单个持久变量。

尚未分析后两个属性带来的好处,但可能可以显著抵消第一个属性带来的损失。一旦编译器适应这些规则,效率可能会进一步提高。

准确指定何时发生访问事件(这构成了 gas 重新定价的大部分复杂性)对于清楚地指定何时需要将数据保存到第一阶段树至关重要。

向后兼容性

由于此 EIP 修改了共识规则,因此需要进行硬分叉。

主要的向后不兼容变更在于代码块访问的 gas 成本,这使得某些应用程序在经济上不太可行。可以通过在实施此 EIP 的同时提高 gas 限制来缓解这种情况,从而降低由于交易 gas 使用量超过区块 gas 限制而导致应用程序完全无法工作的风险。

安全考虑

此 EIP 意味着某些操作,主要是读取和写入同一后缀树中的多个元素,将变得更便宜。如果客户端保留与现在相同的数据库结构,则会导致 DOS 向量。

因此,需要对数据库进行一些调整才能使其正常工作:

  • 在所有可能的未来中,重要的是从逻辑上将承诺方案与数据存储分开。特别是,不需要遍历承诺方案树来查找任何给定的状态元素
  • 为了使对此 EIP 所需的同一 stem 的访问变得便宜,最好的方法可能是将每个 stem 存储在数据库中的同一位置。基本上,每个 32 字节的 256 个叶子将存储在 8kB 的 BLOB 中。读取/写入此 BLOB 的开销很小,因为磁盘访问的大部分成本是寻道而不是传输的量。

版权

版权及相关权利已通过 CC0 放弃。

Citation

Please cite this document as:

Guillaume Ballet (@gballet), Vitalik Buterin (@vbuterin), Dankrad Feist (@dankrad), Ignacio Hagopian (@jsign), Tanishq Jasoria (@tanishqjasoria), Gajinder Singh (@g11tech), "EIP-4762: 无状态性 gas 成本变更 [DRAFT]," Ethereum Improvement Proposals, no. 4762, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4762.