CryptoKitties 源码解读
CryptoKitties https://www.cryptokitties.co 是一款构建在以太坊智能合约之上的交易猫咪的游戏。其热度造成了以太坊交易的拥堵。最近每天的交易量也超过了1000tx/day,区区一千比一天的交易就让以太坊网络拥堵,不禁要思考,区块链还有很长的道路要走~
其开发团队凭借CryptoKitties项目在ETHWaterloo编程马拉松中被选为八个获奖者之一。闲话少说。
源码可见 https://ethfiddle.com/09YbyJRfiI
从代码中,我们能大致得到以下结构图
####0. 合约维护(KittyAccessControl) 我们知道,基于以太坊的应用都属于DApp(去中心化应用),其最大的缺点就是维护更新成本极高,因为DApp的基本性质,决定了开发者一旦发布了应用,就很难修改,历史上比特币的第一次和第二次更新,也是冒着毁于一旦的危险进行的。 团队很聪明的加了一个KittyAccessControl合约,用于维护合约。其中包含三个特殊账号地址。这三个账号中的任意一个账号都有暂停所有交易的权限,此外,他们分别有。 #####CEO:修改新的合约地址,指派新的CFO和COO
倘若有一天团队更新了合约的内容,CryptoKitties是不可能持续运行在原有的合约之上,这时候就需要修改合约地址,指向新的规则所定义的地址。而这个CEO账号就拥有这样的权限。
#####CFO:提取游戏中玩家支付的以太币
COO通过自己的账号卖出创世猫,并且每个用户交易,交配时,都会产生手续费,CFO则有权限提取这笔费用。
#####COO:释放赠送的猫和创世猫
每只创世猫都是由COO账号卖出。
#####源码:
contract KittyAccessControl {
// This facet controls access control for CryptoKitties. There are four roles managed here:
//
// - The CEO: The CEO can reassign other roles and change the addresses of our dependent smart
// contracts. It is also the only role that can unpause the smart contract. It is initially
// set to the address that created the smart contract in the KittyCore constructor.
//
// - The CFO: The CFO can withdraw funds from KittyCore and its auction contracts.
//
// - The COO: The COO can release gen0 kitties to auction, and mint promo cats.
//
// It should be noted that these roles are distinct without overlap in their access abilities, the
// abilities listed for each role above are exhaustive. In particular, while the CEO can assign any
// address to any role, the CEO address itself doesn't have the ability to act in those roles. This
// restriction is intentional so that we aren't tempted to use the CEO address frequently out of
// convenience. The less we use an address, the less likely it is that we somehow compromise the
// account.
/// @dev Emited when contract is upgraded - See README.md for updgrade plan
event ContractUpgrade(address newContract);
// The addresses of the accounts (or contracts) that can execute actions within each roles.
address public ceoAddress;
address public cfoAddress;
address public cooAddress;
// @dev Keeps track whether the contract is paused. When that is true, most actions are blocked
bool public paused = false;
/// @dev Access modifier for CEO-only functionality
modifier onlyCEO() {
require(msg.sender == ceoAddress);
_;
}
/// @dev Access modifier for CFO-only functionality
modifier onlyCFO() {
require(msg.sender == cfoAddress);
_;
}
/// @dev Access modifier for COO-only functionality
modifier onlyCOO() {
require(msg.sender == cooAddress);
_;
}
modifier onlyCLevel() {
require(
msg.sender == cooAddress ||
msg.sender == ceoAddress ||
msg.sender == cfoAddress
);
_;
}
/// @dev Assigns a new address to act as the CEO. Only available to the current CEO.
/// @param _newCEO The address of the new CEO
function setCEO(address _newCEO) external onlyCEO {
require(_newCEO != address(0));
ceoAddress = _newCEO;
}
/// @dev Assigns a new address to act as the CFO. Only available to the current CEO.
/// @param _newCFO The address of the new CFO
function setCFO(address _newCFO) external onlyCEO {
require(_newCFO != address(0));
cfoAddress = _newCFO;
}
/// @dev Assigns a new address to act as the COO. Only available to the current CEO.
/// @param _newCOO The address of the new COO
function setCOO(address _newCOO) external onlyCEO {
require(_newCOO != address(0));
cooAddress = _newCOO;
}
/*** Pausable functionality adapted from OpenZeppelin ***/
/// @dev Modifier to allow actions only when the contract IS NOT paused
modifier whenNotPaused() {
require(!paused);
_;
}
/// @dev Modifier to allow actions only when the contract IS paused
modifier whenPaused {
require(paused);
_;
}
/// @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;
}
/// @dev Unpauses the smart contract. Can only be called by the CEO, since
/// one reason we may pause the contract is when CFO or COO accounts are
/// compromised.
/// @notice This is public rather than external so it can be called by
/// derived contracts.
function unpause() public onlyCEO whenPaused {
// can't unpause if contract was upgraded
paused = false;
}
}
####1. 猫咪属性(Kitty)
在比特币和以太币中,我们携带的数据只有数值本身,但是在智能合约中,这个数值可以是一个数据结构,这就让许多区块链应用成为了可能。
#####uint256 genes
基因列,决定了猫咪的长相,交配后,生成相关与父母的基因对,代码闭源。 一点自己的思考,经过那么久,我冥冥之中觉得,人类的基因组跟哈希加密有着类似的功能,基因携带的信息可以不断溯源,推导出最早的人类,区块链账本也是一样。总之细思极恐~
#####uint64 cooldownEndBlock
繁殖的冷却时间。
#####uint16 generation
代数,创世猫为0gen。 大部分参数跟游戏规则有关,我就不细讲了,但是基于区块链的角度看,有些人会认为区块链中定义的所有数据都是不能更改的,这样理解是有误的,应该理解成:区块链中数据传输的规则是不能更改的。例如比特币实现的是一个最简单的规则: 『sum(input1+ input2 + …) >= sum(output1 + output2 + …)』
#####源码:
/// @dev The main Kitty struct. Every cat in CryptoKitties is represented by a copy
/// of this structure, so great care was taken to ensure that it fits neatly into
/// exactly two 256-bit words. Note that the order of the members in this structure
/// is important because of the byte-packing rules used by Ethereum.
/// Ref: http://solidity.readthedocs.io/en/develop/miscellaneous.html
struct Kitty {
// The Kitty's genetic code is packed into these 256-bits, the format is
// sooper-sekret! A cat's genes never change.
uint256 genes;
// The timestamp from the block when this cat came into existence.
uint64 birthTime;
// The minimum timestamp after which this cat can engage in breeding
// activities again. This same timestamp is used for the pregnancy
// timer (for matrons) as well as the siring cooldown.
uint64 cooldownEndBlock;
// The ID of the parents of this kitty, set to 0 for gen0 cats.
// Note that using 32-bit unsigned integers limits us to a "mere"
// 4 billion cats. This number might seem small until you realize
// that Ethereum currently has a limit of about 500 million
// transactions per year! So, this definitely won't be a problem
// for several years (even as Ethereum learns to scale).
uint32 matronId;
uint32 sireId;
// Set to the ID of the sire cat for matrons that are pregnant,
// zero otherwise. A non-zero value here is how we know a cat
// is pregnant. Used to retrieve the genetic material for the new
// kitten when the birth transpires.
uint32 siringWithId;
// Set to the index in the cooldown array (see below) that represents
// the current cooldown duration for this Kitty. This starts at zero
// for gen0 cats, and is initialized to floor(generation/2) for others.
// Incremented by one for each successful breeding action, regardless
// of whether this cat is acting as matron or sire.
uint16 cooldownIndex;
// The "generation number" of this cat. Cats minted by the CK contract
// for sale are called "gen0" and have a generation number of 0. The
// generation number of all other cats is the larger of the two generation
// numbers of their parents, plus one.
// (i.e. max(matron.generation, sire.generation) + 1)
uint16 generation;
}
####2. 创造创世猫(createGen0Auction)
function _createKitty用于创造猫咪,然而创造创世猫,只有COO通过调用方法 『_createKitty(0, 0, 0, _genes, address(COO_ADDR));』 才能生成。_createKitty方法将在繁殖里面细讲。
#####源码:
/// @dev Creates a new gen0 kitty with the given genes and
/// creates an auction for it.
function createGen0Auction(uint256 _genes) external onlyCOO {
require(gen0CreatedCount < GEN0_CREATION_LIMIT);
uint256 kittyId = _createKitty(0, 0, 0, _genes, address(this));
_approve(kittyId, saleAuction);
saleAuction.createAuction(
kittyId,
_computeNextGen0Price(),
0,
GEN0_AUCTION_DURATION,
address(this)
);
gen0CreatedCount++;
}
####3. 拍卖(transferOwnership)
这是加密猫中最简单的一部分,两行代码搞定。
#####源码:
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) onlyOwner {
if (newOwner != address(0)) {
owner = newOwner;
}
}
}
好吧是三行~
####4. 繁殖(createKitty)
智能合约之所以称为区块链2.0就是因为它对比特币的脚本部分进行了拓展。比特币在每个交易过程中,都有一个通过公钥验证签名的脚本,是一种类似Forth的简单堆栈试编程语言,但是这个脚本语言是非图灵完备的。 而在以太坊体系里,开发者支付一点费用,便可以将自己的合约发布到区块链里,作为程序被人调用。每当用户进行猫咪繁殖时,这个方法就会被调用,程序执行之后的输出值,就会作为结果被打包进入区块。程序是用Solidity编写,执行的过程则是交给以太坊专用虚拟机(EVM)。
繁殖猫咪的过程,是由一系列参数通过_createKitty方法所产生,其中有:
#####uint256 _matronId
父亲id,不存在则为0,创建创世猫时候,就是传0。
#####address _owner
拥有者地址
#####源码:
function _createKitty(
uint256 _matronId,
uint256 _sireId,
uint256 _generation,
uint256 _genes,
address _owner
)
internal
returns (uint)
{
// These requires are not strictly necessary, our calling code should make
// sure that these conditions are never broken. However! _createKitty() is already
// an expensive call (for storage), and it doesn't hurt to be especially careful
// to ensure our data structures are always valid.
require(_matronId == uint256(uint32(_matronId)));
require(_sireId == uint256(uint32(_sireId)));
require(_generation == uint256(uint16(_generation)));
// New kitty starts with the same cooldown as parent gen/2
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;
// It's probably never going to happen, 4 billion cats is A LOT, but
// let's just be 100% sure we never let this happen.
require(newKittenId == uint256(uint32(newKittenId)));
// emit the birth event
Birth(
_owner,
newKittenId,
uint256(_kitty.matronId),
uint256(_kitty.sireId),
_kitty.genes
);
// This will assign ownership, and also emit the Transfer event as
// per ERC721 draft
_transfer(0, _owner, newKittenId);
return newKittenId;
}
// Any C-level can fix how many seconds per blocks are currently observed.
function setSecondsPerBlock(uint256 secs) external onlyCLevel {
require(secs < cooldowns[0]);
secondsPerBlock = secs;
}
}
####5. 基因配对合约(闭源)
这个合约的地址是由CEO通过调用setGeneScienceAddress来设定,目前闭源,其实很好理解,当前比较稀有的猫咪都卖到100+ETH,若是被图谋不轨的程序员研究出基因遗传的规则,这对不明真相的小伙伴来讲,游戏就失去了公平性。
####6. 总结
游戏一直都是技术很好的切入点,CryptoKitties算是目前以太坊落地项目中较为成熟的一个。另外希望大家通过学习能明白以下几点。
#####1. 智能合约中,猫的数据结构是如何表示的。参见第一点 #####2. 猫是如何存储在合约中的。参见第四点 #####3. 以太坊合约中的程序被调用时发生了什么。参见第四点