Optimism的挑战期

1. 引言

前序博客:

  • Optimism的Fault proof

用户将资产从OP主网转移到以太坊主网时需要等待一周的时间。这段时间称为挑战期,有助于保护 OP 主网上存储的资产。
而OP测试网的挑战期仅为60秒,以简化开发过程。

Optimism的挑战期_第1张图片

2. OP与L1数据交互

Optimism的挑战期_第2张图片

L1(以太坊)上的合约,可通过“bridging”,与L2(OP主网)上合约,进行交互。

同一网络内的Solidity合约调用,类似为:

contract MyContract {
    function doTheThing(address myContractAddress, uint256 myFunctionParam) public {
        MyOtherContract(myContractAddress).doSomething(myFunctionParam);
    }
}

MyContract.doTheThing会触发调用MyOtherContract.doSomething。在底层,Solidity通过向MyOtherContract合约发送ABI encoded call来触发调用doSomething函数。为简化开发者体验,此处抽象掉了很多这种复杂性。也可手工encoding(以更底层callabi.encodeCall函数)来实现相同的功能,如:

contract MyContract {
    function doTheThing(address myContractAddress, uint256 myFunctionParam) public {
        myContractAddress.call(
            abi.encodeCall(
                MyOtherContract.doSomething,
                (
                    myFunctionParam
                )
            )
        );
    }
}

以上两种调用方式等价。由于Solidity的限制,OP Stack的bridging接口被设计为看起来像第二个代码片段(即采用更底层的表达方式)。

2.1 L1与L2之间的基础通讯

从高层来看,L1与L2之间的数据发送流程,与以太坊上2个合约间的发送流程类似。L1与L2的通讯,可通过一组特殊的名为“messenger”的合约来实现。L1和L2有各自的messenger合约,用于抽象掉一些更底层的通讯细节,非常像HTTP库抽象掉物理网络连接。

每个messenger合约都有sendMessage函数,来向另一层的合约发送一个消息:

function sendMessage(
    address _target, //所调用的目标层上的合约
    bytes memory _message, //所发送的内容
    uint32 _minGasLimit //在目标层执行内容的最小gas limit
) public;

其等价为:

address(_target).call{gas: _gasLimit}(_message);

从而可调用个不同网络上的合约。
这掩盖了许多技术细节,这些细节使整件事在幕后运作,但这应该足以让你开始。想从以太坊上的合约调用OP主网上的合约吗?非常简单:

// Pretend this is on L2
contract MyOptimisticContract {
    function doSomething(uint256 myFunctionParam) public {
        // ... some sort of code goes here
    }
}
 
// And pretend this is on L1
contract MyContract {
    function doTheThing(address myOptimisticContractAddress, uint256 myFunctionParam) public {
        messenger.sendMessage(
            myOptimisticContractAddress,
            abi.encodeCall(
                MyOptimisticContract.doSomething,
                (
                    myFunctionParam
                )
            ),
            1000000 // or use whatever gas limit you want
        )
    }
}

具体可查看OP主网和测试网上合约地址 中的L1CrossDomainMessenger合约 和 L2CrossDomainMessenger合约。

2.2 通讯速度

