Не так давно был выпущен первый релиз fabric-contract-api-go — реализации новой программной модели чейнкода по RFC 0001. Давайте разберемся, что это и как этим пользоваться.
Вот здесь я подготовил репозиторий с простой Fabric-сетью, где пиры запускаются в dev-режиме. Следуйте инструкциям из репозитория, чтобы запустить сеть, и возвращайтесь (это займет не более 5 минут).
Теперь, когда у вас запущена сеть и установлен чейнкод, давайте посмотрим на внутренности чейнкода, работающего в новой модели.
В SimpleContract.go мы импортируем модуль с новым API:
github.com/hyperledger/fabric-contract-api-go/contractapi
Далее описываем наш контракт с помощью структуры SimpleContract, в которую встраивается структура Contract:
type SimpleContract struct {
contractapi.Contract
}
Встраивать Contract нужно обязательно, чтобы наш контракт удовлетворял интерфейсу ContractInterface. Здесь следует сделать оговорку и сказать, что контракт != чейнкод. Чейнкод — это контейнер неопределенного множества контрактов. Чейнкод хранит свои контракты в мапе, как видно в данном листинге:
type ContractChaincode struct {
DefaultContract string
contracts map[string]contractChaincodeContract
metadata metadata.ContractChaincodeMetadata
Info metadata.InfoMetadata
TransactionSerializer serializer.TransactionSerializer
}
Map contracts используется внутри Invoke для роутинга запросов:
func (cc *ContractChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
nsFcn, params := stub.GetFunctionAndParameters()
li := strings.LastIndex(nsFcn, ":")
var ns string
var fn string
if li == -1 {
ns = cc.DefaultContract
fn = nsFcn
} else {
ns = nsFcn[:li]
fn = nsFcn[li+1:]
}
...
nsContract := cc.contracts[ns]
...
successReturn, successIFace, errorReturn = nsContract.functions[fn].Call(ctx, transactionSchema, &cc.metadata.Components, serializer, params...)
...
return shim.Success([]byte(successReturn))
}
Итак, вернемся к SimpleContract. Все методы должны иметь параметр ctx, удовлетворяющий интерфейсу TransactionContextInterface. По умолчанию все методы получают стандартный TransactionContext, которого в большинстве случаев достаточно.
Этот контекст позволяет нам работать с ClientIdentity, например, так:
func (sc *SimpleContract) Whois(ctx contractapi.TransactionContextInterface) (string, error) {
return ctx.GetClientIdentity().GetID()
}
Или получить уже знакомый нам stub (shim.ChaincodeStubInterface), чтобы выполнять все привычные действия для взаимодействия с леджером:
func (sc *SimpleContract) Write(ctx contractapi.TransactionContextInterface, key string, value []byte) error {
return ctx.GetStub().PutState(key, value)
}
Но! В коде нашего демонстрационного репозитория вы можете видеть совсем другой контекст в методах:
func (sc *SimpleContract) Create(ctx CustomTransactionContextInterface, key string, value string) error {
existing := ctx.GetData()
if existing != nil {
return fmt.Errorf("Cannot create world state pair with key %s. Already exists", key)
}
err := ctx.GetStub().PutState(key, []byte(value))
if err != nil {
return errors.New("Unable to interact with world state")
}
return nil
}
Это кастомный контекст. Он создается очень просто. Обратите внимание на context.go из нашего репозитория:
1. Объявляем интерфейс, совместимый с contractapi.TransactionContextInterface
type CustomTransactionContextInterface interface {
contractapi.TransactionContextInterface
GetData() []byte
SetData([]byte)
}
2. Структуру, в которую встраиваем contractapi.TransactionContext
type CustomTransactionContext struct {
contractapi.TransactionContext
data []byte
}
3. Реализуем объявленные методы
// GetData return set data
func (ctc *CustomTransactionContext) GetData() []byte {
return ctc.data
}
// SetData provide a value for data
func (ctc *CustomTransactionContext) SetData(data []byte) {
ctc.data = data
}
Теперь при инциализации контракта просто передаем данную структуру как хендлер:
simpleContract := new(SimpleContract)
simpleContract.TransactionContextHandler = new(CustomTransactionContext)
А все методы нашего контракта теперь вместо ctx contractapi.TransactionContextInterface принимают ctx CustomTransactionContextInterface.
Кастомный контекст необходим для прокидывания состояния через транзакционные хуки. Транзакционные хуки — это красивое название для middleware, срабатывающего до или после вызова метода контракта.
Пример хука, который перед вызовом метода достает из леджера значение ключа, переданного первым параметром в транзакции:
SimpleContract.go
func GetWorldState(ctx CustomTransactionContextInterface) error {
_, params := ctx.GetStub().GetFunctionAndParameters()
if len(params) < 1 {
return errors.New("Missing key for world state")
}
existing, err := ctx.GetStub().GetState(params[0])
if err != nil {
return errors.New("Unable to interact with world state")
}
ctx.SetData(existing)
return nil
}
main.go
simpleContract.BeforeTransaction = GetWorldState
Теперь мы можем получать значение запрошенного ключа в методах немного лаконичнее:
SimpleContract.go
func (sc *SimpleContract) Read(ctx CustomTransactionContextInterface, key string) (string, error) {
existing := ctx.GetData()
if existing == nil {
return "", fmt.Errorf("Cannot read world state pair with key %s. Does not exist", key)
}
return string(existing), nil
}
Хук после вызова метода почти идентичен, за исключением того, что кроме контекста он принимает пустой интерфейс (зачем он нужен, разберемся далее):
YetAnotherContract.go
func After(ctx contractapi.TransactionContextInterface, beforeValue interface{}) error {
fmt.Println(ctx.GetStub().GetTxID())
fmt.Println("beforeValue", beforeValue)
return nil
}
Данный хук выводит id транзакции и значение, которое вернул метод перед хуком. Чтобы проверить этот постхук, вы можете зайти в CLI контейнер и вызвать метод контракта:
docker exec -it cli sh
peer chaincode query -n mycc -c '{"Args":["YetAnotherContract:SayHi"]}' -C myc
Переключитесь в терминал, в котором запущен чейнкод, вывод будет примерно таким:
e503e98e4c71285722f244a481fbcbf0ff4120adcd2f9067089104e5c3ed0efe # txid
beforeValue Hi there # значение из предыдущего метода
Что если мы хотим обрабатывать запросы с несуществующим именем функции? Для этого у любого контракта есть поле UnknownTransaction:
unknown_handler.go
func UnknownTransactionHandler(ctx CustomTransactionContextInterface) error {
fcn, args := ctx.GetStub().GetFunctionAndParameters()
return fmt.Errorf("Invalid function %s passed with args %v", fcn, args)
}
main.go
simpleContract.UnknownTransaction = UnknownTransactionHandler
Это можно тоже проверить через CLI:
docker exec -it cli sh
peer chaincode query -n mycc -c '{"Args":["BadRequest", "BadKey"]}' -C myc
Вывод:
Error: endorsement failure during query. response: status:500 message:«Invalid function BadRequest passed with args [BadKey]»
Чтобы чейнкод запустился на пире, мы должны как и раньше вызвать метод Start(), перед этим передав в чейнкод все наши контракты:
main.go
cc, err := contractapi.NewChaincode(simpleContract, yetAnotherContract)
if err != nil {
panic(err.Error())
}
if err := cc.Start(); err != nil {
panic(err.Error())
}
Итого
В новой модели чейнкода решена проблема роутинга, middleware, сериализации возвращаемых значений, десериализации строковых аргументов (можно использовать любые типы кроме interface{}). Теперь остается ждать реализации новой модели для Go SDK.
Спасибо за внимание.