Write your first Hyperledger Fabric Chaincode (v1.4) using Node, Babel, Model/Controller and Test it using Mocha

Jonathan Chevalier
10 min readJan 31, 2019

--

Intro

In the last few months, I have been working on several projects using Hyperledger Fabric at Intelipost. As a result, I developed my own set of tools & methods for creating reliable and efficient Chaincode (available on Github).

Writing a Chaincode can be a real hassle without diving deeply into the Hyperledger Fabric documentation. Unfortunately, I found the documentation to be pretty painful to deal with and not really suited for real case scenarios (even though tremendous progress has been made recently).

This tutorial has been written for beginners as well as for more experienced developers wanting to increase their proficiency writing Node.js Chaincode. I will try to explain in a most comprehensive and straightforward way how Chaincode works and how you can implement your own using simple but effective patterns.

For this tutorial, I will take a line by line approach, creating from scratch a real case scenario we are currently working on at Intelipost. It assumes the reader is familiar with the fundamental principles of Fabric network architecture in order to only focus on the Chaincode aspect.

A dev-mode environment is available at the end of this tutorial for testing your chaincode in a Fabric network.

A live demo using this chaincode is available at http://blockchain-codigo-rastreamento.intelipost.com.br/

The project

Logistics in Brazil is an intricate and complex network of thousands of companies. Many companies means: many technologies, many IT infrastructures, complex exchange of information and inefficient communication.

If an e-commerce wants to send a pair of shoes, it will deal with many counter-parties such as third party logistics, post-office institutions, multiple carriers (common to use at least 2-3+), insurances etc. Everyone in this chain is using a different internal ID to identify that pair of shoes. This practice leads to significant communication errors, losses and waste.

Issue: There is no common tracking code to identify the same product within the delivery process between companies.

Solution: Create a tracking code processor suited to delivery procedures.

Before diving into our solution, let’s first create our Chaincode skeleton.

1. Chaincode skeleton

1.1 Basic setup

Create package.json & install dependencies npm install

"fabric-shim" will provide the Chaincode interface to implement our Chaincode, "babel-cli" will be used for transpiling our code, es2015 preset "babel-preset-es2015" and "babel-plugin-transform-object-rest-spread" will be used by Babel to transpile our code into a compatible version of JavaScript (If you don’t know Babel, I encourage you to visit this link).

We are also creating 5 scripts:

"clean" : command to remove and create new build folder

"build-server" : command to transpile our code from app to build folder

"build" : execute "clean" & "build-server"

"start" : start our Chaincode in build/index.js

1.2. Babel setup

Create .babelrc

It tells Babel which presets and plugins it will need for transpiling our code in the build folder.

2. Chaincode core implementation

create app folder & app/chaincode.js

For the Chaincode to properly works it needs to implement 2 mandatory functions Init(stub)and Invoke(stub) .

2.1. Init(stub)

2.1.1 Functioning

Init(stub) is only called once, at Chaincode instantiation.

After installing a Chaincode in a Peer it will not be running yet. It will wait for a start signal.

It occurs when the Peer receive an instantiate transaction. At that moment, it will start a docker container running our Node.js Chaincode by executing npm startand executing the Init(stub) function.

2.1.2 The function

stub encapsulates the APIs between the Chaincode implementation and the Fabric Peer. It can for example, get the transaction context (function to be call, parameters etc.) interact with the ledger (getState, putState, delState), setting an event (setEvent), calling another Chaincode (invokeChaincode) etc. (list of available functions).

Transaction proposal sent from a Client (backend), is usually built as follows (but not limited to):

const request = {
chaincodeId: chaincodeName,
fcn: fcn,
args: [args],
txId: tx_id
}

You probably already guessed whatstub.getFunctionAndParameters() is doing.

const ret = stub.getFunctionAndParameters();

It gets thefcn and args value from the Client transaction proposal and creates the following Object const ret = { fcn: request.fcn, params: request.args }

For this example we did not create any kind of logic/interaction in Init(stub), we only log the ret value.

Takeaway:

Init(stub) is used to set any kind of data at Chaincode instantiation.

2.2. Invoke(stub)

2.2.1 Functioning

Invoke(stub) is called every time a transaction proposal is received (except at instantiation).

2.2.2 The function

We start by logging the transaction ID stub.getTxID() and the arguments passed to the transaction stub.getArgs(). All the logs can be seen at container leveldocker logs -f ChaincodeContainerName

We create a ret Object containing the name of the method ret.fcnand the params ret.params received from the transaction proposal. We assign the method name to method .

We look if the method exists in the Chaincode Class if(!method){...}. If not, it calls shim.error(`funcao com nome "${ret.fcn}" nao encontrado`) which builds an error response as follows :

{
status: 500,
message: "funcao com nome \"NameOfTheFunctionCalled\" nao encontrado"
}

If it exists, it calls the corresponding function and passes the necessary parameters (stub, ret.params, this)(we will see use of this later on this tutorial). We then call shim.success(payload) which builds a success response as follows:

{
status: 200,
payload: payload
}

Payload being the data returned by the method called.

Takeaway:

Invoke(stub)try to call a method defined in our Chaincode Class and sends back a success or error message.

2.3. Create our first function getDataById(stub, args)

Let’s first understand how data is saved in the ledger.

The ledger is the sequenced, tamper-resistant record of all state transitions. State transitions are a result of chaincode invocations (“transactions”) submitted by participating parties. Each transaction results in a set of asset key-value pairs that are committed to the ledger as creates, updates, or deletes.

The takeaway is that every state is represented by a key-value pair. For example, I can associate a key like "BMW" and a corresponding value like "Sport car" or event better a JSON encoded string "{\"price\": \"expensive\", \"type\": \"Sport car\"}" .

2.3.1 The function

getDataById(stub, args) is a simple function that return the value (from the world state) of a key-value pair.

if you remember, we call every method received (by a transaction proposal) in Invoke(stub) as follow const payload = await method(stub, ret.params, this).

Payload being the value returned by the method called.

In getDataById(stub, args) we want to return the value corresponding to a key (also called id) passed as a parameter.

In the context of that function it expectsargsto represent the key of the value we are looking for. It then assigns it to data .

If no key is passed if(!data) {...} it throws an error that will be caught in Invoke(stub)and used by shim.error to build an error response as follows:

{
status: 500,
message: "Por favor especifique um id"
}

If an ID is passed correctly it looks for the corresponding key:value pair using stub.getState(data) and then sends back the value found. Every value stored in the ledger is in Bytes format. That is why we need to convert it to string to make it readable and log the response we are sending back.

As seen in Invoke() the data returned by the function is assigned to payload and used by shim.success(payload) to send back the success response containing the value found.

3. Chaincode advanced implementation

Now that we have built our core skeleton, we can start focusing on the advanced part.

We will create two functions solicitarCodigo(stub, args) & usarCodigo(stub, args) . The first one will create a batch of tracking codes and the second will make use of a tracking code.

3.1. solicitarCodigo(stub, args)

As you can see, we imported a new variable named Codigo that will act as our controller. It will create a batch Object containing the following value.

const batch = {
docType: 'batch',
embarcador: "",
id: "",
codigos: [],
organization: ",
quantitade: ""
}

docType: is used to distinguish the various types of objects in the state database. It is strongly encouraged to use it in every Object you will put into the ledger. It will greatly improve your CouchDB query (see 3.6).

embarcador: the shipper of the product (amazon for example).

id:unique ID that will be used to identify the batch and also as the ledger key.

codigos: array of tracking code that will be created.

organization: name of the organization requesting the tracking codes (can be different from the embarcador depending if the logistics is externalized).

quantitade: the number of tracking codes generated.

Let’s now create our controller.

3.2. Codigo controller: solicitarCodigo(stub, args)

create folder app/controllers & app/controllers/codigo.js

We will first install a new dependency npm install uuid --save that will help us create a Universal Unique Identifier for our batch Object ID.

We are also creating an app/utils folder containing utilities function that will generate unique tracking code (you can study it further in the final project that will be available at the end of this tutorial).

If you want to measure the performance of your function, you can uncomment the corresponding part.

3.2.1 Function

I strongly advise to send your args(in your transaction proposal) as a stringified JSON in order to make your function logics more practical and efficient.

1.Parses the args received from the transaction proposal.

2.Verifies if mandatory fields are present.

3.Limits the available Shippers (you might want to register them in the ledger instead of hard coding them).

4.Thanks to the stub API we are able to get the MSP ID of the transaction proposer.

5.Creates our batch Object.

6.Creates all our tracking codes.

6.1. Make sure the tracking code does not exist yet.

6.2. If it exists, it loops again and create a new one.

6.3. Pushes the tracking code into the batch Object.

6.4. Puts the tracking code into the ledger.

7. Waits for all the promises to end.

8. Puts the batch into the ledger.

9. Sends an event. That event will only be sent when a block containing that transaction is added onto a Peer ledger.

We don’t return anything. The shim.success() will return a response containing status: 200 .

