Optimism欺诈证明

CanonicalTransactionChain (CTC)和 StateCommitmentChain(SCC)

上次说到OVM的作用就是为了在Layer1上重放Layer2上的交易时,保持一致性。也就是为了验证某个交易是否有人作恶。

在这之前我们必须先理解两个合约,分别是CanonicalTransactionChain.sol和StateCommitmentChain.sol。

这两份合约分别实现了两条链,这也是为什么 Optimism 被称为Layer2的原因,因为他是工作在以太坊之上的事务状态。

  • CanonicalTransactionChain 维护的是Layer2上的交易信息,是由交易组成的一个列表。通过enqueue 方法入队。然后emit 一个 TransactionEnqueued 事件,由data-transport-layer 侦听并存储。以供l2geth获取并打包进一个区块,把区块的stateroot再通过 batch-submitter提交到StateCommitmentChain.sol
  • StateCommitmentChain StateCommitmentChain.sol就是维护交易的状态根的列表。

为了能验证某个交易是否在layer2上打包了。也是利用了Merkle树证明。首先,layer2的所有的交易信息,每隔一段时间都会通过batch-submitter 组成一个Batch, 每个Batch中的交易hash再组成Merkle树。 所以CanonicalTransactionChain 也会存储一个个Batch交易的Merkle树根。。用来判断某个具体的交易是否真的被打包。

同理,StateCommitmentChain也会通过每个交易的状态根也由Batch-submitter组成一个个Batch提到StateCommitmentChain 中,然后,每个Batch中的stateRoot再次组成Merkle树。用来判断某个状态是否在链中。

这些交易信息在CanonicalTransactionChain 和 StateCommitmentChain 是一一对应的。所以验证过程就是从CanonicalTransactionChain 中 拿到一个交易在OVM中运行,将运行的结果和StateCommitmentChain 中相应的状态根进行比较。如果一样,表示验证通过。如果不一样,则表示这个提交Batch的 submitter有作恶。则将StateCommitmentChain中的这个交易开始的后面的状态列表都删除。并扣除质押金。

了解了这些信息,我们就可以从源码角度看下验证过程了。

verification

上面的代码就是组成欺诈验证的所有合约。 这些代码工作在Layer1上。只有在需要验证的时候才需要通过OVM执行判断某个交易执行是否正确。

  • Abs_FraudContributor.sol 记录 要验证的交易hash和preStateRoot,及gas消耗
  • OVM_BondManager.sol 押金管理器,管理Sequencer的质押金,和提供证明的gas消耗
  • OVM_FraudVerifier.sol 欺诈验证逻辑的主文件,整个验证过程都在这里面
  • OVM_StateTransitioner.sol 状态转换管理, 控制着 statemanager.sol 和 ExecutionManager.sol,通过 applyTransaction 调用 ExecutionManager 的run 方法执行交易来记录状态根。OVM_FraudVerifier 通过比较提供的stateroot 和 执行的stateroot来比较检查是否有欺诈
  • OVM_StateTransitionerFactory.sol 一个工厂类,OVM_FraudVerifier用来创建OVM_StateTransitioner.

证明过程如下:

