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)
形式的访问事件,确定正在访问的数据。我们将访问事件定义如下:
账户头部的访问事件
当:
- 一个非预编译合约,且不是系统合约,是
*CALL
、CALLCODE
、SELFDESTRUCT
、EXTCODESIZE
或EXTCODECOPY
操作码的目标, - 一个非预编译合约,且不是系统合约,是一个合约创建的目标地址,该地址的 initcode 开始执行,
- 任何地址是
BALANCE
操作码的目标 - 一个 已部署的 合约调用
CODECOPY
处理这个访问事件:
(address, 0, BASIC_DATA_LEAF_KEY)
注意:一个没有值的 SELFDESTRUCT
、*CALL
或 CALLCODE
,以预编译合约或系统合约为目标,不会导致 BASIC_DATA_LEAF_KEY
被添加到见证中。
如果一个 *CALL
、CALLCODE
或 SELFDESTRUCT
是有值的(即,它传输非零的 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)
存储的访问事件
带有给定地址和键的 SLOAD
和 SSTORE
操作码处理一个 (address, tree_key, sub_key)
形式的访问事件
(address, tree_key, sub_key)
其中 tree_key
和 sub_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)
。 - 如果非零读取大小的
CODECOPY
或EXTCODECOPY
读取了字节x...y
(包括),则访问已访问合约的所有代码块(x // CHUNK_SIZE) <= chunk_index <= (min(y, code_size - 1) // CHUNK_SIZE)
。- 示例 1:对于
CODECOPY
,起始位置为 100,读取大小为 50,code_size = 200
,则x = 100
,y = 149
- 示例 2:对于
CODECOPY
,起始位置为 600,读取大小为 0,则不访问任何代码块 - 示例 3:对于
CODECOPY
,起始位置为 1500,读取大小为 2000,code_size = 3100
,则x = 1500
,y = 3099
- 示例 1:对于
CODESIZE
、EXTCODESIZE
和EXTCODEHASH
不访问任何代码块。 当创建一个合约时,访问代码块0 ... (len(code)+30)//31
写入事件
我们将 写入事件 定义如下。请注意,当发生写入时,也会发生访问事件(因此下面的定义应该是访问事件定义的子集)。写入事件的形式为 (address, sub_key, leaf_key)
,确定要写入的数据。
账户头部的写入事件
当发生具有给定发送者和接收者的非零余额发送 CALL
、CALLCODE
或 SELFDESTRUCT
时,处理这些写入事件:
(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)
存储的写入事件
带有给定 address
和 key
的 SSTORE
操作码处理一个 (address, tree_key, sub_key)
形式的写入事件
(address, tree_key, sub_key)
其中 tree_key
和 sub_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
)
对于 i
在 0 ... (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。为了使此收费的行为与访问列表的分叉前行为相匹配:
- 当执行
CALL
、CALLCODE
、DELEGATECALL
或STATICCALL
时,在收取见证成本 之后 检查此最小 1/64 gas 保留 - 当执行
CREATE
或CREATE2
时,在收取见证成本 之前 扣除此 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.