additional infos: generateCodigo() & uuidv4() are creating nondeterministic ID which is not advised in decentralized systems since multiple Peer will come up with different values which, depending on your Chaincode Policy can prevent data from being added to the ledger. We can solve this problem in 4+ ways:

  • Create a specific Chaincode containing this unique function with adapted Policy.
  • Since Fabric v1.3, you can create State-based endorsement FAB-8812
    that allows the default chaincode-level endorsement policy to be overridden by a policy defined in a function.
  • Find an aleatory source like transaction ID and use it to create a deterministic output for your data creation.
  • Adapt your Chaincode Policy.

For this tutorial I will keep things simple and assume Chaincode Policy is adapted, which mean that only 1 Organization need to validate the data.

3.3. usarCodigo(stub, args)

Nothing much different of precedent function. We make use of a new function called Codigo.usarCodigo(stub, args) .

3.4. Codigo controller: usarCodigo(stub, args)

After an organization has requested a batch of tracking codes, she can use it through usarCodigo(stub, args) and fills the additional fields that were not filled yet (transportador, rota, servico, servico_codigo, usado) related to the delivery process.

As your Chaincode will receive more advanced Object from the client/backend , we want to make sure that Object does not contain unwanted fields. That is why we will use an Object Schema Validator library called Yup npm install yup --save .

3.4.1. Tracking code Model usarCodigoSchema

Create folder app/models & app/models/codigo.js

Every argsreceived byusarCodigo(stub, args) will go through that Object Schema Validator to make sure we only have the expected data.

3.4.2. usarCodigo(stub, args)

into app/controllers/codigo.js

We import our Object Schema Validator defined previously.

We also specify our validation options. We are using the option stripUnknown: true in order to remove all the fields we do not want.

  1. Parses the args received from the transaction proposal.
  2. Passes the args through the Schema Validator. If it does not fulfill the validation defined in usarCodigoSchema, it throws an error that will be caught in Invoke(stub) .
  3. Verifies the tracking code we are trying to update already exist.
  4. Parses the tracking code to be updated and verifies the code was not already used.
  5. Merges the existing and updated data.
  6. Transforms JSON data into Bytes format.
  7. Pushes updated data into ledger.
  8. Sends event.

We do not return anything. The shim.success() will return a response containing a status: 200

3.5. Chaincode useful function

3.5.1. Advanced Query

If you study the Chaincode (available at the end), you will see that I added three important functions: richQuery(stub, args, thisClass) , getQueryResultForQueryString(stub, args, thisClass) , getAllResults(iterator, isHistory) . They make Rich Query available when using CouchDB as a state database. It allows you to create more complex and precise query on the ledger. You can also note that we use thisClass third argument of await method(stub, ret.params, this)located inInvoke(stub)function. It allows you to call other methods in the Chaincode Class.

3.5.2. History

I also added getHistory(stub, args, thisClass) that lets you query the history of all the different states of a key-value pair.

3.5. Chaincode Class instantiation

We now have a full working version of our Chaincode Class that we need to instantiate.

create app/index.js

We need to do it in a separate file, because the testing library will not work if the Class is instantiated on the same file (chaincode.js).

Don’t forget to npm run build before trying to install it in your channel as npm start will look for build/index.js .

3.6. CouchDB indexes

Indexes in CouchDB are required in order to make JSON queries efficient and are required for any JSON query with a sort. They are located in META-INF/statedb/couchdb/indexes .

4. Testing your Chaincode

Testing library does not exist for Fabric Node.js Chaincode. Fortunately the guys from @wearetheledger did an awesome work and created a fabric mock stub library.

Let’s install it npm install @theledger/fabric-mock-stub --save-dev with our testing tools: mocha (testing framework), chai (assertion library) and @babel/register for compiling our test code into a compatible version of JavaScript npm install @babel/register chai mocha --save-dev

Add testing scripts in package.json "test": "./node_modules/.bin/mocha --compilers js:babel-core/register

Create test/test.js

The file is pretty straightforward to understand, I encourage you to create your own tests!

4. Try it in a Fabric network

I created a specific dev-mode environment at this link for you to test it using a real Fabric network.

This is it for our Chaincode. You can find the whole project on github. I hope you enjoyed it and learned enough to write your own. Your feedback and suggestions are very welcome. If you have any question don’t hesitate to contact me on linkedin I will be happy to help!

PS: If you think it is worth it, you can Star my project on github to give it visibility for people trying to learn Hyperledger Fabric Chaincode!

--

--

Jonathan Chevalier

Obsessed with DevOps & SREing. Most of my technical work is done around IAC, Security, Infra, Observability, Reactive prog., Data intensive applications [...]