javascript

Dive into Ethereum

  • суббота, 29 апреля 2017 г. в 03:14:20
https://habrahabr.ru/post/327236/
  • Программирование
  • Биллинговые системы
  • JavaScript


Сегодня платформа 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 не стоит быть такими расточительными.


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


Ссылки