链眼社区:专注于区块链安全,区块链数据分析, 区块链信息整合,区块链技术服务和区块链技术咨询。

一篇文读懂 optimistim
扫地僧
2022-07-20 18:44:23

一. 项目概述

1. Optimism 是什么

Optimism 是 EVM 等效的 Optimism Rollup 链。 它被设计成快速、简单和安全。

Optimism 是以太坊应用程序的第 2 层扩展协议。它使交易变得更便宜。我们的目标是让任何人都能负担得起且可以使用以太坊进行交易。

Optimism 看起来、感觉和行为都像以太坊,但更便宜、更快。对于建立在我们 Optimism 基础上的开发人员,我们的目标是使过渡尽可能无缝。除了极少数例外,现有的 Solidity 智能合约可以像在 L1 上运行一样在 L2 上运行。同样,链下代码(即 UI 和钱包)应该能够与 L2 合约交互,只需更新 RPC 端点。

2. 项目架构说明

下图是 optimistim 的项目架构图:

SCC 合约:

太坊数据索引器和 Optimism 客户端软件。以太坊数据索引器,也称为 "数据传输层"。

CTC 合约:

状态做出可证明的陈述需要密码学承诺以乐观状态树的root hash 的形式存在. Optimism 的状态在每个区块之后都会更新,因此这个承诺也会在每个区块之后发生变化。承诺会定期发布(大约每小时一次或两次)到以太坊上的智能合约。

挑战的细节

这些证明可用于对 Optimism 上特定区块高度的任何合约存储中的数据做出可验证的陈述。然后可以使用此基本功能使 Optimism 上的合约能够向以太坊上的合约发送消息。这L2ToL1MessagePasser (打开新窗口)Optimism 上的合约可以使用合约(预部署到 Optimism 网络)将消息存储在 Optimism 状态。然后,用户可以向以太坊上的合约证明 Optimism 上的给定合约实际上意味着通过显示该消息的哈希已存储在L2ToL1MessagePasser合约中来发送某些给定消息

3. L1->L2

触发 CTC 合约,L2geth 下载块执行,更新交易状体的变化就可以完成。

4. L2->L1

交易需要通过 SCC 合约,挑战期通过了才算完成整个交易流程。

5. Op 老版本的欺诈证明

Op 欺诈证明实现较为简单,将L2中执行的交易在L1中再完整执行一次,以确定状态根的hash是否一致来判断欺诈行为。

如上图所示,发布者发布信息到L1上,携带交易信息和新旧整体根hash, L1中的智能合约收到后会默认变更state为新的根hash,这时挑战者提出挑战,挑战者需要提供符合旧根 hash 的merkle状态树,L1智能合约通过旧的状态树执行完整的交易流程得到根hash和发布者提供的新根 hash 比较,不相等则说明有欺诈行为。

6. EVM 等效性

简而言之:EVM 等效性完全符合以太坊黄皮书,协议的正式定义。根据定义,L1 以太坊软件必须符合此规范。这意味着——深入到最深处——现有的以太坊堆栈现在也将与 L2 系统集成。每个调试器。每个工具链。每个节点实现。我们相信任何提供任何 EVM 体验的 L2 都必须达到这个标准——任何低于此标准的都是不可接受的。 OP 实现一个具有更小、更简单指令集的 VM ,并在防欺诈期间在该 VM 中运行 EVM 。为此,我们必须简单地编译现有的 EVM 解释器,例如geth's,以便在更简单的 VM 中运行。

ETH2 合并 API 的解决方案

事实证明,ETH2 合并需要与 EVM 等效汇总完全相同的抽象:信标链提供与 L1 为汇总所做的完全相同的“父链”角色。这将使在 L2 中使用 L1 客户端变得非常简单。

