How to write your first Hyperledger Chaincode smart contract
Prerequisites: Understanding of Blockchain, Ethereum Smart Contracts, and Go or NodeJS
In my opinion, Ethereum is the perfect entry point to begin learning about Blockchain Development and building Decentralised Applications. The ecosystem is growing, documentation is great and the developer community is large with respect to the rest of the Blockchain space.
Once you’ve grasped the basics of developing on Ethereum, you’ll naturally start to cast your net wider and begin to look at other Blockchain solutions and use cases.
This article explains the differences between writing Smart Contracts on Ethereum using Solidity, and writing Chaincode on the Hyperledger Fabric Blockchain.
Solidity Development vs Hyperledger Development
As a Smart Contract developer, you’re probably not as concerned about the underlying Blockchain (or at least not to the extent) as an Ethereum Core developer is. However, it helps to have a basic understanding to avoid certain pitfalls that you won’t stumble across developing “traditional stack” applications (see random number generation).
In contrast, Hyperledger Fabric development will likely require you to delve deeper into the Blockchain layer. This may seem a little daunting at first, especially if Smart Contracts and Solidity are still new to you. Fortunately, the structure of Fabric helps us out a little bit here. Because of its modular design, we can break each module down and focus on them in isolation.
Why Hyperledger?
At this point, it’s unlikely you’ll have thought about spinning up your own Blockchain. Why would you? You can write DApps on Ethereum for the whole world to participate in.
For Blockchain to really grow, Big Business needs to get on board, and there are not many big businesses happy with making their data public. The public Ethereum network is not a great fit since all data on the Ethereum is, by definition, public. Hyperledger Fabric allows anyone to spin up their own Blockchain and fence it off from the outside world in minutes.
5 Core Functions Of Hyperledger Fabric
There are 5 core functional differences we need to understand about Fabric:
- Hyperledger Fabric is a permissioned network. This means that you cannot join the network unless you have been granted permission to do so. Ethereum is a permissionless network because anyone can join and interact with it.
- There’s no underlying currency like Ether on Hyperledger Fabric. Assets and the consensus protocol are defined by the participants.
- There are two types of nodes on the Fabric network: Peer nodes and Order nodes. Peer nodes verify and execute transactions submitted to the network, Order nodes order and propagate these throughout the network.
- The state of the Ethereum network is derived from the history of all transactions stored on the Blockchain. While this is also true with Hyperledger Fabric, an SQL database sits alongside the Blockchain which stores the latest state.
- Ethereum has Smart Contracts. Hyperledger Fabric has a similar concept called Chaincode, and a number of languages can be used to implement them.
Smart Contract vs Chaincode
The Hyperledger Fabric Samples (we’re using v1.2.0) project provides a sample called Fabcar. It contains Chaincode deployed to the Fabric Blockchain which stores an array of car information. It provides the ability to create new cars, and to query the list of cars. We’ll use the code from that project and compare it to an equivalent Solidity contract.
Since we’re coming from an Ethereum standpoint, let’s start there.
Solidity
Figure 1 shows the Fabcar Smart Contract.
pragma solidity ^0.5.0;
contract Fabcar {
struct Car {
string make;
string model;
string color;
string owner;
}
Car[] public cars;
constructor() public {
cars.push(Car('Toyota', 'Prius', 'blue', 'Tomoko'));
cars.push(Car('Ford', 'Mustang', 'red', 'Brad'));
cars.push(Car('Hyundai', 'Tucson', 'green', 'Jin Soo'));
cars.push(Car('Volkswagen', 'Passat', 'yellow', 'Max'));
cars.push(Car('Tesla', 'S', 'black', 'Adriana'));
cars.push(Car('Peugeot', '205', 'purple', 'Michel'));
cars.push(Car('Chery', 'S22L', 'white', 'Aarav'));
cars.push(Car('Fiat', 'Punto', 'violet', 'Pari'));
cars.push(Car('Tata', 'Nano', 'indigo', 'Valeria'));
cars.push(Car('Holden', 'Barina', 'brown', 'Shotaro'));
}
function createCar(string memory _make, string memory _model, string memory _color, string memory _owner) public{
cars.push(Car(_make, _model, _color, _owner));
}
function changeCarOwner(uint _id, string memory _owner) public {
cars[_id].owner = _owner;
}
}
The contract has an array called cars made up of the Car struct. The constructor pushes a bunch of cars to the array, and the rest of the contract exposes functions that add and change the array: createCar() and changeCarOwner(). The cars array is public so that it is queryable externally. Simple stuff!
Hyperledger
Solidity is the proprietary language for developing Smart Contracts on Ethereum. Hyperledger offers the flexibility of a number of existing general-purpose languages to write Chaincode with. This is great for onboarding developers from other disciplines, however, it has the drawback of making Chaincode more complex than Smart Contract code.
Hyperledger supports Go, Node, and a number of other languages. I’m going to use Go and Node for this comparison. Figure 2 shows the Go implementation and Figure 3 shows the Node JS implementation.
Figure 2:
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* The sample smart contract for documentation topic:
* Writing Your First Blockchain Application
*/
package main
/* Imports
* 4 utility libraries for formatting, handling bytes, reading and writing JSON, and string manipulation
* 2 specific Hyperledger Fabric specific libraries for Smart Contracts
*/
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"github.com/hyperledger/fabric/core/chaincode/shim"
sc "github.com/hyperledger/fabric/protos/peer"
)
// Define the Smart Contract structure
type SmartContract struct {
}
// Define the car structure, with 4 properties. Structure tags are used by encoding/json library
type Car struct {
Make string `json:"make"`
Model string `json:"model"`
Colour string `json:"colour"`
Owner string `json:"owner"`
}
/*
* The Init method is called when the Smart Contract "fabcar" is instantiated by the blockchain network
* Best practice is to have any Ledger initialization in separate function -- see initLedger()
*/
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
return shim.Success(nil)
}
/*
* The Invoke method is called as a result of an application request to run the Smart Contract "fabcar"
* The calling application program has also specified the particular smart contract function to be called, with arguments
*/
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
// Retrieve the requested Smart Contract function and arguments
function, args := APIstub.GetFunctionAndParameters()
// Route to the appropriate handler function to interact with the ledger appropriately
if function == "queryCar" {
return s.queryCar(APIstub, args)
} else if function == "initLedger" {
return s.initLedger(APIstub)
} else if function == "createCar" {
return s.createCar(APIstub, args)
} else if function == "queryAllCars" {
return s.queryAllCars(APIstub)
} else if function == "changeCarOwner" {
return s.changeCarOwner(APIstub, args)
}
return shim.Error("Invalid Smart Contract function name.")
}
func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
carAsBytes, _ := APIstub.GetState(args[0])
return shim.Success(carAsBytes)
}
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
cars := []Car{
Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
}
i := 0
for i < len(cars) {
fmt.Println("i is ", i)
carAsBytes, _ := json.Marshal(cars[i])
APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes)
fmt.Println("Added", cars[i])
i = i + 1
}
return shim.Success(nil)
}
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 5 {
return shim.Error("Incorrect number of arguments. Expecting 5")
}
var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}
carAsBytes, _ := json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()
// buffer is a JSON array containing QueryResults
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record is a JSON object, so we write as-is
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("- queryAllCars:\n%s\n", buffer.String())
return shim.Success(buffer.Bytes())
}
func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}
carAsBytes, _ := APIstub.GetState(args[0])
car := Car{}
json.Unmarshal(carAsBytes, &car)
car.Owner = args[1]
carAsBytes, _ = json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
// The main function is only relevant in unit test mode. Only included here for completeness.
func main() {
// Create a new Smart Contract
err := shim.Start(new(SmartContract))
if err != nil {
fmt.Printf("Error creating new Smart Contract: %s", err)
}
}
Figure 3:
/*
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const shim = require('fabric-shim');
const util = require('util');
let Chaincode = class {
// The Init method is called when the Smart Contract 'fabcar' is instantiated by the blockchain network
// Best practice is to have any Ledger initialization in separate function -- see initLedger()
async Init(stub) {
console.info('=========== Instantiated fabcar chaincode ===========');
return shim.success();
}
// The Invoke method is called as a result of an application request to run the Smart Contract
// 'fabcar'. The calling application program has also specified the particular smart contract
// function to be called, with arguments
async Invoke(stub) {
let ret = stub.getFunctionAndParameters();
console.info(ret);
let method = this[ret.fcn];
if (!method) {
console.error('no function of name:' + ret.fcn + ' found');
throw new Error('Received unknown function ' + ret.fcn + ' invocation');
}
try {
let payload = await method(stub, ret.params);
return shim.success(payload);
} catch (err) {
console.log(err);
return shim.error(err);
}
}
async queryCar(stub, args) {
if (args.length != 1) {
throw new Error('Incorrect number of arguments. Expecting CarNumber ex: CAR01');
}
let carNumber = args[0];
let carAsBytes = await stub.getState(carNumber); //get the car from chaincode state
if (!carAsBytes || carAsBytes.toString().length <= 0) {
throw new Error(carNumber + ' does not exist: ');
}
console.log(carAsBytes.toString());
return carAsBytes;
}
async initLedger(stub, args) {
console.info('============= START : Initialize Ledger ===========');
let cars = [];
cars.push({
make: 'Toyota',
model: 'Prius',
color: 'blue',
owner: 'Tomoko'
});
cars.push({
make: 'Ford',
model: 'Mustang',
color: 'red',
owner: 'Brad'
});
cars.push({
make: 'Hyundai',
model: 'Tucson',
color: 'green',
owner: 'Jin Soo'
});
cars.push({
make: 'Volkswagen',
model: 'Passat',
color: 'yellow',
owner: 'Max'
});
cars.push({
make: 'Tesla',
model: 'S',
color: 'black',
owner: 'Adriana'
});
cars.push({
make: 'Peugeot',
model: '205',
color: 'purple',
owner: 'Michel'
});
cars.push({
make: 'Chery',
model: 'S22L',
color: 'white',
owner: 'Aarav'
});
cars.push({
make: 'Fiat',
model: 'Punto',
color: 'violet',
owner: 'Pari'
});
cars.push({
make: 'Tata',
model: 'Nano',
color: 'indigo',
owner: 'Valeria'
});
cars.push({
make: 'Holden',
model: 'Barina',
color: 'brown',
owner: 'Shotaro'
});
for (let i = 0; i < cars.length; i++) {
cars[i].docType = 'car';
await stub.putState('CAR' + i, Buffer.from(JSON.stringify(cars[i])));
console.info('Added <--> ', cars[i]);
}
console.info('============= END : Initialize Ledger ===========');
}
async createCar(stub, args) {
console.info('============= START : Create Car ===========');
if (args.length != 5) {
throw new Error('Incorrect number of arguments. Expecting 5');
}
var car = {
docType: 'car',
make: args[1],
model: args[2],
color: args[3],
owner: args[4]
};
await stub.putState(args[0], Buffer.from(JSON.stringify(car)));
console.info('============= END : Create Car ===========');
}
async queryAllCars(stub, args) {
let startKey = 'CAR0';
let endKey = 'CAR999';
let iterator = await stub.getStateByRange(startKey, endKey);
let allResults = [];
while (true) {
let res = await iterator.next();
if (res.value && res.value.value.toString()) {
let jsonRes = {};
console.log(res.value.value.toString('utf8'));
jsonRes.Key = res.value.key;
try {
jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
} catch (err) {
console.log(err);
jsonRes.Record = res.value.value.toString('utf8');
}
allResults.push(jsonRes);
}
if (res.done) {
console.log('end of data');
await iterator.close();
console.info(allResults);
return Buffer.from(JSON.stringify(allResults));
}
}
}
async changeCarOwner(stub, args) {
console.info('============= START : changeCarOwner ===========');
if (args.length != 2) {
throw new Error('Incorrect number of arguments. Expecting 2');
}
let carAsBytes = await stub.getState(args[0]);
let car = JSON.parse(carAsBytes);
car.owner = args[1];
await stub.putState(args[0], Buffer.from(JSON.stringify(car)));
console.info('============= END : changeCarOwner ===========');
}
};
shim.start(new Chaincode());
Both implementations exhibit the same functionality on Hyperledger Fabric as our Solidity file does on Ethereum, but as you can see takes a lot more code.
Let’s analyse Figure 3: fabcar.js.
The Init() function on line 15 is called when the Chaincode is instantiated. As the comment suggests, best practice dictates that any initialization (like setting state values) is implemented in a separate function. InitLedger() on line 55 does this, which we call separately once the chaincode is deployed. In contrast to Solidity, initialization occurs in the constructor.
When calling functions, it’s important to note that the Chaincode has a single entry point Invoke(), which uses the parameters given to determine which function to call. When interacting with our Solidity Smart Contract, we can call each function directly from Web3.
You’ll notice a variable called shim is appears throughout the code. This references an interface which allows the Chaincode to interact with the Peer node validating the transaction. It basically ensures that the chaincode correctly interacts with the Blockchain.
Conclusion
Fabric allows us to use multiple languages to write Chaincode. However, this flexibility has the drawback of making Chaincode more complex than Ethereum Smart Contracts because of the extra boilerplate code needed to interact with the underlying Blockchain.
Learn More
If you’re interested in Blockchain Development, I write tutorials, walkthroughs, hints, and tips on how to get started and build a portfolio. Check out this evolving list of Blockchain Development Resources.
If you enjoyed this post and want to learn more about Smart Contract Security, Blockchain Development or the Blockchain Space in general, I highly recommend signing up to the Blockgeeks platform. They have courses on a wide range of topics in the industry, from Coding to Marketing to Trading. It has proven to be an invaluable tool for my development in the Blockchain space.