Hyperledger Chaincode Best Practices

Smart contract or chaincode developers should adhere to strict development guidelines and rules.

With Turing complete languages like GoLang becoming the defacto standard for chaincode development and high interest from developers to get into this exciting space.

I am writing down some of the Don'ts and best practices to consider while developing your chaincode.

Use simple and clear statements to write the flow logic of the contracts

Read After Write

func addToAccount(stub shim.ChaincodeStubInterface, account *string, amount int){
balance,_ := stub.GetState(*account)
stub.PutState(*account, balance + amount)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface)
 pb.Response {

args := stub.GetArgs()
// assume args match addToAccount
addToAccount(stub,"AccountNo1", args[0])
// babalce == 150
// do more actions
addToAccount(stub,"AccountNo1", args[1])
// balance == 175
return shim.Success(nil)
}

Assume initial balance of 100 and inputs of [50,25],
What is the end state value of the balance ?

125,150,175 ???

Note here that the first PutState isn't written/committed to the ledger until
the transaction finishes successfully. Hence the next GetState will return the initial state value.

Ignore error handling

Range Iterations over maps are not deterministic

Using range to iterate over the entries of a map is not deterministic. Consistency over peers is therefore not guaranteed.

type BadChaincode struct{
}

    var myMap = map[int]int{
                    1:1,
                    2:5,
                    3:10,
                    4:50,
                    }

func (t *BadChaincode) Invoke (stub shim.ChaincodeStubInterface) peer.Response{
    returnValue := 0
    for i,ii := range myMap{
    returnValue = returnValue * i - ii
    }
    return shim.Success([]byte("value: " + string(returnValue)))
}

Validate the number of arguments before use

The number of arguments received from a client must be validated inorder not to use non existent element.

func (t *BadChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
args := stub.GetStringArgs()
// by assuming passed len(args) == 2 but only 1 argument is passed
result,err := stub.GetState(args[0])
if err != nil{
return shim.Error(err.Error())
}else {
return shim.Success(result)
}
} 

Blacklisted Imports

Libraries such as time,file access that allow access to the real world can lead to non determinism

Global Declarations

Global declared values are not stored on the ledger and can have different values among peers since peers don't need to execute the chaincode for every transaction.
Global states should be managed over the ledger.

package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)

var globalValue = ""

func (t *BadChaincode) Invoke(stub shim.ChaincodeStubInterface){

}

Phantom Reads

Reading the ledger with GetHistoryOfKey or GetQueryResult do not pass the versioning control of the system. Data received from phantom reads should therefore not be used to write new data or update data on the ledger.

iterator,_ := stub.GetHistoryForKey("key")
data,_ := iterator.Next()
err, := stub.PutState("key", data.Value)

Go Routines

The use of concurrency can lead to non deterministic behavior. The result is a non consistent among peers.

package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
type BadChainCode struct{
}
func (t *BadChainCode) Invoke(stub shim.ChaincodeStubInterface){
go writeToLedger(stub, "data1")
go writeToLedger(stub, "data2")
}

func writeToLedger(stub shim.ChaincodeStubInterface, data string){
stub.PutState("key",[]byte(data))
}
0