Plasma Cash 合約解讀

作者介紹

蟲洞社區·簽約作者 steven bai

Plasma Cash 合約解讀

此文來自 SmartMesh 團隊,轉載請聯繫作者。

Plasma 由 V 神在2017年8月提出,希望通過鏈下交易來大幅提高以太坊的 TPS.

每條 Plasma 鏈都會將有關交易順序的消息換算成一個哈希值存儲在根鏈上。比特幣和以太坊都屬於根鏈——這兩條區塊鏈具有很高的安全性,並且通過去中心化保證了(安全性和活性)。

Plasma 設計模型有兩個主要的分支:Plasma MVP 和 Plasma Cash 。這裏我們來研究 SmartPlasma 實現的 Plasma Cash 合約,並通過合約分析來回答大家關於 Plasma Cash 的一系列疑問.

1. 合約代碼

SmartPlasma的合約代碼肯定會不斷升級,我針對他們在今天(2018-09-14)最新版本進行分析,這份代碼目前保存在我的 github 上 plasma cash.

2. 合約文件簡單介紹

文件夾中有不少與 Plasma Cash 無關的合約,這裏只關注直接與 Plasma Cash 相關合約,像 ERC20Token 相關合約就忽略,自行查看.

  • Mediator.sol 是 Plasma Cash 鏈中資產的進出口
  • RootChain.sol 處理 Plasma Cash 子鏈(相對於以太坊而言)中的交易以及打包等
  • libraries/MerkleProof.sol 是子鏈中交易用到的默克爾樹,用於子鏈參與方進行欺詐證明 fraud proof.
  • libraris/RLP.sol RLP編碼支持,可以暫時忽略,子鏈中所有的交易都是用RLP 編碼的.
  • libraries/PlasmaLib.sol 生成 uid 的輔助函數
  • ECRecovery.sol 簽名驗證
  • datastructures/Transaction.sol 描述交易的數據結構

3. Plasma Cash 的基礎數據結構

Plasma Cash 是一種子鏈結構,可以認爲 Plasma Cash 是以太坊的一個是基於 =一種簡化的UTXO模型的子鏈.

3.1 Plasma Cash 中的資產

Plasma Cash 中的資產都來自於以太坊,但是一旦進入 Plasma Cash 就會擁有唯一的 ID,並且不可分割.
可以參考 Mediator.sol的deposit函數. Mediator就是 Plasma Cash 資產存放的地方.

        /** @dev Adds deposits on Smart Plasma.
     *  @param currency Currency address.
     *  @param amount Amount amount of currency.
     */
    function deposit(address currency, uint amount) public {
        require(amount > 0);

        Token token = Token(currency);
        token.transferFrom(msg.sender, this, amount); /// deposit test1

        bytes32 uid = rootChain.deposit(msg.sender, currency, amount); /// deposit test2
        cash[uid] = entry({
            currency: currency,
            amount: amount
        });
    }

通過合約可以看出進入 Plasma Cash 的資產必須是 ERC20 Token,這些資產實際上是存在 Mediator 這個合約上,然後由 RootChain 爲其分配一個唯一的 ID, 也就是 uid. 這個 uid 代表着什麼 token, 有多少個.

3.2 Plasma Cash中的交易

關鍵代碼在 Transaction.sol中.

    struct Tx {
        uint prevBlock;
        uint uid;
        uint amount;
        address newOwner;
        uint nonce;
        address signer;
        bytes32 hash;
    }

這裏可能不太明顯,需要解釋才能看出來這是一個 UTXO 交易的模型. 這裏面的amount 和 hash 實際上都有點囉唆,可以忽略. 那麼剩下的成員需要來解釋.

prevBlock就是 UTXO 中的輸入,來自於哪塊. 至於爲什麼沒有像比特幣一樣的OutPoint 結構,也就是 TxHash+Index, 後續會講到.
uid 就是交易的資產 ID
newOwner 交易輸出給誰, 這裏也不支持像 比特幣一樣的腳本.
nonce 是這筆資產的第多少次交易,在雙花證明中有重要作用.
signer必須由資產原擁有者的簽名.

amount 不重要,是因爲資產不可分割,導致這裏的 Amount 不會隨交易發生而發生變化. 而 hash 則是可以直接計算出來.

3.3 Plasma Cash 中的 Block

如果一般區塊鏈中的 Block 一樣,他是交易的集合.但是不同於一般鏈的是,這裏面的礦工(不一定是 Operator)不僅需要維護好子鏈,還需要週期性的將每一個 Block 對應的默克爾樹根保存到以太坊中,這個工作只能有 Operator 來完成.
具體代碼可見 RootChain.sol的.

    function newBlock(bytes32 hash) public onlyOperator {
        blockNumber = blockNumber.add(uint256(1));
        childChain[blockNumber] = hash;

        NewBlock(hash);
    }

交易證據提交者只能是 Operator, 也就是合約的創建者. 這個 Operator 既可以是普通賬戶,這時他就是這個子鏈的管理員.也可以是一份合約,那麼就可以通過合約來規定子鏈的出塊規則.

3.4 Plasma Cash 中資產的迴歸主鏈以太坊

當資產在 Plasma 中交易一段時間以後,持有者Bob如果想退出Plasma Cash 子鏈,那麼就需要向以太坊合約也就是 RootChain證明,他確實擁有這一筆資產.

3.4.1 資產擁有證明

這個思路和 UTXO 的思路是一樣的,Bob能證明這筆資產是從哪裏轉給我的即可.具體見[RootChain.sol]()中的startExit函數. 其思路非常簡單,證明

  • 這筆資產來自哪裏(在哪 M塊中轉移到了 Alice 手中)
  • 經過 Alice 簽名轉移給了Bob(在N塊中 Alice 做了簽名給我)
    具體看代碼 startExit

    /** @dev Starts the procedure for withdrawal of the deposit from the system.
     *  @param previousTx Penultimate deposit transaction.
     *  @param previousTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block.
     *  @param previousTxBlockNum The number of the block in which the penultimate transaction is included.
     *  @param lastTx Last deposit transaction.
     *  @param lastTxProof Proof of inclusion of a last transaction in a Smart Plasma block.
     *  @param lastTxBlockNum The number of the block in which the last transaction is included.
     */
    function startExit(
        bytes previousTx,
        bytes previousTxProof,
        uint256 previousTxBlockNum,
        bytes lastTx,
        bytes lastTxProof,
        uint256 lastTxBlockNum
    )
        public
    {
        Transaction.Tx memory prevDecodedTx = previousTx.createTx();
        Transaction.Tx memory decodedTx = lastTx.createTx();
        // 證明在 prevBlock的時候 Alice 擁有資產 uid
        require(previousTxBlockNum == decodedTx.prevBlock);
        require(prevDecodedTx.uid == decodedTx.uid);
        //amount 不變,證明資產不可分割
        require(prevDecodedTx.amount == decodedTx.amount);
        //Alice 確實簽名轉移給了我,並且交易是相鄰的兩筆交易
        require(prevDecodedTx.newOwner == decodedTx.signer);
        require(decodedTx.nonce == prevDecodedTx.nonce.add(uint256(1))); //緊挨着的兩筆交易
        //我是 Bob, 我要來拿走這筆資產
        require(msg.sender == decodedTx.newOwner);
        require(wallet[bytes32(decodedTx.uid)] != 0);
    
        bytes32 prevTxHash = prevDecodedTx.hash;
        bytes32 prevBlockRoot = childChain[previousTxBlockNum];
        bytes32 txHash = decodedTx.hash;
        bytes32 blockRoot = childChain[lastTxBlockNum];
    
        require(
            prevTxHash.verifyProof(
                prevDecodedTx.uid,
                prevBlockRoot,
                previousTxProof
            )
        );
        require(
            txHash.verifyProof(
                decodedTx.uid,
                blockRoot,
                lastTxProof
            )
        );
    
        /// Record the exit tx.
        require(exits[decodedTx.uid].state == 0);
        require(challengesLength(decodedTx.uid) == 0);
    
        exits[decodedTx.uid] = exit({
            state: 2,
            exitTime: now.add(challengePeriod),
            exitTxBlkNum: lastTxBlockNum,
            exitTx: lastTx,
            txBeforeExitTxBlkNum: previousTxBlockNum,
            txBeforeExitTx: previousTx
        });
    
        StartExit(prevDecodedTx.uid, previousTxBlockNum, lastTxBlockNum);
    }

    代碼的前一半都是在用來證明在lastTxBlockNum的時候,資產 uid 歸Bob所有.
    然後後一半就是提出來,Bob想把資產 uid 提走. 我的這個想法會暫時保存在合約中,等待別人來挑戰.

3.4.2 等待其他人來挑戰我

有了以上信息, 就可以證明在 N 塊時,這筆資產歸Bob所用.但是這肯定不夠,無法證明現在資產仍然屬於Bob,也無法證明Alice 沒有在 M 塊以後再給別人.
更加不能證明在 M 塊的時候 Alice 真的是 uid 的擁有者?
這些問題,看起來很難回答,其實思路也很簡單.
這個思路和雷電網絡中解決問題的辦法是一樣的, 讓這筆資產的利益攸關者站出來舉證.
比如: 如果 Carol能夠舉證這筆資產Bob 後來又轉移給了 Carol, 那麼實際上 Bob 就是在雙花.
具體的挑戰以及迎戰代碼比較複雜,但是這也是 Plasma Cash 的核心安全性所在.如果沒有這些,所有的參與者都將無法保證自己的權益.

//challengeExit 挑戰資產uid 其實不屬於 Bob
  /** @dev Challenges a exit.
     *  @param uid Unique identifier of a deposit.
     *  @param challengeTx Transaction that disputes an exit.
     *  @param proof Proof of inclusion of the transaction in a Smart Plasma block.
     *  @param challengeBlockNum The number of the block in which the transaction is included.
     */
    function challengeExit(
        uint256 uid,
        bytes challengeTx,
        bytes proof,
        uint256 challengeBlockNum
    )
        public
    {
        require(exits[uid].state == 2);

        Transaction.Tx memory exitDecodedTx = (exits[uid].exitTx).createTx();
        Transaction.Tx memory beforeExitDecodedTx = (exits[uid].txBeforeExitTx).createTx();
        Transaction.Tx memory challengeDecodedTx = challengeTx.createTx();

        require(exitDecodedTx.uid == challengeDecodedTx.uid);
        require(exitDecodedTx.amount == challengeDecodedTx.amount);

        bytes32 txHash = challengeDecodedTx.hash;
        bytes32 blockRoot = childChain[challengeBlockNum];

        require(txHash.verifyProof(uid, blockRoot, proof));

        // test challenge #1 & test challenge #2 最後一筆交易後面又進行了其他交易, Bob 在進行雙花
        if (exitDecodedTx.newOwner == challengeDecodedTx.signer &&
        exitDecodedTx.nonce < challengeDecodedTx.nonce) {
            delete exits[uid];
            return;
        }

        // test challenge #3, 雙花了,  Alice 給了兩個人,並且挑戰者 Carol的BlockNumer 更小,也就是發生的更早.
        if (challengeBlockNum < exits[uid].exitTxBlkNum &&
            (beforeExitDecodedTx.newOwner == challengeDecodedTx.signer &&
            challengeDecodedTx.nonce > beforeExitDecodedTx.nonce)) {
            delete exits[uid];
            return;
        }

        // test challenge #4   在 M塊之前,還有一筆交易,Alice 需要證明自己在 M 塊確實擁有 uid
        if (challengeBlockNum < exits[uid].txBeforeExitTxBlkNum ) {
            exits[uid].state = 1;
            addChallenge(uid, challengeTx, challengeBlockNum);
        }

        require(exits[uid].state == 1);

        ChallengeExit(uid);
    }

//Bob應戰,再次舉證,實際上這個過程就是要不斷的追加證據,將所有的交易連起來,最終證明 Alice 在 M塊確實擁有 uid
 /** @dev Answers a challenge exit.
     *  @param uid Unique identifier of a deposit.
     *  @param challengeTx Transaction that disputes an exit.
     *  @param respondTx Transaction that answers to a dispute transaction.
     *  @param proof Proof of inclusion of the respond transaction in a Smart Plasma block.
     *  @param blockNum The number of the block in which the respond transaction is included.
     */
    function respondChallengeExit(
        uint256 uid,
        bytes challengeTx,
        bytes respondTx,
        bytes proof,
        uint blockNum
    )
        public
    {
        require(challengeExists(uid, challengeTx));
        require(exits[uid].state == 1);

        Transaction.Tx memory challengeDecodedTx = challengeTx.createTx();
        Transaction.Tx memory respondDecodedTx = respondTx.createTx();

        require(challengeDecodedTx.uid == respondDecodedTx.uid);
        require(challengeDecodedTx.amount == respondDecodedTx.amount);
        require(challengeDecodedTx.newOwner == respondDecodedTx.signer);
        require(challengeDecodedTx.nonce.add(uint256(1)) == respondDecodedTx.nonce);
        require(blockNum < exits[uid].txBeforeExitTxBlkNum);

        bytes32 txHash = respondDecodedTx.hash;
        bytes32 blockRoot = childChain[blockNum];

        require(txHash.verifyProof(uid, blockRoot, proof));

        removeChallenge(uid, challengeTx);

        if (challengesLength(uid) == 0) {
            exits[uid].state = 2;
        }

        RespondChallengeExit(uid);
    }

3.4.3 挑戰期過了, Bob 拿回資產 uid

挑戰期過後,Bob 在Mediator.sol 中提出將資產退回到以太坊中

 /** @dev withdraws deposit from Smart Plasma.
     *  @param prevTx Penultimate deposit transaction.
     *  @param prevTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block.
     *  @param prevTxBlkNum The number of the block in which the penultimate transaction is included.
     *  @param txRaw lastTx Last deposit transaction.
     *  @param txProof Proof of inclusion of a last transaction in a Smart Plasma block.
     *  @param txBlkNum The number of the block in which the last transaction is included.
     */
    function withdraw(
        bytes prevTx,
        bytes prevTxProof,
        uint prevTxBlkNum,
        bytes txRaw,
        bytes txProof,
        uint txBlkNum
    )
        public
    {
        bytes32 uid = rootChain.finishExit(
            msg.sender,
            prevTx,
            prevTxProof,
            prevTxBlkNum,
            txRaw,
            txProof,
            txBlkNum
        );

        entry invoice = cash[uid];

        Token token = Token(invoice.currency);
        token.transfer(msg.sender, invoice.amount); /// 真正的資產轉移

        delete(cash[uid]); 
    }

RootChain 再次驗證

 /** @dev Finishes the procedure for withdrawal of the deposit from the system.
     *       Can only call the owner. Usually the owner is the mediator contract.
     *  @param account Account that initialized the deposit withdrawal.
     *  @param previousTx Penultimate deposit transaction.
     *  @param previousTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block.
     *  @param previousTxBlockNum The number of the block in which the penultimate transaction is included.
     *  @param lastTx Last deposit transaction.
     *  @param lastTxProof Proof of inclusion of a last transaction in a Smart Plasma block.
     *  @param lastTxBlockNum The number of the block in which the last transaction is included.
     */
    function finishExit(
        address account,
        bytes previousTx,
        bytes previousTxProof,
        uint256 previousTxBlockNum,
        bytes lastTx,
        bytes lastTxProof,
        uint256 lastTxBlockNum
    )
        public
        onlyOwner
        returns (bytes32)
    {
        Transaction.Tx memory prevDecodedTx = previousTx.createTx();
        Transaction.Tx memory decodedTx = lastTx.createTx();

        require(previousTxBlockNum == decodedTx.prevBlock);
        require(prevDecodedTx.uid == decodedTx.uid);
        require(prevDecodedTx.amount == decodedTx.amount);
        require(prevDecodedTx.newOwner == decodedTx.signer);
        require(account == decodedTx.newOwner);

        bytes32 prevTxHash = prevDecodedTx.hash;
        bytes32 prevBlockRoot = childChain[previousTxBlockNum];
        bytes32 txHash = decodedTx.hash;
        bytes32 blockRoot = childChain[lastTxBlockNum];

        require(
            prevTxHash.verifyProof(
                prevDecodedTx.uid,
                prevBlockRoot,
                previousTxProof
            )
        );

        require(
            txHash.verifyProof(
                decodedTx.uid,
                blockRoot,
                lastTxProof
            )
        );

        require(exits[decodedTx.uid].exitTime < now); //挑戰期過了
        require(exits[decodedTx.uid].state == 2); //並且沒有人挑戰或者我都給出了合適的證據
        require(challengesLength(decodedTx.uid) == 0);

        exits[decodedTx.uid].state = 3;

        delete(wallet[bytes32(decodedTx.uid)]);

        FinishExit(decodedTx.uid);

        return bytes32(decodedTx.uid);
    }

4. Plasma Cash 中的退出示例

Plasma Cash 合約解讀

5. 其他問題

    1. 爲什麼 Plasma Cash 中的資產是不可分割的?
      進入 rootchain 的資產類似於比特幣的 UTXO 模型,但是是不可分割的,這個可以通過交易驗證時每次都要求 amount 不變可以得出.
    1. operator 的作用是什麼
      operator 負責將子鏈中的交易證據(默克爾樹)提交證明到以太坊主鏈中.
    1. operator 是否必須可信任的?
      是的. 雖然 operator 不能將他人資產隨意轉移,但是卻可以阻止他人資產轉移.也就是說無法從 Plasma 子鏈中退回到以太坊中.
      當然這部分是可以改進,降低 operator 作惡帶來的風險.
    1. operator 是否可以是一個合約呢?
      是的. 如果 operator 是一個 Pos 共識合約,那麼可以降低問題3中的風險

添加微信(cdong1024),加入區塊鏈開發者技術交流羣

蟲洞社區:https://www.uzanapp.com/

蟲洞社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章