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. Биллинг покупок


На втором дыхании, которое открылось благодаря то ли энергетикам, то ли раннему завтраку, написали биллинг для покупок в магазине. Общая схема работы такова:


  1. Поставщик создаёт контракт биллинга и прописывает его в своём общем контракте.
  2. Поставщик устанавливает на выходе из магазина рамки, которые умеют считывать RFID, взаимодействовать с EOS и имеют свои аккаунты, прописываемые в контракте биллинга.
  3. Каждый товар в магазине оснащается RFID-меткой, все метки прописываются в контракте биллинга.
  4. Покупатель оплачивает товар, сканируя его RFID, и товар удаляется из контракта биллинга.
  5. На выходе из магазина рамки дополнительно считывают 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, его параметры:


  1. this.network — объект с параметрами сети.
  2. Eos — экземпляр eosjs.
  3. 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)