Optimism 实用学习笔记
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 特有的信息
- L2 Blocks OP 上的区块
- Transaction Batches,均为 batcher 向 L1 地址提交 L2 batch 的交易
- State Batches 为 L2 output root commit 交易
- L1→L2 Transactions L1 到 L2 的跨链交易(deposit)
- L2→L1 Transactions L2 到 L1 的跨链交易(withdrawal)
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 合约
- L2OutputOracle 保存 L2 的 output root
- OptimismPortal 是 L1 -> L2 的 low-level 通信 API。跨链的 ETH 锁在这里。
- L1CrossDomainMessenger
sendMessage()
方法发送 L1 - L2 消息。底层调用 OptimismPortal - L1StandardBridge 跨链桥,跨链的 ERC20 锁在这里。底层调用 L1CrossDomainMessenger
L2 合约:
- L1Block 获取 L1 区块信息
- SequencerFeeVault L2 手续费收款地址
- L2ToL1MessagePasser: 0x4200..0016 发起提款的合约
- L2CrossDomainMessenger: 0x4200..0007 处理 L2 - L1 通信,底层调用 L2ToL1MessagePasser
- L2StandardBridge: 0x4200..0010 L2 跨链桥,底层调用 L2CrossDomainMessenger
- WETH9 ETH wrapper Token
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 的交易。大概流程是:
- L1 上调用
L1CrossDomainMessenger.sendMessage()
发送消息时需要设置 gaslimit 并支付相应的 L2 gas。作用是保护 L2 节点免受 DoS 攻击。 - 调用到
L1CrossDomainMessenger.depositTransaction()
, 生成TransactionDeposited
event 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 的交易。大概流程为:
- L2 上调用
L2CrossDomainMessenger.sendMessage()
- 调用到
L2ToL1MessagePasser.initiateWithdrawal()
生成MessagePassed
事件。已发送的消息保存在sentMessages
storage 中。 op-proposer
提交的 root hash 中包含sentMessages
的变化- 链下SDK 监测
MessagePassed
事件,生成 proof,调用 L1 的OptimismPortal.proveWithdrawalTransaction()
- 挑战期过后,可以调用
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
验证逻辑包括:
- 验证
keccak256(OutputRootProof) == OutputRoot
- OutputRoot 是由 proposer 提交的,此时可以视为可信的。
- OutputRootProof 来自用户输入,通过 hash 检查后,则表示 messagePasserStorageRoot 可信。
- 验证 messagePasser 合约 storage 的
sentMessages[withdrawalHash] == 1
- storage key 为
keccak256(withdrawalHash, 0)
- value 为 0x1
- 调用
SecureMerkleTrie.verifyInclusionProof(key, value, _withdrawalProof, messagePasserStorageRoot)
验证 storage 的 trie 中是否包含对应的 key, value。 - 验证通过说明 L2 上用户确实发起了 withdrawal
- storage key 为
Finalize 时调用 OptimismPortal.finalizeWithdrawalTransaction()
检查逻辑包括:
- withdrawal proof 的时间过了挑战期
- withdrawal proof 中使用的 output root 与 L2OutputOracle 中取出 output root 一致(避免 Prove 之后,output root 被 chanllenge 发生变化)
- 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 按手续费高低进行交易排序。
安全模型
- 目前核心合约的更新权限依赖于多签钱包。
- 目前没有实现 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 Program
是 Cannon
+ 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
生成 Claim
的 witness data
。
链上则使用两个合约进行验证:
MIPS
:链上实现的 MIPS VM,同时要 mock read/write linux syscallPreimageOracle
:链上保存 Preimage 数据。
op-program
就是 op 中的 Fault Proof Program (FPP)
。op-program
的运行 trace 和结果输出都是确定性的。因此可以链下模拟后进行链上验证。
总结:
op-challenger
监听L2OutputOracle
,发现异常则调起Cannon
Cannon
执行op-program
,并二分定位到有问题的指令。生成witness data
。其中用到的 preimage 信息来自op-preimage
op-challenger
调用DisputeGameFactory
合约创建FaultDisputeGame
实例。FaultDisputeGame
调用 MIPS 链上验证有问题的指令。其中用到的 preimage 信息来自PreimageOracle