Привет, я хочу рассказать про проект FabEx — block explorer для Hyperledger Fabric, недавно принятый в Hyperledger Labs и имеющий некоторые преимущества относительно официального эксплорера. Проект написан полностью на Golang (а не на Nodejs, как официальный), хранит данные о блокчейне в MongoDB или Cassandra по выбору (а не в PostgreSQL, как официальный), имеет как GRPC, так и REST API. Eго легко расширять, например, можно легко добавить больше реализаций хранилища. Детали под катом.
Отдельная благодарность Артему Баргеру за ревью данной статьи и ценные инсайды относительно работы Hyperledger Fabric.
Дисклеймер
Инструмент отлично справляется со своими задачами, обрабатывая леджер с миллионами транзакций в проекте РРД КП, но вокруг него нет комьюнити и местами он сыроват. Следует относиться к этому как к экспериментальной технологии в ранней стадии развития.
Я хочу привлечь внимание и дать минимальное введение для всех потенциально заинтересованных лиц. FabEx нуждается в пользователях и коммитерах.
Основная задача block explorer — извлекать, парсить блоки и находить в них нужную информацию.
Структура блока подробно описана в блоге у Senthil Nathan. Каждая отдельная запись в базе данных FabEx содержит данные о транзакции и блоке, в который данная транзакция попала:
Здесь не все возможные данные, но лишь те, которые мне кажутся необходимыми. Txid и Payload содержат, соответственно, ID и write set транзакции. Имена остальных полей говорят сами за себя и относятся к блоку, к которому принадлежит транзакция, но особого внимания заслуживает ValidationCode — выраженный в int32 код валидации, или, иначе говоря, статус, который пир на этапе валидации присваивает транзакции. От присвоенного статуса зависит, повлияет транзакция на world state или нет (будет ли она не только записана, но и учтена в блокчейне). По большому счету, достаточно понимать, что любое ненулевое значение ValidationCode означает, что транзакция невалидна, но для более детального понимания, что произошло с транзакцией, можно обратиться к следующей таблице соответствия:
FabEx получает с пира хеш последнего блока, ищет такой блок в базе данных, если его нет, то находит последний добавленный блок в базе и запрашивает с пира блоки по отсутствующему промежутку. Это происходит в реальном времени и не требует специальных вмешательств. Базу, в которую всё сохраняется, можно выбрать.
Изначально FabEx поддерживал PostgreSQL и MongoDB, просто потому что это одни из самых популярных баз данных. Затем я стал искать подходящее решение для быстрых запросов на чтение, но не in-memory, потому что блоков может быть больше, чем доступной памяти. В итоге одна статья убедила меня, что Cassandra — это именно то, что мне нужно: хранение данных на диске, (относительно) быстрые чтения и удобная кластеризация.
Колоночная (на самом деле гибридная колоночно-ориентированная kv) Cassandra читает данные со скоростью, сравнимой с Redis:
www.sciencedirect.com/science/article/pii/S1319157816300453
Но при этом мы наблюдаем О(1) по памяти:
www.sciencedirect.com/science/article/pii/S1319157816300453
Но нужно понимать, что Cassandra — очень специфичный инструмент, и если вы точно не уверены, что вам нужно именно это решение (советую посмотреть этот доклад для ознакомления), лучше воспользоваться MongoDB. Выбрать хранилище можно при старте FabEx:
PostgreSQL пришлось убрать ввиду сложности поддержки трех разных имплементаций хранилища. Но если у вас есть хорошие идеи, какое ещё хранилище можно использовать, то реализовать его будет не сложно, нужно лишь имплементировать следующий интерфейс:
FabEx изначально создавался как сервис, с которым могут общаться любые другие компоненты системы для получения нужной информации о блокчейне. Он не пытается положить своё состояние в CouchDB, хотя это было бы проще, а хранит его изолированно. Он не пытается быть UI dashboard'ом с красивыми графиками, а предоставляет ясное GRPC API (а также REST API и CLI), позволящее компактно и предсказуемо передавать данные из блокчейна тем сервисам, которым это необходимо. Но минималистичный UI также доступен.
GRPC API пока (и, надеюсь, в будущем будет) простое и описано в fabex.proto:
rpc Explore забирает транзакции из блоков в диапазоне RequestRange из блокчейна и возвращает stream с этими транзакциями.
rpc Get позволяет выполнить фильтрующий запрос к БД для получения всех транзакций, удовлетворяющих определенным критериям (номер блока, ID транзакции, ключ транзакции).
Вы можете написать собственного GRPC-клиента либо воспользоваться готовым из пакета github.com/hyperledger-labs/fabex/client.
Например, так мы можем получить транзакции из блоков в диапазоне с 1 по 15 включительно:
А так можно выполнять фильрующие запросы:
— фильтр по ID транзакции:
— фильтр по номеру блока:
— по ключу транзакции:
Чтобы сгруппировать транзакции по блокам, можно воспользоваться helper'ом PackTxsToBlocks:
Пример использования встроенного GRPC-клиента можно найти здесь.
Доступен также REST API.
Чтобы быстро протестировать fabex, можете воспользоваться готовыми скриптами:
Теперь у вас поднята база данных (для тестов MongoDB), тестовая сеть Fabric и FabEx. Можете закомментить/раскомментить нужные строчки в client.go и запусть клиент, чтобы проверить взаимодействие с FabEx.
Например, давайте отправим транзакцию на создание HabraCar с ключом car0 (make fabric-test поднимает тестовую сетку с чейнкодом fabcar):
Теперь можно вызвать client/example/client.go, который запросит пятый блок:
Вывод будет таким:
Почему мы запросили 5 блок? Потому что первые четыре нерелевантны и неинформативны: создание канала, присоединение анкора первой организации, присоединение анкора второй организации и одна транзакция, отправляемая автоматически скриптом при поднятии сети для наполнения (и поскольку в тестовой сети block time равняется двум секундам, а между этими транзакциями проходит более двух секунд, каждая из них оказалась в разных блоках).
Всё описанное выше можно повторить и без взаимодействия с API, используя CLI. Если мы только что запустили чистую базу, её нужно наполнить транзакциями:
Теперь можно запросить пятый блок:
Есть также простой UI на Vue.js на порту 5252, там тоже можно найти наш блок:
Очень просто. Можете посмотреть таргеты Makefile и readme для более детального понимания особенностей эксплуатации. Или напишите мне в телеграм (ссылка в профиле), постараюсь помочь.
Отдельная благодарность Артему Баргеру за ревью данной статьи и ценные инсайды относительно работы Hyperledger Fabric.
Дисклеймер
Инструмент отлично справляется со своими задачами, обрабатывая леджер с миллионами транзакций в проекте РРД КП, но вокруг него нет комьюнити и местами он сыроват. Следует относиться к этому как к экспериментальной технологии в ранней стадии развития.
Я хочу привлечь внимание и дать минимальное введение для всех потенциально заинтересованных лиц. FabEx нуждается в пользователях и коммитерах.
Что такое block explorer и зачем нужен FabЕx
Основная задача block explorer — извлекать, парсить блоки и находить в них нужную информацию.
Структура блока подробно описана в блоге у Senthil Nathan. Каждая отдельная запись в базе данных FabEx содержит данные о транзакции и блоке, в который данная транзакция попала:
type Tx struct {
ChannelId string
Txid string
Hash string
PreviousHash string
Blocknum uint64
Payload string
ValidationCode int32
Time int64
}
Здесь не все возможные данные, но лишь те, которые мне кажутся необходимыми. Txid и Payload содержат, соответственно, ID и write set транзакции. Имена остальных полей говорят сами за себя и относятся к блоку, к которому принадлежит транзакция, но особого внимания заслуживает ValidationCode — выраженный в int32 код валидации, или, иначе говоря, статус, который пир на этапе валидации присваивает транзакции. От присвоенного статуса зависит, повлияет транзакция на world state или нет (будет ли она не только записана, но и учтена в блокчейне). По большому счету, достаточно понимать, что любое ненулевое значение ValidationCode означает, что транзакция невалидна, но для более детального понимания, что произошло с транзакцией, можно обратиться к следующей таблице соответствия:
"VALID": 0,
"NIL_ENVELOPE": 1,
"BAD_PAYLOAD": 2,
"BAD_COMMON_HEADER": 3,
"BAD_CREATOR_SIGNATURE": 4,
"INVALID_ENDORSER_TRANSACTION": 5,
"INVALID_CONFIG_TRANSACTION": 6,
"UNSUPPORTED_TX_PAYLOAD": 7,
"BAD_PROPOSAL_TXID": 8,
"DUPLICATE_TXID": 9,
"ENDORSEMENT_POLICY_FAILURE": 10,
"MVCC_READ_CONFLICT": 11,
"PHANTOM_READ_CONFLICT": 12,
"UNKNOWN_TX_TYPE": 13,
"TARGET_CHAIN_NOT_FOUND": 14,
"MARSHAL_TX_ERROR": 15,
"NIL_TXACTION": 16,
"EXPIRED_CHAINCODE": 17,
"CHAINCODE_VERSION_CONFLICT": 18,
"BAD_HEADER_EXTENSION": 19,
"BAD_CHANNEL_HEADER": 20,
"BAD_RESPONSE_PAYLOAD": 21,
"BAD_RWSET": 22,
"ILLEGAL_WRITESET": 23,
"INVALID_WRITESET": 24,
"NOT_VALIDATED": 254,
"INVALID_OTHER_REASON": 255
FabEx получает с пира хеш последнего блока, ищет такой блок в базе данных, если его нет, то находит последний добавленный блок в базе и запрашивает с пира блоки по отсутствующему промежутку. Это происходит в реальном времени и не требует специальных вмешательств. Базу, в которую всё сохраняется, можно выбрать.
Cassandra или MongoDB в качестве хранилища
Изначально FabEx поддерживал PostgreSQL и MongoDB, просто потому что это одни из самых популярных баз данных. Затем я стал искать подходящее решение для быстрых запросов на чтение, но не in-memory, потому что блоков может быть больше, чем доступной памяти. В итоге одна статья убедила меня, что Cassandra — это именно то, что мне нужно: хранение данных на диске, (относительно) быстрые чтения и удобная кластеризация.
Колоночная (на самом деле гибридная колоночно-ориентированная kv) Cassandra читает данные со скоростью, сравнимой с Redis:
www.sciencedirect.com/science/article/pii/S1319157816300453
Но при этом мы наблюдаем О(1) по памяти:
www.sciencedirect.com/science/article/pii/S1319157816300453
Но нужно понимать, что Cassandra — очень специфичный инструмент, и если вы точно не уверены, что вам нужно именно это решение (советую посмотреть этот доклад для ознакомления), лучше воспользоваться MongoDB. Выбрать хранилище можно при старте FabEx:
./fabex -configpath=configs -db=mongo
./fabex -configpath=configs -db=cassandra
PostgreSQL пришлось убрать ввиду сложности поддержки трех разных имплементаций хранилища. Но если у вас есть хорошие идеи, какое ещё хранилище можно использовать, то реализовать его будет не сложно, нужно лишь имплементировать следующий интерфейс:
type Manager interface {
Connect() error
Insert(tx Tx) error
QueryBlockByHash(hash string) ([]Tx, error)
GetByTxId(txid string) ([]Tx, error)
GetByBlocknum(blocknum uint64) ([]Tx, error)
GetBlockInfoByPayload(payload string) ([]Tx, error)
QueryAll() ([]Tx, error)
GetLastEntry() (Tx, error)
}
Можно использовать как микросервис
FabEx изначально создавался как сервис, с которым могут общаться любые другие компоненты системы для получения нужной информации о блокчейне. Он не пытается положить своё состояние в CouchDB, хотя это было бы проще, а хранит его изолированно. Он не пытается быть UI dashboard'ом с красивыми графиками, а предоставляет ясное GRPC API (а также REST API и CLI), позволящее компактно и предсказуемо передавать данные из блокчейна тем сервисам, которым это необходимо. Но минималистичный UI также доступен.
GRPC API пока (и, надеюсь, в будущем будет) простое и описано в fabex.proto:
service Fabex {
rpc Explore(RequestRange) returns (stream Entry);
rpc Get(Entry) returns (stream Entry);
}
rpc Explore забирает транзакции из блоков в диапазоне RequestRange из блокчейна и возвращает stream с этими транзакциями.
rpc Get позволяет выполнить фильтрующий запрос к БД для получения всех транзакций, удовлетворяющих определенным критериям (номер блока, ID транзакции, ключ транзакции).
Вы можете написать собственного GRPC-клиента либо воспользоваться готовым из пакета github.com/hyperledger-labs/fabex/client.
Например, так мы можем получить транзакции из блоков в диапазоне с 1 по 15 включительно:
txs, err := client.Explore(1, 15)
А так можно выполнять фильрующие запросы:
— фильтр по ID транзакции:
txs, err := client.Get(&pb.Entry{Txid:"x"})
— фильтр по номеру блока:
txs, err := client.Get(&pb.Entry{Blocknum:42})
— по ключу транзакции:
txs, err := client.Get(&pb.Entry{Payload:"mykey"})
Чтобы сгруппировать транзакции по блокам, можно воспользоваться helper'ом PackTxsToBlocks:
blocks, err := helpers.PackTxsToBlocks(txs)
Пример использования встроенного GRPC-клиента можно найти здесь.
Доступен также REST API.
Как быстро протестить
Чтобы быстро протестировать fabex, можете воспользоваться готовыми скриптами:
make mongo-test
— запуск базыmake fabric-test
— запуск тестовой сетиmake fabex-test
— запуск fabexТеперь у вас поднята база данных (для тестов MongoDB), тестовая сеть Fabric и FabEx. Можете закомментить/раскомментить нужные строчки в client.go и запусть клиент, чтобы проверить взаимодействие с FabEx.
Например, давайте отправим транзакцию на создание HabraCar с ключом car0 (make fabric-test поднимает тестовую сетку с чейнкодом fabcar):
docker exec cli peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n fabcar -c '{"Args":["createCar", "car0", "HabraCar", "V8", "black", "Habr"]}' --waitForEvent --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses peer0.org1.example.com:7051 --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
Теперь можно вызвать client/example/client.go, который запросит пятый блок:
go run client/example/client.go
Вывод будет таким:
Почему мы запросили 5 блок? Потому что первые четыре нерелевантны и неинформативны: создание канала, присоединение анкора первой организации, присоединение анкора второй организации и одна транзакция, отправляемая автоматически скриптом при поднятии сети для наполнения (и поскольку в тестовой сети block time равняется двум секундам, а между этими транзакциями проходит более двух секунд, каждая из них оказалась в разных блоках).
Режим CLI
Всё описанное выше можно повторить и без взаимодействия с API, используя CLI. Если мы только что запустили чистую базу, её нужно наполнить транзакциями:
./fabex -task=explore -configpath=tests -configname=config -enrolluser=true -db=mongo
Теперь можно запросить пятый блок:
./fabex -task=getblock -blocknum=5 -configpath=tests -configname=config -enrolluser=true -db=mongo
Есть также простой UI на Vue.js на порту 5252, там тоже можно найти наш блок:
make stop-mongo-test
— удаление тестовой базыmake stop-fabric-test
— удаление тестовой сетиОчень просто. Можете посмотреть таргеты Makefile и readme для более детального понимания особенностей эксплуатации. Или напишите мне в телеграм (ссылка в профиле), постараюсь помочь.
EuLeEr
vadiminshakov А подскажите, что с общим размером — леджер и база FabEx хранится на пире и по-сути леджер дублируется в базу FabEx ?
vadiminshakov Автор
Действительно, FabEx дублирует данные из леджера, и размер дублированных данных зависит только от базы данных и её настроек. Если это критично, можете поднять сервис на отдельной ноде, FabEx'у не обязательно работать на одной машине с пиром.