10 июня шёл уже третий день нашей акклиматизации в Гонконге. А последние 26 часов мы провели почти без сна, разрабатывая прототип проекта под рабочим названием SensorPay на первом этапе хакатона EOS Global с общим призовым фондом полтора миллиона долларов. Близился момент демонстрации проекта перед судьями.
Если вам не терпится узнать, чем закончилась эта история, загляните сразу в последнюю часть. А мы пока начнём планомерно рассказывать о технологиях EOS и о том, как мы пришли к идее привязать к EOS платежи для IoT. Сразу после этого будет подробное описание технической начинки проекта.
0. Предыстория
EOS — блокчейн нового поколения, некоторые даже считают его убийцей Ethereum. Если вдруг вы не знаете, что такое блокчейн или Ethereum, Гугл в помощь. А мы, так получилось, начали раскапывать EOS ещё около года назад, в том числе изучив предшествующие продукты его авторов BitShares и Steem.
Преимущества EOS по сравнению с Ethereum: пропускная способность транзакций на три порядка выше; развитая permission-система для смарт-контрактов; возможность восстановить утерянный доступ и исправить ошибки в блокчейне; управление сетью onchain. Недостатки: опасения в централизации, потенциально более уязвимый консенсус DPoS, сырой код и более крутая кривая обучения для разработчиков.
Поскольку мы давно увлекаемся этой технологией и считаем её перспективной, мы не смогли оставить без внимания серию хакатонов, которая проходит при поддержке авторов EOS. Мы просто захотели быть там, реализовать свои идеи в этой воодушевляющей обстановке и поделиться ими с широкой аудиторией. Конечно, возможность выиграть хорошие деньги тоже стала дополнительным приятным мотиватором.
Итак, EOS — единственное известное нам рабочее решение для публичного блокчейна, где можно делать МНОГО транзакций. А где оно требуется? Конечно, в IoT! Ведь если каждый тостер станет микроплатежами сам оплачивать каждый кусок хлеба холодильнику (а это круто по умолчанию, как вы понимаете), транзакций будет очень много. Не говоря уж о всяких других применениях в медицине, производстве и быту.
За несколько недель до хакатона периодически всплывали альтернативные идеи, проводились небольшие мозговые штурмы. Мы сравнивали идеи на основе известных критериев судейства: применения возможностей EOS, креативности, общественного воздействия (impact), масштабируемости. В итоге остановились на IoT + EOS — решении, которое бы снимало данные с датчиков и отправляло много платёжных транзакций в EOS.
Кстати, нам очень хотелось рассказать тут ещё и о том, как мы поднимали свой Block Producer для EOS; как планировали запилить для него конструктор ERC721-like токенов и поддержку константных функций; как запилили-таки Merkle Root ACL протокол. Но это всё никак не умещается в статью, поэтому вернёмся к нашему основному проекту.
1. Подготовка
1.1. IoT
Подготовка IoT-части проекта — это выбор подходящего железа. В роли RFID-считывателя был выбран RC522, работающий по шине SPI: он популярен, и его просто использовать.
При поиске счётчика мы ориентировались на наличие импульсного выхода, так как он позволяет очень просто считывать данные: один импульс — это X кВт⋅ч (где X зависит от модели), в итоге остановились на счётчике «Меркурий 201.5».
Сложнее всего было определиться с контроллером, который должен собрать данные с датчиков, сформировать транзакцию, подписать её своим приватным ключом и отправить в сеть. Соответственно, нам требовалось устройство с сетевым модулем, которое могло бы подписать транзакцию с помощью ECDSA (в данном случае на эллиптической кривой secp256k1, так как в EOS для подписи используется именно она).
Изначально выбор пал на микроконтроллер ESP8266, в нём есть модуль Wi-Fi и все нужные интерфейсы для подключения наших датчиков. В то же время он очень компактный. Но ни в одной из прошивок нет нативной реализации эллиптических примитивов. Написать свою реализацию возможно, но это не задача для хакатона. В итоге для прототипа был выбран Raspberry Pi 3 B, a для генерации и подписи транзакций — библиотека eosjs.
1.2. Инфраструктура
За пару дней до хакатона подготовили локально и на сервере eos-hackathon.smartz.io сборку EOS (исходники). Установка зависимостей, сборка и тесты прошли на удивление гладко для столь молодого проекта (при использовании документации). На прочую подготовку инфраструктуры не хватило времени, и пришлось заниматься этим уже в ходе хакатона.
1.3. Архитектура
Накануне хакатона мы обсудили архитектуру и уточнили детали продукта. Мы собирались реализовать следующие основные кейсы: расчёты за электроэнергию и оплата покупок, помеченных RFID-метками. Также планировали сделать продукт легко расширяемым и использовать его в других областях.
Идея архитектуры в том, что поставщик услуг (Producer) создаёт один контракт — центральную точку взаимодействия поставщика и потребителей. У каждого потребителя есть свой баланс, который можно пополнять, с него на основании сигналов датчиков списываются средства. Все данные — пользователи, датчики, статистика — хранятся в контракте поставщика.
С потребителем связаны пользовательские настройки, или флаги (например, льготная категория пользователя) — user_meta
. С потребителем могут быть связаны несколько датчиков, для каждого из них указывается контракт и настройки биллинга (billing_meta
). Так можно получить неизменяемый контракт биллинга без состояния, используемый для большого количества потребителей; необходимые данные будут появляться в ходе вызова метода bill(payload, user_meta, billing_meta)
. Также заложена возможность разной логики биллинга, т. е. разных контрактов: например, один считает электричество, другой — товары. У каждого датчика есть «указатель» на свой контракт биллинга.
Предполагается, что потребитель доверяет производителю датчика, но необязательно доверяет поставщику услуг. Интерфейс взаимодействия с датчиком предельно прост: это вызов метода контракта поставщика с числовым параметром, который будет передан в биллинг. Поставщик услуг заводит в свой контракт потребителей, датчики, контракты биллинга и их взаимосвязи, используя управляющие транзакции — примитивные сеттеры. При получении транзакции от датчика происходит проверка данных, формирование данных для биллинга, вызов биллинга, фиксация платежа и запись статистики.
Пожалуй, больше всего мы обсуждали следующие вопросы, важные для применимости продукта в реальном мире:
- Авансовые платежи или постоплата? Пополни счёт и пользуйся (как сотовая связь) — или пользуйся, а потом расплатись (как AWS)? Правильного и неправильного ответа здесь нет: разные бизнесы предпочитают разные модели. Мы решили для простоты взять авансовые платежи.
- Пользователь должен держать отдельный счёт для каждого поставщика или все списания идут с одного счёта? Опять же — правильного и неправильного решения нет; кроме того, ответ тесно связан с ответом на предыдущий вопрос. Авансовые платежи хорошо дружат с отдельными счетами потребителей — их и взяли.
- Взимать плату в EOS, токене поставщика услуг или stable coin (привязанной к фиатной валюте)? Варианты, кроме stable coin, безусловно, неудобны для потребителя из-за волатильности, а stable coin в рамках EOS пока что не существует. На тот момент даже основной сети EOS ещё не было! Для простоты взяли условный токен.
2. Кодинг
Прежде всего специфицировали API и каркас контракта поставщика, чтобы параллельно начать разработку фронтенда, кода устройств, биллингов и основного контракта (исходники).
2.1. IoT
Первым реализовали код для считывания импульсов со счётчика. Для работы с GPIO (пины общего назначения) использовалась JS-библиотека onoff. Позже к схеме для наглядности добавили два светодиода: первый мигал при получении сигнала из счётчика, а второй — когда от ноды EOS приходил ответ об успешной транзакции. Аналогично разработали схему и код для считывания RFID-меток, с единственным отличием: считывание происходило по шине SPI с использованием библиотеки MFRC522-python. Как оказалось, настройка SPI для Raspberry Pi 3 отличается от настройки в более ранних моделях платы; нам помогла разобраться эта инструкция.
Устройства запитали с power bank, удачно подаренного всем участникам хакатона, а интернет пришлось расшаривать самим с iPhone 5, так как Wi-Fi хакатона работал исключительно на частоте 5 GHz, это не подошло для Raspberry Pi.
2.2. Инфраструктура и утилиты
Организаторы советовали взять docker-образ eos-dev, однако нас смущало отсутствие описания и документации образа. На сервере продолжили работать с подготовленной сборкой, а локально, чтобы избежать установки EOS системно, использовали eos-dev.
Сразу же остро потребовалась возможность быстрой сборки и тестов. Идеальный вариант: собирать и запускать локально исполняемый файл. Однако нельзя было игнорировать тот факт, что после сборки на выходе требовалось получить WebAssembly и в окружении EOS — с соответствующими boost, библиотеками, системными контрактами. Нужные параметры компиляции можно было подсмотреть в eosiocpp.in, однако мы решили не играть в эту игру. Предсказуемый результат, пусть и чуть медленнее, важнее быстрого решения с потенциальными граблями. Поэтому для сборки взяли eoscpp, находящийся в контейнере eos-dev.
Сложнее получилось с запуском, пришлось поднимать локальный блокчейн EOS, причём готового решения опять же не было. Только софт. Так появилась первая версия инфраструктуры запуска. Идея в том, чтобы спрятать нюансы монтирования и конфигурирования и получить самосогласованный набор из четырёх-пяти «кнопок» для типичных действий. Меньше контроля, но и меньше возможности ошибиться, плюс экономия времени.
К основным компонентам EOS относятся демоны nodeos, keosd, консольная утилита cleos и компилятор eoscpp:
- nodeos — нода EOS, демон — участник сети, обеспечивает доступ к блокчейну и опционально производит новые блоки;
- keosd — демон для управления локальными кошельками, хранящими пары ключей;
- cleos предоставляет команды от получения информации о транзакциях до работы с ключами, реализуется на основе вызовов в nodeos и keosd по HTTP API;
- eoscpp компилирует контракты в WebAssembly, а также позволяет получить Application Binary Interface на основе исходного кода.
Сразу стало понятно, что не работают команды cleos, связанные с обращениями в keosd. Поскольку выдавалась ошибка, указывающая на сетевую недоступность keosd, мы потратили время на диагностику сетевых проблем в docker-сети. Однако strace показал, что дело не в сети: cleos обращается по неверному адресу, всегда на localhost (а в случае нашей инфраструктуры у различных демонов различные сетевые адреса в отдельной docker-сети). Был диагностирован баг в cleos: проверка доступности keosd, которая выполняется перед любой командой, связанной с кошельками, учитывает порт, переданный в аргументах, но не учитывает адрес. В условиях хакатона в качестве обходного решения перешли на host-сеть в docker.
Следующим шагом стала утилита компиляции контрактов с использованием компилятора в контейнере (commit). Входные и выходные каталоги монтировались. И наконец, возможность загрузить контракт в блокчейн и посылать транзакции (commit). Опять же — утилиты в согласованном стиле, простые «кнопки». На этом с базовой инфраструктурой было покончено, но сюрпризы продолжились: мы наткнулись на проблему C-функций работы с памятью (более подробно дальше).
В завершение в одном файле стали задавать аккаунты (каждый контракт и участник требуют отдельных аккаунтов) прямо вместе с парами ключей, создаваемые автоматически при запуске блокчейна, чтобы одной командой можно было поднять тестовое окружение. Одну копию этого окружения и развернули на eos-hackathon.smartz.io.
2.3. Смарт-контракты
2.3.1. Контракт поставщика и биллинг электричества
После старта хакатона мы начали накидывать структуру контрактов по схеме выше. Система состояла из следующих контрактов:
supplier
— контракт поставщика;billing_electricity
— контракт для расчёта платежа за электричество на каждый тик счётчика.
В контракте supplier
большую часть занимают обычные CRUD-операции: добавление пользователей, тарифов, счётчиков, увеличение или уменьшение баланса пользователя. Более сложные методы отвечали за приём данных от счётчика, вызов контракта для расчёта платежа (биллинга), списание средств с лицевого счёта пользователя после обратного вызова из контракта биллинга. Нужный контракт для биллинга определялся на основе тарифа пользователя.
В контракте для биллинга после вызова рассчитывался платёж и вызывался метод для списания платежа с лицевого счёта пользователя. Накидав основную логику, мы даже подумали, не слишком ли простые контракты делаем. Чуть позже, после деплоя контрактов в ноду и их тестирования, стало понятно, что контракты, может быть, и простые, но есть нюансы. :)
После деплоя выяснилось, что ожидаемые вызовы контрактов друг из друга не работают. Не хватает прав. В отличие от смарт-контрактов в Ethereum, где вызов контракта из контракта происходит от имени вызывающего контракта, в EOS вся цепочка начинается с инициатора транзакции. При вызове контракта из контракта проверяется, разрешил ли инициатор контракту это делать.
Менторы сразу подсказали, как действовать в простом случае. Права добавляются следующим образом (через вызов системного смарт-контракта eosio):
./cleos push action eosio updateauth '{"account":"electricity","permission":"active","parent":"owner","auth":{"keys":[{"key":"EOS7oPdzdvbHcJ4k9iZaDuG4Foh9YsjQffTGniLP28FC8fbpCDgr5","weight":1}],"threshold":1,"accounts":[{"permission":{"actor":"supplier","permission":"eosio.code"},"weight":1}],"waits":[]}}' -p electricity
В данном случае аккаунт electricity
разрешает контракту supplier
вызывать другие контракты от его имени. Более подробно о правах можно почитать в Technical WP EOS. У нас же контракт supplier
вызывал контракт billing
, а тот, в свою очередь, опять вызывал supplier
. Добавление по аналогии прав в таком виде не срабатывало:
./cleos push action eosio updateauth '{"account":"electricity","permission":"active","parent":"owner","auth":{"keys":[{"key":"EOS7oPdzdvbHcJ4k9iZaDuG4Foh9YsjQffTGniLP28FC8fbpCDgr5","weight":1}],"threshold":1,"accounts":[{"permission":{"actor":"supplier","permission":"eosio.code"},"weight":1},{"permission":{"actor":"billelectro","permission":"eosio.code"},"weight":1}],"waits":[]}}' -p electricity
Выдавалась ошибка: Invalid authority. Тут у менторов уже не вышло нам помочь: они сказали, что сами такого не делали. А кто делал? Может быть, только Дэн Лаример. Мы не смогли быстро найти причину ошибки в коде EOS и уже начали обдумывать альтернативные варианты, без цепочки вызовов. Красиво сделать мешало то, что механизм вызовов других контрактов в EOS также отличается от эфира. При вызове другого контракта этот вызов ставится в очередь и будет выполнен только после выполнения текущего вызова. Не получится вызвать контракт и после вызова прочитать записанные этим контрактом данные.
Позже всё-таки нашли в коде EOS причину ошибки при установке прав для двух контрактов. Оказывается, аккаунты в списке прав должны быть отсортированы по аккаунту: Makes sure all keys are unique and sorted and all account permissions are unique and sorted (authority.hpp). После изменения порядка аккаунтов обновление прав сработало — и наша система контрактов стала действовать.
2.3.2. Проблема C-функций работы с памятью
Смешно сказать, но нам в итоге не удалось использовать готовые функции разбора чисел (!) для чтения конфигурации биллинга. Вслед за std::istringstream
подтягивалась функция env.calloc
, что довольно странно. А при использовании atof
и подобных, а также sscanf
— подтягивалась env.realloc
. Упомянутые функции работы с памятью стандартной библиотеки C почему-то не находились в ходе загрузки кода в nodeos. Функции C++ работы с памятью при этом работали.
Конечно, при исполнении WebAssembly контракта используется не стандартный аллокатор памяти, а своя «песочница», предоставляемая каждой транзакции на определённых условиях. Также довольно давно реализована поддержка C-функций работы с памятью поверх этой песочницы, их реализации есть в стандартных контрактах EOS. Вероятно, что-то шло не так на этапе линковки.
Потратив около часа на поиски выхода, в том числе с помощью одного из менторов, мы решили не продолжать и сделать обходное решение: написать свой код, решающий задачу разбора чисел. Механизм datastream EOS нам не подошёл: требовалась возможность сохранять пакеты данных разной структуры в одном поле и формировать их руками (те самые конфигурации биллингов).
2.3.3. Биллинг покупок
На втором дыхании, которое открылось благодаря то ли энергетикам, то ли раннему завтраку, написали биллинг для покупок в магазине. Общая схема работы такова:
- Поставщик создаёт контракт биллинга и прописывает его в своём общем контракте.
- Поставщик устанавливает на выходе из магазина рамки, которые умеют считывать RFID, взаимодействовать с EOS и имеют свои аккаунты, прописываемые в контракте биллинга.
- Каждый товар в магазине оснащается RFID-меткой, все метки прописываются в контракте биллинга.
- Покупатель оплачивает товар, сканируя его RFID, и товар удаляется из контракта биллинга.
- На выходе из магазина рамки дополнительно считывают RFID покупок. Если товар всё ещё в магазине — транзакция не проходит, и рамка должна бить тревогу (да, на самом деле можно даже не посылать транзакцию, а просто читать таблицу).
На самом коде контракта останавливаться нет смысла: это стандартный C++14 с некоторыми специфичными для EOS конструкциями и библиотеками. Лучше скажут в EOSIO Wiki и EOSIO Developer Portal.
2.3.4. Фронтенд
Frontend-часть проекта реализовали с помощью React. Вместо привычного многим Redux решили использовать MobX, который значительно ускоряет разработку и позволяет управлять глобальным состоянием без головной боли.
Стадия интеграции front-blockchain прошла не так гладко, как ожидалось. Пакет eosjs дорабатывается очень активно, вслед за ним и EOS-кошелёк для браузера Scatter. В данной связке это часто вызывает неполадки. И не факт, что код, работавший вчера, будет отлично работать и сегодня. На эти грабли мы и наступили (и не первый раз). Час попыток и дебага в полусонном состоянии — проблема решена.
Рассмотрим упрощённую схему взаимодействия клиентской части приложения с eos. Для этого понадобятся библиотека eosjs и браузерное расширение Scatter.
Напоминаем! Scatter активно обновляется вслед за eosjs, не забывайте обновлять библиотеку.
Далее кратко рассмотрим чтение и запись. Существует два способа общения со смарт-контрактами в EOS: отправка транзакций (она вызывает модификацию блокчейна, при этом никаких возвращаемых значений не предусмотрено) и чтение таблиц (read-only действие).
Рассмотрим код отправки транзакции:
sendTransaction(funcName, data) {
return this.scatter
.suggestNetwork(this.network)
.then(() => this.scatter.getIdentity({ accounts: [this.network] }))
.then((identity) => {
let accountName = this.getAccountName(identity);
// wrap eos instance
const eos = this.scatter.eos(this.network, Eos, this.configEosInstance);
return eos.transaction(accountName, (contract) => {
contract[funcName](data, { authorization: accountName });
});
});
}
Аргументы на входе: имя функции и объект, его значения — аргументы этой функции. Третья строка: подтверждаем сеть, через которую взаимодействуем с EOS. Четвёртая строка: получаем identity
, параметр — объект с полем accounts
(для данной сети). Функция getAccountName
возвращает первый аккаунт из полученного списка аккаунтов (в объекте identity
).
В данном примере Scatter используется для подписи транзакции. Scatter — это обертка над экземпляром класса Eos
. На строке 9 вызываем метод eos
, его параметры:
this.network
— объект с параметрами сети.Eos
— экземпляр eosjs.this.configEosInstance
— объект с параметрами для экземпляра Eos (см. доку eosjs).
Последний метод transaction
на вход принимает accountName
и callback
, аргумент callback
’а — контракт, находящийся в аккаунте accountName
. В callback
’e вызываем метод полученного контракта с объектом, его ключи — аргументы вызываемого метода.
Рассмотрим метод чтения таблиц:
readTable(data) {
this.eos = this.scatter.eos(this.network, Eos, this.configEosInstance);
return this.eos.getTableRows({
json: true,
limit: 1,
...data,
});
}
Тут для чтения нам необходим только экземпляр eos
.
Для оформления интерфейса мы, отбросив Materialize, Semantic и Ant, на скорую руку накидали стили сами. В последние часы хакатона появилась идея оживить UI, добавить визуализацию процесса. Подсветили новые строки таблицы на две секунды зелёным цветом и получили крутой эффект а-ля биржевые котировки. Улучшение значительно повысило привлекательность проекта и стало заключительным этапом построения UI.
3. Сборка
За три часа до окончания времени у нас был Raspberry Pi с подключёнными к нему счётчиком электричества «Меркурий» и считывателем RFID, а также световая индикация. Всё электричество стола шло через «Меркурий». На каждые потраченные 0,3125 Вт⋅ч, а также на каждую «покупку» Raspberry Pi отправлял транзакции в наш блокчейн, а поставщик услуг мог управлять пользователями, датчиками, биллингом и видеть статистику потребления.
Ещё час мы спокойно занимались проверками и добавляли последние штрихи. За два часа до окончания времени мы получили целостный продукт с двумя реализованными кейсами, полностью иллюстрирующий концепт, и даже никаких коммитов в последние минуты!
4. Демонстрация
Демонстрации проектов (ака питчи) состояли из двух этапов. На первом этапе 69 команд-участниц разделили на четыре группы, каждая из них питчилась отдельно перед двумя судьями и без зрителей. Судьи выставляли оценки (четыре критерия по 5 баллов каждый), и на основании этих оценок отбирался топ-10 команд на второй этап. Эти команды получали возможность выступить с проектом на большой сцене перед зрителями и всеми восемью судьями.
Мы оказались в первой группе, нашими судьями были CEO и президент (интересно, чем эти должности отличаются) компании Everipedia. На выступление отводилось три минуты, их строго отслеживали п�� таймеру. Мы свою сбивчивую, но призванную впечатлить речь закончили на 30 секунд раньше срока. Судьи что-то поверхностно и достаточно коротко спросили, и демонстрация была окончена.
Мы, наивные, удивились, что реальной работоспособности продукта и тем более коду судьи совершенно не уделили внимания. Технические вопросы реализации их не интересовали даже в малейшей степени. Мы могли бы с таким же успехом просто показать мигающий лампочками Raspberry Pi и картинку на фронте.
Возникло ощущение, что с презентацией проекта мы провалились, поскольку рассчитывали произвести впечатление фактическим решением, прототипом, а не просто красочным описанием социально значимого и амбициозного проекта. Всё было разыграно как по нотам: мы описали проблему, боль, своё решение, показали, как оно работает, и описали планы развития проекта. Зная заранее о методах судейства, мы бы сделали многое иначе.
Судьи из четырёх потоков первого раунда свели свои оценки и обменялись мнениями за 15 минут после окончания питчей. После началось оглашение победителей. В зале царила нервная обстановка: уставшие после 26-часового марафона люди хотели выиграть, сильных команд было много, и они знали, что могут претендовать на победу. И мы это знали — и ждали результатов.
Чтобы зрители не расслаблялись, результаты объявляли тремя частями. Первые четыре финалиста, потом ещё три, затем ещё три. Между объявлениями и в конце — выступления. Мы не попали в топ-10 и не получили возможности выйти на большую сцену. В десятку пробились две русскоязычные команды, одна из которых в итоге стала третьей. Поздравляем победителей, они достойны своих призовых мест.
5. Заключение и планы
Организатор хакатона команда AngelHack великолепно выполнила свою работу. Хакатон прошёл на высшем уровне, практически без нареканий. Наши призы на нём — приятное путешествие, полезный опыт, общение с сильными разработчиками и менторами. Да, мы ожидали более технического судейства, но в любом случае мы прекрасно понимали, что наш шанс на победу — это только шанс.
За 26 часов мы успели подготовить полностью рабочую модель фреймворка IoT-платежей через блокчейн EOS. Мы гордимся этим результатом и уверены в его ценности, применимости и масштабируемости.
В дальнейших планах — снабдить решение полным и удобным UI (в помощь — наша кросс-блокчейн-платформа Smartz), поработать над конкретными кейсами применения. Если кто-то знает, как наладить массовое производство blockchain-ready счётчиков электричества, воды и газа, — нам будет интересно пообщаться. :)
Мы примерно прикинули, исходя из некоторых инсайдов, что только в России в организациях типа условных «РосГорГаз», «МосОблСвет» и т. п. более 5 тысяч человек чуть ли не вручную сверяют и уточняют коммунальные платежи. По крупной прикидке, наша система позволит сэкономить на этой работе около 100 млн долларов в год, а уж количество потенциально сбережённых нервных клеток не поддаётся исчислению. Так что не за горами проникновение нашего решения под рабочим названием SensorPay в ваш электрощиток!
Команда проекта и соавторы статьи:
Юрий Yuvasee Васильчиков (entrepreneur)
Алгыс algs Иевлев (hardware & backend developer)
Алексей therealal Макеев (architect, backend developer, devops)
Вячеслав bolbu Мельников (frontend developer)
Владимир quantum Храмов (blockchain & backend developer)