Как стать автором
Обновить

Dive into Ethereum

Время на прочтение 14 мин
Количество просмотров 116K

Сегодня платформа Ethereum стала одним из самых узнаваемых брендов блокчейн сферы, вплотную приблизившись по популярности (и капитализации) к Bitcoin. Но из-за отсутствия "полноценного" рускоязычного гайда, отечественные разработчики все еще не очень понимают, что это за зверь и как с ним работать. Поэтому в данной статье я попытался максимально подробно охватить все аспекты разработки умных контрактов под Ethereum.


Я расскажу про инструменты разработки, сам ЯП, процесс добавления UI и еще много интересного. В конечном итоге мы получим обычный сайт-визитку, но "под капотом" он будет работать на умных контрактах Ethereum. Кого заинтересовало — прошу под кат.


preview



Содержание



Введение в Ethereum


Эта статья не расчитана на тех, кто совсем не знаком с Ethereum (или технологией блокчейн вообще), поэтому объяснений базовых вещей вроде блоков, транзакций или контрактов здесь не будет. Я подразумеваю, что вы хотя бы чуть-чуть в курсе происходящего. В противном случае полистайте статьи из списка ниже, а потом возвращайтесь :)



Больше ссылок на интересные статьи вы найдете в конце.


P.S. Я работаю под Ubuntu 16.04, так что весь процесс установки, разработки и деплоя будет описан под эту ОС. Тем не менее все используемые инструменты кроссплатформенны (скорее всего, не проверял), так что при желании можете поэкспериментировать на других ОС.


Инструменты


Geth


Работа с Ethereum возможна через огромное число клиентов, часть из которых terminal-based, часть GUI и есть несколько гибридных решений. Своего рода стандартом является [Geth](), который разрабатывается командой Ethereum. Про него я уже писал в предыдущих статьях, но на всякий случай повторюсь.


Клиент написан на Go, устанавливается стандартным способом:


sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum

Сам Geth не имеет GUI, но работать с ним из терминала довольно приятно. Здесь описан весь набор аргументов командной строки, я же опишу несколько самых популярных.


Вот команда, которую я чаще всего использую в работе: $ geth --dev --rpc --rpcaddr "0.0.0.0" --rpcapi "admin,debug,miner,shh,txpool,personal,eth,net,web3" console


  • --dev запускает geth в режиме приватного блокчейна, то есть не синхронизирет основную / тестовую ветку. Вместо этого вы получаете стерильную цепочку без единого блока. Это самый удобный вариант в плане разработки, так как, например, майнинг блока занимает несколько секунд и нет никакой нагрузки на сеть или диск.


  • --rpc включает RPC-HTTP сервер. По сути это API к вашей ноде — через него сторонние приложения, вроде кошельков или IDE, смогут работать с блокчейном: загружать контракты, отправлять транзакции и так далее. По дефолту запускается на localhost:8545, можете изменить эти параметры с помощью --rpcaddr и --rpcport соответственно.
  • --rpcapi устанавливает что-то вроде прав доступа для приложений, подключенных к RPC серверу. Например, если вы не укажете "miner", то, подключив к ноде кошелек и запустив майнер, вы получите ошибку. В примере я указал все возможные права, подробнее можете почитать здесь.
  • console — как можно догадаться, эта опция запускает консоль разработчика. Она поддерживает самый обычный JS и ряд встроенных функций для работы с Ethereum, вот простой пример (пункт — Поднимаем ноду).

Parity


Geth довольно хорош, но в последнее время все чаще можно встретить другой клиент — Parity, написанный на Rust. Главным его отличием от Geth является встроенный web интерфейс, на мой взгляд, самый удобный среди всех ныне существующих. Установка:


sudo <(curl https://get.parity.io -Lk)

По окончании загрузки запустите в консоли parity и по адресу localhost:8180 можете найти сам кошелек.


parity


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


Единственный нюанс — своей консоли в parity нет. Но можно без проблем использовать для этих целей Geth:


$ parity --geth # Run parity in Geth mode
$ geth attach console # Attach Geth to the PArity node (Do it in another window)

TestRPC


Этот инструмент, в отличие от предыдущих, будет полезен только разработчикам. Он позволяет одной командой testrpc поднять приватный блокчейн с включенным RPC протоколом, десятком заранее созданных аккаунтов с этерами на счету, работающим майнером и так далее. Весь список здесь. По сути, testrpc — это тот же geth --dev --rpc ..., только на этот раз не надо тратить время на создание аккаунтов, включение / выключение майнера и прочие рутинные действия.


Установка — npm install -g ethereumjs-testrpc.


testrpc


Mist


Самый популярный кошелек для Ethereum, хотя на самом деле он умеет намного больше. Вот отличная статья, где step-by-step объясняется весь процесс работы с Mist. Скачать самую свежую версию можно со страницы релизов. Помимо работы с кошельком, есть возможность работы с контрактами.


mist


Remix


Самая популярная IDE для разработки контрактов. Работает в браузере по адресу ethereum.github.io/browser-solidity/, поддерживает огромное число функций:


  • Подключение к указанному RPC провайдеру
  • Компиляция кода в байткод / опкоды
  • Публикация в Github gist
  • Пошаговый дебагер
  • Подсчет стоимости исполнения функций в газе
  • Сохранение вашего кода в localstorage
  • И многое другое

При этом нет автокомплита, что очень печально.


remix


Cosmo


Еще одна IDE для разработки умных контрактов, написана на Meteor, работает из коробки. Для начала откройте новый терминал и поднимите ноду с включенным RPC интерфесом geth --rpc --rpcapi="db,eth,net,web3,personal" --rpcport "8545" --rpcaddr "127.0.0.1" --rpccorsdomain "localhost" console. После этого можете запускать саму IDE:


$ git clone http://github.com/SilentCicero/meteor-dapp-cosmo.git
$ cd meteor-dapp-cosmo/app
$ meteor

Далее открываете localhost:3000 и можете начинать работать:


cosmo_screenshot


Etheratom


Последний на сегодня инструмент для ускорения разработки умных контрактов. Это плагин для редактора Atom, устанавливается с помощью apm install atom-ethereum-interface. Штука удобная, сам пользуюсь. Позволяет работать c JS EVM или подключиться к ноде через RPC. Компилирует контракт на CTRL + ALT + C, деплоит в сеть на CTRL + ALT + S. Ну и предоставляет неплохой интерфейс для работы с самим контрактом.


atom_ethereum


Если вам не нужен такой навороченный функционал внутри редактора, то для Atom есть отдельный плагин с подсветкой синтаксиса Solidity — language-ethereum. Последний по сути является плагином под Sublime text, только конвертированный для работы в Atom.


Solidity


Возможно, вы слышали про то, что можно писать контракты не только на Solidity, но и на других языках, например Serpent (внешне напоминает Python). Но последний комит в develop ветке ethereum/serpent был примерно полгода назад, так что, по-видимому, язык, увы, deprecated.


Поэтому писать будем только на Solidity. Пока что язык находится на относительно раннем этапе развития, так что никаких сложных конструкций или уникальных абстракций в нем нет. Поэтому отдельно рассказывать про него я не вижу смысла — любой человек с опытом в программировании сможет свободно писать на нем после 20 минут чтения документации. На случай, если у вас такого опыта нет, — ниже я довольно подробно прокомментировал весь код контракта.


Для самостоятельного обучения есть несколько очень хороших примеров с максимально подробными описаниями:



Еще раз отмечу (отличную!) документацию языка, местами даже переведена на русский язык.


Создаем контракт-визитку


Самое время создать наш контракт. В конечном итоге это будет приложение-визитка, на которую мы поместим само "резюме":


  • Имя, почта, контакты и так далее
  • Список проектов
  • Образование: вузы, курсы и тд
  • Навыки
  • Публикации

Первый шаг


Первым делом создадим шаблон контракта и функцию-конструктор. Она должна называться также как и сам контракт и вызывается лишь однажды — при загрузке контракта в блокчейн. Мы будем использовать ее для инициализации одной единственной переменной — address owner. Как вы уже наверное догадались, в нее будет записан адрес того, кто залил контракт в сеть. А использоваться она будет для реализации функций администратора контракта, но об этом позже.


pragma solidity ^0.4.0;

contract EthereumCV is Structures {
    address owner;

    // =====================
    // ==== CONSTRUCTOR ====
    // =====================
    function EthereumCV() {
        owner = msg.sender;
    }
}

Базовая информация


Следующим шагом добавим возможность указывать базовую информацию об авторе — имя, почту, адрес и так далее. Для этого будем использовать самый обычный mapping, который нужно объявить в начало контракта:


address owner;
mapping (string => string) basic_data;

Для того, чтобы иметь возможность "получать" от контракта эти данные, создадим следующую функцию:


function getBasicData (string arg) constant returns (string) {
    return basic_data[arg];
}

Здесь все просто, стоит только отметить модификатор constant — его можно (и нужно) использовать для тех функций, которые не изменяют state приложения. Главный плюс таких функций (sic!), в том что их можно использовать как обычные функции.


Администрирование


Теперь стоит задуматься о наполнении своего резюме контентом. В самом простом случае мы могли бы обойтись функцией вроде


function setBasicData (string key, string value) {
    basic_data[key] = value;
}

Но в этом случае любой при желании смог бы изменить, например, наше имя, вызвав setBasicData("name", "New Name"). К счастью, есть способ всего в одну строку пресечь любые такие попытки:


function setBasicData (string key, string value) {
    if (msg.sender != owner) { throw; }
    basic_data[key] = value;
}

Так как нам еще не раз придется использовать подобную конструкцию (при добавлении нового проекта, например), то стоит создать специальный модификатор:


modifier onlyOwner() {
    if (msg.sender != owner) { throw; }
    _; // Will be replaced with function body
}

// Now you can use it with any function
function setBasicData (string key, string value) onlyOwner() {
    basic_data[key] = value;
}

При желании, можно использовать другие способы авторизации, например по паролю. Хэш будет храниться в контракте и сравниваться с введенным при каждом вызове функции. Но понятно, что этот способ не такой безопасный, благо радужные таблицы и атаки по словарю никто не отменял. С другой стороны, наш способ тоже не идеален, так как если вы потеряете доступ к адресу owner, то ничего редактировать вы уже не сможете.


Модульность


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


Для этого в той же директории создадим новый файл structures.sol и библиотеку Structures. А уже внутри нее опишем каждую из структур:


pragma solidity ^0.4.0;

library Structures {
    struct Project {
        string name;
        string link;
        string description;
    }

    struct Education {
        string name;
        string speciality;
        int32 year_start;
        int32 year_finish;
    }

    struct Publication {
        string name;
        string link;
        string language;
    }

    struct Skill {
        string name;
        int32 level;
    }
}

Теперь осталось только импортировать полученный файл


pragma solidity ^0.4.0;

import "./structures.sol";

contract EthereumCV {
    mapping (string => string) basic_data;
    address owner;

    Structures.Project[] public projects;
    Structures.Education[] public educations;
    Structures.Skill[] public skills;
    Structures.Publication[] public publications;

    // ...
}

Самые сообразительные уже догадались, что нотация Structures.Project[] projects означает создание динамического массива с элеметнами типа Project. А вот с модификатором public уже сложнее. По сути, он заменяет нам написание функции вроде get_project(int position) { return projects[position]; } — компилятор сам создаст такую функцию. Называться она будет так же как и переменная, в нашем случае — projects.


Вы можете спросить — почему мы в самом начале не написали mapping (string => string) public basic_data, а вместо этого сами создавали такую функцию? Причина банальна — public пока что не умеет работать c переменными, для которых ключом является динамический тип данных (string именно такой тип).


Unimplemented feature (/src/libsolidity/codegen/ExpressionCompiler.cpp:105): Accessors for mapping with dynamically-sized keys not yet implemented.

Для этого нужно объявлять basic_data как например mapping (bytes32 => string).


BTW На всякий случай отмечу, что кроме локального файла, Remix умеет импортировать .sol файлы по ссылке на Github и даже с помощью протокола Swarm (это что-то вроде распределенного хранилища для Ethereum, подробнее здесь)


Загружаем и удаляем данные


Думаю многие из вас уже сами догадались, как стоит реализовать работу с новыми данными. Покажу на примере списка публикаций, в остальных случаях все аналогично:


function editPublication (bool operation, string name, string link, string language) onlyOwner() {
    if (operation) {
        publications.push(Structures.Publication(name, link, language));
    } else {
        delete publications[publications.length - 1];
    }
}

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


Отдаем данные


Как я уже сказал, модификатор public в строке Project[] public projects обеспечил нас функцией которая по индексу i вернет проект projects[i]. Но мы не знаем, сколько у нас всего проектов, и здесь есть два пути. Первый — итерироваться по i до того момента, пока мы не получим ошибку о несуществующем элементе. Второй — написать отдельную функцию, которая вернет нам размер projects. Я пойду вторым путем, чуть позже скажу почему:


function getSize(string arg) constant returns (uint) {
    if (sha3(arg) == sha3("projects")) { return projects.length; }
    if (sha3(arg) == sha3("educations")) { return educations.length; }
    if (sha3(arg) == sha3("publications")) { return quotes.length; }
    if (sha3(arg) == sha3("skills")) { return skills.length; }
    throw;
}

Заметьте, что мы не можем сравнить две строки привычным способом 'aaa' == 'bbb'. Причина все та же, string — это динамический тип данных, работа с ними довольно болезненна. Так что остается либо сравнивать хэши, либо использовать функцию для посимвольного сравнения. В этом случае можете использовать популярную библиотеку stringUtils.sol, в ней есть такая функция.


Деплой


В разных средах разработки процесс компиляции и деплоя разумеется отличается, поэтому я ограничусь Remix, как самым популярным.


Сначала, само собой, заливаем весь код (финальную версию можете найти в репозитории проекта). Далее в выпадающем списке Select execution environment выберите Javascript VM — пока что протестируем контракт на JS эмуляторе блокчейна, чуть позже научимся работать и с настоящим. Если с контрактом все в порядке, то вам будет доступна кнопка Create — нажимаем и видим:


remix_create


Теперь, когда контракт залит в блокчейн (его эмуляцию, но не суть), можем попробовать вызвать какую-нибудь функцию и посмотреть, что из этого выйдет. Например можно сохранить в контракте email — для этого найдите функцию setBasicData, заполните поле и нажмите кнопку с именем функции:


remix_set_basic_data


Функция ничего не возвращает, поэтому result: 0x. Теперь можно запросить у контракта email: ищем функцию getBasicData и пробуем:


remix_get_basic_data


С остальными функциями предлагаю вам поэксперементировать самим.


Добавляем UI


Ниже я расскажу про самый распостраненный способ добавить UI к вашему контракту. Он позволяет с помощью JS и HTML создавать интерфейсы любой сложности, достаточно иметь доступ к рабочей ноде Ethereum (или ее аналогам).


Web3.js


This is the Ethereum compatible JavaScript API which implements the Generic JSON RPC spec. It's available on npm as a node module, for bower and component as an embeddable js and as a meteor.js package.

Это JS библиотека, позовляющая использовать API Ethereum с помощью обычного JS. По сути с ее помощью вы просто подключаетесь ноде и у вас появляется что-то вроде консоли geth в браузере. Устанавливается через npm или bower:


$ sudo npm install web3
$ bower install web3

Вот пример работы с web3 через node.js (предварительно запустите testrpcили любую другую ноду с RPC интерфейсом):


$ node
> var Web3 = require('web3');
> var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
> web3.eth.accounts
[ '0x5f7aaf2199f95e1b991cb7961c49be5df1050d86',
  '0x1c0131b72fa0f67ac9c46c5f4bd8fa483d7553c3',
  '0x10de59faaea051b7ea889011a2d8a560a75805a7',
  '0x56e71613ff0fb6a9486555325dc6bec8e6a88c78',
  '0x40155a39d232a0bdb98ee9f721340197af3170c5',
  '0x4b9f184b2527a3605ec8d62dca22edb4b240bbda',
  '0x117a6be09f6e5fbbd373f7f460c8a74a0800c92c',
  '0x111f9a2920cbf81e4236225fcbe17c8b329bacd7',
  '0x01b4bfbca90cbfad6d6d2a80ee9540645c7bd55a',
  '0x71be5d7d2a53597ef73d90fd558df23c37f3aac1' ]
>

Тоже самое, только из JS консоли браузера (не забудьте про <script src="path_to/web3.js"></script>)


browser_js_web3


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


Второй нюанс — RPC не имеет никакого встроенного механизма авторизации, поэтому любой желающий может узнать адрес вашей ноды из исходников JS и пользоваться ей в свое удовольствие. Тут конечно можно писать какую-нибудь обертку на Nginx с простейшей HTTP basic auth, но это как-нибудь в другой раз.


Metamask


Поэтому сейчас мы воспользуемся плагином Metamask (увы, только для Chrome). По сути это и есть та прослойка между нодой и браузером, которая позволит вам использовать web3 в браузере, но без своей ноды. Metamask работает очень просто — в каждую страницу он встраивает web3.js, который автоматически подключается к RPC серверам Metamask. После этого вы можете использовать Ethereum на полную катушку.


После установки плагина, в левом верхнем углу выберите Testnet и получите несколько эфиров на кране Metamask. На этом моменте вы должны получить что-то вроде такого (с чистой историей разумеется):


metamask_ready


Deploy with Metamask


С Metamask задеплоить контракт в сеть так же просто, как и в случаем с JS EVM. Для этого снова открываем Remix и в списке Select execution environment выбираем пункт Injected Web3 (скорее всего он выбран автоматически). После этого нажимаем Create и видим всплывающее окно:


metamask_popup


Чуть позже надпись Waiting for transaction to be mined... сменится на информацию об опубликованном контракте — это значит что он попал в блокчейн. Адрес контракта можете узнать, открыв Metamask и нажав на запись вида:


metamask_info


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


Пример


Теперь дело за малым — надо научиться получать данные от контракта через Web3. Для этого, во-первых, надо научиться определять наличие web3 на странице:


window.addEventListener('load', function() {
  // Checking if Web3 has been injected by the browser (Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Use Mist/MetaMask's provider
    console.log("Web3 detected!");
    window.web3 = new Web3(web3.currentProvider);
    // Now you can start your app & access web3 freely:
    startApp()
  } else {
    alert('Please use Chrome, install Metamask and then try again!')
  }
})