三.代码模块

  • packages
  • common-ts: Optimism 的 L1 和 L2 智能合约。
  • contracts-periphery : 乐观的外围合约。
  • core-utils : 使构建 Optimism 更容易应用程序。
  • data-transport-layer : 用于索引 Optimism 相关的 L1 数据的服务。
  • Drippie-mon : Drippie 实例监控服务。
  • fault-detector : 检测Sequencer故障的服务。
  • integration-tests- bedrock(BEDROCK 升级):Bedrock 集成测试。
  • message-relayer: 开发中自动中继 L1<>L2 消息的工具。
  • replica-healthcheck:用于监控副本节点健康状况的服务。
  • sdk : 提供一套与Optimism交互的工具。
  • Production
  • batch-submitter : 批量提交交易和结果到L1的服务。
  • bss-core : 核心批处理提交逻辑和实用程序。
  • gas-oracle : 在 L2 上更新 L1 gas 价格的服务。
  • indexer : 索引和同步事务。
  • infra/op-replica : 运行 Optimism 副本的部署示例和资源。
  • integration-tests : Optimism 网络的各种集成测试。
  • l2geth : Optimism 客户端软件,geth v1.9.10 的一个分支 (不推荐用于 BEDROCK 升级)。
  • l2geth-exporter:prometheus 导出器,用于从 L2 geth 节点收集/提供指标。
  • op-exporter : prometheus 导出器,用于从 Optimism 节点收集/提供指标。
  • proxyd : 可配置的 RPC 请求路由器和代理。
  • technical-documents: 审计和验收文件。
  • teleportr : 以低成本在 L1 和 L2 之间传送 ETH 的桥梁。
  • BEDROCK
  • contracts-bedrock: 基岩智能合约。与 ./packages/contracts 合并。
  • op-bindings:Bedrock 智能合约的 Go 绑定。
  • op-batcher : L2-Batch Submitter,将批次的 bundle 提交到L1。
  • op-e2e : Go 中所有 Bedrock 组件的端到端测试。
  • op-node:汇总共识层客户端。
  • op-proposer : L2-Output Submitter,向L1提交提案。
  • ops-bedrock : Bedrock 开发网络。
  • specs : 从 bedrock 升级开始的汇总。

四. 各代码模块流程解释

1. 合约

链由运行在以太坊主网上的一组合约组成。这些合约存储以下的有序列表:

  • 应用于 L2 状态的所有事务的有序列表。
  • 提议的状态根将由每笔交易的应用产生。
  • 从 L1 发送到 L2 的事务,等待包含在有序列表中。

该链由以下具体合约组成:

1.1 CanonicalTransactionChain

规范交易链 (CTC) 合约是一个只能附加的交易日志,必须应用于 OVM 状态。它通过将交易写入CTC:batches链存储容器的实例来定义交易的顺序。CTC 还允许任何帐户加入enqueue()L2 事务,Sequencer 最终必须将其附加到汇总状态。

1.2 StateCommitmentChain

状态承诺链 (SCC) 合约包含提议的状态根列表,提议者声称这些状态根是规范交易链 (CTC) 中每笔交易的结果。这里的元素与 CTC 中的交易有 1:1 的对应关系,应该是通过一一应用规范交易计算出的链下唯一状态根。

1.3 ChainStorageContainer

以“环形缓冲区”数据结构的形式提供可重用的存储,它将覆盖不再需要的存储槽。部署了三个 ChainStorageContainer,两个由 CTC 控制,一个由 SCC 控制。

1.4 BondManager

Bond Manager 合约以 ERC20 代币的形式处理来自保税提议者的存款。它还处理验证者在挑战过程中花费的气体成本的核算。如果挑战成功,则有问题的 Proposer 的保证金将被削减,并且 Verifier 的 gas 费用将被退还。

1.5 L1CrossDomainMessenger

L1 Cross Domain Messenger (L1xDM) 合约将消息从 L1 发送到 L2,并将消息从 L2 中继到 L1。如果从 L1 发送到 L2 的消息因超过 L2 epoch gas 限制而被拒绝,可以通过该合约的重播功能重新提交。

1.6 L2CrossDomainMessenger

L2 Cross Domain Messenger (L2xDM) 合约将消息从 L2 发送到 L1,并且是通过 L1 Cross Domain Messenger 发送的 L2 消息的入口点。

1.7 L1StandardBridge

标准桥的 L1 部分。负责完成从 L2 的提款并开始向 ETH 的 L2 和符合 ERC20 的存款。

1.8 L2StandardBridge

标准桥的 L2 部分。负责从 ETH 和合规的 ERC20 中完成 L1 的存款并启动 L2 的提款。

1.9 L2StandardTokenFactory

用于创建与标准桥兼容并在其上工作的 L1 ERC20 的标准 L2 代币表示的工厂合约。

1.10 OVM_L1MessageSender

L1MessageSender 是在 L2 上运行的预部署合约。在从 L1 到 L2 的跨域交易执行过程中,它返回通过规范交易链的enqueue()功能将消息发送到 L2 的 L1 账户(EOA 或合约)的地址。 请注意,此合约不是用 Solidity 编写的。但是,上面链接的界面仍然可以正常工作。通过这种方式,它类似于 EVM 的预部署。

1.11 OVM_L2ToL1MessagePasser

L2 到 L1 消息传递器是一种实用合同,可促进 L2 上消息的 L1 证明。L1 Cross Domain Messenger 在其 _verifyStorageProof 函数中执行此证明,该函数验证此合约sentMessages映射中交易哈希的存在。