1. 初始化

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
    )
        override
        public
        contributesToFraudProof(_preStateRoot, Lib_OVMCodec.hashTransaction(_transaction))
    {
        bytes32 _txHash = Lib_OVMCodec.hashTransaction(_transaction);

        if (_hasStateTransitioner(_preStateRoot, _txHash)) {
            return;
        }

        iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
        iOVM_CanonicalTransactionChain ovmCanonicalTransactionChain = iOVM_CanonicalTransactionChain(resolve("OVM_CanonicalTransactionChain"));

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

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

        require (
            _preStateRootBatchHeader.prevTotalElements + _preStateRootProof.index + 1 == _transactionBatchHeader.prevTotalElements + _transactionProof.index,
            "Pre-state root global index must equal to the transaction root global index."
        );

        _deployTransitioner(_preStateRoot, _txHash, _preStateRootProof.index);

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

用户 调用OVM_FraudVerifier 的initializeFraudVerification方法,提供前置状态根及其在SCC中的证明和要要验证的交易 及其在CTC 中的证明。
上面的代码逻辑分为

  • 验证前置状态的有效性,和要验证交易的有效性:就是通过在SCC Chain和CTC Chain中的merkle 树来证明其有效性的。
  • 保证交易顺序是一致的
  • 通过前置状态根,txHash, 前置状态号初始化一个状态转换器(OVM_StateTransitioner), 就是通过OVM_StateTransitionerFactory创建的,这时状态转换器处于预执行阶段(PRE_EXECUTION)

2 上传所有交易状态

现在虽然创建的状态转换器,但现在还不能执行交易,因为现在Layer1上的stateManager,还没有任何涉及的Layer2上的状态,这时需要调用状态转换器的proveContractState,将Layer2的OVM合约与Layer1的OVM合约进行链接,下面是代码:

function proveContractState(
        address _ovmContractAddress,
        address _ethContractAddress,
        bytes memory _stateTrieWitness
    )
        override
        public
        onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
        contributesToFraudProof(preStateRoot, transactionHash)
    {
        // Exit quickly to avoid unnecessary work.
        require(
            (
                ovmStateManager.hasAccount(_ovmContractAddress) == false
                && ovmStateManager.hasEmptyAccount(_ovmContractAddress) == false
            ),
            "Account state has already been proven."
        );

        // Function will fail if the proof is not a valid inclusion or exclusion proof.
        (
            bool exists,
            bytes memory encodedAccount
        ) = Lib_SecureMerkleTrie.get(
            abi.encodePacked(_ovmContractAddress),
            _stateTrieWitness,
            preStateRoot
        );

        if (exists == true) {
            // Account exists, this was an inclusion proof.
            Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.decodeEVMAccount(
                encodedAccount
            );

            address ethContractAddress = _ethContractAddress;
            if (account.codeHash == EMPTY_ACCOUNT_CODE_HASH) {
                // Use a known empty contract to prevent an attack in which a user provides a
                // contract address here and then later deploys code to it.
                ethContractAddress = 0x0000000000000000000000000000000000000000;
            } else {
                // Otherwise, make sure that the code at the provided eth address matches the hash
                // of the code stored on L2.
                require(
                    Lib_EthUtils.getCodeHash(ethContractAddress) == account.codeHash,
                    "OVM_StateTransitioner: Provided L1 contract code hash does not match L2 contract code hash."
                );
            }

            ovmStateManager.putAccount(
                _ovmContractAddress,
                Lib_OVMCodec.Account({
                    nonce: account.nonce,
                    balance: account.balance,
                    storageRoot: account.storageRoot,
                    codeHash: account.codeHash,
                    ethAddress: ethContractAddress,
                    isFresh: false
                })
            );
        } else {
            // Account does not exist, this was an exclusion proof.
            ovmStateManager.putEmptyAccount(_ovmContractAddress);
        }
    }

通过上面的代码可以看到执行这个方法前需要当前状态是 PRE_EXECUTION。相应的参数就是要链接的合约在Layer2和在layer1上相应的地址。及账户状态。最后 通过ovmStateManager.putAccount进行相应的链接。
链接后再 调用调用proveStorageSlot,设置执行交易前的账户状态。以方便在执行交易后的账户状态和layer2上的账户状态相比较。

    function proveStorageSlot(
        address _ovmContractAddress,
        bytes32 _key,
        bytes memory _storageTrieWitness
    )
        override
        public
        onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
        contributesToFraudProof(preStateRoot, transactionHash)
    {
        // Exit quickly to avoid unnecessary work.
        require(
            ovmStateManager.hasContractStorage(_ovmContractAddress, _key) == false,
            "Storage slot has already been proven."
        );

        require(
            ovmStateManager.hasAccount(_ovmContractAddress) == true,
            "Contract must be verified before proving a storage slot."
        );

        bytes32 storageRoot = ovmStateManager.getAccountStorageRoot(_ovmContractAddress);
        bytes32 value;

        if (storageRoot == EMPTY_ACCOUNT_STORAGE_ROOT) {
            // Storage trie was empty, so the user is always allowed to insert zero-byte values.
            value = bytes32(0);
        } else {
            // Function will fail if the proof is not a valid inclusion or exclusion proof.
            (
                bool exists,
                bytes memory encodedValue
            ) = Lib_SecureMerkleTrie.get(
                abi.encodePacked(_key),
                _storageTrieWitness,
                storageRoot
            );

            if (exists == true) {
                // Inclusion proof.
                // Stored values are RLP encoded, with leading zeros removed.
                value = Lib_BytesUtils.toBytes32PadLeft(
                    Lib_RLPReader.readBytes(encodedValue)
                );
            } else {
                // Exclusion proof, can only be zero bytes.
                value = bytes32(0);
            }
        }

        ovmStateManager.putContractStorage(
            _ovmContractAddress,
            _key,
            value
        );
    }

3.执行交易

做完执行交易前的准备后,就由用户调用OVM_StateTransitioner的 applyTransaction 来执行交易

function applyTransaction(
        Lib_OVMCodec.Transaction memory _transaction
    )
        override
        public
        onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
        contributesToFraudProof(preStateRoot, transactionHash)
    {
        require(
            Lib_OVMCodec.hashTransaction(_transaction) == transactionHash,
            "Invalid transaction provided."
        );

        // We require gas to complete the logic here in run() before/after execution,
        // But must ensure the full _tx.gasLimit can be given to the ovmCALL (determinism)
        // This includes 1/64 of the gas getting lost because of EIP-150 (lost twice--first
        // going into EM, then going into the code contract).
        require(
            gasleft() >= 100000 + _transaction.gasLimit * 1032 / 1000, // 1032/1000 = 1.032 = (64/63)^2 rounded up
            "Not enough gas to execute transaction deterministically."
        );

        iOVM_ExecutionManager ovmExecutionManager = iOVM_ExecutionManager(resolve("OVM_ExecutionManager"));

        // We call `setExecutionManager` right before `run` (and not earlier) just in case the
        // OVM_ExecutionManager address was updated between the time when this contract was created
        // and when `applyTransaction` was called.
        ovmStateManager.setExecutionManager(address(ovmExecutionManager));

        // `run` always succeeds *unless* the user hasn't provided enough gas to `applyTransaction`
        // or an INVALID_STATE_ACCESS flag was triggered. Either way, we won't get beyond this line
        // if that's the case.
        ovmExecutionManager.run(_transaction, address(ovmStateManager));

        // Prevent the Execution Manager from calling this SM again.
        ovmStateManager.setExecutionManager(address(0));
        phase = TransitionPhase.POST_EXECUTION;
    }

通过代码可以看到,执行前有状态检查和Abs_FraudContributor中contributesToFraudProof,来记录验证的花费和交易. 然后通过OVM_ExecutionManager来run 这个交易,状态存在ovmStateManager中。最后将状态设置为POST_EXECUTION 我们就叫提交状态吧。

在这个阶段下,由于状态转换器/状态管理器对不知道整个L2状态,因此它们无法自动计算新的后状态根。需要用户调用 commitContractState 和 commitStorageSlot 来提交用户的账户和存储信息。

function commitContractState(
        address _ovmContractAddress,
        bytes memory _stateTrieWitness
    )
        override
        public
        onlyDuringPhase(TransitionPhase.POST_EXECUTION)
        contributesToFraudProof(preStateRoot, transactionHash)
    {
        require(
            ovmStateManager.getTotalUncommittedContractStorage() == 0,
            "All storage must be committed before committing account states."
        );

        require (
            ovmStateManager.commitAccount(_ovmContractAddress) == true,
            "Account state wasn't changed or has already been committed."
        );
    
        Lib_OVMCodec.Account memory account = ovmStateManager.getAccount(_ovmContractAddress);
    
        postStateRoot = Lib_SecureMerkleTrie.update(
            abi.encodePacked(_ovmContractAddress),
            Lib_OVMCodec.encodeEVMAccount(
                Lib_OVMCodec.toEVMAccount(account)
            ),
            _stateTrieWitness,
            postStateRoot
        );
    
        // Emit an event to help clients figure out the proof ordering.
        emit AccountCommitted(
            _ovmContractAddress
        );
    }
    
    /**
     * Allows a user to commit the final state of a contract storage slot.
     * @param _ovmContractAddress Address of the contract on the OVM.
     * @param _key Claimed account slot key.
     * @param _storageTrieWitness Proof of the storage slot.
     */
    function commitStorageSlot(
        address _ovmContractAddress,
        bytes32 _key,
        bytes memory _storageTrieWitness
    )
        override
        public
        onlyDuringPhase(TransitionPhase.POST_EXECUTION)
        contributesToFraudProof(preStateRoot, transactionHash)
{
        require(
            ovmStateManager.commitContractStorage(_ovmContractAddress, _key) == true,
            "Storage slot value wasn't changed or has already been committed."
        );
    
        Lib_OVMCodec.Account memory account = ovmStateManager.getAccount(_ovmContractAddress);
        bytes32 value = ovmStateManager.getContractStorage(_ovmContractAddress, _key);
    
        account.storageRoot = Lib_SecureMerkleTrie.update(
            abi.encodePacked(_key),
            Lib_RLPWriter.writeBytes(
                Lib_Bytes32Utils.removeLeadingZeros(value)
            ),
            _storageTrieWitness,
            account.storageRoot
        );
    
        ovmStateManager.putAccount(_ovmContractAddress, account);
    
        // Emit an event to help clients figure out the proof ordering.
        emit ContractStorageCommitted(
            _ovmContractAddress,
            _key
        );
    }

提交完所有用户账户数据后,就是completeTransition 转换结束阶段。

   function completeTransition()
        override
        public
        onlyDuringPhase(TransitionPhase.POST_EXECUTION)
    {
        require(
            ovmStateManager.getTotalUncommittedAccounts() == 0,
            "All accounts must be committed before completing a transition."
        );

        require(
            ovmStateManager.getTotalUncommittedContractStorage() == 0,
            "All storage must be committed before completing a transition."
        );
    
        phase = TransitionPhase.COMPLETE;
    }

这个阶段就是检查目前阶段是否是提交状态(POST_EXECUTION),然后检查未提交的账户是否是0,和未提交的合约存储是否是0.最后设置转换状态为完成状态(COMPLETE).

4.完成验证 finalizeFraudVerification

当状态转换器状态是COMPLETE后,就要执行OVM_FraudVerifier的finalizeFraudVerification方法来对验证结果做处理,来看代码

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);
       iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));

       require(
           transitioner.isComplete() == true,
           "State transition process must be completed prior to finalization."
       );
   
       require (
           _postStateRootBatchHeader.prevTotalElements + _postStateRootProof.index == _preStateRootBatchHeader.prevTotalElements + _preStateRootProof.index + 1,
           "Post-state root global index must equal to the pre state root global index plus one."
       );
   
       require(
           ovmStateCommitmentChain.verifyStateCommitment(
               _preStateRoot,
               _preStateRootBatchHeader,
               _preStateRootProof
           ),
           "Invalid pre-state root inclusion proof."
       );
   
       require(
           ovmStateCommitmentChain.verifyStateCommitment(
               _postStateRoot,
               _postStateRootBatchHeader,
               _postStateRootProof
           ),
           "Invalid post-state root inclusion proof."
       );
   
       // 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
       );
   }

这个方法会检查状态转换器是否是COMPLETE状态(transitioner.isComplete),并且拿_postStateRoot和转换器执行的stateRoot相比较。如果不一样,表示有欺诈.就会调用_cancelStateTransition 来运行处罚程序.

    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
        );
    }

这里面会调用SCC Chain中的deleteStateBatch来删除(包括)有争议的交易之后的所有状态根Batch.CTC保持不变,因此原始交易将以相同顺序重新执行。来更新删除后的SCC Chain. 最后通过 质押管理器处理质押金和奖励验证者 ovmBondManager.finalize。

好了。到这篇文章后,基本上对Optimism这个系统也有了初步认识,,可能是不对的地方。望纠正。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,540评论 1 302
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,539评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,005评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,159评论 0 180
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,946评论 1 258
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,864评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,468评论 2 273
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,218评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,070评论 6 234
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,574评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,353评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,683评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,270评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,168评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,591评论 3 210
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,638评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,035评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,590评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,685评论 2 233

推荐阅读更多精彩内容