【CryptoKitties源码解析】养猫的正确姿势!

【CryptoKitties源码解析】养猫的正确姿势!_第1张图片

今天想介绍一个最近比较火的一个“区块链”应用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

所以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;
}

根据代码中的解释,这个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; // 第几代
}

喵是没有性别的,属性中父母是看谁生下的新喵。所有的喵都是通过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;

这里定义了合约运行中所有的存储数据结构,可以看出总共存储的变量也并不多。接下来又定义了两个函数_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;
    }

上述函数展示了创建新喵的详细过程,主要就是计算新喵的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;
    }

两只喵繁殖需要调用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;
    }

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;

其中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这部分替换为开源的随机基因组合可能会更好,至少不会让其他人觉得这其中有什么猫腻。但是前面两个问题却是的的确确存在的,所以从技术上来讲这并不能完全算是个区块链应用,但却是个不错且成功的尝试!

参考资料

  • https://medium.com/loom-network/your-crypto-kitty-isnt-forever-why-dapps-aren-t-as-decentralized-as-you-think-871d6acfea
  • https://medium.com/loom-network/how-to-code-your-own-cryptokitties-style-game-on-ethereum-7c8ac86a4eb3

你可能感兴趣的:(【CryptoKitties源码解析】养猫的正确姿势!)