1.12 OVM_SequencerFeeVault

该合约持有支付给定序器的费用,直到有足够的费用证明将它们发送到 L1 的交易成本,在那里它们用于支付 L1 交易成本(主要是在 L1 上将所有 L2 交易数据发布为 CALLDATA 的成本)。

1.13 Lib_AddressManager

这是一个存储名称与其地址之间映射关系的库。它被L1CrossDomainMessenger.

1.14 合约代码执行总结

1.14.1 L1 — L2 bridge 的代码

该桥通过在 L1 上锁定资金并在 L2 上铸造等价物来运作。为了提取资金,桥会烧掉 L2 资金并释放锁定的 L1 资金。 这是存入资金的功能:

contract L1StandardBridge is StandardBridge, Semver {
    function depositETHTo(
        address _to,
        uint32 _minGasLimit,
        bytes calldata _extraData
    ) external payable {
        _initiateETHDeposit(msg.sender, _to, _minGasLimit, _extraData);
    }

    function _initiateETHDeposit(
        address _from,
        address _to,
        uint32 _minGasLimit,
        bytes calldata _extraData
    ) internal {
        emit ETHDepositInitiated(_from, _to, msg.value, _extraData);
        _initiateBridgeETH(_from, _to, msg.value, _minGasLimit, _extraData);
    }
}

该函数是存在于以太坊上的L1StandardBridge合约的一部分。很简单:接受ETH(用payable关键字自动完成),把函数的所有参数编码成一条消息,然后把消息发送给一个跨域的信使。 跨域信使在 L1 和 L2 之间广播消息。 在 L2 上有一个相应的功能可以收听这些消息。L2StandardBridge合同就是这样做的。该合约存在于单独的 L2 区块链上(比以太坊更快)

contract L2StandardBridge is StandardBridge, Semver {
    function finalizeDeposit(
        address _l1Token,
        address _l2Token,
        address _from,
        address _to,
        uint256 _amount,
        bytes calldata _extraData
    ) external payable virtual {
        if (_l1Token == address(0) && _l2Token == PredeployAddresses.LEGACY_ERC20_ETH) {
            finalizeBridgeETH(_from, _to, _amount, _extraData);
        } else {
            finalizeBridgeERC20(_l2Token, _l1Token, _from, _to, _amount, _extraData);
        }
        emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _extraData);
    }
}

该函数只运行一些检查并生成新令牌。我应该提到你可以使用这个桥来移动任意 ERC-20 令牌,而不仅仅是 ETH(ETH 只是包装在 ERC-20 接口中)。 将资金从 L2 转移到 L1 有相应的功能。也使用 x 域信使完成。

1.14.2 跨域消息传递

L1 和 L2 之间的通信通过 x 域信使合约发生(每条链上都有一个副本)。在内部,该合约只存储消息并依靠“中继器”来通知另一条链(L1 或 L2)有新消息。 没有本地 L1 ↔ L2 通信。每边都有一些功能onNewMessage,中继器应该使用传统的 web2 HTTPs 调用它们。 例如,这是 L1 → L2 事务在 L1 上存储/排队的方式:

contract CanonicalTransactionChain is ICanonicalTransactionChain, Lib_AddressResolver {
    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.");

        // Transactions submitted to the queue lack a method for paying gas fees to the Sequencer.
        // So we need to prevent spam attacks by ensuring that the cost of enqueueing a transaction
        // from L1 to L2 is not underpriced. For transaction with a high L2 gas limit, we do this by
        // burning some extra gas on L1. Of course there is also some intrinsic cost to enqueueing a
        // transaction, so we want to make sure not to over-charge (by burning too much L1 gas).
        // Therefore, we define 'enqueueL2GasPrepaid' as the L2 gas limit above which we must burn
        // additional gas on L1. This threshold is the product of two inputs:
        // 1. enqueueGasCost: the base cost of calling this function.
        // 2. l2GasDiscountDivisor: the ratio between the cost of gas on L1 and L2. This is a
        //    positive integer, meaning we assume L2 gas is always less costly.
        // The calculation below for gasToConsume can be seen as converting the difference (between
        // the specified L2 gas limit and the prepaid L2 gas limit) to an L1 gas amount.
        if (_gasLimit > enqueueL2GasPrepaid) {
            uint256 gasToConsume = (_gasLimit - enqueueL2GasPrepaid) / l2GasDiscountDivisor;
            uint256 startingGas = gasleft();

            // Although this check is not necessary (burn below will run out of gas if not true), it
            // gives the user an explicit reason as to why the enqueue attempt failed.
            require(startingGas > gasToConsume, "Insufficient gas for L2 rate limiting burn.");

            uint256 i;
            while (startingGas - gasleft() < gasToConsume) {
                i++;
            }
        }

        // Apply an aliasing unless msg.sender == tx.origin. This prevents an attack in which a
        // contract on L1 has the same address as a contract on L2 but doesn't have the same code.
        // We can safely ignore this for EOAs because they're guaranteed to have the same "code"
        // (i.e. no code at all). This also makes it possible for users to interact with contracts
        // on L2 even when the Sequencer is down.
        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);
    }
}

