感谢Péter Garamvölgyi 、 Thomas Thiery 、 Francesco Risitano和Jihoon Song的反馈和审阅。
本文基于或涉及处于不同纳入阶段的EIP,特别是: FOCIL (SFI)、可选执行证明(PFI)、块级访问列表(SFI)、 仅有效性部分无状态(无 EIP)以及原生 Rollup (尚未提出)。因此,细节预计会随时间而变化。虽然这项研究的主要动机是为原生 Rollup 寻找一种简单的强制事务机制,但其发现普遍适用于所有 EVM L2,包括现有的 L2。
抽象的
我们提出了一种通过 FOCIL 实现的强制交易机制,该机制无需对状态转换函数或新的交易类型进行任何修改,即可绕过 EVM L2 的集中式序列器,这与现有的解决方案不同。
背景
FOCIL 通过添加一个新的交易列表(“包含列表”)来更新以太坊的 STF,The Block必须满足该列表上的交易才能获得认证。这些交易由 CL 端的“IL 委员会”选择,该委员会由 16 个验证者组成,并通过更新后的 Engine API 传递给 EL。
def state_transition ( chain: BlockChain, block: Block, inclusion_list_transactions: Tuple [LegacyTransaction | Bytes, ...] ) -> None :IL 中的交易仍然可以出于以下三个原因被合法地从区块中排除:
内部检查失败:交易格式错误、链 ID 错误、gas 不足、签名无效、参数超出范围;
状态检查失败:随机数不匹配,余额不足;
区块相关检查失败:燃气不足(相对于基本费用),区块空间不足。
即使建造者可以通过“区块填充”故意排除交易,该协议也能通过增加EIP-1559基本费用并对攻击者施加指数级成本来提供抗审查性,并将交易保留在内存池中,该交易将被插入到下一个 IL 中。
目前所有现有的 EVM L2 区块都通过引入新的交易类型并修改状态转换函数来实现强制交易。OPOP引入了“已存入交易”类型,该类型源自 L1 事件,会自动插入到相应 L2 区块的顶部,在 L2 上无需签名,在 L1 上支付 gas 费用,但在 L2 上不消耗 gas。op-geth 基于 geth 的所有更改可以在这里找到。ArbitrumArbitrum也引入了一种新的交易类型(实际上是几种),该类型无需签名,其包含由链上强制交易队列强制执行,但在 L2 上支付 gas 费用。基于 geth 的所有更改可以在这里找到。最重要的是,在这两种情况下,进入强制包含交易列表的有效交易永远不会因为区块已满或基础费用而被有效排除:系统始终会为其预留空间,并且要么不考虑基础费用,要么会实施重试机制。
该机制
核心思路是,FOCIL 的 CL 和内存池逻辑可以完全被 L1 上的智能合约替代和复制:用户向 L1 智能合约提交强制交易,而不是传统的 L2 内存池(或许可以通过 L1 FOCIL!),该智能合约构建包含列表并将其作为输入传递给 L2 STF 验证器,类似于 Engine API 将其传递给 EL 的方式。假设 L2 STF 与 EIP-8025 引入的无状态 STF 函数完全相同。由于 8025 尚未基于Hegotá构建,因此没有考虑 FOCIL,我们可以自由地设想无状态 IL 接口。
图示“L2 FOCIL”如何取代 L1 FOCIL 检查中涉及的 CL 和 EL 组件。
为了复现内存池的行为并保证抗审查性,我们需要确保最终进入 L2 IL 的交易不会被自动丢弃,因为与现有的强制交易机制不同,FOCIL 实际上并不保证包含在特定区块中。此外,所有无效交易都应尽可能减少对 IL 的污染,以防止证明者端的计算资源浪费以及对有效交易的拒绝服务攻击。
我们假设运营商发布的每个 L2 批次都对应一个 L2 区块,否则运营商可以不断生成空区块来降低基础费用,并低成本地进行区块填充攻击。目前大多数 Rollup 并非如此,但可以使用诸如Flashblocks之类的技术来模拟更快的区块生成速度。
因此,我们设计了一个强制交易合约,如下所示:用户将已签名的交易提交到链上列表,该列表按 `maxFeePerGas` 值降序排列。提交时,所有内在(即无状态)检查都会执行。正如在“仅有效性部分无状态 (VOPS)”研究论文和此处所述,执行有状态检查对于维护健康的内存池至关重要:即使在 L2 FOCIL 中,提交的交易会支付 L1 的 gas 费用,但将 `maxFeePerGas` 值很高但 nonce 无效或余额不足的交易塞满列表头部成本很低,这会导致 IL 仅包含无效交易。因此,VOPS 建议无状态节点仍然应该通过BAL来维护每个账户的余额和 nonce。虽然 EVM L2 节点也能够生成和发布 BAL,但在智能合约中维护余额和 nonce 是不可行的:对于 L1 节点,估计的存储空间已经达到约 8.4GB,而对于 L2 节点,存储空间可能会更高。因此,我们要求用户提交账户证明,该证明需通过eth_getProof 函数针对最新的 L2 状态获取,方可被纳入列表。由于 FOCIL 无法告知我们交易是否已被纳入包含列表以及原因,因此即使经过这些检查,最终进入包含列表的交易也无法自动删除。可以使用两种机制:
新增了一个无需许可的“修剪”函数,该函数根据针对新区块的账户证明,证明 nonce 值已更改或余额不足。需要注意的是,仅凭 nonce 检查是不够的,因为EIP-7702打破了账户余额只能在 nonce 值增加时减少这一不变性。为了实现激励相容性,可以要求强制交易提交者提交少量保证金,以便在交易失效时退还修剪者的款项。
运营商在结算期间提供针对交易根的默克尔证明。鉴于 IL 由 STF 验证(例如通过零知识证明),我们可以检查:如果某个交易未被包含在区块中,且The Block中仍有剩余空间,则该交易必定无效,可以将其丢弃。预计成本:IL 中每个交易约 27.5 万 gas,32 笔交易的成本在 1000 万 gas 以内,使用多重证明时成本可能更低。如果 IL 中的某个交易无效,但The Block中没有剩余空间,则该交易不会被丢弃,因为我们无法区分区块已满和无效的情况。1559 基本费用机制保证最终会生成一个具有足够空间的区块,前提是具有足够的弹性。
L2 操作员在调用结算函数并传入 L2 区块时,会从列表顶部的交易中提取当前白名单 (IL),直至达到预设的 gas 预算或交易支付的费用不足以支付当前基础费用为止。该白名单作为链上验证器的输入强制执行,其满足情况被视为有效性规则。为防止竞态条件和恶意攻击,白名单可以仅包含至少早于某个Threshold的交易,以便证明器能够预先知道它将被强制包含哪些交易。如果 L2 中心化序列器完全拒绝生成区块,则可以在链上触发超时,以移除白名单并恢复抗审查能力。
具体实现示例请参见此处。提交操作预计消耗约 130 万 gas,而修剪操作预计消耗约 110 万 gas,两者均约合 0.001 ETH (按 1 gwei/gas 计算)。
虽然超出了本文的研究范围,但强制交易合约自然可以根据具体的 L2 需求进行定制,以便在接受之前执行额外的检查。
仅限账户节点
目前,获取账户证明需要连接到完整节点,这对大多数用户来说成本过高,尤其是对于二级用户而言。VOPS提案结合 zkEVM,旨在通过仅使用证明验证状态并仅存储通过BAL获取的余额和 nonce 来维护健康的内存池,从而将证明者和包含者的存储负载从约 233GiB 降低到约 8.4GiB。由于二级用户也会发布证明并能够发布 BAL,因此可以设想一种类似的节点供二级用户使用,使他们能够在审查的情况下更轻松地提交强制交易,而无需维护完整状态。然而,由于 BAL 仅发布存储差异,因此必须跟踪完整状态才能重建提供账户证明所需的存储根。为了方便参考, BAL 的定义如下:
BlockAccessList = List [AccountChanges]AccountChanges = [Address, # address List [SlotChanges], # storage_changes (slot -> [block_access_index -> new_value]) List [StorageKey], # storage_reads (read-only storage keys) List [BalanceChange], # balance_changes ([block_access_index -> post_balance]) List [NonceChange], # nonce_changes ([block_access_index -> new_nonce]) List [CodeChange] # code_changes ([block_access_index -> new_code]) ]当 Account 对象在 trie 树中被序列化为:
def encode_account ( raw_account_data: Account, storage_root: Bytes ) -> Bytes: """Encode `Account` dataclass.Storage is not stored in the `Account` dataclass, so `Accounts` cannot beencoded without providing a storage root.""" return rlp.encode((raw_account_data.nonce,raw_account_data.balance,storage_root,raw_account_data.code_hash,))如果修改 BAL,使其也能提供存储根变更信息(其中只需要The Block末尾的最后一个变更),节点就能构建账户证明而无需维护完整的状态。EIP -8268 (感谢Toni提供参考!)正是提出了这样的变更。




