В первых числах декабря 2017 года, пользователи блокчейн-проекта Ethereum столкнулись с неприятным открытием — любые их транзакции просто перестали подтверждаться. Фактически, вся сеть перестала функционировать из-за неожиданно разросшегося в размерах мемпула.

Совсем скоро стало понятно в чем же дело — во всем виноват оказался проект CryptoKitties. Это забавная игрушка, работающая на блокчейне Ethereum и позволяющая пользователям разводить «котят», скрещивать их и продавать как обычные критовалютные токены. В какой-то момент 15% всех транзакций в Ethereum приходились на криптокотят! А к моменту написания этой статьи, игроки потратили на котят уже 23 миллиона долларов!

Так что я просто не мог пройти мимо такой интересной темы и решил рассказать, как же сделать игру такого рода. В этой статье (и ее продолжении) будет подробно описано, как можно создать похожий проект на Ethereum, в первую очередь с помощью разбора кода оригинального контракта CryptoKitties.

cryptocat

Исходный код игры 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”!
Индекс в массиве для этого котика — “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, русская локализация присутствует.




За помощь в переводе большое спасибо Саше Ивановой!

Продолжение следует.

Комментарии (11)


  1. php7
    09.03.2018 01:58

    А что, если люди — тоже криптолюди? :)


  1. mike_y_k
    09.03.2018 09:46
    +2

    А на каждом углу кричат про пирамиды ;).
    Отличная идея, вполне добротная реализация.
    Собственно в пространство криптовалют пора все игрушки с оплатой засунуть…


  1. Vnukkarpov
    09.03.2018 12:28

    Купил еще осенью пару котиков, завел еще парочку, игра конечно прикольная, только вот мешает немного то, что за всё приходится платить, то есть любое действие оплачивается эфиром, поэтому играть и постоянно платить комиссии быстро надоело. Теперь думаю продать или еще подождать, может и вырастет на них цена ))


  1. novikovag
    09.03.2018 12:28

    Мир сошел с ума.


  1. Dubus
    09.03.2018 12:28
    +1

    Совсем поехали со своими криптами.


  1. Merkusheva
    09.03.2018 18:03
    +1

    Мне криптокотики напомнили Тамагочи! Все дети 90х сходили по ним с ума. Не думаю, что стоит тратить свое время на разработку «новых» криптокотиков — их успех вы не повторите. Просто потому, что сливки уже сняли и втрой раз мир уже не поведеться на крипто-слоников или крипто-зайчиков. Хотя не спорю, что gameиндустрия это жирный пирог от которого стоит попытаться оторвать кусочек, но не с помощью такой пустышки как криптокотики. Это лишь хайп.


    1. novikovag
      10.03.2018 07:42

      Поведется и во второй и в третий раз.


  1. Alexey2005
    09.03.2018 21:49

    Когда Ethereum только появился, у меня возникла идея использовать эти смарт-контракты для автоматической оплаты контента, не требующей регистрации или почты. Ну, к примеру, автор выкладывает запароленный архив с неким контентом, а пароль к нему автоматически получает каждый, кто перечисляет определённую сумму на некоторый адрес.
    Увы, очень похоже на то, что ни смарт-контракты на блокчейне, ни блокчейн в целом такую задачу решить не способны, ведь их основная фишка заключается в общедоступности любой информации о любых событиях, происходящих в системе. И если кто-то получит пароль, то и все остальные смогут этот пароль увидеть и прочесть, потому что результат выполнения смарт-контракта будет храниться в блокчейне.


    1. KodyWiremane
      11.03.2018 11:34

      Закрытый контент и без блокчейна прекрасно сливается.

      Как насчёт: продавцу отправляется открытый ключ покупателя и оплата; в блокчейне оказывается пароль от архива, зашифрованный этим ключом, и расшифровать его может, условно, только обладатель закрытого ключа. Реализуемо такое в смарт-контрактах?


      1. alhimik45
        11.03.2018 14:41

        В такой схеме вам нужен свой сервер, который будет шифровать архивы.


        1. KodyWiremane
          11.03.2018 17:06

          Я имел в виду шифрование паролей, а не архивов, но да, моя схема требует как сервера, так и, видимо, регистрации/почты, потому что, как я понимаю, сам пароль нельзя поместить в контракт (
          Что-то я промахнулся