Dive into Ethereum
- суббота, 29 апреля 2017 г. в 03:14:20
Сегодня платформа Ethereum стала одним из самых узнаваемых брендов блокчейн сферы, вплотную приблизившись по популярности (и капитализации) к Bitcoin. Но из-за отсутствия "полноценного" рускоязычного гайда, отечественные разработчики все еще не очень понимают, что это за зверь и как с ним работать. Поэтому в данной статье я попытался максимально подробно охватить все аспекты разработки умных контрактов под Ethereum.
Я расскажу про инструменты разработки, сам ЯП, процесс добавления UI и еще много интересного. В конечном итоге мы получим обычный сайт-визитку, но "под капотом" он будет работать на умных контрактах Ethereum. Кого заинтересовало — прошу под кат.
Эта статья не расчитана на тех, кто совсем не знаком с Ethereum (или технологией блокчейн вообще), поэтому объяснений базовых вещей вроде блоков, транзакций или контрактов здесь не будет. Я подразумеваю, что вы хотя бы чуть-чуть в курсе происходящего. В противном случае полистайте статьи из списка ниже, а потом возвращайтесь :)
Больше ссылок на интересные статьи вы найдете в конце.
P.S. Я работаю под Ubuntu 16.04, так что весь процесс установки, разработки и деплоя будет описан под эту ОС. Тем не менее все используемые инструменты кроссплатформенны (скорее всего, не проверял), так что при желании можете поэкспериментировать на других ОС.
Работа с 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, вот простой пример (пункт — Поднимаем ноду).Geth довольно хорош, но в последнее время все чаще можно встретить другой клиент — Parity, написанный на Rust. Главным его отличием от Geth является встроенный web интерфейс, на мой взгляд, самый удобный среди всех ныне существующих. Установка:
sudo <(curl https://get.parity.io -Lk)
По окончании загрузки запустите в консоли parity
и по адресу localhost:8180 можете найти сам кошелек.
Еще один плюс: 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
поднять приватный блокчейн с включенным RPC протоколом, десятком заранее созданных аккаунтов с этерами на счету, работающим майнером и так далее. Весь список здесь. По сути, testrpc
— это тот же geth --dev --rpc ...
, только на этот раз не надо тратить время на создание аккаунтов, включение / выключение майнера и прочие рутинные действия.
Установка — npm install -g ethereumjs-testrpc
.
Самый популярный кошелек для Ethereum, хотя на самом деле он умеет намного больше. Вот отличная статья, где step-by-step объясняется весь процесс работы с Mist. Скачать самую свежую версию можно со страницы релизов. Помимо работы с кошельком, есть возможность работы с контрактами.
Самая популярная IDE для разработки контрактов. Работает в браузере по адресу ethereum.github.io/browser-solidity/, поддерживает огромное число функций:
При этом нет автокомплита, что очень печально.
Еще одна 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 и можете начинать работать:
Последний на сегодня инструмент для ускорения разработки умных контрактов. Это плагин для редактора Atom, устанавливается с помощью apm install atom-ethereum-interface
. Штука удобная, сам пользуюсь. Позволяет работать c JS EVM или подключиться к ноде через RPC. Компилирует контракт на CTRL + ALT + C
, деплоит в сеть на CTRL + ALT + S
. Ну и предоставляет неплохой интерфейс для работы с самим контрактом.
Если вам не нужен такой навороченный функционал внутри редактора, то для Atom есть отдельный плагин с подсветкой синтаксиса Solidity — language-ethereum. Последний по сути является плагином под Sublime text, только конвертированный для работы в Atom.
Возможно, вы слышали про то, что можно писать контракты не только на 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 — нажимаем и видим:
Теперь, когда контракт залит в блокчейн (его эмуляцию, но не суть), можем попробовать вызвать какую-нибудь функцию и посмотреть, что из этого выйдет. Например можно сохранить в контракте email — для этого найдите функцию setBasicData
, заполните поле и нажмите кнопку с именем функции:
Функция ничего не возвращает, поэтому result: 0x
. Теперь можно запросить у контракта email: ищем функцию getBasicData
и пробуем:
С остальными функциями предлагаю вам поэксперементировать самим.
Ниже я расскажу про самый распостраненный способ добавить UI к вашему контракту. Он позволяет с помощью JS и HTML создавать интерфейсы любой сложности, достаточно иметь доступ к рабочей ноде Ethereum (или ее аналогам).
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>
)
То есть мы уже на этом моменте можем запустить ноду, синхронизировать ее с текущей цепочкой и останется только сверстать наше приложение. Но тут есть два тонких момента: во-первых, вам нужно синхронизировать блокчейн Ethereum, а вы этого скорее всего до сих пор не сделали.
Второй нюанс — RPC не имеет никакого встроенного механизма авторизации, поэтому любой желающий может узнать адрес вашей ноды из исходников JS и пользоваться ей в свое удовольствие. Тут конечно можно писать какую-нибудь обертку на Nginx с простейшей HTTP basic auth, но это как-нибудь в другой раз.
Поэтому сейчас мы воспользуемся плагином Metamask (увы, только для Chrome). По сути это и есть та прослойка между нодой и браузером, которая позволит вам использовать web3 в браузере, но без своей ноды. Metamask работает очень просто — в каждую страницу он встраивает web3.js, который автоматически подключается к RPC серверам Metamask. После этого вы можете использовать Ethereum на полную катушку.
После установки плагина, в левом верхнем углу выберите Testnet
и получите несколько эфиров на кране Metamask. На этом моменте вы должны получить что-то вроде такого (с чистой историей разумеется):
С Metamask задеплоить контракт в сеть так же просто, как и в случаем с JS EVM. Для этого снова открываем Remix и в списке Select execution environment выбираем пункт Injected Web3
(скорее всего он выбран автоматически). После этого нажимаем Create и видим всплывающее окно:
Чуть позже надпись Waiting for transaction to be mined..
. сменится на информацию об опубликованном контракте — это значит что он попал в блокчейн. Адрес контракта можете узнать, открыв Metamask и нажав на запись вида:
Однако теперь, если вы захотите, например, вызвать функцию 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. Я использовал Vue.js и Spectre.css, для визуализации навыков добавил Google Charts. Результат можете увидеть на pavlovdog.github.io:
Только что вы увидели, как можно довольно быстро создать приложение, которое самым непосредственным образом использует технологию blockchain. Хотя в погоне за простотой (все таки это обучающая статья) я допустили некоторые упрощения, которые по-хорошему допускать нельзя.
Например, мы используем чей-то шлюз (я про Metamask), вместо того, чтобы работать со своей нодой. Это удобно, но технология блокчейн в первую очередь — децентрализация и отсутствие посредников. У нас же всего этого нет — мы доверяем парням из Metamask.
Другая, не такая критичная проблема, — мы забыли про стоимость деплоя контрактов и транзакций к ним. На практике, стоит десять раз подумать, прежде чем использовать string
вместо bytes
, потому как такие вещи прежде всего влияют на затраты при работе с контрактом. Опять же, в примере я использовал Testnet
, так что никаких денег мы не потратили, но при работе с Main net
не стоит быть такими расточительными.
В любом случае, я надеюсь что статья оказалась полезной, если есть вопросы — задавайте в комментариях или пишите мне на почту.