中继器会通知 L2 队列中有一条新消息。

1.14.3 用于汇总交易的代码

Optimism 上有一个排序器,其工作是接受 L2 交易,检查其有效性,并将状态更新应用到其本地状态作为待处理块。这些待处理的区块会定期大批量提交给以太坊(L1)以进行最终确定。 在以太坊上接受这些批次的功能是L1 合约appendSequencerBatch的一部分。CanonicalTransactionChain在内部,appendSequencerBatch使用下面的函数来处理批次。

contract CanonicalTransactionChain is ICanonicalTransactionChain, Lib_AddressResolver {
    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);
        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);
    }
}
  • batchesRef是用于数据存储的助手合约。那是存储批次的地方。
  • 该函数首先计算批头,然后计算其哈希值。
  • 然后它计算批处理上下文。批次标题和上下文只是有关批次的附加信息。
  • 然后它将散列和上下文存储在存储(batchesRef)中。

稍后,哈希和上下文将用于验证争议。

现在,将事务汇总成批次并提交它们的排序器角色是集中的——由 Optimism 组织控制。但他们计划在未来分散这个角色。您也可以CanonicalTransactionChain直接提交您自己的批次,而无需通过排序器,但它会更昂贵,因为提交批次的固定成本完全由您支付,并且不会在许多不同的交易中摊销。

1.14.4 争议守则

在高层次上,争议通过提交一个状态更新无效的证据并根据存储的状态更新(存储的批处理元数据:哈希和上下文)验证该证明来起作用。 负责处理纠纷的合同是OVMFraudVerifier。该合约是 OVM — Optimism 虚拟机(类似于 EVM — 以太坊虚拟机)的一部分。以下是争议的主要功能:

contract OVM_FraudVerifier is Lib_AddressResolver, OVM_FraudContributor, iOVM_FraudVerifier {
    /**
     * Finalizes the fraud verification process.
     * @param _preStateRoot State root before the fraudulent transaction.
     * @param _preStateRootBatchHeader Batch header for the provided pre-state root.
     * @param _preStateRootProof Inclusion proof for the provided pre-state root.
     * @param _txHash The transaction for the state root
     * @param _postStateRoot State root after the fraudulent transaction.
     * @param _postStateRootBatchHeader Batch header for the provided post-state root.
     * @param _postStateRootProof Inclusion proof for the provided post-state root.
     */
    function finalizeFraudVerification(
        bytes32 _preStateRoot,
        Lib_OVMCodec.ChainBatchHeader memory _preStateRootBatchHeader,
        Lib_OVMCodec.ChainInclusionProof memory _preStateRootProof,
        bytes32 _txHash,
        bytes32 _postStateRoot,
        Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
        Lib_OVMCodec.ChainInclusionProof memory _postStateRootProof
    )
        override
        public
        contributesToFraudProof(_preStateRoot, _txHash)
    {
        iOVM_StateTransitioner transitioner = getStateTransitioner(_preStateRoot, _txHash);

        // ... a bunch of require statements omitted

        // If the post state root did not match, then there was fraud and we should delete the batch
        require(
            _postStateRoot != transitioner.getPostStateRoot(),
            "State transition has not been proven fraudulent."
        );

        _cancelStateTransition(_postStateRootBatchHeader, _preStateRoot);

        // TEMPORARY: Remove the transitioner; for minnet.
        transitioners[keccak256(abi.encodePacked(_preStateRoot, _txHash))] = iOVM_StateTransitioner(0x0000000000000000000000000000000000000000);

        emit FraudProofFinalized(
            _preStateRoot,
            _preStateRootProof.index,
            _txHash,
            msg.sender
        );
    }

    /**
     * Removes a state transition from the state commitment chain.
     * @param _postStateRootBatchHeader Header for the post-state root.
     * @param _preStateRoot Pre-state root hash.
     */
    function _cancelStateTransition(
        Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
        bytes32 _preStateRoot
    )
        internal
    {
        iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
        iOVM_BondManager ovmBondManager = iOVM_BondManager(resolve("OVM_BondManager"));

        // Delete the state batch.
        ovmStateCommitmentChain.deleteStateBatch(
            _postStateRootBatchHeader
        );

        // Get the timestamp and publisher for that block.
        (uint256 timestamp, address publisher) = abi.decode(_postStateRootBatchHeader.extraData, (uint256, address));

        // Slash the bonds at the bond manager.
        ovmBondManager.finalize(
            _preStateRoot,
            publisher,
            timestamp
        );
    }
}