Внутри startApp() я определелил всю логику работы с контрактом, тем самым избегая ложных срабатываний и ошибок.


function startApp() {
  var address = {
    "3" : "0xf11398265f766b8941549c865d948ae0ac734561" // Ropsten
  }

  var current_network = web3.version.network;
  // abi initialized ealier, in abi.js
  var contract = web3.eth.contract(abi).at(address[current_network]);

  console.log("Contract initialized successfully")

  contract.getBasicData("name", function(error, data) {
    console.log(data);
  });

  contract.getBasicData("email", function(error, data) {
    console.log(data);
  });

  contract.getSize("skills", function(error, data) {
    var skills_size = data["c"][0];
    for (var i = 0; i < skills_size; ++i) {
      contract.skills(i, function(error, data) {
        // Don't forget to check blank elements!
        if (data[0]) { console.log(data[0], data[1]["c"][0]); }
      })
    }
  })
}

js_logs


Итог


Теперь, когда вы со всем разобрались, можно браться за верстку и JS. Я использовал Vue.js и Spectre.css, для визуализации навыков добавил Google Charts. Результат можете увидеть на pavlovdog.github.io:


cv


Вместо заключения


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


Например, мы используем чей-то шлюз (я про Metamask), вместо того, чтобы работать со своей нодой. Это удобно, но технология блокчейн в первую очередь — децентрализация и отсутствие посредников. У нас же всего этого нет — мы доверяем парням из Metamask.


Другая, не такая критичная проблема, — мы забыли про стоимость деплоя контрактов и транзакций к ним. На практике, стоит десять раз подумать, прежде чем использовать string вместо bytes, потому как такие вещи прежде всего влияют на затраты при работе с контрактом. Опять же, в примере я использовал Testnet, так что никаких денег мы не потратили, но при работе с Main net не стоит быть такими расточительными.


В любом случае, я надеюсь что статья оказалась полезной, если есть вопросы — задавайте в комментариях или пишите мне на почту.


Ссылки


Теги:
Хабы:
+14
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн