В первых числах декабря 2017 года, пользователи блокчейн-проекта Ethereum столкнулись с неприятным открытием — любые их транзакции просто перестали подтверждаться. Фактически, вся сеть перестала функционировать из-за неожиданно разросшегося в размерах мемпула.
Совсем скоро стало понятно в чем же дело — во всем виноват оказался проект CryptoKitties. Это забавная игрушка, работающая на блокчейне Ethereum и позволяющая пользователям разводить «котят», скрещивать их и продавать как обычные критовалютные токены. В какой-то момент 15% всех транзакций в Ethereum приходились на криптокотят! А к моменту написания этой статьи, игроки потратили на котят уже 23 миллиона долларов!
Так что я просто не мог пройти мимо такой интересной темы и решил рассказать, как же сделать игру такого рода. В этой статье (и ее продолжении) будет подробно описано, как можно создать похожий проект на Ethereum, в первую очередь с помощью разбора кода оригинального контракта CryptoKitties.
Исходный код игры CryptoKitties
Почти весь код игры CryptoKitties является открытым, поэтому лучший способ понять, как он работает — прочитать исходники.
Код насчитывает примерно 2000 строк, так что в этой статье пробежимся только по самым важным его частям. Но если вы хотите самостоятельно прочитать исходный код, вот его копия, причем сразу в online IDE EthFiddle.
Общий обзор
Если вы в первый раз слышите об игре CryptoKitties, то знайте, что в ней игроки продают, покупают и разводят цифровых котиков. Каждый котик уникален, его внешний вид определяется генами. Когда вы скрещиваете двух котиков, их гены уникальным образом объединяются и получается совершенно особенный котенок, которого вы можете продать или продолжить скрещивать с другими котиками.
Код CryptoKitties разделен на ряд небольших взаимосвязанных контрактов, так что хранить один огромный файл со всей информацией не приходится.
Solidity позволят наследовать контракты, вот как выглядит цепочка наследования в случае с котятами:
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.
Этот контракт нужен для управления и никак не относится к механике игры. По сути, он приписывает методы пользователям “CEO”, “COO” и “CFO” — адресам на платформе Ethereum, которые владеют и контролируют конкретные функции контракта.
KittyAccessControl определяет модификаторы функций, например, onlyCEO
(ограничивает функцию, чтобы ее мог осуществлять только пользователь CEO), а также добавляет методы для таких действий, как пауза/возобновление игры или снятие денег:
modifier onlyCLevel() {
require(
msg.sender == cooAddress ||
msg.sender == ceoAddress ||
msg.sender == cfoAddress
);
_;
}
//...some other stuff
// Only the CEO, COO, and CFO can execute this function:
function pause() external onlyCLevel whenNotPaused {
paused = true;
}
Функция pause()
скорее всего была добавлена, чтобы разработчики могли выиграть себе время, если в конракте обнаружатся баги… Но вот Люк говорит, она также может позволить разработчикам полностью заморозить контракт и никто не будет ни передавать, ни продавать, ни разводить котят! Не то, чтобы разработчики хотели это сделать — но это интересно, особенно с учетом того факта, что многие люди думают, что DApp полностью децентрализована, потому что находится на Ethereum.
2. KittyBase: что же это за котик такой?
Именно в этом контракте мы определяем самый важный код, который отвечает за основной функционал, в том числе основное хранилище данных, константы, типы данных и внутренние функции для управления ими.
KittyBase определяет многие основные данные приложения. Во-первых, этот контракт задает котика как структуру:
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
Так что котик — это все лишь ворох целых чисел:) Разберем каждый элемент:
genes
— это 256-битное целое число представляет генетический код котика. Это ключевая часть информации, которая определяет внешний вид котика.birthTime
— временная метка блока, момент, в который рождается
котик.cooldownEndBlock
— минимальный временной период, после которого котик опять может размножаться.matronId
&sireId
— учетные записи мамы и папы котика
соответственно.siringWithId
— этот показатель с отсылкой на беременную маму
добавляется к учетной записи папы. В противном случае показатель равен нулю.cooldownIndex
— текущая продолжительность отдыха котика (через какое время после скрещивания котик снова сможет размножаться).generation
— «номер поколения» котика. Первые котики в игре
принадлежат к нулевому поколению, а каждое последующее поколение котиков имеет номер, на 1 превосходящий поколение своих родителей.
Обратите внимание, что в игре CryptoKitties котики бесполые, и скрещивать можно любых двух котиков.
Затем контракт KittyBase определяет массивы наших кошачьих структур:
Kitty[] kitties;
Этот массив содержит данные всех существующих котиков, так что это своего рода общая база данных.
Когда вы создаете нового котика, он добавляется в массив, и его индекс в массиве становится учетной записью котика. В примере ниже учетная запись котика Genesis — 1.
Индекс в массиве для этого котика — “1”!
Также в этом контракте содержится словарь (в Solidity это называется mapping, но я думаю мы поняли друг-друга), связывающий учетные записи котиков и адреса их владельцев, чтобы можно было следить, кому принадлежит тот или иной котик:
mapping (uint256 => address) public kittyIndexToOwner;
В контракте содержатся и другие словари, рекомендую вам самостоятельно пробежаться по ним глазами, там нет ничего сложного.
Когда владелец передает котика другому человеку, словарь kittyIndexToOwner
обновляется и начинает связывать котика с новым владельцем:
/// @dev Assigns ownership of a specific Kitty to an address.
function _transfer(address _from, address _to, uint256 _tokenId) internal {
// Since the number of kittens is capped to 2^32 we can't overflow this
ownershipTokenCount[_to]++;
// transfer ownership
kittyIndexToOwner[_tokenId] = _to;
// When creating new kittens _from is 0x0, but we can't account that address.
if (_from != address(0)) {
ownershipTokenCount[_from]--;
// once the kitten is transferred also clear sire allowances
delete sireAllowedToAddress[_tokenId];
// clear any previously approved ownership exchange
delete kittyIndexToApproved[_tokenId];
}
// Emit the transfer event.
Transfer(_from, _to, _tokenId);
Передача котика устанавливает связь kittyIndexToOwner
учетной записи котика и адреса нового владельца _to
.
А теперь давайте посмотрим, что происходит, когда мы создаем нового котика:
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;
}
Эта функция «принимает» учетные записи матери и отца, поколение котика, 256-битный генетический код и адрес владельца. Затем она создает Kitty
, направляет его в основной кошачий массив и вызывает команду _transfer()
, чтобы привязать котика к новому владельцу.
Круто — теперь мы можем видеть, как CryptoKitties определяет котиков в виде типа данных, как хранит их в блокчейне и как следит за тем, кому принадлежит тот или иной котик.
3. KittyOwnership: котики как токены
Этот контракт содержит методы, необходимые для базовых невзаимозаменяемых операций с токенами в соответствии со стандартом ERC-721.
Игра CryptoKitties подчиняется стандарту токена ERC721. Это невзаимозаменяемый токен, который отлично зарекомендовал себя для отслеживания владения цифровыми коллекциями, например, цифровыми игральными картами или редкими предметами в ролевых играх.
Примечание о взаимозаменяемости: Криптовалюта эфириум взаимозаменяема, потому что любые пять эфириумов равны другим пяти эфириумам. Но если мы говорим о токенах типа CryptoKitties, один котик не равен другому, поэтому они не взаимозаменяемы.
Из определения этого контракта мы можем видеть, что KittyOwnership
наследует контракт ERC721
:
contract KittyOwnership is KittyBase, ERC721
Все токены ERC721 соответствуют одному стандарту, так что в результате наследования, контракт KittyOwnership отвечает за осуществление следующих функций:
/// @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);
}
Так как все эти методы являются открытыми, существует стандартный способ взаимодействий пользователей с токенами CryptoKitties — и точно так же они могут взаимодействовать с любым другим токеном ERC721. Вы можете передавать токены другому пользователю, напрямую взаимодействуя с контрактом CryptoKitties на платформе Ethereum без необходимости пользоваться их веб-интерфейсом, так что в этом смысле вы действительно являетесь хозяином своих котиков. (Если только CEO не остановит контракт).
Если вам интересно написать свою игру на Ethereum, можно потренироваться на CryptoZombies: интерактивной школе обучения программированию на Solidity, русская локализация присутствует.
За помощь в переводе большое спасибо Саше Ивановой!
Продолжение следует.
Автор: Сергей