不同于同一链上的合约调用,以太坊和OP主网间的调用不是即时的。跨链交易的通讯速度取决于方向:

  • 1)对于由L1->L2的交易:交易由L1(以太坊)到L2(OP主网),需约1到3分钟。因Sequencer需等待(包含该L1->L2交易区块之后的)一定数量的L1区块,以避免烦人的reorg问题。
  • 2)对于由L2->L1的交易:交易由L2(OP主网)到L1(以太坊),需要约7天。原因在于L1上的bridge合约,需等待该L2状态to be proven to the L1 chain之后,才能relay该message。
    由L2->L1的交易,分为4个不同的步骤:
    • 2.1)Step 1:将“给L1发送message”的L2交易,发送给Sequencer。这与其它L2交易类似,仅需数秒就可由Sequencer确认。
    • 2.2)Step 2:包含该L2交易的L2区块被提交给L1。这通常用时20分钟。
    • 2.3)Step 3:向L1上的OptimismPortal合约 提交该交易的proof。可在上面Step 2完成之后的任意时间提交。
    /// @notice Proves a withdrawal transaction.
    /// @param _tx              Withdrawal transaction to finalize.
    /// @param _l2OutputIndex   L2 output index to prove against.
    /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root.
    /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract.
    function proveWithdrawalTransaction(
        Types.WithdrawalTransaction memory _tx,
        uint256 _l2OutputIndex,
        Types.OutputRootProof calldata _outputRootProof,
        bytes[] calldata _withdrawalProof
    )
        external
        whenNotPaused
    {
        // Prevent users from creating a deposit transaction where this address is the message
        // sender on L2. Because this is checked here, we do not need to check again in
        // `finalizeWithdrawalTransaction`.
        require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract");
    
        // Get the output root and load onto the stack to prevent multiple mloads. This will
        // revert if there is no output root for the given block number.
        bytes32 outputRoot = l2Oracle.getL2Output(_l2OutputIndex).outputRoot;
    
        // Verify that the output root can be generated with the elements in the proof.
        require(
            outputRoot == Hashing.hashOutputRootProof(_outputRootProof), "OptimismPortal: invalid output root proof"
        );
    
        // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier.
        bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
        ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash];
    
        // We generally want to prevent users from proving the same withdrawal multiple times
        // because each successive proof will update the timestamp. A malicious user can take
        // advantage of this to prevent other users from finalizing their withdrawal. However,
        // since withdrawals are proven before an output root is finalized, we need to allow users
        // to re-prove their withdrawal only in the case that the output root for their specified
        // output index has been updated.
        require(
            provenWithdrawal.timestamp == 0
                || l2Oracle.getL2Output(provenWithdrawal.l2OutputIndex).outputRoot != provenWithdrawal.outputRoot,
            "OptimismPortal: withdrawal hash has already been proven"
        );
    
        // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract.
        // Refer to the Solidity documentation for more information on how storage layouts are
        // computed for mappings.
        bytes32 storageKey = keccak256(
            abi.encode(
                withdrawalHash,
                uint256(0) // The withdrawals mapping is at the first slot in the layout.
            )
        );
    
        // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract
        // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have
        // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore
        // be relayed on L1.
        require(
            SecureMerkleTrie.verifyInclusionProof(
                abi.encode(storageKey), hex"01", _withdrawalProof, _outputRootProof.messagePasserStorageRoot
            ),
            "OptimismPortal: invalid withdrawal inclusion proof"
        );
    
        // Designate the withdrawalHash as proven by storing the `outputRoot`, `timestamp`, and
        // `l2BlockNumber` in the `provenWithdrawals` mapping. A `withdrawalHash` can only be
        // proven once unless it is submitted again with a different outputRoot.
        provenWithdrawals[withdrawalHash] = ProvenWithdrawal({
            outputRoot: outputRoot,
            timestamp: uint128(block.timestamp),
            l2OutputIndex: uint128(_l2OutputIndex)
        });
    
        // Emit a `WithdrawalProven` event.
        emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target);
    }
    
    • 2.4)Step 4:仅当fault挑战期(当前OP主网为7天) 结束之后,该交易才在L1上固化。该等待期是OP Stack安全模型的核心,无法规避。

2.3 访问msg.sender

合约会频繁使用msg.sender来基于calling address做决定。如,许多合约使用Ownable模式来选择性地约束对特定函数的访问。因为消息本质上是通过messenger合约在L1和L2之间穿梭的,所以当你接收到其中一条消息时,你会看到的 msg.sender将是与你所在的层相对应的messenger合约。
为此,每个messenger合约内均有xDomainMessageSender函数:

	/// @notice Retrieves the address of the contract or wallet that initiated the currently
    ///         executing message on the other chain. Will throw an error if there is no message
    ///         currently being executed. Allows the recipient of a call to see who triggered it.
    /// @return Address of the sender of the currently executing message on the other chain.
    function xDomainMessageSender() external view returns (address) {
        require(
            xDomainMsgSender != Constants.DEFAULT_L2_SENDER, "CrossDomainMessenger: xDomainMessageSender is not set"
        );

        return xDomainMsgSender;
    }

若你的合约被其中一个messenger合约调用,则可使用该函数来看实际上究竟是谁在发送该消息。在L2上实现onlyOwner modifier示例为:

modifier onlyOwner() {
    require(
        msg.sender == address(messenger)
        && messenger.xDomainMessageSender() == owner
    );
    _;
}

2.4 L1与L2之间发送数据的手续费

