Криптовалюта и CryptoKitties: собственные котики на Ethereum

Пока криптовалюта, блокчейн и другие новинки взрывают сеть, давайте создавать крипто-котиков на виртуальной платформе Ethereum.

Разбираемся, что же это за криптовалюта такая.

Начнем немного с теории, чтобы иметь общее представление о происходящем. Блокчейн – популярная и быстрорастущая технология децентрализованной базы данных, которая позволит избавиться от посредников в финансовых отношениях, а сделки станут безопасными и эффективными.

CryptoKitties – это игра по созданию криптовалюты, в основании которой лежит технология блокчейн со всеми вытекающими.

Исходный код котиков

Эта игрушка имеет открытый исходный код, что позволит разобраться, как она работает. Будем рассматривать только важные блоки кода, а кто хочет самостоятельно поковыряться – вот ссылка на ethFiddle.

Углубленный обзор

Итак, суть котиков в том, чтобы их покупать, продавать и размножать эту "криптовалюту". У каждого кота есть свои параметры, которые передаются на генном уровне при скрещивании. Чтобы получить большую сумму от продажи животного – нужно заполучить редкую породу и заняться скрещиванием двух котов.

Весь код поделен на несколько контрактов и имеет множественное наследование, чтобы избежать огромного размера файла. Наследование субконтракта для контракта главного котика выглядит так:

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 – это основной контракт, который наследует все методы и субконтракты всего приложения. Переходим к рассмотрению всех наследуемых субконтрактов.

KittyAccessControl: криптовалюта контролирует контракт

Этот контракт предназначен для управления и вообще не связан с игровой механикой. В основном это методы “сеттера” для CEO, COO и CFO, которые являются адресами Ethereum, которые имеют особое владение и контроль над конкретными функциями контракта. KittyAccessControl имеет модификаторы функций по ролям, и позволяет выполнять такие действия, как пауза, отказ от договора или вывод средств.

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() – это фича для разработчиков, которая позволяет остановить всякие операции с котами по любым причинам.

KittyBase: кто эти котята?

Это наиболее фундаментальный код, общий для всех основных функциональных возможностей. Он включает в себя хранилище данных, константы и типы данных, а также внутренние функции для управления этими элементами.

Сначала описывается котенок как структура:

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

Рассмотрим отдельно каждый параметр.

  • genes  – 256-битное целое, которое описывает гены котенка. Этот ключевой параметр отвечает за то, как будет выглядеть животное.
  • birthTime – timestamp для ДР кошечки.
  • cooldownEndBlock – минимальное время, через которое можно снова размножаться.
  • matronId & sireId – ID отца и матери.
  • siringWithId – установлен ID отца, если мать беременна, а так обычно 0.
  • cooldownIndex – как долго кошка должна ждать следующей возможности забеременеть.
  • generation – коты от первого окота имеют поколение 0, а все последующие будут +1.

После этого объявляется массив всех кошек:

Kitty[] kitties;

Этот массив хранит информацию о всех кошечках. Если создается новое животное, то сюда записывается вся нужная информация.

Этот контракт имеет еще одну особенность – mapping, что помогает контролировать, кто хозяин котика:

mapping (uint256 => address) public kittyIndexToOwner;

Если кошка перемещается к другому хозяину, mapping обновляется:

/// @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);
}

Теперь, рассмотрим, что происходит, когда котик создается:

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 (), чтобы назначить его новому владельцу.

KittyOwnership: Котята как токены

CryptoKitties соответствует спецификации ERC721, который отлично зарекомендовал себя для отслеживания владения цифровыми коллекционными предметами, такими как цифровые игровые карты, криптовалюта или редкие предметы в MMORPG.

Вот так в контракте реализовано наследование спецификации 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 без использования веб-интерфейса.

KittyBreeding: разведение котят

KittyBreeding содержит метод для CEO, который устанавливает адрес этого внешнего контракта:

/// @dev Update the address of the genetic contract, can only be called by the CEO.
/// @param _address An address of a GeneScience contract instance to be used from this point 
/// forward.
function setGeneScienceAddress(address _address) external onlyCEO {
    GeneScienceInterface candidateContract = GeneScienceInterface(_address);

    // NOTE: verify that a contract is what we expect - https://github.com/Lunyr/
    // crowdsale-contracts/blob/cfadd15986c30521d8ba7d5b6f57b4fefcc7ac38/contracts/
    // LunyrToken.sol#L117
    require(candidateContract.isGeneScience());

    // Set the new contract address
    geneScience = candidateContract;
}

Это было сделано для того, чтобы игра не была слишком легкой – если бы вы могли заранее узнать ДНК котенка, было бы намного легче понять, какие кошки размножаются, чтобы получить «причудливую породу».

Этот внешний контракт geneScience позже используется в функции giveBirth(), чтобы определить ДНК новой кошки.

Теперь посмотрим на процесс размножения:

/// @dev Internal utility function to initiate breeding, assumes that all breeding
///  requirements have been checked.
function _breedWith(uint256 _matronId, uint256 _sireId) internal {
    // Grab a reference to the Kitties from storage.
    Kitty storage sire = kitties[_sireId];
    Kitty storage matron = kitties[_matronId];

    // Mark the matron as pregnant, keeping track of who the sire is.
    matron.siringWithId = uint32(_sireId);

    // Trigger the cooldown for both parents.
    _triggerCooldown(sire);
    _triggerCooldown(matron);

    // Clear siring permission for both parents. This may not be strictly necessary
    // but it's likely to avoid confusion!
    delete sireAllowedToAddress[_matronId];
    delete sireAllowedToAddress[_sireId];

    // Every time a kitty gets pregnant, counter is incremented.
    pregnantKitties++;

    // Emit the pregnancy event.
    Pregnant(kittyIndexToOwner[_matronId], _matronId, _sireId, matron.cooldownEndBlock);
}

Таким образом, эта функция принимает идентификатор матери и отца, просматривает их в массиве kitties и устанавливает идентификатор отца siringWithId на мать. (Когда siringWithId отличен от нуля, это указывает на то, что мать беременна).

Он также выполняет triggerCooldown у обоих родителей для того,чтобы они не могли размножаться снова определенное количество времени.

Затем у нас есть функция giveBirth(), которая создает новую кошку:

/// @notice Have a pregnant Kitty give birth!
/// @param _matronId A Kitty ready to give birth.
/// @return The Kitty ID of the new kitten.
/// @dev Looks at a given Kitty and, if pregnant and if the gestation period has passed,
///  combines the genes of the two parents to create a new kitten. The new Kitty is assigned
///  to the current owner of the matron. Upon successful completion, both the matron and the
///  new kitten will be ready to breed again. Note that anyone can call this function (if they
///  are willing to pay the gas!), but the new kitten always goes to the mother's owner.
function giveBirth(uint256 _matronId)
    external
    whenNotPaused
    returns(uint256)
{
    // Grab a reference to the matron in storage.
    Kitty storage matron = kitties[_matronId];

    // Check that the matron is a valid cat.
    require(matron.birthTime != 0);

    // Check that the matron is pregnant, and that its time has come!
    require(_isReadyToGiveBirth(matron));

    // Grab a reference to the sire in storage.
    uint256 sireId = matron.siringWithId;
    Kitty storage sire = kitties[sireId];

    // Determine the higher generation number of the two parents
    uint16 parentGen = matron.generation;
    if (sire.generation > matron.generation) {
        parentGen = sire.generation;
    }

    // Call the sooper-sekret gene mixing operation.
    uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, /
      matron.cooldownEndBlock - 1);

    // Make the new kitten!
    address owner = kittyIndexToOwner[_matronId];
    uint256 kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, /
      childGenes, owner);

    // Clear the reference to sire from the matron (REQUIRED! Having siringWithId
    // set is what marks a matron as being pregnant.)
    delete matron.siringWithId;

    // Every time a kitty gives birth counter is decremented.
    pregnantKitties--;

    // Send the balance fee to the person who made birth happen.
    msg.sender.send(autoBirthFee);

    // return the new kitten's ID
    return kittenId;
}

Сначала выполняются некоторые проверки, чтобы узнать, готова ли мать рожать. Затем определяются гены ребенка, используя geneScience.mixGenes(), котенок назначается владельцу матери, затем вызывается функция _createKitty(), которую мы рассмотрели в KittyBase.

KittyAuctions: покупка и продажа

Функциональность блока аукциона также разделена на мелкие контракты. Таким образом, KittyAuctions содержит функции setSaleAuctionAddress() и setSaleAuctionAddress(), которые, как и setGeneScienceAddress(), могут быть вызваны только CEO, и которые задают адрес внешнего контракта, обрабатывающего эти функции.

Это означает, что даже если контракт CryptoKitties сам по себе является неизменным, CEO может изменить адрес этого контракта, что изменит правила аукциона.

KittyMinting: криптовалюта и кошачья фабрика

Есть возможность сделать до 5000 “промо-кошек”, которые можно отдать (особенно важно, когда сообщество новое), а все остальные могут быть созданы, а затем немедленно выставлены на аукцион с помощью алгоритмически определенной стартовой цены. Независимо от того, как они созданы, существует жесткий предел 50k gen0 кошек. После этого все сообщество должно размножаться.

Количество “промо-кошек” и кошек gen0, которые может создать контракт, жестко закодировано здесь:

uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;

А тут COO может создавать “промо-кошек” и кошек gen0:

/// @dev we can create promo kittens, up to a limit. Only callable by COO
/// @param _genes the encoded genes of the kitten to be created, any value is accepted
/// @param _owner the future owner of the created kittens. Default to contract COO
function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
    address kittyOwner = _owner;
    if (kittyOwner == address(0)) {
         kittyOwner = cooAddress;
    }
    require(promoCreatedCount < PROMO_CREATION_LIMIT);

    promoCreatedCount++;
    _createKitty(0, 0, 0, _genes, kittyOwner);
}

/// @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++;
}

Таким образом, при помощи createPromoKitty() COO может создать нового котенка с любыми генами, которые он хочет, и отправить его абсолютно всем (до 5000 котиков). Скорее всего, что это используется для ранних бета-тестеров, друзей и семьи, чтобы раздавать бесплатных котят для продвижения по службе и т. д.

Но это также означает, что ваша кошка может быть не такой уникальной, как вы думаете, так как COO может напечатать 5000 одинаковых копий!

KittyCore: Мастер-контракт

Это основной контракт CryptoKitty, который скомпилирован и запущен в Ethereum. Такой контракт связывает все вместе.

/// @notice Returns all the relevant information about a specific kitty.
/// @param _id The ID of the kitty of interest.
function getKitty(uint256 _id)
    external
    view
    returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
) {
    Kitty storage kit = kitties[_id];

    // if this variable is 0 then it's not gestating
    isGestating = (kit.siringWithId != 0);
    isReady = (kit.cooldownEndBlock <= block.number);
    cooldownIndex = uint256(kit.cooldownIndex);
    nextActionAt = uint256(kit.cooldownEndBlock);
    siringWithId = uint256(kit.siringWithId);
    birthTime = uint256(kit.birthTime);
    matronId = uint256(kit.matronId);
    sireId = uint256(kit.sireId);
    generation = uint256(kit.generation);
    genes = kit.genes;
}

Этот общедоступный метод возвращает все данные для конкретного котенка из блокчейна. Данные передаются на веб-сервер разработчиков, для отрисовки кошек на сайте. Как видно из приведенного выше кода, «котенок» в основном сводится к 256-битному беззнаковому целому, представляющему его генетический код.

В коде контракта Solidity нет ничего, что бы содержало изображение кошки или его описание или определяло то, что на самом деле означает это 256-битное целое. Интерпретация этого генетического кода происходит на веб-сервере CryptoKitty.

Заключение

Данная демонстрация создания "фабрики криптовалюты" на базе блокчейна, на самом деле, не на 100% основана на блокчейне. Если веб-сайт CryptoKitty внезапно упадет, и резервное копирование выполнено не будет, вам останется только бесполезное 256-битное целое число. Можно накодить отличный продукт, который будет походить на любую криптовалюту, но популярности и долговечности достичь не получится. Перевод на русский осуществлен Библиотекой Программиста.

Оригинал

Другие материалы по теме:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
21 февраля 2017

Какие алгоритмы нужно знать, чтобы стать хорошим программистом?

Данная статья содержит не только самые распространенные алгоритмы и структу...
admin
29 января 2017

Изучаем алгоритмы: полезные книги, веб-сайты, онлайн-курсы и видеоматериалы

В этой подборке представлен список книг, веб-сайтов и онлайн-курсов, дающих...