Optimistic Rollup 的难点在于 OVM,需要在 EVM 的基础上模拟 OVM 的执行,并判断状态的正确性。

原文标题:《L2 - 深入理解 OVM》
撰文:Star Li

Optimistic Rollup 是 Layer 2 潜在的一种方案。周末有点时间,在网络上翻了翻。网络上的文章,Optimistic Rollup 深入技术的文章不多,介绍 OVM 底层技术细节的文章则更少。感兴趣看了看 Optimism 实现的 OVM 功能相关的智能合约,对 Optimistic Rollup 的理解很有帮助。总结一下,感兴趣的小伙伴可以看看。

Optimistic Rollup vs. ZK Rollup

网络上对比这两种 Rollup 的方案文章不多。

主要的一篇文章:

https://medium.com/matter-labs/optimistic-vs-zk-rollup-deep-pe-ea141e71e075

对应的翻译文章:

https://www.chainnews.com/articles/932935429481.htm

具体的性能和安全性对比,感兴趣的小伙伴可以直接看这篇文章。个人觉得,因为方案都不够成熟,目前方案能够达到的 TPS 都只是理论值。没必要太多的讨论。主要说说两种 Rollup 的技术实现的区别:

硬核 | 深入理解 Optimistic Rollup 执行环境 OVM

两种方案都是 Rollup,Layer 2 的所有 Transaction 的信息都会作为 CallData「存储」在 Layer 1,并且 Layer 2 的状态也及时同步到 Layer 1。两者的区别在于,Layer 2 的状态在 Layer 1 的正确性保证。Optimistic Rollup 采用的是「检察」的方式,任何一个节点发现 Layer 2 的状态的错误,提交相关的证明。如果错误的状态被验证,在 Layer 1 的 Layer 2 的状态需要回滚,提交错误状态的节点被惩罚。ZK Rollup 采用的方式直接了当,在向 Layer 1 提交 Layer 2 状态的同时,提交相关状态改变的证明。这些证明都是在 Layer 2 生成。也就是说,ZK Rollup 在向 Layer 1 提交 Layer 2 状态的同时,同时提交了 Layer 2 状态转换的计算证明。这个计算证明是通过零知识证明的算法产生。简单的说,如果转换的状态复杂,生成零知识证明的时间越长。

目前,ZK Rollup 只是支持简单的账户系统以及世界状态,并不能支持智能合约等复杂的世界状态。Optimistic Rollup 虽然能支持智能合约,事实上,因为 Layer 1 的计算能力比较弱,智能合约的支持也比较有限。Optimistic Rollup 支持的智能合约的执行环境,类似 EVM,称为 OVM (Optimistic Virtual Machine)。

OVM

OVM - Optimistic Virtual Machine。OVM 是 Layer 2 交易的执行环境。因为提交到 Layer 1 的状态需要检验正确性,Layer 1 需要「重放」Layer 2 的交易,也就是说,Layer 1 在有些情况下需要执行 OVM 交易的执行。Optimistic Rollup 最复杂的地方也在于此,用 EVM 模拟 OVM,并执行 Layer 2 的交易。

硬核 | 深入理解 Optimistic Rollup 执行环境 OVM

Optimism 实现了 EVM 模拟 OVM 的逻辑,相关的项目的 Github 地址