对于L1->L2交易:

  • L1->L2交易的大部分开销,源自L1上合约的执行。当发送L1->L2交易时,实际是调用了L1上的L1CrossDomainMessenger合约,然后调用L1上的OptimismPortal合约。这些在L1上的执行,会花费gas。L1->L2交易的总开销,最终由以太坊上的gas费决定。
  • L1->L2交易,会触发L2上的合约执行。OptimismPortal合约,会为L2执行,代为收取费用——burn掉动态量的L1 gas,具体量取决于所请求L2的gas limit。当更多人发送L1->L2交易时,所收取的L1 gas费会增加。当更少发送L1->L2交易时,所收取的L1 gas费会减少。
    • 由于代收取的L2执行gas量是动态的,所burn的gas在各个区块会各不相同。为避免gas燃尽,应总是为L1->L2交易gas limit增加至少20%的buffer。

对于L2->L1方向:

  • 由L2->L1的每个消息,需要3笔交易:
    • 1)L2交易:发起L2->L1启动交易,其定价与OP主网上的任何其他交易相同。
    • 2)L1证明交易:证明该L2交易。只有在L1上提交了包括该L2交易在内的L2区块后,才能提交此L1证明交易。该交易是昂贵的,因为它包括验证L1上的Merkle trie包含证明。
    • 3)L1固化交易:用于固化该L2交易。仅在该L2交易超过7天挑战期之后,才能提交L1固化交易。
  • L2->L1发送单个消息的总开销,包括:1笔L2启动交易和2笔L1交易开销。L1 proof交易和L1固化交易,通常要比L2启动交易贵得多。

2.5 挑战期

由L2->L1的消息,至少需要7天才能relay。这即意味着由L2发送的任意消息,只能过了一周的挑战期之后,才能在L1上收到。称其为“挑战期”的原因在于,在该期间,交易可被a fault proof挑战。
Optimistic Rollups是“乐观的”,因为其核心思想为:将某交易的结果发送到以太坊,而不在以太坊上实际执行该交易。在“乐观”情况下,该交易结果是正确的,可完全避免在以太坊上执行复杂(且昂贵)的逻辑。

但是,仍需要某种方式,来避免发布不正确的,而不是正确的,交易结果。为此,引入了“fault proof”。当某交易结果发布后,可将其看成“pending”一段时间,又名挑战期。在挑战期,任何人都可在以太坊上重新执行该交易,以试图证明所发布的结果是不正确的。

若某人证明该交易结果是错误的,则该结果将被删除,任何人都可以在该位置发布另一个结果(希望这次是正确的结果,经济惩罚会使错误的结果对发布者来说代价高昂)。一旦给定交易结果的窗口完全通过而没有受到质疑,则该结果可被视为完全有效(否则会有人对其提出质疑)。

无论如何,这里的重点是,在这个挑战期结束之前,不从L1上的智能合约内部对L1交易结果做出决定。否则,可能会根据无效的交易结果做出决策。结果,L2->L1 使用standard messenger合约发送的L1消息在等待整个挑战期之后才能被relay。

3. OP原理

为使L1gas开销最小化,OP Bedrock中的L2区块存储在以太坊链的非合约地址内:https://etherscan.io/address/0xff00000000000000000000000000000000000010。
这些L2区块作为交易calldata提交到以太坊,当该“交易”被包含在某具有足够attestations的区块内,则没法对其仅需修改或审查。从而OP主网可继承以太坊的可用性和完整性。

为降低开销,写入L1内的L2区块为压缩格式:

以压缩格式来在以太坊上存储L2区块,这很重要,因为写入到L1是OP主网交易的主要开销。

3.1 区块生成

Optimism区块生产主要由称为“Sequencer”的单一方管理,Sequencer主要提供如下服务:

  • 提供交易确认和状态更新。
  • 构建和执行 L2 区块。
  • 将用户交易提交到 L1。

在 Bedrock 中,Sequencer确实像以太坊那样有一个mempool,但该内存池是私有的,以避免为 MEV 提供机会。在 OP 主网中,区块每两秒生成一次,无论它们是空的(无交易)、被交易填充到区块 Gas limit、还是介于两者之间。

交易通过两种方式到达Sequencer:

  • 1)在 L1 上提交的交易(称为存款)包含在适当的 L2 链区块中。每个 L2 块都由“epoch”(它对应的 L1 块,通常发生在 L2 区块之前几分钟)及其在该epoch内的序列号来标识。该epoch的第一个区块包含其对应的 L1 区块中发生的所有存款。如果epoch试图忽略合法的 L1 交易,它最终会得到与Verifier不一致的状态,就像epoch试图通过其他方式伪造状态一样。这为 OP 主网提供了 L1 以太坊级别的抗审查能力。 可在协议规范Deriving the Transaction List中阅读有关此机制的更多信息。
  • 2)交易直接提交给Sequencer。这些交易的提交成本要低得多(因为不需要单独的 L1 交易费用),但它们当然不能抵抗审查,因为Sequencer是唯一了解它们的参与者。

目前,Optimism基金会在 OP 主网上运行唯一的区块生产者。有关未来计划对 Sequencer 角色进行去中心化。

Optimism的挑战期_第3张图片
基本工作流程为:

  • 1)用户将交易发送到主处理程序(Sequencer),然后主处理程序在其版本的第 2 层 (L2) 链上处理这些交易。
  • 2)处理后,Sequencer将交易详细信息和更新的第 2 层状态发送到第 1 层 (L1)。
  • 3)然后,其他第 2 层节点使用此交易更新其第 2 层链的版本。
  • 4)为了检查准确性,Verifier验证者节点将其更新状态与排序器提交的状态进行比较。

3.2 区块执行

op-geth组件所实现的执行引擎,使用两种机制接收区块:

  • 1)执行引擎可以使用对等网络与其他执行引擎进行自我更新。这与 L1 执行客户端通过网络同步状态的方式相同。具体见happy-path sync。
  • 2)op_node组件所实现的rollup节点从 L1 派生出 L2 区块。这种机制速度较慢,但​​具有抗审查性。具体见Worst-case sync。

3.3 在各层之间bridge ETH或token

Optimism 的设计目的是让用户可以在 L2(OP 主网、OP Sepolia 等)和底层 L1(以太坊主网、Sepolia 等)上的智能合约之间发送任意消息。这使得在两个网络之间传输 ETH 或代币(包括 ERC20 代币)成为可能。这种通信发生的确切机制根据消息发送的方向而有所不同。

OP 主网在标准桥中使用此功能,允许用户将代币从以太坊存入 OP 主网,也允许将代币从 OP 主网提取回以太坊。详情见Using the Standard Bridge。

  • 1)从以太坊迁移到 OP 主网:从以太坊(L1)到 OP 主网(L2)的交易称为存款。使用L1CrossDomainMessenger合约 或 L1StandardBridge合约,存款交易成为与存款所在的 L1 区块相对应的“epoch”的第一个 L2 区块中规范区块链的一部分。该 L2 块通常会在相应的 L1 块几分钟后创建。详情见Deposits。
  • 2)从 OP 主网迁移到以太坊:提款分为3个阶段:【详情见Withdrawals】
    • 2.1)通过 L2 交易初始化提款。
    • 2.2)等待下一个输出根提交到L1,然后使用 提交提款证明proveWithdrawalTransaction。这一新步骤可以对提款进行链下监控,从而更容易识别不正确的提款或输出根。这可保护 OP 主网用户免受一系列潜在的桥接漏洞的影响。
    • 2.3)故障挑战期结束后(主网一周,比测试网短),完成提现。

3.4 Fault proof 故障证明

在 Optimistic Rollup 中,状态承诺被发布到 L1(OP 主网的以太坊),没有任何直接证据证明这些承诺的有效性。相反,这些承诺被视为在一段时间内悬而未决(称为“挑战窗口”)。如果拟议的状态承诺在挑战窗口(当前设置为 7 天)期间没有受到挑战,则该承诺将被视为最终承诺。一旦承诺被认为是最终的,以太坊上的智能合约就可以安全地接受基于该承诺的有关 OP 主网状态的提款证明。

当状态承诺受到质疑时,可以通过“fault proof”过程使其无效。如果该承诺被成功质疑,那么它将被从承诺中删除,StateCommitmentChain(SCC)最终被另一个提议的承诺所取代。值得注意的是,成功的挑战不会回滚 OP 主网本身,只会回滚有关链状态的已发布承诺。交易的顺序和 OP 主网的状态不会因故障证明挑战而改变。

Optimism的挑战期_第4张图片

  • SCC:状态承诺链(SCC)合约包含提议的状态根列表,提议者断言这些状态根是规范交易链(CTC)中每笔交易的结果。这里的元素与CTC中的交易一一对应。
  • CTC:规范交易链(CTC)是必须应用于 OVM 状态的交易协议。
  • OVM:乐观虚拟机是一个符合第 2 层 (L2) 要求的执行框架,使汇总实现能够与以太坊的主区块链进行通信。
  • GETH:Geth 是以太坊区块链的执行客户端,用于处理交易。智能合约的部署和执行包括内置的 EVM。

作为 2021 年 11 月 11 日EVM 等效性的副作用,fault proof流程目前正在进行重大重新开发更新。

