Новая программная модель чейнкода Hyperledger Fabric

от автора

Не так давно был выпущен первый релиз 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. Спасибо за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/494880/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *