Contents

Optimism 实用学习笔记

   Feb 19, 2024     8 min read      views

OP stack 核心组件、Optimism 架构、L1/L2 通信机制。

OP stack 概述

笔记时间:2024/02/19

  • 官网 https://www.optimism.io/
  • 文档 https://docs.optimism.io/
  • 官方跨链桥 https://app.optimism.io/bridge/deposit
  • OP 服务状态 https://status.optimism.io/
  • 合约地址 https://docs.optimism.io/chain/addresses
  • OP Stack Specs https://specs.optimism.io/root.html
  • OP 区块链浏览器 展示了许多 OP L2 特有的信息

OP Stack 由 Optimism Collective 维护,是支持 Optimism 链的开源完整技术栈,其目标是实现 Superchain:共享 L1,共享 Bridge,可互相通信的多链网络。

OP Stack 包括如下部分:

  • Data Availability, DA
    • 保存完整的 OP 交易数据。可从 DA 中同步、构建完整的链。
    • Ethereum DA 是最常见的解决方案,使用 calldata, blob 保存交易信息
    • 使用 op-batcher 模块向 L1 提交交易 batch
  • Sequencing
    • Sequencer 决定 L2 交易顺序,确定其上传 DA 的方式。
    • 目前 OP 架构使用单一的 Sequencer
    • 未来可能会使用多 Sequencer 架构,由多个 Sequencer 择一出块。
  • Derivation
    • 定义从 DA 解析原始数据并传给 Execution Layer 的标准 API
    • 使用 op-node 模块完成。L1 发送给 L2 的交易即由 op-node 监听并处理。
  • Execution
    • 执行交易,完成 L2 状态转换。
    • 与以太坊 EVM 基本一致,主要添加了 L1-L2 跨链交易处理逻辑和 收取 L1 Data Fee 逻辑。
    • 使用 op-geth 模块完成
  • Settlement Layer
    • 结算层,主要让外部确认链的共识状态。
    • 当 L2 数据推送到 L1 DA 并在 L1 finalize 时,L2 状态也是 finalized。这些数据无法删除和修改。但结算层会验证这些数据的正确性,可能拒绝。如果验证通过,则接受这个数据,从而完成结算(解锁资产)
    • op-proposer 模块会将 L2 的状态更新以 output root 提交到 L1。主要验证的就是这个 output root 的正确性。
    • 可以有以下多种验证方式。
      • Attestation-based Fault Proof(目前的方案):Proposer 提交 proposals 后在挑战期(challenge period)内如果多签没有否决,则状态生效。此方案将信任假设建立在多签上。
      • Fault Proof Optimistic Settlement:与上面方案类似,不过否决不依赖多签,而是使用欺诈证明(Fault Proof ),任何人均可在挑战期内提交有效的 Fault Proof。此方案的信任假设是 Fault Proof 可以被正确的构建并在挑战期内提交
      • Validity Proof Settlement:使用数学证明 L2 状态更新,通常使用 ZK Proof。此方案的信任假设 Validity Proof 可以被正确构建并提交。此方案无挑战期,Validity Proof 提交后则完成结算。
  • Governance
    • MultiSig Contracts (目前的方案):使用多签管理关键系统合约、桥合约的参数配置及升级权限。
    • Governance Tokens:使用 Token 进行 DAO 治理。

L2 交易 rollup 到 L1 的过程:

  • Sequencer 收集 L2 交易
  • 交易提交给 op-geth 进行执行,完成状态转换。
  • 新的 output root 由 op-proposer 提交到 L1 上的 L2OutputOracle。(7天挑战期过后最终生效)
  • 此与同时,op-batcher 将交易压缩打包提交到 L1

注:如果发生 Sequencer Outages ,可以与 L1 OptimismPortal 交互提交 L2 交易。正常情况下在 OptimismPortal 上提交的交易会也会被 Sequencer 处理。如果超过 24 小时未处理,则从合约层自动包含进 L2。

协议分析

合约概述

L1 合约

L2 合约:

Transaction Batches & L2 Blocks

Optimism 链采用 Optimistic Rollup 方案。L2 区块信息保存在 L1 上。具体来说由 op-batcher 提交 Transaction Batches 到 L1 地址 0xff00..0010 上。这是一笔示例交易

  • from 为 Optimism: Batcher 地址
  • 以目前来看大约 3 分钟左右提交一次 batch。每个 batch 大约 2M gas (以太坊 block gas limit 为 30M)
  • 目前使用 calldata 保存 batch 数据。在坎昆升级后,将改为 blob 存储。

OP L2 Blocks 从区块链浏览器角度上看,与 ETH 区别不大。这是 116324018 号区块的示例

  • 每个区块的 Gas limit 仍为 30,000,000。
  • 由于 L2 出块较快,因此每个 L2 block 包含的交易通常较少,为 10 ~ 20 左右。

Bedrock 版本 Sequencer 使用 Private mempool。每 2 秒钟产生一个区块。除了 L2 提交给 sequencer 的交易会打包外,L1 提交的交易也必须被打包。每个 L2 block 有一个 epoch,标识其关联的 L1 block(通常是 L2 block 前几分钟产生的 L1 block)。每个 epoch 中的第一个 L2 block 需要包含 L1 block 提交的跨链交易。如果不包含则可以被挑战。

State Batches

State Batches op-proposer 向 L1 提交 L2 output root 的交易。

OP Bedrock 升级后的第一笔交易

OP Bedrock 升级前的最后一笔交易

现在我们只关注 Bedrock 升级后的情况,以这笔交易为例

Optimism: State Root Proposer 发起调用。目标合约为 L2OutputOracle。调用其 proposeL2Output() 方法。

参数包括:

  • _outputRoot L2 output root,是 version, state root, message passer storage root, block hash 的 hash。
  • _l2BlockNumber L2 区块高度
  • _l1BlockHash L2 block epoch 对应的 L1 区块 hash。
  • _l1BlockNumber L2 block epoch 对应的 L1 区块高度。注意要和 proposeL2Output() 交易所在的区块高度相区分,通常低 6-7 个区块。

output root 可以看作是 L2 当前状态的快照。Fraud Proof 就是针对这个值进行挑战。另外要注意,output root 除了 state root 外,还有 message passer storage root (可看作是 message passer 合约 storage 的快照,这个 root 主要用来证明 L2 提款)等信息。

因此 output root 与 state root 不完全等价。但其实 output root 中最关键的信息就是 state root,这个也是 fraud proof 中要挑战的部分。因此某些场景下将 output root 与 state root 表达为同样的含义也是可以的。

Deposit

Deposit 交易指 L1 发往 L2 的交易。大概流程是:

  1. L1 上调用 L1CrossDomainMessenger.sendMessage() 发送消息时需要设置 gaslimit 并支付相应的 L2 gas。作用是保护 L2 节点免受 DoS 攻击。
  2. 调用到 L1CrossDomainMessenger.depositTransaction() , 生成 TransactionDeposited event
  3. op-node 监听该事件,调用 L2CrossDomainMessenger.relayMessage() 在 L2 上转发消息,执行 L2 合约调用。

示例:

L1 交易是一笔 ETH 跨链交易,

  • 调用 L1StandardBridge.depositETH()
  • 调用 L1CrossDomainMessenger.sendMessage()
  • 调用 OptimismPortal.depositTransaction() ETH 最终锁在 OptimismPortal 合约 中。发送给 L2 的 Message 是要求调用 L2CrossDomainMessenger.relayMessage()
  • OptimismPortal 中提交的交易,op-node 必须在 L2 区块中包含,否则可以被挑战。

L2 交易完成 ETH 转账

  • 交易 sender 是 Optimism: Aliased L1 Cross-Domain Messenger 这个地址是 L1CrossDomainMessenger 的 alias。该地址上不存有 ETH,但其发起的交易可携带 value,相当于可以进行 ETH 铸币。
  • 交易 to 由 L1 调用时指定,这里是 L2CrossDomainMessenger
  • 调用 L2CrossDomainMessenger.relayMessage(),这个 message
    • sender 是 Optimism: Gateway
    • 调用 L2StandardBridge.finalizeBridgeETH()
    • 完成 ETH 转账。接收地址是 L1 交易的 sender。

Withdrawal

Withdrawal 交易指 L2 发往 L1 的交易。大概流程为:

  1. L2 上调用 L2CrossDomainMessenger.sendMessage()
  2. 调用到 L2ToL1MessagePasser.initiateWithdrawal() 生成 MessagePassed 事件。已发送的消息保存在 sentMessages storage 中。
  3. op-proposer 提交的 root hash 中包含 sentMessages 的变化
  4. 链下SDK 监测 MessagePassed 事件,生成 proof,调用 L1 的 OptimismPortal.proveWithdrawalTransaction()
  5. 挑战期过后,可以调用 OptimismPortal.finalizeWithdrawalTransaction() 发起消息中对 L1 合约的调用。

示例:

L2 交易是一笔 withdraw 交易

  • 调用 L2StandardBridge.withdraw() 指定提款 token 和数量等。
  • 这里会 burn 掉对应的 L2StandardERC20
  • 然后调用 L2CrossDomainMessenger.sendMessage()
  • 最终调用 L2ToL1MessagePasser.initiateWithdrawal() 更新 sentMessages storage, 生成 MessagePassed event

Prove 交由链下监测 MessagePassed 事件,调用 OptimismPortal.proveWithdrawalTransaction()。merkle proof 证明通过后,结果会保存在 provenWithdrawals storage 变量中。同时记录当前的时间。

Finalize 交易在挑战期过后发起,调用 OptimismPortal.finalizeWithdrawalTransaction()

  • 这里 sender 是 0x4200000000000000000000000000000000000007
  • 调用 L1CrossDomainMessenger.relayMessage()
    • 这个 sender 是 0x4200000000000000000000000000000000000010
    • 调用 L1StandardBridge.finalizeBridgeERC20()
      • 完成 ERC20 转账。

Prove & Finalize 详解

Prove 时调用 OptimismPortal.proveWithdrawalTransaction(), 函数原型:

library Types {
    struct OutputRootProof {
        bytes32 version;
        bytes32 stateRoot;
        bytes32 messagePasserStorageRoot;
        bytes32 latestBlockhash;
    }

    struct WithdrawalTransaction {
        uint256 nonce;
        address sender;
        address target;
        uint256 value;
        uint256 gasLimit;
        bytes data;
    }
}
    
function proveWithdrawalTransaction(
        Types.WithdrawalTransaction memory _tx,
        uint256 _l2OutputIndex,
        Types.OutputRootProof calldata _outputRootProof,
        bytes[] calldata _withdrawalProof
) external whenNotPaused {
   // ..
}

其中参数:

  • _tx L2 withdraw 交易。注意这里是不包含 L2 交易签名的。也就是说 L1 上并未验证 L2 交易的签名。因此如果 proposer 伪造了 output root,并且未受挑战,那么是可以提走任何人锁定的资金的。
  • _l2OutputIndex L2 output root 的 index,根据这个 index 从 L2OutputOracle 中取出 output root。
  • _outputRootProof output root hash 前的内容。包含 stateRoot, messagePasserStorageRoot 等)
  • _withdrawalProof storage root 包含 _tx hash 的 proof

验证逻辑包括:

  1. 验证 keccak256(OutputRootProof) == OutputRoot
    • OutputRoot 是由 proposer 提交的,此时可以视为可信的。
    • OutputRootProof 来自用户输入,通过 hash 检查后,则表示 messagePasserStorageRoot 可信。
  2. 验证 messagePasser 合约 storage 的 sentMessages[withdrawalHash] == 1
    • storage key 为 keccak256(withdrawalHash, 0)
    • value 为 0x1
    • 调用 SecureMerkleTrie.verifyInclusionProof(key, value, _withdrawalProof, messagePasserStorageRoot) 验证 storage 的 trie 中是否包含对应的 key, value。
    • 验证通过说明 L2 上用户确实发起了 withdrawal

Finalize 时调用 OptimismPortal.finalizeWithdrawalTransaction() 检查逻辑包括:

  1. withdrawal proof 的时间过了挑战期
  2. withdrawal proof 中使用的 output root 与 L2OutputOracle 中取出 output root 一致(避免 Prove 之后,output root 被 chanllenge 发生变化)
  3. output root 时间也过了挑战期。

完成检查后才会执行 withdrawal tx 中的合约调用,完成最终的提款操作。

与以太坊的差异

整体 EVM 等价。EVM Opcodes 差异 包括:

  • COINBASE 返回 Sequencer’s fee wallet,每个区块都不变。
  • PREVRANDAO 返回 RANDAO 的伪随机数
  • ORIGIN, CALLER 如果是由合约触发的 L1 到 L2 的跨链交易,这里返回该合约的 aliased address

Address Aliasing 是处理跨链交易时的一种地址映射

  • 如果 L1 sender 是 EOA,则 L2 sender 也是该 EOA
  • 如果 L1 sender 是合约,则 L2 sender 是 L1 sender + 0x1111000000000000000000000000000000001111。之所以这样处理是因为 L1 和 L2 的合约可能地址相同但内容不同,因此区别处理。

手续费处理:OP 的手续费包括 L2 Execution Gas Fee + L1 Data Fee。

  • L2 Execution Gas Fee 的 gas 计算逻辑与以太坊完全一致,并且支持 EIP-1559。只是 gas price 相比 L1 低很多。这部分手续费支持给 Sequencer’s fee wallet
  • L1 Data Fee 是交易 Rollup 后提交到 L1 上要支付给 L1 的手续费。坎昆升级前,这部分数据使用 calldata 保存,受主网当前 base fee 的影响。坎昆升级后,这部分数据使用 blob 保存,受 blob base fee 影响。L1 数据费可通过调用 OP: Gas Oracle 合约 预估。
  • 通常一笔交易中 L1 Data Fee 是决定性因素,占比 >90%

内存池:OP 目前没有公开内存池,Sequencer 按手续费高低进行交易排序

安全模型

Security Model

  • 目前核心合约的更新权限依赖于多签钱包。
  • 目前没有实现 fault proof。如果发现问题,则由多签进行更新。
  • 未来 fault proof 上线,会移除多签特权。

OP 当前架构中有如下特权地址

  • Batcher 提交交易打包 rollup
  • Proposer 提交 L2 output roots
  • MintManager 可以 Mint OP
  • System Config Owner Safe 5/7
    • 可配置 OP 在 L1 的系统信息 SystemConfig
    • 可调用 deleteL2Outputs() 删除错误的 state commit (未来会变成 fault proof)
    • L1 ProxyAdmin Owner,有升级权限
    • 可 Pause OptimismPortal
  • L2 ProxyAdmin Owner
    • 可升级大部分 L2 Proxy 升级权限

Fault Proof

注:目前 2024/02/19 Fault Proof 并未正式上线主网,仅在测试网中使用。主网仍依靠多签进行 output root 检查。

OP Fault Proof 介绍视频 https://youtu.be/nIN5sNc6nQM

OP stack 的 Fault Proof System 被设计成模块化的,可以支持乐观证明,错误证明(ZK Proof)等多种证明模式。系统分为三个模块:

  • Fault Proof Program (FPP)
  • Fault Proof Virtual Machine (FPVM)
  • Dispute game protocol

DisputeGame 是用于检查 32 bytes root(此场景下称为 Claim)是否有效(true/false)的状态机。DisputeGame 本身不包含具体功能,更多的是对 Fault Proof 问题进行一种抽象,以模块化的对外提供统一的接口(IDisputeGame)。Dispute game protocol 协议中支持向 DisputeGameFactory 注册不同的 DisputeGame 以解决不同类型的争议。

FaultDisputeGame 是 OP 实现的用于验证程序错误的 DisputeGame ,通过检查 Fault Proof Program 的 execution trace 来判定 Claim 是否有效。FaultDisputeGame 设计上可以支持不同的 Fault Proof Program。在 OP 场景下,Fault Proof ProgramCannon + op-program。进行争议解决时,会通过二分计算 L2 output root 的 execution trace 找到最终出错的指令,并在链上验证这条指令。

如何进行二分查找:最终的 state root 是 merkle tree 的 root,因此是通过 keccak256 得到的。在链下执行 op-program 得到 execution trace 后,将所有计算 keccak256 的中间结果保存下来(Preimage)。可以按照 keccak256 计算的位置分割 trace。如果某个 hash 未在 Preimage 找到,则说明此时计算已经发生错误。如果 hash 可以在 Preimage 中找到,则可进一步检查 Preimage 的 hash。以此二分下去,最终定位到产生错误的指令。

链下的模拟执行过程由 Cannon 完成。Cannon 是 Optimism 的 Fault Proof Virtual Machine (FPVM)

It's Go code
... that runs an EVM
... emulating a MIPS machine
... running compiled Go code
... that runs an EVM

Cannon 执行 op-program 生成 Claimwitness data

链上则使用两个合约进行验证:

  • MIPS:链上实现的 MIPS VM,同时要 mock read/write linux syscall
  • PreimageOracle:链上保存 Preimage 数据。

op-program 就是 op 中的 Fault Proof Program (FPP)op-program 的运行 trace 和结果输出都是确定性的。因此可以链下模拟后进行链上验证。

总结:

  1. op-challenger 监听 L2OutputOracle,发现异常则调起 Cannon
  2. Cannon 执行 op-program,并二分定位到有问题的指令。生成 witness data。其中用到的 preimage 信息来自 op-preimage
  3. op-challenger 调用 DisputeGameFactory 合约创建 FaultDisputeGame 实例。
  4. FaultDisputeGame 调用 MIPS 链上验证有问题的指令。其中用到的 preimage 信息来自 PreimageOracle