Optimism Rollup

Hacker Dojo

Optimistic Rollup基本概念

1、区块链扩容方案Rollup

有两种方法可以扩展区块链生态系统:

  • 可以让区块链本身拥有更高的交易能力
  • 可以改变使用区块链的方式

Rollup属于第二种方法,通过将计算移到链下,在链上保留每个交易的一些数据的方式来更快、更便宜地使用区块链,实现扩展。
Rollup的原理:在链上会存在一个智能合约,其中包含一个状态根(state root)。

任何人都可以发布一批汇总交易 (batch),这是一个经由高度压缩的交易集合,其中包含之前的状态根和处理交易之后的新默克尔根。合约会检查批处理中的前一个状态根是否与其当前的状态根相匹配,如果匹配则将状态根切换到新的状态根。


关于如何知道一个batch执行后提交的新root是正确的,存在着两种Rollups的解决方案:

  • Optimistic rollup:利用欺诈证明(fraud proofs)
  • ZK rollup:利用有效性证明(validity proofs)

2、Optimistic Rollup工作流程

OP Rollup中目前有2种类型的节点,sequencer(排序器)和verifier(验证器)。sequencer和verifier节点都运行L2Geth(由Optimism创建的Geth的轻微修改版本)。
当User提交一个L2 Transaction给sequencer时,sequencer处理这笔交易,将新的状态根提交给L1,同时其他L2节点在其L2副本上处理交易。为了确保L1新的状态是正确的,验证着节点会将自己的心状态根与sequencer提交的状态根进行比较。如果存在差异会开始欺诈证明,存在欺诈行为开始的state root均要被删除,Sequencer需要重新计算丢失的state root。

在L2提交交易:

  • 用户将交易发送给sequencer节点,如果是有效交易,sequencer会立即将其添加到L2链中。L2的块大小仅1 个事务,因此能立即将一个新块与新事务一起添加到链中
  • 然后在 sequencer 向L2链添加一些交易后,它将调用 L1 上的智能合约(CanonicalTransactionChain.sol)并将这些 L2 交易的交易数据与执行过交易后L2的新state roots
  • L1 上的智能合约将交易数据和state roots存储在另一个智能合约中(StateCommitmentChain.sol)
  • 交易数据存储在L1之后,validator节点将把交易包含在它们的 L2 链副本中。验证者也可以选择从L2同步,可以将直接从sequencer获得新交易

跨链交易:L1->L2
从L1到L2的交易速度很快,在这个过程中只需依靠sequencer将消息中继到L2链,用户需要将他们的交易数据发送到 L1 上的智能合约(L1StandardBridge.sol)。例如,如果用户想发送10 ETH到他们在L2上的地址,步骤如下:

  • 用户向 L1 上的桥接合约(L1StandardBridge.sol)发送 10 ETH
  • 合约将ETH锁定在L1上
  • 合约将用户的交易添加到交易队列中(CanonicalTransactionChain.sol->enqueue)
  • sequencer处理此交易,并将ETH存入用户的L2帐户。

跨链交易:L2->L1
用户会将交易发送到L2上的特定智能合约,然后中继器将读取它并将其发送到 L1。例如用户想将他们在 L2 上的 10 WETH 转移回他们 L1 地址上的 ETH,步骤如下:

  • 用户将10 WETH发送到 L2 上的桥接合约(L2StanderBridger.sol)
  • 桥接合约销毁WETH并将交易信息发送到另一个称为L2ToL1MessagePasser的智能合约。该智能合约记录了需要从L2发送到L1的交易数据。
  • 中继节点从L2ToL1MessagePasser读取此交易数据,并等待欺诈证明窗口(7 天)完成,然后再将交易发送到 L1。
  • 随后交易在L1上处理,用户可以从L1中当时锁定资产的桥接合约(L1StandardBridge.sol)中锁提取 ETH提取他们的ETH。

存储在L1上的状态:
由于每个交易的交易数据和结果状态根必须存储在L1上,因此最小化此数据的大小对于降低系统的存储成本至关重要。这就是术语“rollup”的由来。以下步骤解释了每个 L2 事务的数据如何存储在 L1 上:

  • sequencer获取许多顺序L2事务的调用数据,并将它们组合成一个batch
  • sequencer将这个batch发送到CanonicalTransactionChain合约
  • 合约对每个交易的调用数据进行哈希处理,并创建这些哈希值的merkel tree
  • CanonicalTransactionChain获取该批次的merkle根并将其发送到专门用于存储的智能合约中

3、欺诈证明

欺诈证明是Optimism系统的一个非常重要的部分,这可以保证sequencer的诚实性。
Optimistic Rollup对状态转移的保证是在一定时间窗口内,允许挑战者提交欺诈证明,在此期间任何人都可以对状态转换提出异议,并开始仲裁。仲裁有两种方式:

  • 非交互式欺诈证明
    • 如果sequencer将欺诈状态根发布到L1,则验证者节点可以启动欺诈证明并在L1上执行相应的L2交易
    • Optimism OVM1.0
  • 交互式欺诈证明
    • 在L2上由双方进行多轮交互后锁定某条存疑指令后在 L1 仲裁
    • Arbitrum/Optimism cannon(2022.3)

在 Optimism 实现中,一个L2区块只包含一笔交易,使得区块的 stateRoot 实际就是这笔交易的 stateRoot;即每笔交易都会有一个对应的状态被提交到 L1,因此它可以被单独挑战

结合代码分析

L1与L2上涉及的主要合约:

位置:optimism/packages/contracts/contracts

1、 L1->L2

在使用L2之前,用户首先需要在L1StandardBridge进行充值。用户通过四个deposit函数可以存入对应的ETH或ERC20代币到L1StandardBridge合约。

/**
* @inheritdoc IL1StandardBridge
*/
function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
      _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
    }

/**
* @inheritdoc IL1StandardBridge
*/
function depositETHTo(
      address _to,
      uint32 _l2Gas,
      bytes calldata _data
    ) external payable {
      _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
    }

或存入ERC20 token:

/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20(
      address _l1Token,
      address _l2Token,
      uint256 _amount,
      uint32 _l2Gas,
      bytes calldata _data
    ) external virtual onlyEOA {
      _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
    }

/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20To(
      address _l1Token,
      address _l2Token,
      address _to,
      uint256 _amount,
      uint32 _l2Gas,
      bytes calldata _data
    ) external virtual {
      _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
    }

其中在_initiateETHDeposit函数中,设置了到L2的时候需要调用的函数IL2ERC20Bridge.finalizeDeposit

/**
* @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of
* the deposit.
* @param _from Account to pull the deposit from on L1.
* @param _to Account to give the deposit to on L2.
* @param _l2Gas Gas limit required to complete the deposit on L2.
* @param _data Optional data to forward to L2.
*        solely as a convenience for external contracts. Aside from enforcing a maximum
*        length, these contracts provide no guarantees about its content.
*/
function _initiateETHDeposit(
      address _from,
      address _to,
      uint32 _l2Gas,
      bytes memory _data
    ) internal {
      // Construct calldata for finalizeDeposit call
      bytes memory message = abi.encodeWithSelector(
          IL2ERC20Bridge.finalizeDeposit.selector,
          address(0),
          Lib_PredeployAddresses.OVM_ETH,
          _from,
          _to,
          msg.value,
          _data
        );

      // Send calldata into L2
      // slither-disable-next-line reentrancy-events
      sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

      // slither-disable-next-line reentrancy-events
      emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

sendCrossDomainMessage函数触发L1CrossDomainMessenger.sol中的sendMessage函数:

    /**
     * Sends a cross domain message to the target messenger.
     * @param _target Target contract address.
     * @param _message Message to send to the target.
     * @param _gasLimit Gas limit for the provided message.
     */
    // slither-disable-next-line external-function
    function sendMessage(
        address _target,
        bytes memory _message,
        uint32 _gasLimit
    ) public {  
        address ovmCanonicalTransactionChain = resolve("CanonicalTransactionChain");
        // Use the CTC queue length as nonce
        uint40 nonce = ICanonicalTransactionChain(ovmCanonicalTransactionChain).getQueueLength();

        bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
            _target,
            msg.sender,
            _message,
            nonce
        );

        // slither-disable-next-line reentrancy-events
        _sendXDomainMessage(ovmCanonicalTransactionChain, xDomainCalldata, _gasLimit);

        // slither-disable-next-line reentrancy-events
        emit SentMessage(_target, msg.sender, _message, nonce, _gasLimit);
    }

首先,在这个函数中encodeXDomainCalldata构造了在L2中对应的L2CrossDomainMessenger.sol合约中,会调用relayMessage函数:

    function encodeXDomainCalldata(
        address _target,
        address _sender,
        bytes memory _message,
        uint256 _messageNonce
    ) internal pure returns (bytes memory) {
        return
            abi.encodeWithSignature(
                "relayMessage(address,address,bytes,uint256)",
                _target,
                _sender,
                _message,
                _messageNonce
            );
    }

其次,在_sendXDomainMessag内部调用了CanonicalTransactionChain合约的enqueue函数:

    /**
     * Sends a cross domain message.
     * @param _canonicalTransactionChain Address of the CanonicalTransactionChain instance.
     * @param _message Message to send.
     * @param _gasLimit OVM gas limit for the message.
     */
    function _sendXDomainMessage(
        address _canonicalTransactionChain,
        bytes memory _message,
        uint256 _gasLimit
    ) internal {
        // slither-disable-next-line reentrancy-events
        ICanonicalTransactionChain(_canonicalTransactionChain).enqueue(
            Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER,
            _gasLimit,
            _message
        );
    }

CanonicalTransactionChain中的enqueue接收,经过了以下几个步骤:

  • 首先,如果攻击者在提交L1->L2的交易时将_gasLimit设置的很大,然后在 L2 合约中消耗,可能会导致Sequencer性能下降。于是Optimism设置了阈值enqueueL2GasPrepaid_gasLimit超过阈值的部分合约将根据超出的大小按比例燃烧gas。
  • msg.sender的说法:对于 L1->L2 消息,如果 msg.sender 为合约,那么在 L2 执行时,Origin 会被加上偏移。
    uint160 constant offset = uint160(0x1111000000000000000000000000000000001111);

    function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) {
        unchecked {
            l2Address = address(uint160(l1Address) + offset);
        }
    }
Call source tx.origin
L2 user (Externally Owned Account) The user’s address (same as in Ethereum)
L1 user (Externally Owned Account) The user’s address (same as in Ethereum)
L1 contract (using CanonicalTransactionChain.enqueue) L1_contract_address + 0x1111000000000000000000000000000000001111
  • 将交易压入queueElements队列中
  • 触发TransactionEnqueued事件
/**
     * Adds a transaction to the queue.
     * @param _target Target L2 contract to send the transaction to.
     * @param _gasLimit Gas limit for the enqueued L2 transaction.
     * @param _data Transaction data.
     */
    function enqueue(
        address _target,  
        uint256 _gasLimit,
        bytes memory _data
    ) external {
        require(
            _data.length <= MAX_ROLLUP_TX_SIZE,
            "Transaction data size exceeds maximum for rollup transaction."
        );
        require(
            _gasLimit <= maxTransactionGasLimit,
            "Transaction gas limit exceeds maximum for rollup transaction."
        );
        require(_gasLimit >= MIN_ROLLUP_TX_GAS, "Transaction gas limit too low to enqueue.");
      	// ...
        if (_gasLimit > enqueueL2GasPrepaid) {
            uint256 gasToConsume = (_gasLimit - enqueueL2GasPrepaid) / l2GasDiscountDivisor;
            uint256 startingGas = gasleft();
						require(startingGas > gasToConsume, "Insufficient gas for L2 rate limiting burn.");
            // 消耗多余的gas
            uint256 i;
            while (startingGas - gasleft() < gasToConsume) {
                i++;
            }
        }
      	// .. 
        address sender;
        if (msg.sender == tx.origin) {
            sender = msg.sender;
        } else {
            sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
        }
        bytes32 transactionHash = keccak256(abi.encode(sender, _target, _gasLimit, _data));
        queueElements.push(
            Lib_OVMCodec.QueueElement({
                transactionHash: transactionHash,
                timestamp: uint40(block.timestamp),
                blockNumber: uint40(block.number)
            })
        );
        uint256 queueIndex = queueElements.length - 1;
        emit TransactionEnqueued(sender, _target, _gasLimit, _data, queueIndex, block.timestamp);
    }

data-transport-layer会监听事件并存入数据库,由Sequencer查询TransactionEnqueued,转换为交易并挖出对应的区块。
在L2中的L2CrossDomainMessenger合约中的relayMessage函数会对应的处理这个交易,首先对交易进行如下检查:

  • 首先检查了msg.sender,msg.sender需要是L1CrossDomainMessenger
  • 并且同一条消息之前没有处理过
  • 保证消息的target不是L2_TO_L1_MESSAGE_PASSER,防止L2的消息又回环到L1
/**
     * Relays a cross domain message to a contract.
     * @inheritdoc IL2CrossDomainMessenger
     */
    // slither-disable-next-line external-function
    function relayMessage(
        address _target,
        address _sender,
        bytes memory _message,
        uint256 _messageNonce
    ) public {
        require(
            AddressAliasHelper.undoL1ToL2Alias(msg.sender) == l1CrossDomainMessenger,
            "Provided message could not be verified."
        );
        bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
            _target,
            _sender,
            _message,
            _messageNonce
        );
        bytes32 xDomainCalldataHash = keccak256(xDomainCalldata);
        require(
            successfulMessages[xDomainCalldataHash] == false,
            "Provided message has already been received."
        );
        if (_target == Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER) {
            // Write to the successfulMessages mapping and return immediately.
            successfulMessages[xDomainCalldataHash] = true;
            return;
        }
        xDomainMsgSender = _sender;
        // slither-disable-next-line reentrancy-no-eth, reentrancy-events, reentrancy-benign
        (bool success, ) = _target.call(_message);
        // slither-disable-next-line reentrancy-benign
        xDomainMsgSender = Lib_DefaultValues.DEFAULT_XDOMAIN_SENDER;
        // Mark the message as received if the call was successful. Ensures that a message can be
        // relayed multiple times in the case that the call reverted.
        if (success == true) {
            // slither-disable-next-line reentrancy-no-eth
            successfulMessages[xDomainCalldataHash] = true;
            // slither-disable-next-line reentrancy-events
            emit RelayedMessage(xDomainCalldataHash);
        } else {
            // slither-disable-next-line reentrancy-events
            emit FailedRelayedMessage(xDomainCalldataHash);
        }
        // Store an identifier that can be used to prove that the given message was relayed by some
        // user. Gives us an easy way to pay relayers for their work.
        bytes32 relayId = keccak256(abi.encodePacked(xDomainCalldata, msg.sender, block.number));
        // slither-disable-next-line reentrancy-benign
        relayedMessages[relayId] = true;
    }

随后会调用到L2StandardBridge.sol中的finalizeDeposit函数,在这个函数中最终实现mint操作:

/**
     * @inheritdoc IL2ERC20Bridge
     */
    function finalizeDeposit(  
        address _l1Token,
        address _l2Token,
        address _from,
        address _to,
        uint256 _amount,
        bytes calldata _data
    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
        // Check the target token is compliant and
        // verify the deposited token on L1 matches the L2 deposited token representation here
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&  
            _l1Token == IL2StandardERC20(_l2Token).l1Token()  
        ) {
            // When a deposit is finalized, we credit the account on L2 with the same amount of
            // tokens.
            // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
        }
        ...
    }

2、L2->L1

当用户需要在L2上销毁一些代币并在L1上使用它们时,用户可以调用L2StandardBridge.sol中的withdraw函数:

    function withdrawTo(
        address _l2Token,
        address _to,
        uint256 _amount,
        uint32 _l1Gas,
        bytes calldata _data
    ) external virtual {
        _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
    }

或用户或合约在L2调用入口函数L2CrossDomainMessenger.sendMessage()

/**
     * Sends a cross domain message to the target messenger.
     * @param _target Target contract address.
     * @param _message Message to send to the target.
     * @param _gasLimit Gas limit for the provided message.
     */
    // slither-disable-next-line external-function
    function sendMessage(
        address _target,
        bytes memory _message,
        uint32 _gasLimit
    ) public {
        bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
            _target,
            msg.sender,
            _message,
            messageNonce
        );
        sentMessages[keccak256(xDomainCalldata)] = true;
        // Actually send the message.
        // slither-disable-next-line reentrancy-no-eth, reentrancy-events
        iOVM_L2ToL1MessagePasser(Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER).passMessageToL1(
            xDomainCalldata
        );
        // Emit an event before we bump the nonce or the nonce will be off by one.
        // slither-disable-next-line reentrancy-events
        emit SentMessage(_target, msg.sender, _message, messageNonce, _gasLimit);
        // slither-disable-next-line reentrancy-no-eth
        messageNonce += 1;
    }

sendMessage会进一步进一步调用OVM_L2ToL1MessagePasser.passMessageToL1(),它计算消息哈希并更新合约状态:

    /**
     * @inheritdoc iOVM_L2ToL1MessagePasser
     */
    // slither-disable-next-line external-function
    function passMessageToL1(bytes memory _message) public {
        // Note: although this function is public, only messages sent from the
        // L2CrossDomainMessenger will be relayed by the L1CrossDomainMessenger.
        // This is enforced by a check in L1CrossDomainMessenger._verifyStorageProof().
        sentMessages[keccak256(abi.encodePacked(_message, msg.sender))] = true;
    }

batch-submitter会监听 L2 区块,进行两个操作:

  • 打包批量交易 txBatch 提交到 L1
  • 打包批量状态 stateBatch 提交到 L1

每隔几秒钟Sequencer就会检查接收到的新交易,将它们与所需的任何其他元数据分批汇总,然后利用CanonicalTransactionChain.appendSequencerBatch()函数进行提交。appendSequencerBatch没有任何参数。批次以紧密打包的格式提交,避免了ABI编码的低效率。
appendSequencerBatch函数:

  • 首先计算批次头,然后计算其哈希值
  • 然后计算出批次的上下文
  • 然后将哈希值和上下文存储在存储器中(batchesRef)

[header, context1, context2, …, tx1, tx2, … ]

    function appendSequencerBatch() external {  // 通过这个函数L2的交易会打包成一个块作为大批量的交易交给L1处理
        uint40 shouldStartAtElement;
        uint24 totalElementsToAppend;
        uint24 numContexts;
        assembly {
            shouldStartAtElement := shr(216, calldataload(4))
            totalElementsToAppend := shr(232, calldataload(9))
            numContexts := shr(232, calldataload(12))
        }
        ...
        _appendBatch(
            blockhash(block.number - 1),
            totalElementsToAppend,
            numQueuedTransactions,
            blockTimestamp,
            blockNumber
        );

        // slither-disable-next-line reentrancy-events
        emit SequencerBatchAppended(
            nextQueueIndex - numQueuedTransactions,
            numQueuedTransactions,
            getTotalElements()
        );

        // Update the _nextQueueIndex storage variable.
        // slither-disable-next-line reentrancy-no-eth
        _nextQueueIndex = nextQueueIndex;
    }

    function _appendBatch(
        bytes32 _transactionRoot,
        uint256 _batchSize,
        uint256 _numQueuedTransactions,
        uint40 _timestamp,
        uint40 _blockNumber
    ) internal {
        IChainStorageContainer batchesRef = batches();
        (uint40 totalElements, uint40 nextQueueIndex, , ) = _getBatchExtraData();
        Lib_OVMCodec.ChainBatchHeader memory header = Lib_OVMCodec.ChainBatchHeader({
            batchIndex: batchesRef.length(),
            batchRoot: _transactionRoot,
            batchSize: _batchSize,
            prevTotalElements: totalElements,
            extraData: hex""
        });
        emit TransactionBatchAppended(
            header.batchIndex,
            header.batchRoot,
            header.batchSize,
            header.prevTotalElements,
            header.extraData
        );
        bytes32 batchHeaderHash = Lib_OVMCodec.hashBatchHeader(header);  // 1
        bytes27 latestBatchContext = _makeBatchExtraData(
            totalElements + uint40(header.batchSize),
            nextQueueIndex + uint40(_numQueuedTransactions),
            _timestamp,
            _blockNumber
        );
        // slither-disable-next-line reentrancy-no-eth, reentrancy-events
        batchesRef.push(batchHeaderHash, latestBatchContext);
    }

同时在sequencer执行L2->L1交易时,将修改OVM_L2ToL1MessagePasser状态树(sentMessages[slot]),因而进导致状态的变化,反应在区块头部的stateRoot字段。batch-submitter随后通过appendStateBatch函数打包批量状态到state batch:

	/**
     * @inheritdoc IStateCommitmentChain
     */
    // slither-disable-next-line external-function
    function appendStateBatch(bytes32[] memory _batch, uint256 _shouldStartAtElement) public {
        // Fail fast in to make sure our batch roots aren't accidentally made fraudulent by the
        // publication of batches by some other user.
        require(
            _shouldStartAtElement == getTotalElements(),
            "Actual batch start index does not match expected start index."
        );
        // Proposers must have previously staked at the BondManager
        require(
            IBondManager(resolve("BondManager")).isCollateralized(msg.sender),
            "Proposer does not have enough collateral posted"
        );
        require(_batch.length > 0, "Cannot submit an empty state batch.");
        require(
            getTotalElements() + _batch.length <=
                ICanonicalTransactionChain(resolve("CanonicalTransactionChain")).getTotalElements(),
            "Number of state roots cannot exceed the number of canonical transactions."
        );
        // Pass the block's timestamp and the publisher of the data
        // to be used in the fraud proofs
        _appendBatch(_batch, abi.encode(block.timestamp, msg.sender));
    }

在_appendBatch函数中:

  • Lib_MerkleTree.getMerkleRoot(_batch)计算这个batch的merkel root
  • Lib_OVMCodec.ChainBatchHeader生成这个batch的header
  • 然后存储batch到数组中
  	/**
     * Appends a batch to the chain.
     * @param _batch Elements within the batch.
     * @param _extraData Any extra data to append to the batch.
     */
    function _appendBatch(bytes32[] memory _batch, bytes memory _extraData) internal {
        address sequencer = resolve("OVM_Proposer");
        (uint40 totalElements, uint40 lastSequencerTimestamp) = _getBatchExtraData();

        if (msg.sender == sequencer) {
            lastSequencerTimestamp = uint40(block.timestamp);
        } else {
            // We keep track of the last batch submitted by the sequencer so there's a window in
            // which only the sequencer can publish state roots. A window like this just reduces
            // the chance of "system breaking" state roots being published while we're still in
            // testing mode. This window should be removed or significantly reduced in the future.
            require(
                lastSequencerTimestamp + SEQUENCER_PUBLISH_WINDOW < block.timestamp,
                "Cannot publish state roots within the sequencer publication window."
            );
        }
        // For efficiency reasons getMerkleRoot modifies the `_batch` argument in place
        // while calculating the root hash therefore any arguments passed to it must not
        // be used again afterwards
        Lib_OVMCodec.ChainBatchHeader memory batchHeader = Lib_OVMCodec.ChainBatchHeader({
            batchIndex: getTotalBatches(),
            batchRoot: Lib_MerkleTree.getMerkleRoot(_batch),
            batchSize: _batch.length,
            prevTotalElements: totalElements,
            extraData: _extraData
        });
        emit StateBatchAppended(
            batchHeader.batchIndex,
            batchHeader.batchRoot,
            batchHeader.batchSize,
            batchHeader.prevTotalElements,
            batchHeader.extraData
        );

        batches().push(
            Lib_OVMCodec.hashBatchHeader(batchHeader),
            _makeBatchExtraData(
                uint40(batchHeader.prevTotalElements + batchHeader.batchSize),
                lastSequencerTimestamp
            )
        );
    }

在经过预定的欺诈证明期并与StateCommitmentChain核实后,任何人都可以调用relayMessage函数,并在L1上最终完成提款。同时relayMessage也是一个可以pause的函数。

    function relayMessage(
        address _target,
        address _sender,
        bytes memory _message,
        uint256 _messageNonce,
        L2MessageInclusionProof memory _proof
    ) public nonReentrant whenNotPaused {
        bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
            _target,
            _sender,
            _message,
            _messageNonce
        );
      	// verify
        require(
            _verifyXDomainMessage(xDomainCalldata, _proof) == true,
            "Provided message could not be verified."
        );
        ...
    }

  • _verifyXDomainMessage函数负责验证L2->L1交易存在
    • 证明 交易 存在 stateRoot 中
    • 证明 stateRoot 存在 batchRoot中
/**
     * Verifies that the given message is valid.
     * @param _xDomainCalldata Calldata to verify.
     * @param _proof Inclusion proof for the message.
     * @return Whether or not the provided message is valid.
     */
    function _verifyXDomainMessage(
        bytes memory _xDomainCalldata,
        L2MessageInclusionProof memory _proof
    ) internal view returns (bool) {
        return (_verifyStateRootProof(_proof) && _verifyStorageProof(_xDomainCalldata, _proof));
    }

_verifyStateRootProof:

  • 验证是否在欺诈证明周期中
  • verifyStateCommitment函数验证_proof.stateRoot, _proof.stateRootBatchHeader, _proof.stateRootProof之间的一致性

/**
     * Verifies that the state root within an inclusion proof is valid.
     * @param _proof Message inclusion proof.
     * @return Whether or not the provided proof is valid.
     */
    function _verifyStateRootProof(L2MessageInclusionProof memory _proof)
        internal
        view
        returns (bool)
    {
        IStateCommitmentChain ovmStateCommitmentChain = IStateCommitmentChain(
            resolve("StateCommitmentChain")
        );
        return (ovmStateCommitmentChain.insideFraudProofWindow(_proof.stateRootBatchHeader) ==
            false &&
            ovmStateCommitmentChain.verifyStateCommitment(
                _proof.stateRoot,
                _proof.stateRootBatchHeader,
                _proof.stateRootProof
            ));
    }
// StateCommitmentChain.sol
    function verifyStateCommitment(
        bytes32 _element,
        Lib_OVMCodec.ChainBatchHeader memory _batchHeader,
        Lib_OVMCodec.ChainInclusionProof memory _proof
    ) public view returns (bool) {
        require(_isValidBatchHeader(_batchHeader), "Invalid batch header.");
        require(
            Lib_MerkleTree.verify(
                _batchHeader.batchRoot,
                _element,
                _proof.index,
                _proof.siblings,
                _batchHeader.batchSize
            ),
            "Invalid inclusion proof."
        );
        return true;
    }

_verifyStorageProof验证

  • 信息是由L2CrossDomainMessenger通过L2_TO_L1_MESSAGE_PASSER发送
  • 并且与_proof一致。
