Hyperledger Fabric (HLF) — платформа с открытым исходным кодом, использующая технологию распределенного реестра (DLT — distributed ledger technology), предназначенная для разработки приложений, работающих в среде бизнес-сетей, созданных и контролируемых консорциумом организаций с применением правил доступа (permissioned).
Платформа поддерживает смарт-контракты, в терминах HLF — чейнкоды (chaincode), создаваемые на языках общего назначения, таких как Golang, JavaScript, Java, в отличие, от, например, Ethereum, в котором используется контрактно-ориентированный, ограниченный по функциональности язык Solidity (LLL, Viper и др).
Разработка и тестирование чейнкодов, в силу необходимости развертывания значительного количества компонент блокчейн-сети, может быть достаточно долгим процессом с высокими временными затратами на тестирование изменений. В статье рассматривается подход к быстрой разработке и тестированию HLF смарт-контрактов на Golang с помощью библиотеки CCKit.
Приложение на базе HLF
С точки зрения разработчика блокчейн-приложение состоит из двух основных частей:
- On-chain — смарт-контракты (программы), работающие в изолированном окружении блокчейн-сети, определяющие правила создания и состав атрибутов транзакций. В смарт-контракте основные действия — это чтение, обновление и удаление данных из состояния (state) блокчейн-сети. Следует подчеркнуть, что удаление данных из состояния оставляет информацию о том, что эти данные присутствовали.
- Off-chain — приложение (например, API), взаимодействующее с блокчейн средой через SDK. Под взаимодействием понимается вызов функций смарт-контрактов и наблюдение за событиями смарт-контракта — внешние события могут порождать изменение данных в смарт-контракте, в то же время события в смарт контракте могут инициировать действия во внешних системах.
Чтение данных производится обычно через “домашний” узел блокчейн сети. Для записи данных приложение отправляет запросы на узлы организаций, участвующих в “политике одобрения” определенного смарт-контракта.
Для разработки off-chain кода (API и пр.) используется специализированный SDK, который инкапсулирует взаимодействие с блокчейн узлами, сбор ответов и т.п. Для HLF есть реализации SDK на Go (1, 2), Node.Js и Java
Компоненты Hyperledger Fabric
Канал
Канал — это отдельная подсеть узлов, поддерживающая изолированную цепь блоков (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+), в которой определяются правила создания транзакций, изменяющих состояние цепи блоков. Выполнение программы производится одновременно на нескольких независимых узлах распределенной сети блокчейн узлов, создающей нейтральную среду выполнения смарт-контрактов за счет сверки результатов выполнения программы на всех необходимых для "подтверждения" транзакции узлах.
Чейнкод должен имплементировать интерфейс, состоящий из методов:
type Chaincode interface {
// Init is called during Instantiate transaction
Init(stub ChaincodeStubInterface) pb.Response
// Invoke is called to update or query the ledger
Invoke(stub ChaincodeStubInterface) pb.Response
}
- Метод Init вызывается при инcтанциации или апгрейде чейнкода. В этом методе выполняется необходимая инициализация состояния чейнкода. Важно отличать в коде метода является ли вызов инстанциацией или апгрейдом, чтобы по ошибке не проинициализировать (обнулить) данные, которые в процессе работы чейнкода получили уже ненулевое состояние.
- Метод Invoke вызывается при обращении к любой функции чейнкода. В этом методе идет работа с состоянием смарт контрактов.
Чейнкод устанавливается на узлах (peer) блокчейн-сети. На системном уровне каждому экземпляру чейнкода соответствует отдельный docker-контейнер, привязанный к определенному узлу сети, который выполняет диспетчеризацию вызовов на исполнения чейнкода.
В отличие от смарт-контрактов Ethereum, логика работы чейнкода может обновляться, но для этого требуется чтобы все узлы, на которых размещается чейнкод, установили обновленную версию.
В ответ на вызов функции чейнкода извне через SDK, чейнкод создает изменение состояние цепи блоков (Read-Write Set), а также события. Чейнкод относится к определенному каналу и может изменять данные только в одном канале. При этом, если узел сети, на котором установлен чейнкод, также имеет доступ к другим каналам, в логике чейнкода может быть чтение данных из этих каналов.
Специальные чейнкоды для управления различными аспектами работы блокчейн сети называются системными чейнкодами.
Политика одобрения (Endorsement Policy)
Политика одобрения определяет правила консенсуса на уровне транзакций, создаваемых определенным чейнкодом. В политике задаются правила, определяющие какие узлы канала должны создать транзакцию. Для этого каждый из узлов, указанный в политике одобрения, должен запустить на исполнение метод чейнкода (шаг "Execute"), провести "симуляцию", после чего подписанные результаты будут собраны и проверены SDK, который инициировал транзакцию (все результаты симуляции должны быть идентичными, должны присутствовать подписи всех необходимых согласно политике узлов). Далее SDK отправляет транзакцию на orderer, после чего все узлы, имеющие доступ к каналу, через orderer получат транзакцию и выполнят шаг "Validate". Важно подчеркнуть, что не все узлы канала должны участвовать в шаге "Execute".
Политика одобрения определяется в момент инстанциации (instantiate) или обновления (upgrade) чейнкода. В версии 1.3 появилась возможность задавать политику не только на уровне чейнкода, но и на уровне отдельных ключей состояния цепи блоков (state based endorsement). Примеры политик одобрения:
- Узлы A,B,C,D
- Большинство узлов канала
- Не менее 3 узлов из A,B,C,D,E,F
Событие
Событие — это именованный набор данных, который позволяет публиковать “ленту обновлений” состояния блокчейн цепи. Набор атрибутов события определяет чейнкод.
Инфраструктура сети
Узел сети (Peer)
Узел сети подключается к произвольному количеству каналов, на которые у него есть права доступа. Узел сети поддерживает свою версию цепи блоков и состояния цепи блоков, а также обеспечивает среду для запуска чейнкодов. Если узел сети не входит в политику одобрения, то на нем не обязательно должны быть установлены чейнкоды.
На уровне ПО узла сети текущее состояние цепи блоков (world state) может хранится в LevelDB или в CouchDB. Преимуществом CouchDB является поддержка расширенных запросов (rich query), использующих синтаксис MongoDB.
Orderer
Сервис упорядочивания транзакций принимает на вход подписанные транзакции и обеспечивает распространение транзакций по узлам сети в правильном порядке.
Orderer не запускает смарт-контракты и не содержит цепь блоков и состояние цепи блоков. На текущий момент (1.3) есть две реализации orderer — solo для разработки и версия основанная на Kafka, обеспечивающая устойчивость к сбоям (crash fault tolerance). Реализация orderer, поддерживающая устойчивость к некорректному поведению некоторой доли участников (Byzantine fault tolerance) ожидается в конце 2018 года.
Сервис идентификации (Membership services)
В сети Hyperledger Fabric все участники имеют известные другим участникам реквизиты (identity). При идентификации используется инфраструктура публичных ключей (PKI), с помощью которой создаются X.509 сертификаты для организаций, элементов инфраструктуры (узел, orderer), приложений и конечных пользователей. Как результат, доступ на чтение и изменение данных, может контролироваться посредством правил доступа на уровне сети, отдельного канала или в логике смарт-контракта. В одной блокчейн сети может одновременно работать несколько сервисов идентификации различных типов.
Реализация чейнкода
Чейнкод может рассматриваться как объект, имеющий методы, реализующие определенную бизнес-логику. В отличие от классического ООП у чейнкода не может быть полей — атрибутов. Для работы с состоянием (state), хранение которого обеспечивает блокчейн-нлатформа HLF, используется прослойка ChaincodeStubInterface, передаваемая при вызове методов Init и Invoke. Она обеспечивает возможность получить аргументы вызова функции и провести изменения в состоянии цепи блоков:
type ChaincodeStubInterface interface {
// GetArgs returns the arguments intended for the chaincode Init and Invoke
GetArgs() [][]byte
// InvokeChaincode locally calls the specified chaincode
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response
// GetState returns the value of the specified `key` from the ledger.
GetState(key string) ([]byte, error)
// PutState puts the specified `key` and `value` into the transaction's writeset as a data-write proposal.
PutState(key string, value []byte) error
// DelState records the specified `key` to be deleted in the writeset of the transaction proposal.
DelState(key string) error
// GetStateByRange returns a range iterator over a set of keys in the ledger.
GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
// CreateCompositeKey combines the given `attributes` to form a composite key.
CreateCompositeKey(objectType string, attributes []string) (string, error)
// GetCreator returns `SignatureHeader.Creator` (e.g. an identity of the agent (or user) submitting the transaction.
GetCreator() ([]byte, error)
// and many more methods
}
В смарт-контракте Ethereum, разработанном на Solidity, каждому методу соответствует публичная функция. В чейнкоде Hyperledger Fabric в методах Init и Invoke с помощью функции ChaincodeStubInterface.GetArgs() можно получить аргументы вызова функции в виде массива массивов байт, при этом первый элемент массива при вызове Invoke содержит название функции чейнкода. Т.к. через метод Invoke проходит вызов любого метода чейнкода, можно сказать, что это реализация паттерна front controller.
Например, если рассматривать реализацию стандартного для Ethereum интерфейса токена ERC-20 смарт-контракт должен реализовывать методы:
- totalSupply()
- balanceOf(address _owner)
- transfer(address _to, uint256 _value)
и др. В случае реализации HLF чейнкод в функции Invoke должен уметь обрабатывать случаи, когда первый аргумент вызова Invoke содержит название ожидаемых методов (например, “totalSupply” или “balanceOf”). Пример реализации стандарта ERC-20 можно увидеть здесь.
Примеры чейнкодов
Помимо документации Hyperledger Fabric можно привести еще несколько примеров чейнкодов:
Реализация чейнкодов в данных примерах довольно многословна и содержит много повторяющейся логики выбора вызываемой функций «маршрутизация»), проверки числа аргументов, json marshalling / unmarshalling:
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
fmt.Println("invoke is running " + function)
// Handle different functions
if function == "initMarble" { //create a new marble
return t.initMarble(stub, args)
} else if function == "transferMarble" { //change owner of a specific marble
return t.transferMarble(stub, args)
} else if function == "readMarble" { //read a marble
return t.readMarble(stub, args)
} else ...
Подобная организация кода приводит к ухудшению читабельности кода и возможным ошибкам, наподобие этой, когда просто забыли провести анмаршаллинг входных данных. В презентациях по поводу планов развития HLF есть упоминание о переработке подхода к разработке чейнкодов, в частности внедрение аннотаций в Java-чейнкод и т.п., однако планы относятся к версии, которая ожидается только в 2019 году. Опыт разработки смарт-контрактов привел выводу, что разрабатывать и тестировать чейнкоды будет проще, если выделить базовый функционал в отдельную библиотеку.
CCKit — библиотека для разработки и тестирования чейнкодов
В библиотеке CCKit обобщаются практики разработки и тестирования чейнкодов. В части разработки расширений чейнкодов как пример использовалась библиотека расширений OpenZeppelin для смарт-контрактов Ethereum. CCKit использует следующие архитектурные решения:
Маршрутизация (routing) обращений к функциям смарт контракта
Под маршрутизацией понимается алгоритм, по которому приложение реагирует на клиентский запрос. Данный подход применяется, например, практически во всех http-фреймворках. Роутер использует определенные правила для того чтобы связать запрос и обработчик запроса. Применительно к чейнкоду — это связать название функции чейнкода и функцию-обработчик.
В последних примерах смарт контрактов, например в Insurance App, для этого используется маппинг между названием функции чейнкода и функцией в Golang коде вида:
var bcFunctions = map[string]func(shim.ChaincodeStubInterface, []string) pb.Response{
// Insurance Peer
"contract_type_ls": listContractTypes,
"contract_type_create": createContractType,
...
"theft_claim_process": processTheftClaim,
}
В маршрутизаторе CCKit применен подход, аналогичный http-роутеру, а также добавлена возможность использования контекста запроса к функции чейнкода и функций промежуточной обработки (middleware)
Контекст обращения к функции чейнкода
По аналогии с контекстом http-запроса, в котором обычно есть доступ к параметрам http-запроса, в маршрутизаторе CCKit используется контекст обращения к функции смарт-контракта, который является абстракцией поверх shim.ChaincodeStubInterface. Контекст может являться единственным аргументом обработчика функции чейнкода, через него обработчик может получить аргументы вызова функции, а также доступ к вспомогательным функциональностям работы с состоянием смарт-контракта (State), создания ответов (Response) и др.
Context interface {
Stub() shim.ChaincodeStubInterface
Client() (cid.ClientIdentity, error)
Response() Response
Logger() *shim.ChaincodeLogger
Path() string
State() State
Time() (time.Time, error)
Args() InterfaceMap
Arg(string) interface{}
ArgString(string) string
ArgBytes(string) []byte
SetArg(string, interface{})
Get(string) interface{}
Set(string, interface{})
SetEvent(string, interface{}) error
}
Т.к. Context — это интерфейс, в определенных чейнкодах он может расширяться.
Функции промежуточной обработки (middleware)
Функции промежуточной обработки (middleware) вызываются перед вызовом обработчика метода чейнкода, имеют доступ к контексту обращения к методу чейнкода и к следующей промежуточной функции или непосредственно обработчику метода чейнкода (next). Middleware может использоваться для:
- конвертации входных данных ( в примере ниже p.String и p.Struct — это middleware)
- ограничения доступа к функции (например, owner.Only )
- завершения цикла обработки запроса
- вызова следующей функции промежуточной обработки из стека
Конвертация структур данных
Интерфейс чейнкода предполагает что на вход подается массив массивов байт, каждый из элементов которого является атрибутом функции чейнкода. Для того чтобы в каждом обработчике функции чейнкода не проводить ручной анмаршаллинг данных из массива байт в golang тип данных (int, string, структура, массив) из аргументов вызова функций, в маршрутизаторе CCKit ожидаемые типы данных задаются в момент создания правила роутинга и тип преобразуется автоматически. В примере, который рассматривается далее, функция carGet ожидает аргумент строкового типа, а функция carRegister структуру CarPayload. Аргумент также именуется, что позволяет в обработчике получать его значение из контекста по имени. Пример обработчика будет дан ниже по тексту. Для описания схемы данных чейнкода также может использоваться Protobuf.
r.Group(`car`).
Query(`List`, cars). // chain code method name is carList
Query(`Get`, car, p.String(`id`)). // chain code method name is carGet, method has 1 string argument "id"
Invoke(`Register`, carRegister, p.Struct(`car`, &CarPayload{}), // 1 struct argument
owner.Only) // allow access to method only for chaincode owner (authority)
Также автоматическая конвертация (маршаллинг) используется при записи данных в состояние смарт-контракта и при создании событий (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 структур данных. Соответственно, для работы с данными в чейнкоде, после чтения из состояния нужно провести анмаршаллинг массива байт.
Для записи об автомобиле будем использовать следующий набор атрибутов:
- Идентификатор (номер автомобиля)
- Модель автомобиля
- Информацию о владельце автомобиля
- Информацию о времени изменения данных
// Car struct for chaincode state
type Car struct {
Id string
Title string
Owner string
UpdatedAt time.Time // set by chaincode method
}
Для передачи данных в чейнкод создадим отдельную структуру, содержащую только поля, поступающие извне чейнкода:
// CarPayload chaincode method argument
type CarPayload struct {
Id string
Title string
Owner string
}
Работа с ключами
Ключи записи в состоянии смарт контракта — это строка. Также поддерживается возможность создания композитных ключей, в которых части ключа разделяются нулевым байтом (U+0000)
func CreateCompositeKey(objectType string, attributes []string) (string, error)
В CCKit функции работы с состоянием смарт контракта могут автоматически создавать ключи для записей, если переданные структуры поддерживают Keyer интерфейс
// Keyer interface for entity containing logic of its key creation
type Keyer interface {
Key() ([]string, error)
}
Для записи об автомобиле функция создания ключа будет следующей:
const CarEntity = `CAR`
// Key for car entry in chaincode state
func (c Car) Key() ([]string, error) {
return []string{CarEntity, c.Id}, nil
}
Декларация функций смарт-контракта (маршрутизация)
В методе-конструкторе чейнкода мы можем определить функции чейнкода и их аргументы. В чейнкоде регистрации автомобилей будет 3 функции
- carList, возвращает массив структур Car
- carGet, принимает идентификатор автомобиля и возвращает структуру Car
- carRegister, принимает сериализованный экземпляр структуры CarPayload и возвращает результат регистрации. Доступ к этому методу возможен только для владельца чейнкода, который сохраняется с применением middleware из пакета owner
func New() *router.Chaincode {
r := router.New(`cars`) // also initialized logger with "cars" prefix
r.Init(invokeInit)
r.Group(`car`).
Query(`List`, queryCars). // chain code method name is carList
Query(`Get`, queryCar, p.String(`id`)). // chain code method name is carGet, method has 1 string argument "id"
Invoke(`Register`, invokeCarRegister, p.Struct(`car`, &CarPayload{}), // 1 struct argument
owner.Only) // allow access to method only for chaincode owner (authority)
return router.NewChaincode(r)
}
В примере выше используется структура Chaincode в которой обработка методов Init и Invoke делегируется роутеру:
package router
import (
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
// Chaincode default chaincode implementation with router
type Chaincode struct {
router *Group
}
// NewChaincode new default chaincode implementation
func NewChaincode(r *Group) *Chaincode {
return &Chaincode{r}
}
//======== Base methods ====================================
//
// Init initializes chain code - sets chaincode "owner"
func (cc *Chaincode) Init(stub shim.ChaincodeStubInterface) peer.Response {
// delegate handling to router
return cc.router.HandleInit(stub)
}
// Invoke - entry point for chain code invocations
func (cc *Chaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// delegate handling to router
return cc.router.Handle(stub)
}
Использование роутера и базовой структуры Chaincode позволяет повторно использовать функции-обработчики. Например, чтобы реализовать чейнкод без проверки доступа к функции carRegister
будет достаточно создать новый метод-конструктор
Реализация функций смарт-контракта
Функции Golang — обработчики функций смарт контракта в CCKit роутере могут быть трех типов:
- StubHandlerFunc — стандартный интерфейс обработчика, принимает shim.ChaincodeStubInterface, возвращает стандартный ответ peer.Response
- ContextHandlerFunc — принимает контекст и возвращает peer.Response
- HandlerFunc — принимает контекст, возвращает интерфейс и ошибку. Возвращаться может массив байт или любой golang тип, который автоматически конвертируется в массив байт, на базе которого создается peer.Response. Статус ответа будет shim.Ok или shim.Error, в зависимости от переданной ошибки.
// StubHandlerFunc acts as raw chaincode invoke method, accepts stub and returns peer.Response
StubHandlerFunc func(shim.ChaincodeStubInterface) peer.Response
// ContextHandlerFunc use stub context as input parameter
ContextHandlerFunc func(Context) peer.Response
// HandlerFunc returns result as interface and error, this is converted to peer.Response via response.Create
HandlerFunc func(Context) (interface{}, error)
Аргументы функций чейнкода, описанные в роутере, будут автоматически сконвертированы из массивов байт в целевые типы данных (строку или структуру CarPayload)
Функция чейнкода использует State методы, которые упрощают извлечение и сохранение в состояние чейнкода данных за счет автоматического создания ключей и конвертации передаваемых данных в массивы байт ( в состоянии чейнкода записывается массив байт)
// car get info chaincode method handler
func car(c router.Context) (interface{}, error) {
return c.State().Get( // get state entry
Key(c.ArgString(`id`)), // by composite key using CarKeyPrefix and car.Id
&Car{}) // and unmarshal from []byte to Car struct
}
// cars car list chaincode method handler
func cars(c router.Context) (interface{}, error) {
return c.State().List(
CarKeyPrefix, // get list of state entries of type CarKeyPrefix
&Car{}) // unmarshal from []byte and append to []Car slice
}
// carRegister car register chaincode method handler
func carRegister(c router.Context) (interface{}, error) {
// arg name defined in router method definition
p := c.Arg(`car`).(CarPayload)
t, _ := c.Time() // tx time
car := &Car{ // data for chaincode state
Id: p.Id,
Title: p.Title,
Owner: p.Owner,
UpdatedAt: t,
}
return car, // peer.Response payload will be json serialized car data
c.State().Insert( //put json serialized data to state
Key(car.Id), // create composite key using CarKeyPrefix and car.Id
car)
}
Тесты смарт-контракта
Общий принцип тестирования смарт-контрактов схож с тестированием любого другого кода — создается набор эталонных вызовов методов в предопределенном окружении, для результатов которых прописываются утверждения. Для тестирования удобно использовать практики BDD – Behavior Driven Development, которые наряду с тестами позволяют создать документацию и примеры использования.
Для тестирования, например, смарт-контрактов Ethereum можно использовать эмулятор ноды ganache-cli и фреймворк truffle. Для тестирования golang смарт-контрактов достаточно Mockstub.
Пример теста
В примере создадим тест, проверяющий корректность работы функций чейнкода. Полный пример теста доступен тут.
В коде тестов используется библиотека Ginkgo, которая расширяет инфраструктуру тестов Go, что позволяет использовать стандартную команду go test
. В тестах также будет использоваться пакет gomega для создания утверждений общего характера и пакет expect, в котором содержатся утверждения, используемые при работе с чейнкодами.
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
examplecert "github.com/s7techlab/cckit/examples/cert"
"github.com/s7techlab/cckit/extensions/owner"
"github.com/s7techlab/cckit/identity"
"github.com/s7techlab/cckit/state"
testcc "github.com/s7techlab/cckit/testing"
expectcc "github.com/s7techlab/cckit/testing/expect"
)
func TestCars(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cars Suite")
}
В тестах также будут использоваться фикстуры, содержащие примеры CarPayload:
var Payloads = []*Car{{
Id: `A777MP77`,
Title: `VAZ`,
Owner: `victor`,
}, {
Id: `O888OO77`,
Title: `YOMOBIL`,
Owner: `alexander`,
}, {
Id: `O222OO177`,
Title: `Lambo`,
Owner: `hodl`,
}}
Далее потребуется создать экземпляр MockStub с чейнкодом Cars.
//Create chaincode mock
cc := testcc.NewMockStub(`cars`, New())
Т.к. в чейнкоде cars используется информация о сертификате создающего транзакцию, далее мы загружаем тестовые сертификаты.
// load actor certificates
actors, err := identity.ActorsFromPemFile(`SOME_MSP`, map[string]string{
`authority`: `s7techlab.pem`,
`someone`: `victor-nosov.pem`}, examplecert.Content)
В функции BeforeSuite мы инициализируем чейнкод Car от имени сертификата под кодом authority и ожидаем что метод Init чейнкода возвратит успешный результат. Нужно отметить, что в чейнкоде Cars в методе Init в состояние чейнкода заносятся реквизиты вызвавшего Init, он считается владельцем чейнкода.
BeforeSuite(func() {
// init chaincode
expectcc.ResponseOk(cc.From(actors[`authority`]).Init()) // init chaincode from authority
})
Далее мы вызываем функции чейнкода и сравниваем ответы с эталонными. Например, мы можем проверить что владелец чейнкода может вызвать метод CarRegister, в тоже время при попытке вызова функции чейнкода от другого сертификата должна вернуться ошибка.
It("Allow authority to add information about car", func() {
//invoke chaincode method from authority actor
expectcc.ResponseOk(cc.From(actors[`authority`]).Invoke(`carRegister`, Payloads[0]))
})
It("Disallow non authority to add information about car", func() {
//invoke chaincode method from non authority actor
expectcc.ResponseError(
cc.From(actors[`someone`]).Invoke(`carRegister`, Payloads[0]),
owner.ErrOwnerOnly) // expect "only owner" error
})
Мы также можем убедиться что при попытке занесения дублируюшей информации также будет ошибка:
It("Disallow authority to add duplicate information about car", func() {
expectcc.ResponseError(
cc.From(actors[`authority`]).Invoke(`carRegister`, Payloads[0]),
state.ErrKeyAlreadyExists) //expect car id already exists
})
Заключение
Смарт-контракты HLF предлагают разработку на таких языках программирования как Go, Java, JavaScript, что, по сравнению со специализированными, контрактно-ориентированными языками (Solidity) позволяет использовать имеющиеся фреймворки / библиотеки в смарт-контрактах. Также в подход к разработке / тестированию чейнкодов технически возможно внедрять собственные наработки.
Архитектура чейнкодов в HLF активно дорабатывается, появляются функции, которых явно раньше не хватало (постраничный запрос списка записей и пр.). Контрибьюторы Hypeledger Fabric активно призывают заинтересованных разработчиков подключаться к развитию проекта, т.к. поле для развития достаточно большое.