Skip to content

Instantly share code, notes, and snippets.

@rahulEth
Last active February 14, 2020 08:59
Show Gist options
  • Save rahulEth/23262d7d6cf25ad6edc782e182fefda1 to your computer and use it in GitHub Desktop.
Save rahulEth/23262d7d6cf25ad6edc782e182fefda1 to your computer and use it in GitHub Desktop.
Guidelines for writing Hyperledger Fabric Chaincodes and Testing Using dev mode and Testing

Writing Hyperledger Fabric Chaincode and Testing using dev-mode

Chaincode initializes and manages the ledger state through transactions submitted by applications. A chaincode typically handles business logic agreed to by members of the network, so it similar to a “smart contract”. A chaincode can be invoked to update or query the ledger in a proposal transaction. Given the appropriate permission, a chaincode may invoke another chaincode, either in the same channel or in different channels, to access its state.

Below are a few general guidelines / caveats that can be adhered to (although there are exceptions) while writing chaincodes. These I have particularly written for chaincodes written for Hyperledger fabric v.1.4 network in golang. But, I believe they can be extrapolated to chaincodes written in any language for Hyperledger Fabric.

Use Chaincode DevMode

Normally chaincodes are started and maintained by peer. However in “dev” mode, chaincode is built and started by the user. This mode is useful during chaincode development phase for rapid code/build/run/debug cycle turnaround.

P.S. - Although the tutorial is for Golang, using it for other languages should not be different except for building the chaincode part.

Use Chaincode Logging

Well, this is perhaps the first useful thing that you can do to debug your chaincode and find bugs quickly. Using logging is simple and easy. Use Fabric's inbuilt logger. Fabric provides logging mechanism as follows:

For Golang: https://pkg.go.dev/github.com/hyperledger/fabric/core/chaincode/shim?tab=doc#NewLogger For NodeJS: https://hyperledger.github.io/fabric-chaincode-node/release-1.4/api/fabric-shim.Shim.html#.newLogger__anchor For Java: You can use any standard logging framework like Log4J

Avoid using Global Keys

While writing chaincode, we often find our hands tied when finding data. To keep track of keys registered in the Key Value Store, we try and use some sort of Global Collection.

For example, when keeping track of registered marbles in your application, you might want to make a global counter and keep counting the number of all the marbles and generate the next ID of the marble too. But while doing so, you are introducing dependency on a single variable to write to when you add a new user. This might not seem a problem at first, but underlying is a bug waiting to surprise you when you do concurrent transactions. How? Let me explain.

Consider this code :

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

//DON'T DO THIS	
totalNumberOfMarbles := 0

func (t *SimpleChaincode) initMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    var err error
    
	marbleId := fmt.Sprintf("MARBLE_%06d",totalNumberOfMarbles)
	marbleName := args[0]
	color := strings.ToLower(args[1])
	owner := strings.ToLower(args[3])
	size, err := strconv.Atoi(args[2])
	
	//other code to initialize
	objectType := "marble"
	marble := &marble{objectType, marbleId, marbleName, color, size, owner}
	
	//--------------CODE SMELL----------------
	//BIG source of Non-determinism as well as performance hit.
	totalNumberOfMarbles = totalNumberOfMarbles + 1 
	//--------------CODE SMELL----------------
	

	//regular stuff...		
	err = stub.PutState(marbleId, marbleJSONasBytes)
	if err != nil {
		return shim.Error(err.Error())
	}
}

Well. Why do I hate this?

Reason 1: Consider you have written this code and all is going well and one fine day, one of the peers running this chaincode, crashes. Well, the ledger data is still there, but something dreaded has happened behind the scenes. You might start the peer and everything might seem normal at first. But suddenly all transactions that this peer was endorsing started to fail. Why? You had started the peer. But wait, the global counter variable you had kept, has now lost track of the last counted value. All the peers had counted till say 15K and this peer suddenly starts counting from zero. And your marbleId starts giving you IDs from zero again. So, when you send this transaction to the orderer and it reaches the committing peer, the Validation system on the committing peer compare the proposal responses from all the endorsed transactions as well as checks if there are sufficient signatures present as per the endorsement policy when the chaincode was instantiated. If there is a single proposal response which does not match, it throws an ENDORSEMENT_POLICY_FAILURE.

Reason 2: Well lets try and solve the above problem by adding the statement stub.PutState("marble_count", totalNumberOfMarbles) at the end. Is it any better? NO.

Consider two concurrent transactions trying to insert a marble.

For example, one transaction is updating the value of marble_count to 34 with a new_version(marble_count) = 10 and another to 35 again with a new_version(marble_count) = 10. Remember, since they are concurrent both transactions see that the current_version(marble_count) = 09.

Now one transaction will reach the orderer before the other and the key marble_count will have already updated it to a new value with current_version(marble_count) = 10. Therfore the transaction that arrives later will fail because the current_version(marble_count) = 10 now, and the late transaction was supposed to read version 09 and update it to version 10. This is a classical problem of double spending.

Hyperledger Fabric uses an Optimistic Locking Model while committing transactions. As I have explained, that first the proposal responses are collected from the endorsing peers by the client and then sent to the Orderer for ordering and finally orderer delivers it to the Committing Peers. In this two stage process, if some versions of the keys that you had read in the Endorsement has changed till your transactions reach the committing stage, you get an MVCC_READ_CONFLICT error. This often is a probability when one or more concurrent transactions is updating the same key.

You can read more here: https://medium.com/wearetheledger/hyperledger-fabric-concurrency-really-eccd901e4040

[P.S. This is also applicable even when you are not doing concurrent transactions but your block determination criteria is such that one or more transactions updating the same key is landing up in the same block. Because, transactions in Hyperledger Fabric are not committed until the block is committed.]

Use Couch DB Queries wisely

Couch DB queries [a.k.a Mongo Queries] is such a boon when searching for data in the Key Value store. But there are a few caveats one needs to take care.

Couch DB Queries DO NOT alter the READ SET of a transaction

Mongo Queries are for querying the Key Value store aka StateDB only. It does not alter the read set of a transaction. This might lead to phantom reads in the transaction.

Only the DATA that you have stored in the couchDB is searchable

Do not be tempted to search for a key by its name using the MangoQuery. Although you can access the Fauxton console of the CouchDB, you cannot access a key by querying a key by which it is stored in the database. Example : Querying by channelName\0000KeyName is not allowed. It is better to store your key as a property in your data itself.

Write Deterministic Chaincode

Never write chaincode that is not deterministic. It means that if I execute the chaincode in 2 or more different environments at different times, result should always be the same, like setting the value as the current time or setting a random number. For example: Avoid statements like calling rand.New(...) , t := time.Now() or even relying on a global variable (check ) that is not persisted to the ledger.

This is because, that if the read write sets generated are not the same, the Validation System chaincode might reject it and throw an ENDORSEMENT_POLICY_FAILURE.

Be cautions when calling Other Chaincodes from your chaincode

Invoking a chaincode from another is okay when both chaincodes are on the same channel. But be aware that if it is on the other channel then you get only what the chaincode function returns (only if the current invoker has rights to access data on that channel). NO data will be committed in the other channel, even if it attempts to write some. Currently, cross channel chaincode chaincode invocation does not alter data (change writesets) on the other channel. So, it is only possible to write to one channel at a time per transaction.

Remember to Set Chaincode Execution Timeout

Often it might so happen that during high load your chaincode might not complete its execution under 30s. It is a good practice to custom set your timeout as per your needs. This is goverened by the parameter in the core.yaml of the peer. You can override it by setting the environment variable in your docker compose file :

Example: CORE_CHAINCODE_EXECUTETIMEOUT=60s

Refrain from Accessing External Resources

Accessing external resources (http) might expose vulnerability and security threats to your chaincode. You do not want malicous code from external sources to influence your chaincode logic in any way. So keep away from external calls as much as possible.


Official Golang Interfaces Definitions for different Functions: https://pkg.go.dev/github.com/hyperledger/fabric/core/chaincode/shim?tab=doc


Simple Asset Chaincode Example.

Basic sample chaincode to create assets (key-value pairs) on the ledger.

Choosing a Location for the Code.

mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc touch sacc.go

package main

import (
	"fmt"

	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/protos/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
	// Get the args from the transaction proposal
	args := stub.GetStringArgs()
	if len(args) != 2 {
			return shim.Error("Incorrect arguments. Expecting a key and a value")
	}

	// Set up any variables or assets here by calling stub.PutState()

	// We store the key and the value on the ledger
	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
			return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
	}
	return shim.Success(nil)
}

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
	// Extract the function and args from the transaction proposal
	fn, args := stub.GetFunctionAndParameters()

	var result string
	var err error
	if fn == "set" {
			result, err = set(stub, args)
	} else { // assume 'get' even if fn is nil
			result, err = get(stub, args)
	}
	if err != nil {
			return shim.Error(err.Error())
	}

	// Return the result as success payload
	return shim.Success([]byte(result))
}

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 2 {
			return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
	}

	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
			return "", fmt.Errorf("Failed to set asset: %s", args[0])
	}
	return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 1 {
			return "", fmt.Errorf("Incorrect arguments. Expecting a key")
	}

	value, err := stub.GetState(args[0])
	if err != nil {
			return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
	}
	if value == nil {
			return "", fmt.Errorf("Asset not found: %s", args[0])
	}
	return string(value), nil
}

// main function starts up the chaincode in the container during instantiate
func main() {
	if err := shim.Start(new(SimpleAsset)); err != nil {
			fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
	}
}

Building Chaincode

go get -u github.com/hyperledger/fabric/core/chaincode/shim go build

Install Hyperledger Fabric Samples

if you don't have fabric-sample repo run given command : curl -sSL http://bit.ly/2ysbOFE | bash -s -- 1.4.0 1.4.0 0.4.15. Navigate to the chaincode-docker-devmode directory of the fabric-samples clone:

Terminal 1 - Start the network

docker-compose -f docker-compose-simple.yaml up -d

Terminal 2 - Build & start the chaincode

docker exec -it chaincode bash

Now, compile your chaincode: cd sacc go build

Now run the chaincode:

CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=myasset:1.0 ./sacc

Terminal 3 - Use the chaincode

docker exec -it cli bash

peer chaincode install -p chaincodedev/chaincode/sacc -n myasset -v 1.0

peer chaincode instantiate -n myasset -v 1.0 -c '{"Args":["a","10"]}' -C myc

Now issue an invoke to change the value of “a” to “20”.

peer chaincode invoke -n myasset -c '{"Args":["set", "a", "20"]}' -C myc

peer chaincode query -n myasset -c '{"Args":["query","a"]}' -C myc

Testing new chaincode

By default, we mount only sacc. However, you can easily test different chaincodes by adding them to the chaincode subdirectory and relaunching your network. At this point they will be accessible in your chaincode container.

Managing external dependencies for chaincode written in Go

if your chaincode have some external dependencies, we can use govendor or other avaiable tools: https://github.com/golang/go/wiki/PackageManagementTools to manage external dependencies

govendor init

govendor add +external // Add all external package, or

govendor add github.com/external/pkg // Add specific external package

This imports the external dependencies into a local vendor directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment