将FOCIL重新用作L2强制交易机制

以太坊中文 发布于 2026-06-20 阅读 51

本文提出一种基于FOCIL的L2强制交易机制,无需修改状态转移函数或新交易类型。用户通过L1智能合约提交带状态证明的交易,L2操作者从合约中拉取包含列表并强制包含在区块中。该方案通过EIP-1559基础费用和剪枝函数提供抗审查性,并讨论了账户证明节点等实现细节。

感谢 Péter GaramvölgyiThomas ThieryFrancesco RisitanoJihoon Song 的反馈与审阅。

以下文章基于或关联到处于不同 纳入阶段 的 EIP,特别是:FOCIL(SFI)、可选执行证明(PFI)、区块级访问列表(SFI)、仅有效性部分无状态(无 EIP)、原生 Rollup(尚未提出)。因此,具体细节可能会随时间变化。虽然这项研究的主要动机是为原生 Rollup 寻找一种简单的强制交易机制,但研究结果可以普遍适用于所有 EVM L2,包括现有的 L2。

摘要

我们提出了一种通过 FOCIL 实现的强制交易机制,该机制可用于绕过 EVM L2 的中心化排序器,而无需像现有解决方案那样修改状态转换函数或引入新的交易类型。

背景

FOCIL 通过添加一个新的交易列表(“包含列表”)来更新以太坊的状态转换函数,区块必须满足该列表才能获得认证。此类交易由 CL 侧的“包含列表委员会”选择,该委员会由 16 名验证者组成,并通过更新后的 Engine API 传递给 EL。

def state_transition(chain: BlockChain, block: Block, inclusion_list_transactions: Tuple[LegacyTransaction | Bytes, ...]) -> None:

IL 中的交易仍可能因以下三个原因被有效排除在区块之外:

  • 内在检查失败:交易格式错误、链 ID 错误、Gas 不足、签名无效、参数超出范围;

  • 状态检查失败:nonce 不匹配、余额不足;

  • 区块相关检查失败:Gas 不足(相对于基础费用)、区块空间不足。

即使构建者可以通过“区块填充”故意排除交易,但协议通过提高 EIP-1559 基础费用并让交易保留在 mempool 中,从而对攻击者施加指数级成本,提供抗审查性,该交易将在下一个 IL 中被插入。

目前所有现有的 EVM L2 都通过引入新的交易类型并修改状态转换函数来实现强制交易。OP Stack 引入了“存款交易”类型,该类型源自 L1 事件,自动插入到对应 L2 区块的顶部,在 L2 上无签名,在 L1 上支付 Gas,在 L2 上不消耗 Gas。op-geth 在 geth 之上的所有更改可在此处找到。Arbitrum Stack 也引入了一种新的交易类型(实际上是多种),无签名,其包含由链上强制交易队列强制执行,但在 L2 上支付 Gas。在 geth 之上的所有更改可在此处找到。最重要的是,在这两种情况下,进入强制包含列表的有效交易永远不会因区块已满或基础费用而被有效排除:总是为它们预留空间,并且要么不计算基础费用,要么实现重试机制。

机制

核心直觉是,FOCIL 的 CL 和 mempool 逻辑可以完全被 L1 上的智能合约替换和复制:用户将强制交易提交给 L1 智能合约,而不是传统的 L2 mempool(可能通过 L1 FOCIL!),该合约构建包含列表,并将其作为输入传递给 L2 状态转换函数验证器,类似于 Engine API 将其传递给 EL 的方式。L2 状态转换函数假定与 EIP-8025 引入的无状态状态转换函数完全相同。由于 8025 尚未构建在 Hegotá 之上,因此未考虑 FOCIL,我们在此自由想象无状态 IL 接口。

“L2 FOCIL”如何替换 L1 上 FOCIL 检查所涉及的 CL 和 EL 组件的示意图。

“L2 FOCIL”如何替换 L1 上 FOCIL 检查所涉及的 CL 和 EL 组件的示意图。

为了复制 mempool 行为并保证抗审查性,我们需要确保最终进入 L2 IL 的交易不会被自动丢弃,因为与现有的强制交易机制不同,FOCIL 实际上并不保证在特定区块中包含。此外,应尽可能避免无效交易污染 IL,以防止验证方侧的计算浪费和针对有效交易的拒绝服务攻击。

我们假设运营商发布的每个 L2 批次对应一个 L2 区块,否则运营商可以始终生成空区块以降低基础费用并廉价执行区块填充攻击。目前大多数 Rollup 并非如此,但可以通过闪速区块等技术模拟更快的区块。

因此,我们设计了一个强制交易合约如下:用户将签名的交易提交到一个链上列表,该列表按 maxFeePerGas 降序排列。提交时,执行所有内在(即无状态)检查。正如仅有效性部分无状态(VOPS)研究文章和此处所解释的,执行状态检查对于维持健康的 mempool 至关重要:尽管在 L2 FOCIL 中,提交的交易在 L1 上支付 Gas,但用具有极高 maxFeePerGas 但 nonce 无效或余额不足的交易填充列表头部是廉价的,这会导致 IL 只包含无效交易。因此,VOPS 建议无状态节点仍应通过 BAL 维护每个账户的余额和 nonce。虽然 EVM L2 也能生成并发布 BAL,但在智能合约中维护余额和 nonce 是不可行的:对于 L1,估计存储总计约 8.4 GB,对于 L2 则可能显著更高。因此,我们要求用户提交一个账户证明,该证明通过针对近期 L2 状态调用 eth_getProof 获得,以便被列表接受。由于 FOCIL 不告诉我们包含列表中的交易是否已被包含及其原因,因此即使经过这些检查,进入 IL 的交易也不能被自动丢弃。可以使用两种机制:

  1. 添加一个无需许可的 prune(修剪)函数,该函数根据针对较新区块的账户证明,证明 nonce 已更改或余额已不足。需要注意的是,仅 nonce 检查是不够的,因为 EIP-7702 打破了“账户余额减少仅当 nonce 增加”的不变性。为了实现激励兼容性,可以要求强制交易提交者提交一小笔债券,以便在交易失效时补偿修剪者。

  2. 运营商在结算时提供针对交易根的默克尔证明。由于 IL 由状态转换函数验证(例如通过零知识证明),我们可以检查:如果一笔交易未被包含且区块中仍有空间,则该交易必定无效,可以丢弃。估计成本:每个 IL 中的交易约 275k Gas,32 笔交易在 10M Gas 内,使用多重证明可能更低。如果 IL 中的交易无效但区块已满,则该交易不会被丢弃,因为我们无法区分区块已满和交易无效的情况。1559 基础费用机制保证了最终会生成一个具有足够空间的区块,前提是弹性足够。

L2 运营商在调用包含 L2 区块的结算函数时,从列表顶部的交易中拉取当前 IL,直到达到预定义的 Gas 预算或交易不足以支付当前基础费用。该 IL 作为输入强制传递给链上验证器,其满足被视为有效性规则。为防止竞态条件和 griefing 攻击,IL 只能使用至少早于某个时间阈值的交易来构建,这样证明者可以提前知道其必须包含的确切交易。如果 L2 中心化排序器完全拒绝生成区块,则可以触发链上超时以移除白名单并恢复抗审查性。

具体实现可参见此处。一次提交估计成本约 1.3M Gas,一次修剪约 1.1M Gas,两者在 Gas 价格 1 gwei 时对应约 0.001 ETH。

虽然不在本研究帖的范围之内,但强制交易合约可以根据具体 L2 需求进行定制,以在接受前执行额外的检查。

仅账户节点

目前,获取账户证明需要连接全节点,这对大多数用户来说成本过高,尤其对于 L2 来说。VOPS 提案与 zkEVM 结合时,旨在将证明者和包含者的存储负载从约 233GiB 减少到约 8.4 GiB,通过仅使用证明验证状态,并仅存储通过 BAL 获得的余额和 nonce 来维护健康的 mempool。由于 L2 也发布证明并能够发布 BAL,可以设想为 L2 用户构建类似类型的节点,使其在遭遇审查时更容易提交强制交易,而无需维护完整状态。不幸的是,由于 BAL 仅发布存储差异,要重建提供账户证明所需的存储根,就必须跟踪完整状态。作为快速参考,BAL 定义如下:

BlockAccessList = List[AccountChanges]

AccountChanges = [
    Address,                    # 地址
    List[SlotChanges],          # 存储变更 (slot -> [block_access_index -> new_value])
    List[StorageKey],           # 存储读取 (只读存储键)
    List[BalanceChange],        # 余额变更 ([block_access_index -> post_balance])
    List[NonceChange],          # nonce变更 ([block_access_index -> new_nonce])
    List[CodeChange]            # 代码变更 ([block_access_index -> new_code])
]

而账户在 trie 中序列化为:

def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes:
   """
   将`Account`数据类编码。

   存储不存储在`Account`数据类中,因此如果没有提供存储根,则无法编码`Accounts`。
   """

   return rlp.encode(
       (
           raw_account_data.nonce,
           raw_account_data.balance,
           storage_root,
           raw_account_data.code_hash,
       )
   )

如果 BAL 被修改为也提供存储根变更,而只需要区块末尾的最后一个存储根,那么节点将能够在无需维护完整状态的情况下构建账户证明。EIP-8268(感谢 Toni 提供参考!)正好提出了这一更改。

  • 原文链接: ethresear.ch/t/repurposi...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论