【CryptoKitties源碼解析】養貓的正確姿勢!

今天想介紹一個最近比較火的一個“區塊鏈”應用CryptoKitties,這個應用本質上實現的功能就是電子貓的繁殖與交易兩個功能,功能上雖然比較簡單但是再加上區塊鏈這個強大的底層技術作爲支撐,讓它在整個行業掀起了一波熱潮,甚至還導致了以太坊主網的堵塞,使得以太坊中未確認的交易數量從平常的2.5k增漲到了15k,網絡中其他的交易也都受到了極大的影響。整個項目的代碼總共2000行左右,其中還包含了詳細的註釋,使得即使沒有學過Solidity編程(例如我)的同學也能夠很容易的看懂,代碼可以通過以太坊瀏覽器(https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code)直接查看,也可以查看另外一個在線的版本(https://ethfiddle.com/09YbyJRfiI),後一個支持在線編譯調試。

High Level Overview

作爲一個資深(僞)擼貓後期患者,首先還是想先介紹一下這個項目,以便讓衆多深夜貓癮發作夜不能寐的患者找到心靈的港灣。項目官網:https://www.cryptokitties.co/,操作其實比較簡單,首先你得有以太幣,然後安裝一個Chrome插件,也就是一個eth輕量錢包,接下來就可以進入Marketplace選擇心儀的小可愛了,作爲一個交易失敗數次的過來人建議大家購買8頁以後的喵喵,因爲前面幾頁基本上都已經被人買走了,只不過交易還在等待確認,網站上更新有些延遲,這時候如果再買的話很有可能就是浪費Gas(交易費)。每隻喵都是由一個256位的整數DNA來確定的,沒有性別,任意兩隻沒有直系血緣關係的喵都是可以繁殖後代的,和父母以及兄弟姐妹則無法繁殖後代。所有的喵都是可以掛到拍賣市場上去賣的,繁殖是需要支付手續費的,其他的交易類型則只需要支付Gas(交易費)就行,不過現在網絡擁堵,大量未確認的交易依然存在,所以Gas建議設置到50-60。

按照項目中合約的繼承關係,總共有以下幾個合約:

contract KittyAccessControl
contract KittyBase is KittyAccessControl
contract KittyOwnership is KittyBase, ERC721
contract KittyBreeding is KittyOwnership
contract KittyAuction is KittyBreeding
contract KittyMinting is KittyAuction
contract KittyCore is KittyMinting
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

所以KittyCore就是最終應用的合約地址,它繼承了前面合約的所有數據和方法,下面我們就一次來看看每一個合約的具體實現。

1 KittyAccessControl:訪問控制

這個合約的主要目的是設置了三個地址:CEO/CFO/COO,以及定義了一些function modifiers(http://solidity.readthedocs.io/en/develop/contracts.html#function-modifiers)例如onlyCEO/onlyCFO/onlyCLevel等等,接下來定義了一些函數並加上了function modifier標識使得這些函數都只能由特定的角色來調用,例如凍結和解除凍結整個合約。

modifier onlyCLevel() {
    require(
        msg.sender == cooAddress ||
        msg.sender == ceoAddress ||
        msg.sender == cfoAddress
    );
    _;
}

///...

/// @dev Called by any "C-level" role to pause the contract. Used only when
///  a bug or exploit is detected and we need to limit damage.
function pause() external onlyCLevel whenNotPaused {
    paused = true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

根據代碼中的解釋,這個pause()函數原本的設計目的是隻有當程序出現漏洞時纔會暫停合約從而減少漏洞帶來的損失,暫停合約意味着停止處理所有發往該合約的交易,這也就意味着CLevel的成員具有控制項目運行的絕對權力,而不需要像一般區塊鏈中所有的決定都必須經過礦工的投票分叉來執行,所以我們所謂的許多DApp其實本質上並不像我們想象中那麼Decentralized。

2 KittyBase:存儲結構

這個合約定義了每隻喵所包含的基本的屬性、合約運行過程中的數據存儲變量(每隻喵的主人,掛上拍賣場的喵,等待交配的喵等)以及一些基本操作函數(例如喵的屬權轉移,新喵的誕生)。

首先定義了喵的基本屬性,

struct Kitty {
    uint256 genes; // 基因
    uint64 birthTime; // 出生區塊的時間戳
    uint64 cooldownEndBlock; // 再次繁殖的區塊號
    uint32 matronId; // 母親的ID
    uint32 sireId; // 父親的ID
    uint32 siringWithId; // 如果正在繁殖期那麼就是當前交配對象的ID,否則爲0
    uint16 cooldownIndex; // 繁殖冷卻時間
    uint16 generation; // 第幾代
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

喵是沒有性別的,屬性中父母是看誰生下的新喵。所有的喵都是通過genes來決定外表的,而這個genes又是通過一個非開源的庫來產生的,避免通過父母的基因來直接推測出新喵的基因,從而減少fancy cat的比例。另外值得注意的一點是,所有喵的外表都是通過前端的web服務器來解析的,也就是說開發人員可以隨意定義哪些喵是fancy cat,並且一旦web服務器崩潰那麼所有的喵除了cooldownIndex之外將沒有任何區別。這也從側面反應了區塊鏈應用的一個缺陷——不可能將所有的應用數據都存儲再區塊鏈上,因爲鏈上存儲數據的代價太高了,所以未來如果要開發真正的完全去中心化的應用,如何將這些數據的存儲也去中心化是一個值得考慮的問題。

/*** STORAGE ***/

    /// 保存所有的喵的ID
    Kitty[] kitties;

    /// 所有喵的ID到owner的地址的映射
    mapping (uint256 => address) public kittyIndexToOwner;

    // owner地址到owner的token數的映射
    mapping (address => uint256) ownershipTokenCount;

    /// 待出售的喵ID到owner地址的映射
    mapping (uint256 => address) public kittyIndexToApproved;

    /// 待交配的喵的ID到owner的地址的映射
    mapping (uint256 => address) public sireAllowedToAddress;

    /// 拍賣合約的地址,處理用戶之間的交易和每隔15分鐘系統生成的gen0代喵
    SaleClockAuction public saleAuction;

    /// 交配的合約地址,和上面拍賣的不同因爲兩者的處理方式不同
    SiringClockAuction public siringAuction;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

這裏定義了合約運行中所有的存儲數據結構,可以看出總共存儲的變量也並不多。接下來又定義了兩個函數_transfer()_createKitty()用來轉移喵的屬權和創建新喵,這兩個函數都只能從內部調用,

    /// 一個內部函數用來創建新的喵然後保存起來,這個函數假設所有的輸入數據都是有效的,
    /// 函數最後將觸發一個Brith和Transfer事件。
    function _createKitty(
        uint256 _matronId, // 母親的ID,不存在則爲0
        uint256 _sireId,  // 父親的ID,不存在則爲0
        uint256 _generation, // 當前第幾代,由調用者計算
        uint256 _genes, // 基因
        address _owner // 擁有者
    )
        internal
        returns (uint)
    {
        // 保證數據都在正常範圍內
        require(_matronId == uint256(uint32(_matronId)));
        require(_sireId == uint256(uint32(_sireId)));
        require(_generation == uint256(uint16(_generation)));

        // 新喵的cooldown是由generation/2來決定
        // generation = max(motherGeneration, fatherGeneration)+1
        uint16 cooldownIndex = uint16(_generation / 2);
        if (cooldownIndex > 13) {
            cooldownIndex = 13;
        }

        // 創建新喵
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(now),
            cooldownEndBlock: 0,
            matronId: uint32(_matronId),
            sireId: uint32(_sireId),
            siringWithId: 0,
            cooldownIndex: cooldownIndex,
            generation: uint16(_generation)
        });
        uint256 newKittenId = kitties.push(_kitty) - 1; // 保存新喵ID

        // 確保總喵的個數小於2^32
        require(newKittenId == uint256(uint32(newKittenId)));

        // 觸發Birth事件
        Birth(
            _owner,
            newKittenId,
            uint256(_kitty.matronId),
            uint256(_kitty.sireId),
            _kitty.genes
        );

        // 分配新喵屬權
        _transfer(0, _owner, newKittenId);

        return newKittenId;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

上述函數展示了創建新喵的詳細過程,主要就是計算新喵的generation,然後根據generation分配相應的屬性。

3 KittyOwnership:屬權

這部分主要實現的是將喵的ID和實際的以太坊地址進行對應起來,實現的函數包括_owns()transfer()等等涉及屬權獲取或者轉換的操作,實現操作相對較爲簡單。

4 KittyBreeding:繁殖

這個合約包含了兩隻喵一起繁殖下一代喵的必要的步驟,繁殖依賴一個外部的基因組合合約(geneScience),但是這個合約卻不是開源的,並且這個合約的地址是由CEO通過調用setGeneScienceAddress來設定的,也就是說CEO可以隨意更改基因組合的方法。

    function setGeneScienceAddress(address _address) external onlyCEO {
        GeneScienceInterface candidateContract = GeneScienceInterface(_address);
        require(candidateContract.isGeneScience());
        geneScience = candidateContract;
    }
  • 1
  • 2
  • 3
  • 4
  • 5

兩隻喵繁殖需要調用breedWithAuto(),首先會檢查一系列條件,例如繁殖費用是否足夠、雙方是否都允許、雙方是否沒有直系關係等等,這些條件都滿足以後調用內部函數_breedWith()來進行繁殖,同時改變雙方的生殖狀態並觸發_triggerCooldown()來改變下次生殖的冷卻時間,在_breedWith()函數最後觸發Pregnant()事件,而web前端的繁殖界面就是通過監聽Pregnant()事件來進行修改的,同時在時間到達時調用giveBirth()來產生新的喵。

function giveBirth(uint256 _matronId)
        external
        whenNotPaused
        returns(uint256)
    {
        // 通過母親的ID獲取對象
        Kitty storage matron = kitties[_matronId];

        // 通過birthTime驗證是否是合法的喵
        require(matron.birthTime != 0);

        // 檢查母親是否該生了
        require(_isReadyToGiveBirth(matron));

        // 獲取父親的對象
        uint256 sireId = matron.siringWithId;
        Kitty storage sire = kitties[sireId];

        // 計算父母的generation大的一個
        uint16 parentGen = matron.generation;
        if (sire.generation > matron.generation) {
            parentGen = sire.generation;
        }

        // 傳入父母基因,調用外部基因組合函數,得到新喵的基因
        uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1);

        // 新喵的owner設置爲母親的owner
        address owner = kittyIndexToOwner[_matronId];
        uint256 kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner);

        // 清除母親的交配對象,使得可以再次繁殖
        delete matron.siringWithId;

        // 懷孕的喵數減一
        pregnantKitties--;

        // 將費用發送給父親
        msg.sender.send(autoBirthFee);

        // 返回新喵的ID
        return kittenId;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

5 KittyAuction:拍賣

拍賣合約包括買、賣以及繁殖,繁殖指的是提供一方作爲父親,拿取繁殖費用並由母方獲取繁殖的新喵。根據開發者所描述的,他們將拍賣合約分成幾個子合約,因爲“其中邏輯比較複雜,總是存在bug的風險,將每個過程分成單獨的一個合約,我們就可以在不改變主合約的情況下升級子合約。”因此這個合約提供了兩個函數setSaleAuctionAddress()setSiringAuctionAddress用來設置子合約的地址,以便更方便的升級子合約。從安全性來講這的確有利於代碼的修復與升級,但是從另外一個角度來講,合約中的CEO就有了任意更改拍賣規則的權利。

6 KittyMinting:初代喵

這部分合約規定了有合約生成的初代喵的個數,合約中是通過硬編碼的形式寫入的,

// Limits the number of cats the contract owner can ever create.
    uint256 public constant PROMO_CREATION_LIMIT = 5000;
    uint256 public constant GEN0_CREATION_LIMIT = 45000;
  • 1
  • 2
  • 3

其中5000指的是用來促銷的喵的數量,45000指的是合約產生初代喵的限制,產生的過程是分別通過調用createPromoKitty()createGen0Auction(),通過createGen0Auction()產生的喵會被直接掛到拍賣場上,價格是通過過去5只初代喵的平均價格*1.5來作爲初始價格。這兩個函數都只能由COO來調用,並且創建是可以直接傳入基因,也就是說COO可以將任意一直喵複製出來最多5000份,所以你覺得獨一無二的喵也許並不像你想象中那麼獨特。

7 KittyCore:主合約

主合約的作用是控制合約的運行與更新,它繼承了以上所有的合約,所以也就包括了以上所有的存儲結構和函數。合約通過變量paused來控制停止與運行,通過setNewAddress()函數來設置合約更新的地址,通過unpaused()函數來啓動合約。同時還包括兩個外部調用函數getKitty()withdrawBalance(),其中getKitty()是用來讀取每隻喵的所有屬性,應該是應用於web server的後臺;而withdrawBalance()則是用於提取合約中所有的餘額,但是要保留pregnant kitties繁殖的費用,因爲這需要從外部調用giveBirth()函數。

總結

如果從實際運行效果來看的話,不得不說這個項目非常的成功,至少到目前爲止還佔據了網絡的一大部交易量,作爲一個區塊鏈應用,也的確是能夠把握住時代的潮流,但是從上述代碼來看還存在幾個問題:

  • 所有的喵本質上只是一個256位的整數,解釋權都歸前端所有;
  • CEO具有控制應用運行的絕對權利,可以無條件暫停合約或者更新子合約;
  • 開源的合約卻又調用了非開源的合約(gene science)。

最後一個可能不能稱之爲問題,但是從我個人來講開源就應該全部開源,如果能gene science這部分替換爲開源的隨機基因組合可能會更好,至少不會讓其他人覺得這其中有什麼貓膩。但是前面兩個問題卻是的的確確存在的,所以從技術上來講這並不能完全算是個區塊鏈應用,但卻是個不錯且成功的嘗試!

參考資料

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