finalizeFraudVerification检查_postStateRoot(由验证者提交)是否不等于排序器提交的根。 如果不是,那么我们删除该批次_cancelStateTransition并削减定序器的押金(要成为序列器,您需要锁定押金。当您提交欺诈批次时,您的押金将被削减,这笔钱会转到验证者作为保持整个机制运行的动力)。

2. batch-submitter 核心模块代码解析

该模块的代码是批量提交交易和结果到L1的服务,代码回去操作 SCC 和 CTC 合约,设置每次最小提交的数量和最大交易提交数量,每次提交 state 的最大数量和最小数量,然后根据初始化的定时定量的把 L2 上的交易提交到 L1 上去。

提交数据流程代码:

3. gas-oracle 代码解析

4. indexer 代码解析

Indexer 模块的代码功能主要是索引和同步事务,核心代码位于 L1 service 和 L2 Service 的 loop 函数里面。目前数据索引支持 PG, L1 service 和 L2 Service 里面的 loop 是数据同步的核心逻辑。NewIndexer 里面把数据库, L1 service 和 L2 Service 服务封装,在 start 启动的时候会同时启动 L1 service 和 L2 Service 服务进行数据同步。

四. 总结

1. Optimistim 和 Arbitrum 的区别与联系

  • Arbitrum 与 Optimism 非常相似,两个项目之间的主要区别在于它们生成欺诈证明的方式。与执行整个第 2 层事务的 Optimism 不同,Arbitrum 采用多轮方法,执行 L2 事务的小块直到发现差异。这种方法的好处是能够实现更高的交易容量。不利的一面是,以这种方式生成欺诈证明通常需要一周时间——在某些情况下可能需要长达两周——比使用 Optimism 使用的方法要长得多。
  • 与 Arbitrum 相比,Optimism 的争议解决更加依赖于以太坊虚拟机 (EVM)。当用户在 Optimism 上提交挑战时,与之相关的整个交易将通过 EVM 运行, Arbitrum 使用一种链下争议解决方式来将争议缩减为交易中的一个单一步骤。然后协议会将这个单一步骤断言发送给 EVM 以做最终的验证,而不是发送整个交易到 EVM 中,因此 Optimism 解决争议的过程比较简单,但他付出的成本高于 Arbitrum。
  • Arbitrum 和 Optimism 上的交易处理者都不会去处理欺诈性的交易。他们被强制事先向 Arbitrum 或者 Optimism 放入质押资产,如果他们处理了欺诈交易,其资产便会被罚光。
  • 监控 rollup 链的各方也不愿提交错误的欺诈证明,在 Optimism 中,因为挑战者必须支付欺诈证明的链上 gas 费,挑战成功之后会退回手续费;而在 Arbitrum 中,因为挑战者在争议失败后便会被没收其事先提交的保证金。
  • 因为 Optimism 必须在争议发生时能够通过 EVM 运行每笔交易,所以它不能处理超过以太坊 gas limit 的交易,因为这些交易无法在链上正确验证。相反,Arbitrum 可以执行任意大小的交易,尽管这些交易超过了以太坊的 gas limit,因为这些交易不需要通过 EVM 批量运行,而是首先被分解为微小的“指令片段”
  • Optimism 的代码库相对简单,而 Arbitrum 的代码库更加复杂和更加有野心;Optimism 过去曾表示它倾向于 MEV 拍卖方法,而 Arbitrum 计划于提供公平定序服务 (FFS, Fair Sequencing Service)
  • 两个项目都使用可信任的定序模式 (trusted sequencer model),目前,我们只能相信sequencer是诚实,后面发展未知,这可能是我们可以创新的点。
  • Optimism 生态不如 Arbitrum, 项目方更愿意把项目部署在 Arbitrum 上面。

2. 补充说明

Optimistim 升级成等效的 EVM 之后,老版的欺诈证明就被弃用了,目前整个 optimistim 上面是没有欺诈证明的,是一个完全中心化的东西。 不使用老版本的欺诈证明的原因

合作伙伴