Hyperledger Fabric (HLF) — платформа с открытым исходным кодом, использующая технологию распределенного реестра (DLT — distributed ledger technology), предназначенная для разработки приложений, работающих в среде бизнес-сетей, созданных и контролируемых консорциумом организаций с применением правил доступа (permissioned). Платформа поддерживает смарт-контракты, в терминах HLF — чейнкоды (chaincode), создаваемые на языках общего назначения, таких как Golang, JavaScript, Java, в отличие, от, например, Ethereum, в котором используется контрактно-ориентированный, ограниченный по функциональности язык Solidity (LLL, Viper и др).  Разработка и тестирование чейнкодов, в силу необходимости развертывания значительного количества компонент блокчейн-сети, может быть достаточно долгим процессом с высокими временными затратами на тестирование изменений. В статье рассматривается подход к быстрой разработке и тестированию HLF смарт-контрактов на Golang с помощью библиотеки CCKit. С точки зрения разработчика блокчейн-приложение состоит из двух основных частей: Чтение данных производится обычно через “домашний” узел блокчейн сети. Для записи данных приложение отправляет запросы на узлы организаций, участвующих в “политике одобрения” определенного смарт-контракта.  Для разработки off-chain кода (API и пр.) используется специализированный SDK, который инкапсулирует взаимодействие с блокчейн узлами, сбор ответов и т.п. Для HLF есть реализации SDK на Go (1, 2), Node.Js и Java  Канал — это отдельная подсеть узлов, поддерживающая изолированную цепь блоков (ledger), а также текущее состояние (ключ-значение) цепи блоков (world state), используемое для работы смарт контрактов. Узел сети может иметь доступ к произвольному количеству каналов. Транзакция в Hyperledger Fabric — это атомарное обновление состояния цепи блоков, результат выполнения метода чейнкода. Транзакция состоит из запроса на вызов метода чейнкода с некоторыми аргументами (Transaction Proposal), подписанного вызывающим узлом, и набора ответов (Transaction Proposal Response) от узлов, на которых проводилось "подтверждение" (Endorsement) транзакции. Ответы содержат информацию об изменяющихся парах ключ-значение состояния цепи блоков Read-Write Set и служебную информацию (подписи и сертификаты подтвердивших транзакцию узлов). Т.к. цепи блоков отдельных каналов физически разделены, транзакция может выполняться только в контексте одного канала. "Классические" блокчейн-платформ, таких как Bitcoin и Ethereum, используют цикл исполнения транзакций "Упорядочивание-Исполнение", выполняемый всеми узлами, что ограничивает масштабируемость блокчейн-сети.  Hyperledger Fabric использует архитектуру исполнения и распространения транзакций, в которой есть 3 основных операции: Исполнение (execute) — создание смарт-контрактом, запущенным на одном или нескольких узлах сети, транзакции — атомарного изменения состояния распределенного реестра (endorsement) Упорядочивание (order) — упорядочивание и группировка транзакций в блоки специализированным сервисом orderer с использованием подключаемого (pluggable) алгоритма консенсуса. Проверка (validate) — проверка узлами сети поступающих от orderer транзакций перед размещением информации из них в своей копии распределенного реестра  Подобный подход позволяет проводить этап исполнения транзакции до ее поступления в блокчейн-сеть, а также горизонтально масштабировать работу узлов сети. Чейнкод, который также можно называть смарт-контрактом, это программа, написанная на Golang, JavaScript ( HLF 1.1+) или Java ( HLF 1.3+), в которой определяются правила создания транзакций, изменяющих состояние цепи блоков. Выполнение программы производится одновременно на нескольких независимых узлах распределенной сети блокчейн узлов, создающей нейтральную среду выполнения смарт-контрактов за счет сверки результатов выполнения программы на всех необходимых для "подтверждения" транзакции узлах. Чейнкод должен имплементировать интерфейс, состоящий из методов: Чейнкод устанавливается на узлах (peer) блокчейн-сети. На системном уровне каждому экземпляру чейнкода соответствует отдельный docker-контейнер, привязанный к определенному узлу сети, который выполняет диспетчеризацию вызовов на исполнения чейнкода.
В отличие от смарт-контрактов Ethereum, логика работы чейнкода может обновляться, но для этого требуется чтобы все узлы, на которых размещается чейнкод, установили обновленную версию. В ответ на вызов функции чейнкода извне через SDK, чейнкод создает изменение состояние цепи блоков (Read-Write Set), а также события. Чейнкод относится к определенному каналу и может изменять данные только в одном канале. При этом, если узел сети, на котором установлен чейнкод, также имеет доступ к другим каналам, в логике чейнкода может быть чтение данных из этих каналов. Специальные чейнкоды для управления различными аспектами работы блокчейн сети называются системными чейнкодами. Политика одобрения определяет правила консенсуса на уровне транзакций, создаваемых определенным чейнкодом. В политике задаются правила, определяющие какие узлы канала должны создать транзакцию. Для этого каждый из узлов, указанный в политике одобрения, должен запустить на исполнение метод чейнкода (шаг "Execute"), провести "симуляцию", после чего подписанные результаты будут собраны и проверены SDK, который инициировал транзакцию (все результаты симуляции должны быть идентичными, должны присутствовать подписи всех необходимых согласно политике узлов). Далее SDK отправляет транзакцию на orderer, после чего все узлы, имеющие доступ к каналу, через orderer получат транзакцию и выполнят шаг "Validate". Важно подчеркнуть, что не все узлы канала должны участвовать в шаге "Execute". Политика одобрения определяется в момент инстанциации (instantiate) или обновления (upgrade) чейнкода. В версии 1.3 появилась возможность задавать политику не только на уровне чейнкода, но и на уровне отдельных ключей состояния цепи блоков (state based endorsement). Примеры политик одобрения: Событие — это именованный набор данных, который позволяет публиковать “ленту обновлений” состояния блокчейн цепи. Набор атрибутов события определяет чейнкод. Узел сети подключается к произвольному количеству каналов, на которые у него есть права доступа. Узел сети поддерживает свою версию цепи блоков и состояния цепи блоков, а также обеспечивает среду для запуска чейнкодов. Если узел сети не входит в политику одобрения, то на нем не обязательно должны быть установлены чейнкоды. На уровне ПО узла сети текущее состояние цепи блоков (world state) может хранится в LevelDB или в CouchDB. Преимуществом CouchDB является поддержка расширенных запросов (rich query), использующих синтаксис MongoDB. Сервис упорядочивания транзакций принимает на вход подписанные транзакции и обеспечивает распространение транзакций по узлам сети в правильном порядке. Orderer не запускает смарт-контракты и не содержит цепь блоков и состояние цепи блоков. На текущий момент (1.3) есть две реализации orderer — solo для разработки и версия основанная на Kafka, обеспечивающая устойчивость к сбоям (crash fault tolerance). Реализация orderer, поддерживающая устойчивость к некорректному поведению некоторой доли участников (Byzantine fault tolerance) ожидается в конце 2018 года. В сети Hyperledger Fabric все участники имеют известные другим участникам реквизиты (identity). При идентификации используется инфраструктура публичных ключей (PKI), с помощью которой создаются X.509 сертификаты для организаций, элементов инфраструктуры (узел, orderer), приложений и конечных пользователей. Как результат, доступ на чтение и изменение данных, может контролироваться посредством правил доступа на уровне сети, отдельного канала или в логике смарт-контракта. В одной блокчейн сети может одновременно работать несколько сервисов идентификации различных типов. Чейнкод может рассматриваться как объект, имеющий методы, реализующие определенную бизнес-логику. В отличие от классического ООП у чейнкода не может быть полей — атрибутов. Для работы с состоянием (state), хранение которого обеспечивает блокчейн-нлатформа HLF, используется прослойка ChaincodeStubInterface, передаваемая при вызове методов Init и Invoke. Она обеспечивает возможность получить аргументы вызова функции и провести изменения в состоянии цепи блоков:  В смарт-контракте Ethereum, разработанном на Solidity, каждому методу соответствует публичная функция. В чейнкоде Hyperledger Fabric в методах Init и Invoke с помощью функции ChaincodeStubInterface.GetArgs() можно получить аргументы вызова функции в виде массива массивов байт, при этом первый элемент массива при вызове Invoke содержит название функции чейнкода. Т.к. через метод Invoke проходит вызов любого метода чейнкода, можно сказать, что это реализация паттерна front controller. Например, если рассматривать реализацию стандартного для Ethereum интерфейса токена ERC-20 смарт-контракт должен реализовывать методы: и др. В случае реализации HLF чейнкод в функции Invoke должен уметь обрабатывать случаи, когда первый аргумент вызова Invoke содержит название ожидаемых методов (например, “totalSupply” или “balanceOf”). Пример реализации стандарта ERC-20 можно увидеть здесь. Помимо документации Hyperledger Fabric можно привести еще несколько примеров чейнкодов: Реализация чейнкодов в данных примерах довольно многословна и содержит много повторяющейся логики выбора вызываемой функций «маршрутизация»), проверки числа аргументов, json marshalling / unmarshalling: Подобная организация кода приводит к ухудшению читабельности кода и возможным ошибкам, наподобие этой, когда просто забыли провести анмаршаллинг входных данных. В презентациях по поводу планов развития HLF есть упоминание о переработке подхода к разработке чейнкодов, в частности внедрение аннотаций в Java-чейнкод и т.п., однако планы относятся к версии, которая ожидается только в 2019 году. Опыт разработки смарт-контрактов привел выводу, что разрабатывать и тестировать чейнкоды будет проще, если выделить базовый функционал в отдельную библиотеку. В библиотеке CCKit обобщаются практики разработки и тестирования чейнкодов. В части разработки расширений чейнкодов как пример использовалась библиотека расширений OpenZeppelin для смарт-контрактов Ethereum. CCKit использует следующие архитектурные решения: Под маршрутизацией понимается алгоритм, по которому приложение реагирует на клиентский запрос. Данный подход применяется, например, практически во всех http-фреймворках. Роутер использует определенные правила для того чтобы связать запрос и обработчик запроса. Применительно к чейнкоду — это связать название функции чейнкода и функцию-обработчик.  В последних примерах смарт контрактов, например в Insurance App, для этого используется маппинг между названием функции чейнкода и функцией в Golang коде вида: В маршрутизаторе CCKit применен подход, аналогичный http-роутеру, а также добавлена возможность использования контекста запроса к функции чейнкода и функций промежуточной обработки (middleware) По аналогии с контекстом http-запроса, в котором обычно есть доступ к параметрам http-запроса, в маршрутизаторе CCKit используется контекст обращения к функции смарт-контракта, который является абстракцией поверх shim.ChaincodeStubInterface. Контекст может являться единственным аргументом обработчика функции чейнкода, через него обработчик может получить аргументы вызова функции, а также доступ к вспомогательным функциональностям работы с состоянием смарт-контракта (State), создания ответов (Response) и др.  Т.к. Context — это интерфейс, в определенных чейнкодах он может расширяться. Функции промежуточной обработки (middleware) вызываются перед вызовом обработчика метода чейнкода, имеют доступ к контексту обращения к методу чейнкода и к следующей промежуточной функции или непосредственно обработчику метода чейнкода (next). Middleware может использоваться для: Интерфейс чейнкода предполагает что на вход подается массив массивов байт, каждый из элементов которого является атрибутом функции чейнкода. Для того чтобы в каждом обработчике функции чейнкода не проводить ручной анмаршаллинг данных из массива байт в golang тип данных (int, string, структура, массив) из аргументов вызова функций, в маршрутизаторе CCKit ожидаемые типы данных задаются в момент создания правила роутинга и тип преобразуется автоматически. В примере, который рассматривается далее, функция carGet ожидает аргумент строкового типа, а функция carRegister структуру CarPayload. Аргумент также именуется, что позволяет в обработчике получать его значение из контекста по имени. Пример обработчика будет дан ниже по тексту. Для описания схемы данных чейнкода также может использоваться Protobuf. Также автоматическая конвертация (маршаллинг) используется при записи данных в состояние смарт-контракта и при создании событий (golang тип сериализуется в массив байт) Для отладки чейнкода можно воспользоваться расширением debug, в котором реализованы методы смарт-контракта, которые позволят провести инспекцию наличия ключей в состоянии смарт-контракта, а также напрямую читать / изменить / удалить значение по ключу. Для логгирования в контексте вызова функции чейнкода может использоваться метод Log(), который возвращает экземпляр логгера, используемого в HLF. В составе расширения owner реализованы базовые примитивы хранения информации о владельце инстанциированного чейнкода и модификаторы доступа (middleware) к методам смарт-контракта. Разворачивание блокчейн сети, инсталляция и инициализация чейнкодов — это достаточно сложная по настройке и долгая процедура. Время на повторную инсталляцию / апгрейд кода смарт-контракта можно сократить за счет использования DEV режима работы смарт-контракта, однако процесс обновления кода будет все равно небыстрым. Пакет shim содержит реализацию MockStub, который оборачивает вызовы к коду чейнкода, имитируя его работу в блокчейн среде HLF. Использование MockStub позволяет получить результаты тестирования практически мгновенно и позволяет сокращать время разработки. Если рассмотреть общую схему работы чейнкода в HLF, MockStub по сути заменяет SDK, позволяя делать вызовы функций чейнкода, и имитирует среду запуска чейнкода на узле сети.  MockStub из поставки HLF содержит имплементацию почти всех методов интерфейса shim.ChaincodeStubInterface, однако в текущей версии (1.3), в нем отсутствует реализация некоторых важных методов, таких как GetCreator. Т.к. чейнкод может использовать этот метод для получения сертификата создателя транзакции с целью контроля доступа, для максимального покрытия в тестах важной является возможность иметь заглушку этого метода. Библиотека CCKit содержит расширенную версию MockStub, в которой содержится имплементация недостающих методов, а также методы работы с каналами событий и др. Для примера, создадим простой чейнкод для для хранения информации о зарегистрированных автомобилях Состояние чейнкода — это хранилище ключ-значение, в котором ключ — это строка, значение — массив байт. Базовой практикой является в качестве значения хранить сериализованные в json экземпляры golang структур данных. Соответственно, для работы с данными в чейнкоде, после чтения из состояния нужно провести анмаршаллинг массива байт. Для записи об автомобиле будем использовать следующий набор атрибутов: Для передачи данных в чейнкод создадим отдельную структуру, содержащую только поля, поступающие извне чейнкода: Ключи записи в состоянии смарт контракта — это строка. Также поддерживается возможность создания композитных ключей, в которых части ключа разделяются нулевым байтом (U+0000) В CCKit функции работы с состоянием смарт контракта могут автоматически создавать ключи для записей, если переданные структуры поддерживают Keyer интерфейс  Для записи об автомобиле функция создания ключа будет следующей: В методе-конструкторе чейнкода мы можем определить функции чейнкода и их аргументы. В чейнкоде регистрации автомобилей будет 3 функции В примере выше используется структура Chaincode в которой обработка методов Init и Invoke делегируется роутеру: Использование роутера и базовой структуры Chaincode позволяет повторно использовать функции-обработчики. Например, чтобы реализовать чейнкод без проверки доступа к функции carRegister будет достаточно создать новый метод-конструктор Функции Golang — обработчики функций смарт контракта в CCKit роутере могут быть трех типов: Аргументы функций чейнкода, описанные в роутере, будут автоматически сконвертированы из массивов байт в целевые типы данных (строку или структуру CarPayload)
Функция чейнкода использует State методы, которые упрощают извлечение и сохранение в состояние чейнкода данных за счет автоматического создания ключей и конвертации передаваемых данных в массивы байт ( в состоянии чейнкода записывается массив байт) Общий принцип тестирования смарт-контрактов схож с тестированием любого другого кода — создается набор эталонных вызовов методов в предопределенном окружении, для результатов которых прописываются утверждения. Для тестирования удобно использовать практики BDD – Behavior Driven Development, которые наряду с тестами позволяют создать документацию и примеры использования. Для тестирования, например, смарт-контрактов Ethereum можно использовать эмулятор ноды ganache-cli и фреймворк truffle. Для тестирования golang смарт-контрактов достаточно Mockstub. В примере создадим тест, проверяющий корректность работы функций чейнкода. Полный пример теста доступен тут. В коде тестов используется библиотека Ginkgo, которая расширяет инфраструктуру тестов Go, что позволяет использовать стандартную команду go test. В тестах также будет использоваться пакет gomega для создания утверждений общего характера и пакет expect, в котором содержатся утверждения, используемые при работе с чейнкодами. В тестах также будут использоваться фикстуры, содержащие примеры CarPayload: Далее потребуется создать экземпляр MockStub с чейнкодом Cars.  Т.к. в чейнкоде cars используется информация о сертификате создающего транзакцию, далее мы загружаем тестовые сертификаты. В функции BeforeSuite мы инициализируем чейнкод Car от имени сертификата под кодом authority и ожидаем что метод Init чейнкода возвратит успешный результат. Нужно отметить, что в чейнкоде Cars в методе Init в состояние чейнкода заносятся реквизиты вызвавшего Init, он считается владельцем чейнкода. Далее мы вызываем функции чейнкода и сравниваем ответы с эталонными. Например, мы можем проверить что владелец чейнкода может вызвать метод CarRegister, в тоже время при попытке вызова функции чейнкода от другого сертификата должна вернуться ошибка. Мы также можем убедиться что при попытке занесения дублируюшей информации также будет ошибка: Смарт-контракты HLF предлагают разработку на таких языках программирования как Go, Java, JavaScript, что, по сравнению со специализированными, контрактно-ориентированными языками (Solidity) позволяет использовать имеющиеся фреймворки / библиотеки в смарт-контрактах. Также в подход к разработке / тестированию чейнкодов технически возможно внедрять собственные наработки. Архитектура чейнкодов в HLF активно дорабатывается, появляются функции, которых явно раньше не хватало (постраничный запрос списка записей и пр.). Контрибьюторы Hypeledger Fabric активно призывают заинтересованных разработчиков подключаться к развитию проекта, т.к. поле для развития достаточно большое. S7 Airlines, Техлаб