智能合约分析 -- 以太猫之如何表示数字资产

在以太猫这个应用中,其中的每一只以太猫都被看成了一种资产,就如世界名画,邮票,古董这种收藏品一样,不过不同的是以太猫这种资产是完全数字化的,所以我们一般称其为数字资产(digital asset)。数字资产并不是一个陌生的概念,大部分人都已经接触他们很久,各种游戏中的装备,各种积分,平台中的各种金币等等都是一种数字资产。在以前,数字资产通常是由一家公司直接发放,而有了区块链技术之后,就让数字资产可以不依托于特定的公司,直接在区块链上进行发行,交易变得更为容易。

到底一只以太猫是什么

在以太猫的 KittyBase 合约中,下边的代码就用于表示一只以太猫。

struct Kitty {
    uint256 genes;
    uint64 birthTime;
    uint64 cooldownEndBlock;
    uint32 matronId;
    uint32 sireId;
    uint32 siringWithId;
    uint16 cooldownIndex;
    uint16 generation;
}

基本上就是一系列整数的集合。每个整数是什么意思,从名字基本也就能看出来。比如 birthTime 指的就是这只以太猫诞生的时间。其中需要特别说明的是 genes 这个属性,genes 是用来表示一只以太猫的基因特征。换句话说,就是这个属性的不同让以太猫有了不一样的外观,也是这个属性决定了一只以太猫的稀有程度。举个例子,如果现在全网有 100 只以太猫被孕育出来,其中有 99 只的基因代码为 1,而只有 1 只的基因代码为 2,那么很显然,这只基因代码为 2 的以太猫就比基因代码为 1 的要稀有得多。

刚才我们提到全网有 100 只以太猫,在合约中又是怎么来表示这一点呢?同样在 KittyBase 合约中,有下边这一行简单的代码。

Kitty[] kitties;

就是这个简单的数组,表示了全网所有的以太猫。区块链虽然是分布式的,但是有意思的是,合约的状态却是全网统一,所以只需要一个简单的数组,就可以保存所有已经产生的以太猫,同时也不会有任何的数据不同步状态。之后只要在生成新的以太猫的方法里边,把对应的以太猫直接加的这个数组中就完成了以太猫的登记工作。

唯一标识一只以太猫

如果我们看一下以太猫的各种属性,会发现没有任何一个属性来唯一标识一只以太猫。而通常在一般的程序中都会有一个类似 kittyId 的属性来区分不同的猫。以太猫的开发者用了一个很巧妙的办法。那就是直接使用 Kitty[] kitties 中的数组的索引作为了以太猫的唯一标识。由于全网的以太猫都被存储在了这个唯一的数组中,所以数组的索引反而是一个非常好的标识。

这个唯一标识的存在,让我们能够对两只以太猫进行区分。这就涉及到了数字资产中的一个非常重要的概念—可替换性(fungible)。所谓可替换性,就是指两份数字资产之间是否需要进行区分。比如某种平台发行的平台金币,两枚金币之间其实没有任何区别,把我的 100 枚金币给你,然后把你的 100 枚金币给我,完全不会影响我们两的资产情况,所以说是可替换的。但是以太猫就不一样,两只以太猫是完全不同的个体,把我手里的以太猫给你,然后把你手里的以太猫给我,除非正好这两只以太猫的基因代码一致,不然结果将会极大的影响我们之间的资产情况。所以以太猫这种资产被称为不可以替换资产(non-fungible)。而这个唯一标识就让不同的以太猫成为了完全不同的个体,也就实现了不可替换性。

谁拥有一只以太猫

既然作为数字资产,那么就必然得有拥有者的概念。目前我们所看到的代码中,只知道了哪些数据表示了一只以太猫,但是这只以太猫属于谁并不知道。为了实现这一点,在 KittyBase 中有如下代码。

mapping (uint256 => address) public kittyIndexToOwner;

mapping 是以太坊智能合约中的一种数据类型,和我们最常见的 hashtable 类似,存储的是从一个属性到另一个属性的对应关系。在以太猫中,就是用这个 mapping 来表示了以太猫的拥有者概念。其中会把以太猫的唯一标识,也就是前边所说的以太猫在全网以太猫数组中的索引,对应到一个以太坊中的交易地址。当需要确认拥有权的时候,就去这个 mapping 中查询对应关系。

如何交易一只以太猫

如果不能交易,那么以太猫就很难被称为一种完整的数字资产。而以太猫的疯狂某种程度上也是因为他的可交易性导致。当我们能够标识一只以太猫,同时又能够标识以太猫的拥有者,再来实现交易也就没有太大的难度,只需要实现一个拥有权的转移即可。当一个交易发生,修改 kittyIndexToOwner 中的以太猫标识到交易地址的对应关系,也就完成了以太猫的拥有权的转移。

不过以太猫使用了一种更好的方法,那就是使用了 ERC721 这个规范。ERC721 是社区所认可的一种规范,听起来复杂,其实所谓规范也就是一组方法的名字,只要你实现这一组方法就可以认为你遵循了该规范。无非就是你在做交易这个功能的时候不要给方法乱起名字,直接按这个规范中的要求去取名字就可以。付出这点小小的不自由,得到的的好处则不少,比如如果有第三方要扩展以太猫的功能,完全就可以通过这组大家都认可的方法去管理以太猫的资产,实现的成本会降低不少。甚至可以用管理其他 ERC721 资产的办法来管理以太猫。

通常实现了 ERC721 之后,我们就会称其为 ERC721 代币(token)。具体看一下,ERC721 的规范如下。

/// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens
/// @author Dieter Shirley <dete@axiomzen.co> (https://github.com/dete)
contract ERC721 {
    // Required methods
    function totalSupply() public view returns (uint256 total);
    function balanceOf(address _owner) public view returns (uint256 balance);
    function ownerOf(uint256 _tokenId) external view returns (address owner);
    function approve(address _to, uint256 _tokenId) external;
    function transfer(address _to, uint256 _tokenId) external;
    function transferFrom(address _from, address _to, uint256 _tokenId) external;

    // Events
    event Transfer(address from, address to, uint256 tokenId);
    event Approval(address owner, address approved, uint256 tokenId);

    // Optional
    // function name() public view returns (string name);
    // function symbol() public view returns (string symbol);
    // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds);
    // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl);

    // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165)
    function supportsInterface(bytes4 _interfaceID) external view returns (bool);
}

其实也没有什么特别神秘的地方。比如其中的 transfer 方法就是用于拥有权的转移,作为以太猫的实现,就像我们之前所说,在这里边把以太猫到交易地址的对应关系进行修改即可。

把以太猫看成一种 ERC721 代币是一个非常有意思的行为。这也是区块链社区很有价值的部分。大家在交易,资产管理这类功能上都遵从一套统一的标准。结果就是虽然是不同的合约,但是由于遵循了同一份标准,那么第三方也就能够开发应用来进行统一管理,甚至复用这些资产。而这在传统的项目中是很难看到的,举个例子,在一个游戏中所获得的稀有装备和在另一个地方获得的稀有数字卡片,这两者完全没有关系。但是在区块链世界中,如果他们都实现了同一个代币标准,那就有可能很方便的实现互相交换的功能,光这一点就充满了不小的想象空间。

写在最后

以太猫作为 DApps 的开先河者,非常高兴看到有人能在区块链世界作出这种有意义的探索。分析以太猫的合约代码也是一种有意思的体验。以太猫的系列也到此暂告一段落。以后我会继续进行其他智能合约的分析。敬请期待。