/**
     * Verifies that the storage proof within an inclusion proof is valid.
     * @param _xDomainCalldata Encoded message calldata.
     * @param _proof Message inclusion proof.
     * @return Whether or not the provided proof is valid.
     */
    function _verifyStorageProof(
        bytes memory _xDomainCalldata,
        L2MessageInclusionProof memory _proof
    ) internal view returns (bool) {
        bytes32 storageKey = keccak256(
            abi.encodePacked(
                keccak256(
                    abi.encodePacked(
                        _xDomainCalldata,
                        Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER // 1
                    )
                ),
                uint256(0)
            )
        );
        (bool exists, bytes memory encodedMessagePassingAccount) = Lib_SecureMerkleTrie.get(
            abi.encodePacked(Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER),
            _proof.stateTrieWitness,
            _proof.stateRoot
        );
        require(
            exists == true,
            "Message passing predeploy has not been initialized or invalid proof provided."
        );
        Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.decodeEVMAccount(
            encodedMessagePassingAccount
        );
        return
            Lib_SecureMerkleTrie.verifyInclusionProof(
                abi.encodePacked(storageKey),
                abi.encodePacked(uint8(1)),
                _proof.storageTrieWitness,
                account.storageRoot
            );
    }

在relayMessage的剩余部分验证

  • 之前没有relay过相同的交易
  • 确保这个交易没有被Optimism阻止。
  • target不是CanonicalTransactionChain,因此L2->L1消息不能被回送到L2
  • 在调用前修改sender
  • 触发合约调用
  • 调用成功后标记为已经吊用
    function relayMessage(
        address _target,
        address _sender,
        bytes memory _message,
        uint256 _messageNonce,
        L2MessageInclusionProof memory _proof
    ) public nonReentrant whenNotPaused {
        ...
        bytes32 xDomainCalldataHash = keccak256(xDomainCalldata);

        require(
            successfulMessages[xDomainCalldataHash] == false,  //1
            "Provided message has already been received."
        );

        require(
            blockedMessages[xDomainCalldataHash] == false,  //2
            "Provided message has been blocked."
        );

        require(
            _target != resolve("CanonicalTransactionChain"),  //3
            "Cannot send L2->L1 messages to L1 system contracts."
        );
        xDomainMsgSender = _sender; //4
        // slither-disable-next-line reentrancy-no-eth, reentrancy-events, reentrancy-benign
        (bool success, ) = _target.call(_message);  // 5
        // slither-disable-next-line reentrancy-benign
        xDomainMsgSender = Lib_DefaultValues.DEFAULT_XDOMAIN_SENDER;

        // Mark the message as received if the call was successful. Ensures that a message can be
        // relayed multiple times in the case that the call reverted.
        if (success == true) {
            // slither-disable-next-line reentrancy-no-eth
            successfulMessages[xDomainCalldataHash] = true;  // 6
            // slither-disable-next-line reentrancy-events
            emit RelayedMessage(xDomainCalldataHash);
        } else {
            // slither-disable-next-line reentrancy-events
            emit FailedRelayedMessage(xDomainCalldataHash);
        }

        // Store an identifier that can be used to prove that the given message was relayed by some
        // user. Gives us an easy way to pay relayers for their work.
        bytes32 relayId = keccak256(abi.encodePacked(xDomainCalldata, msg.sender, block.number));
        // slither-disable-next-line reentrancy-benign
        relayedMessages[relayId] = true;
    }

3、OVM1.0与OVM2.0

前面提到,欺诈证明存有两种实现方式:

  • 非交互式欺诈证明
  • 交互式欺诈证明

OVM1.0

由于OP Rollup依赖于欺诈证明,所以如果一条交易存在争议,就需要重放该交易以证明交易的结果是否正确,但是有些EVM操作码依赖于系统范围内的参数,这些参数可能会改变(例如存储状态或时间戳),这会导致交易在L1与L2间产生不同的行为。
因此Optimsim实现了OVM来处理L1上的L2争端的机制,这个机制保证可以重现在L1上执行L2事务时存在的 任何“上下文”,并且在理想情况下不引入太多开销。OVM是通过将上下文相关的EVM操作码替换为其对应的OVM操作码来实现的。

OVM2.0

cannon的欺诈证明解决方案:


Reference

nice workshop! Very useful!