4. 为什么 Optimistic Rollup 挑战期为 7 天?

提款延迟是 Optimistic Rollup 的基本组成部分。当用户向以太坊提出有关 Optimistic Rollup 状态的声明时,就会开始提款。 如,这个声明可能是“我在 Optimism 上烧掉了 20 个代币,所以让我在以太坊上提取 20 个代币。”
由于 Optimistic Rollup 的重点在于 L1 并未实际执行 L2 链,因此 L1 不知道此声明是否有效。ZK Rollups 通过为 L1 提供一个加密证明来证明给定的声明是有效的,从而解决了这个问题。乐观汇总通过要求声明必须通过挑战期才能被视为有效来解决此问题。每个claim必须等待一段挑战期,在此期间挑战者可以声明该claim无效。如果有人对某个主张提出质疑,那么就会开始一些链上游戏来确定该主张是否真正有效。

由于某人检测无效声明并提交挑战可能需要一些时间,因此我们不可避免地需要挑战期大于零。毕竟,如果挑战期的持续时间是零秒,那么就没有机会提交挑战了。那么我们的问题就变成了:挑战期应该是多长?

最终状态的现代乐观汇总挑战游戏本质上采取提出主张的用户和质疑该主张的用户之间来回的形式(实际上,这些协议通常被设计为允许任何人参与要么是“团队”,但现在让我们保持简单)。为了举例,我们假设整个过程中有大约 10 个来回步骤(确切的数量有所不同,但在这里并不重要)。

如果双方都非常非常快,那么整个挑战过程至少需要 10 个以太坊区块(2 分钟)。当然,用户并不是像这样完全快,因此您可能需要添加一些至少是基线数字 10 倍的填充,即大约 100 个块(20 分钟)。尽管如此,100 个区块还是比进入 7 天挑战期的 50400 多个区块要短得多。这里一定还有别的东西。

攻击者可以用他们的钱做什么?攻击者的目标是阻止挑战者将他们的挑战交易包含在链上。毕竟,如果挑战交易成功,那么攻击就会失败。攻击者基本上有三种潜在的策略:

  • 1)对挑战者进行直接 DoS 攻击,首先阻止他们与 L1 网络交互
  • 2)通过昂贵的交易向 L1 网络发送垃圾邮件,以推高 Gas 价格并阻止挑战者进行交易
  • 3)通过控制大量验证者来直接审查挑战者

为什么挑战期是7天?

  • 因为它比保守的餐巾纸数学下限要长得多,也许更重要的是,它为整个以太坊社区留下了足够的时间陷入大规模困境。简而言之,利用 Optimistic Rollup 所需的攻击类型会在很长一段时间内显着降低以太坊上的交易体验。每个人都会非常生气。诚实的验证者会突然出现,愿意提交挑战交易以阻止攻击。一周给了我们足够的时间来协调社交层面的这种恢复。

更多详细内容,见:

  • Why is the Optimistic Rollup challenge period 7 days?

5. OP发展历程

OP发展历程为:

  • 2019年
    • 6 月,Plasma Group 开发人员创建了一个名为 Optimism Rollup 的可扩展性解决方案
  • 2020年
    • 2月,兼容EVM的乐观虚拟机(OVM)上线测试阶段
    • 9月,推出带有Optimism Rollup解决方案的测试网
  • 2021年
    • 一月份,Optimism 推出了 Alpha 版本
    • 10月,EVM主网升级启动
    • 8 月,以太坊和 Optimism 之间的成熟区块链桥梁启动。该桥此前只允许传输已启用的 ERC-20 代币
    • 12 月,加密世界庆祝 Optimism 主网发布
  • 2022年
    • 4月,宣布成立乐观集体(治理实验),并进行了多次空投。由于这一治理变化,乐观主义人民银行被解散,并创建了一个非营利组织——乐观主义基金会
    • 5 月,$OP 币开始分配

参考资料

[1] 2023年4月 为什么从 OP 主网转出资产需要等待一周?
[2] Sending Data Between L1 and L2
[3] L2IV RESEARCH 2024年1月29日 Why we invested in Rome Protocol? The Solana-Based Shared Sequencer
[4] Dispute Game
[5] Rollup Protocol Overview
[6] Withdrawal Flow
[7] Why is the Optimistic Rollup challenge period 7 days?
[8] 2023年10月博客 Ethereum’s Optimistic Future: An Introduction to Optimistic Rollups
[9] Optimism

你可能感兴趣的:(区块链,区块链)