New software model for the Hyperledger Fabric chaincode



Not so long ago, the first release of fabric-contract-api-go was released - the implementation of the new software model for the RFC 0001 chaincode . Let's see what it is and how to use it.

Here I prepared a repository with a simple Fabric network, where peers run in dev mode. Follow the instructions from the repository to start the network and return (it will take no more than 5 minutes).

Now that you have the network running and the chaincode is installed, let's look at the insides of the chaincode working in the new model.

In SimpleContract.go, we import the module with the new API:

github.com/hyperledger/fabric-contract-api-go/contractapi

Next, we describe our contract using the SimpleContract structure into which the Contract structure is embedded :

type SimpleContract struct {
	contractapi.Contract
}


It’s imperative to build in Contract so that our contract satisfies the ContractInterface interface . Here you should make a reservation and say that the contract! = Chaincode. A chaincode is a container of an indefinite number of contracts. Chaincode stores its contracts in the map, as seen in this listing:


type ContractChaincode struct {
	DefaultContract       string
	contracts             map[string]contractChaincodeContract
	metadata              metadata.ContractChaincodeMetadata
	Info                  metadata.InfoMetadata
	TransactionSerializer serializer.TransactionSerializer
}

Map contracts are used internally by Invoke to route requests:

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))
}  

So back to SimpleContract. All methods must have a ctx parameter that satisfies the TransactionContextInterface interface . By default, all methods get a standard TransactionContext , which in most cases is enough.

This context allows us to work with ClientIdentity , for example, like this:

func (sc *SimpleContract) Whois(ctx contractapi.TransactionContextInterface) (string, error) {
	return ctx.GetClientIdentity().GetID()
}

Or get the familiar stub (shim.ChaincodeStubInterface) to perform all the usual actions for interacting with the ledger:

func (sc *SimpleContract) Write(ctx contractapi.TransactionContextInterface, key string, value []byte) error {
	return ctx.GetStub().PutState(key, value)
}

But! In the code of our demo repository, you can see a completely different context in the methods:

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
}

This is a custom context. It is created very simply. Pay attention to context.go from our repository:

1. We declare an interface compatible with contractapi.TransactionContextInterface

type CustomTransactionContextInterface interface {
	contractapi.TransactionContextInterface
	GetData() []byte
	SetData([]byte)
}

2. The structure into which we embed contractapi.TransactionContext

type CustomTransactionContext struct {
	contractapi.TransactionContext
	data []byte
}

3. We implement the declared methods

// 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
}

Now, when initializing the contract, we simply pass this structure as a handler:

simpleContract := new(SimpleContract)

simpleContract.TransactionContextHandler = new(CustomTransactionContext)

And all the methods of our contract now instead of ctx contractapi.TransactionContextInterface accept ctx CustomTransactionContextInterface .

A custom context is needed to push a state through transactional hooks . Transactional hooks are a beautiful name for middleware that fires before or after calling the contract method.

An example of a hook that, before calling a method, extracts from the ledger the value of the key passed in by the first parameter in the transaction:

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

Now we can get the value of the requested key in the methods a little more concise:

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
}

The hook after the method call is almost identical, except that in addition to context, it accepts an empty interface (why we need it, we will figure it out later):

YetAnotherContract.go
func After(ctx contractapi.TransactionContextInterface, beforeValue interface{}) error {
	fmt.Println(ctx.GetStub().GetTxID())
	fmt.Println("beforeValue", beforeValue)
	return nil
}

This hook displays the transaction id and the value that the method returned before the hook . To check this posthook, you can go into the CLI container and call the contract method: Switch to the terminal where the code is running, the output will look something like this:

docker exec -it cli sh
peer chaincode query -n mycc -c '{"Args":["YetAnotherContract:SayHi"]}' -C myc




e503e98e4c71285722f244a481fbcbf0ff4120adcd2f9067089104e5c3ed0efe # txid
beforeValue Hi there # value from the previous method

What if we want to process requests with a non-existent function name? For this, any contract has an UnknownTransaction field :

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

This can also be verified through the CLI: Conclusion:

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]"

In order for the code to start on the peer, we must call the Start () method as before, before transferring all our contracts to the code :

main.go
cc, err := contractapi.NewChaincode(simpleContract, yetAnotherContract)

	if err != nil {
		panic(err.Error())
	}

	if err := cc.Start(); err != nil {
		panic(err.Error())
	}

Total


The new model of the codecode solved the problem of routing, middleware, serialization of return values, deserialization of string arguments (any types except interface {} can be used ). Now it remains to wait for the implementation of the new model for the Go SDK.

Thank you for the attention.

Part two

All Articles