Привет, я хочу рассказать про проект FabEx — block explorer для Hyperledger Fabric, недавно принятый в Hyperledger Labs и имеющий некоторые преимущества относительно официального эксплорера. Проект написан полностью на Golang (а не на Nodejs, как официальный), хранит данные о блокчейне в MongoDB или Cassandra по выбору (а не в PostgreSQL, как официальный), имеет как GRPC, так и REST API. Eго легко расширять, например, можно легко добавить больше реализаций хранилища. Детали под катом.





Отдельная благодарность Артему Баргеру за ревью данной статьи и ценные инсайды относительно работы 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 для более детального понимания особенностей эксплуатации. Или напишите мне в телеграм (ссылка в профиле), постараюсь помочь.