本文中使用的代码的最后一个提交信息如下:

    commit ca1fede6c8cb9e4eacd8205c1d53284d0c8debdc  
    Author: Mark Tyneway   
    Date:   Fri Oct 30 12:14:50 2020 -0700  

        deploy: use layer 2 chainid (#42)

核心代码在 contracts-v2/contracts/optimistic-ethereum/OVM 目录中。除了 OVM 目录,iOVM 目录是接口定义,libraries 目录是各种库的实现,包括编解码,二叉树等等。

OVM/chain

Layer 1 的智能合约中用两条链维护交易信息和状态信息,分别是 CanonicalTransactionChain 和 StateCommitmentChain。

硬核 | 深入理解 Optimistic Rollup 执行环境 OVM

Layer 2 的所有的交易信息,一个个 Batch 的通过 CallData 提交到 Layer 1。每个 Batch 中的交易的 Hash 信息组织成 Merkle 树。简单的说,CanonicalTransactionChain 存储的是一个个 Batch 交易的 Merkle 树根。这些树根用来判断某个具体的交易是否在链中。

硬核 | 深入理解 Optimistic Rollup 执行环境 OVM

Layer 2 的世界状态,通过一个个交易的状态改变来表示。每个交易后的状态也是通过一个个 Batch 提交到 Layer 1。每个 Batch 中的状态,也再次组织成 Merkle 树。这些树根用来判断某个状态是否在链中。

具体两条链的存储信息,可以查看源代码:OVM_CanonicalTransactionChain.sol 和 OVM_StateCommitmentChain.sol。

OVM/execute

execute 是 OVM 在 EVM 执行的核心逻辑,包括 ExecuteManagerStateManager 以及 SafetyChecker。对应的源代码分别是:OVM_ExecutionManager.sol,OVM_SafetyChecker.sol 和 OVM_StateManager.sol。

ExecuteManager 是整个智能合约执行环境以及指令集的处理。OVM 其实和 EVM 逻辑上采用同样的指令集,但是在 OVM 的环境下,特别在 Layer 1 的 EVM 执行 OVM 时,需要将这些指令集「转义」。之所以叫 OVM 的原因,可能很大程度为了区分 EVM,表述方便。蛮多指令需要转义,把 OVM 在 Layer 1 的实现想象成虚拟机。这些指令包括:TIMESTAMP,CALL,STATICCALL,DELEGATECALL,GASLIMIT,SLOAD,SSTORE 等等。一个交易的执行从 ExecuteManager 的 run 函数开始:

         function run(  
             Lib_OVMCodec.Transaction memory_transaction,  
             address_ovmStateManager  
         )

run 函数提供了执行的交易,以及执行交易前的状态。

StateManager 实现了智能合约以及账户的存储状态管理。ExecuteManager 在执行一个交易时会通过 StateManager 更新状态。

SafetyChecker 检查 OVM 的指令合约中的指令集是否正常,有没有超过目前可以执行的范围。安全性检查通过 OVM_SafetyChecker.sol 的 isBytecodeSafe 函数实现。

         function isBytecodeSafe(  
             bytes memory_bytecode  
         )  
             override  
             external  
             pure  
             returns (bool)  
         {

OVM/verification

verification 是 OVM 调用的业务逻辑。在 Layer 1,只是在验证的时候才需要通过 OVM 执行判断某个交易执行是否正确。verification 逻辑中包括了 BondManager (抵押管理),StateTransitioner (状态转换管理)和 FraudVerifier (错误状态验证逻辑)。FraudVerifier 逻辑是最核心的逻辑。整个验证过程的逻辑调用关系如下:

硬核 | 深入理解 Optimistic Rollup 执行环境 OVM

通过调用 initializeFraudVerification 函数,开始让 Layer 1 开始验证某个交易执行后的状态是否正确。StateTransitioner 准备交易之前的世界状态以及交易执行的中间状态存储。在世界状态准备就绪后(proveContractState/proveStorageSlot),通过调用 ExecutionManager 的 run 函数执行交易并更新状态。更新后的状态通过 StateTransitioner 的 completeTransition 函数生成世界状态。生成的世界状态和提交的世界状态进行对比,如果不一致,之前提交世界状态的节点通过 BondManager 进行惩罚。

仔细的分析一下 FraudVerifier 的 initializeFraudVerification 和 finalizeFraudVerification 函数。先从 initializeFraudVerification 函数开始:

         function initializeFraudVerification(  
             bytes32_preStateRoot,  
             Lib_OVMCodec.ChainBatchHeader memory_preStateRootBatchHeader,  
             Lib_OVMCodec.ChainInclusionProof memory_preStateRootProof,  
             Lib_OVMCodec.Transaction memory_transaction,  
             Lib_OVMCodec.TransactionChainElement memory_txChainElement,  
             Lib_OVMCodec.ChainBatchHeader memory_transactionBatchHeader,  
             Lib_OVMCodec.ChainInclusionProof memory_transactionProof  
         )

_preStateRoot 是之前的世界状态的 Merkle 树根。通过 _preStateRootBatchHeader 和 _preStateRootProof 可以验证某个状态是在 StateCommitmentChain 上。

             require(  
                 ovmStateCommitmentChain.verifyStateCommitment(  
                   _preStateRoot,  
                   _preStateRootBatchHeader,  
                   _preStateRootProof  
                 ),  
                 "Invalid pre-state root inclusion proof."  
             );

_transction 信息是需要验证的交易信息。通过 _txChainElement,_transactionBatchHeader 以及 _transactionProof 可以验证某个交易是否在 CanonicalTransactionChain 上。

             require(  
                 ovmCanonicalTransactionChain.verifyTransaction(  
                   _transaction,  
                   _txChainElement,  
                   _transactionBatchHeader,  
                   _transactionProof  
                 ),  
                 "Invalid transaction inclusion proof."  
             );

在确定了交易以及状态都合法后,创建 StateTransitioner 准备执行交易。

             transitioners[_preStateRoot] = iOVM_StateTransitionerFactory(  
                 resolve("OVM_StateTransitionerFactory")  
             ).create(  
                 address(libAddressManager),  
               _preStateRootProof.index,  
               _preStateRoot,  
                 Lib_OVMCodec.hashTransaction(_transaction)  
             );

执行交易的逻辑,直接忽略,感兴趣的小伙伴可以看 OVM_StateTransitioner.sol 的 applyTransaction 函数。交易执行完,通过 finalizeFraudVerification 函数检查执行后的世界状态的结果。

         function finalizeFraudVerification(  
             bytes32_preStateRoot,  
             Lib_OVMCodec.ChainBatchHeader memory_preStateRootBatchHeader,  
             Lib_OVMCodec.ChainInclusionProof memory_preStateRootProof,  
             bytes32_postStateRoot,  
             Lib_OVMCodec.ChainBatchHeader memory_postStateRootBatchHeader,  
             Lib_OVMCodec.ChainInclusionProof memory_postStateRootProof  
         )

先检查提供的两个世界状态是否在 StateCommitmentChain 上存在:

             require(  
                 ovmStateCommitmentChain.verifyStateCommitment(  
                   _preStateRoot,  
                   _preStateRootBatchHeader,  
                   _preStateRootProof  
                 ),  
                 "Invalid pre-state root inclusion proof."  
             );  

             require(  
                 ovmStateCommitmentChain.verifyStateCommitment(  
                   _postStateRoot,  
                   _postStateRootBatchHeader,  
                   _postStateRootProof  
                 ),  
                 "Invalid post-state root inclusion proof."  
             );

并且,保证两个状态是连续的:

             require(  
               _postStateRootProof.index ==_preStateRootProof.index + 1,  
                 "Invalid post-state root index."  
             );

查看 OVM 执行的世界状态是否和提交的状态一致:

             require(  
               _postStateRoot != transitioner.getPostStateRoot(),  
                 "State transition has not been proven fraudulent."  
             );

如果不一致,需要回滚世界状态:

             ovmStateCommitmentChain.deleteStateBatch(  
               _postStateRootBatchHeader  
             );

并且对提交世界状态的节点进行惩罚:

             ovmBondManager.finalize(  
               _preStateRoot,  
               _postStateRootBatchHeader.batchIndex,  
                 publisher,  
                 timestamp  
             );

简单的看,OVM 在 EVM 的模拟,涉及到两个重要的点:1/ 之前世界状态的表示 2/ 当前交易的执行。整个逻辑涉及到多次 Layer 1 的交易,除此之外,还需要足够的时间保证链上数据能够同步并检查。目前,世界状态的挑战过程必须在相应交易后的 7 天内完成:

         /// The dispute period  
         uint256 public constant disputePeriodSeconds = 7 days;

总结

Optimistic Rollup 是 Layer 2 潜在的一种方案。和 ZK Rollup 一样,所有 Transaction 的信息都会作为 CallData「存储」在 Layer 1。在 Layer 2, Optimistic Rollup 通过 OVM 执行智能合约,并使用「检察」的方式确定 Layer 2 世界状态在 Layer 1 的正确性。Optimistic Rollup 的难点也在 OVM,需要在 EVM 的基础上模拟 OVM 的执行,并判断状态的正确性。目前,Optimistic Rollup 的挑战期为 7 天。也就是说,只有 7 天前的状态是「确定」的,不会回滚。