Module 3: Chaincode Development
3.2 Chaincode Development
Hyperledger Fabric supports chaincode development in multiple programming languages, giving developers the flexibility to use their preferred language and leverage existing skills. This section covers chaincode development in the three officially supported languages: Go, Node.js, and Java.
Supported Programming Languages
Go
Go (Golang) was the first supported language for Hyperledger Fabric chaincode and remains the most commonly used. It offers several advantages:
- Performance: Compiled language with efficient execution
- Strong Typing: Catches errors at compile time
- Concurrency Support: Goroutines and channels for concurrent operations
- Simplicity: Clean syntax and standard library
- Official Samples: Most Fabric samples are written in Go
Node.js
Node.js support was added to provide JavaScript developers an easy entry point to blockchain development:
- Popularity: Large developer community and ecosystem
- Asynchronous: Event-driven, non-blocking I/O model
- JSON Handling: Native JSON support simplifies working with complex data
- NPM Ecosystem: Access to thousands of packages
- Lower Barrier to Entry: Familiar to web developers
Java
Java support caters to enterprise developers and existing Java codebases:
- Enterprise Adoption: Widely used in enterprise environments
- Object-Oriented: Strong OOP paradigm
- Type Safety: Robust type system
- Mature Ecosystem: Rich libraries and frameworks
- Portability: Write once, run anywhere
Chaincode Structure and Interfaces
Go Chaincode Structure
A basic Go chaincode structure looks like this:
package main
import (
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// SmartContract provides functions for managing assets
type SmartContract struct {
contractapi.Contract
}
// Asset represents a general asset that might be on the ledger
type Asset struct {
ID string `json:"ID"`
Owner string `json:"owner"`
Value int `json:"value"`
AdditionalData string `json:"additionalData"`
}
// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Owner: "Tom", Value: 100, AdditionalData: "Sample data"},
{ID: "asset2", Owner: "Jane", Value: 200, AdditionalData: "More sample data"},
}
for _, asset := range assets {
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state: %v", err)
}
}
return nil
}
// CreateAsset issues a new asset to the world state with given details
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, owner string, value int, additionalData string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
asset := Asset{
ID: id,
Owner: owner,
Value: value,
AdditionalData: additionalData,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// ReadAsset returns the asset stored in the world state with given id
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
func main() {
chaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
fmt.Printf("Error creating asset chaincode: %v\n", err)
return
}
if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting asset chaincode: %v\n", err)
}
}
Node.js Chaincode Structure
A basic Node.js chaincode structure looks like this:
'use strict';
const { Contract } = require('fabric-contract-api');
class AssetContract extends Contract {
async InitLedger(ctx) {
const assets = [
{
ID: 'asset1',
Owner: 'Tom',
Value: 100,
AdditionalData: 'Sample data'
},
{
ID: 'asset2',
Owner: 'Jane',
Value: 200,
AdditionalData: 'More sample data'
}
];
for (const asset of assets) {
await ctx.stub.putState(asset.ID, Buffer.from(JSON.stringify(asset)));
console.info(`Asset ${asset.ID} initialized`);
}
}
async CreateAsset(ctx, id, owner, value, additionalData) {
const exists = await this.AssetExists(ctx, id);
if (exists) {
throw new Error(`The asset ${id} already exists`);
}
const asset = {
ID: id,
Owner: owner,
Value: parseInt(value),
AdditionalData: additionalData
};
await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset)));
return JSON.stringify(asset);
}
async ReadAsset(ctx, id) {
const assetJSON = await ctx.stub.getState(id);
if (!assetJSON || assetJSON.length === 0) {
throw new Error(`The asset ${id} does not exist`);
}
return assetJSON.toString();
}
async AssetExists(ctx, id) {
const assetJSON = await ctx.stub.getState(id);
return assetJSON && assetJSON.length > 0;
}
}
module.exports = AssetContract;
Java Chaincode Structure
A basic Java chaincode structure looks like this:
package org.example;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import com.owlike.genson.Genson;
@Contract(name = "AssetContract")
@Default
public class AssetContract implements ContractInterface {
private final Genson genson = new Genson();
@Transaction
public void InitLedger(final Context ctx) {
ChaincodeStub stub = ctx.getStub();
Asset[] assets = new Asset[] {
new Asset("asset1", "Tom", 100, "Sample data"),
new Asset("asset2", "Jane", 200, "More sample data")
};
for (Asset asset : assets) {
String assetJSON = genson.serialize(asset);
stub.putStringState(asset.getID(), assetJSON);
}
}
@Transaction
public Asset CreateAsset(final Context ctx, final String id, final String owner, final int value, final String additionalData) {
ChaincodeStub stub = ctx.getStub();
if (AssetExists(ctx, id)) {
throw new ChaincodeException("The asset " + id + " already exists");
}
Asset asset = new Asset(id, owner, value, additionalData);
String assetJSON = genson.serialize(asset);
stub.putStringState(id, assetJSON);
return asset;
}
@Transaction
public Asset ReadAsset(final Context ctx, final String id) {
ChaincodeStub stub = ctx.getStub();
String assetJSON = stub.getStringState(id);
if (assetJSON == null || assetJSON.isEmpty()) {
throw new ChaincodeException("The asset " + id + " does not exist");
}
Asset asset = genson.deserialize(assetJSON, Asset.class);
return asset;
}
@Transaction
public boolean AssetExists(final Context ctx, final String id) {
ChaincodeStub stub = ctx.getStub();
String assetJSON = stub.getStringState(id);
return (assetJSON != null && !assetJSON.isEmpty());
}
public static class Asset {
private String ID;
private String Owner;
private int Value;
private String AdditionalData;
public Asset() {
}
public Asset(String ID, String Owner, int Value, String AdditionalData) {
this.ID = ID;
this.Owner = Owner;
this.Value = Value;
this.AdditionalData = AdditionalData;
}
// Getters and setters
public String getID() { return ID; }
public void setID(String ID) { this.ID = ID; }
public String getOwner() { return Owner; }
public void setOwner(String Owner) { this.Owner = Owner; }
public int getValue() { return Value; }
public void setValue(int Value) { this.Value = Value; }
public String getAdditionalData() { return AdditionalData; }
public void setAdditionalData(String AdditionalData) { this.AdditionalData = AdditionalData; }
}
}
State Management
State management is a critical aspect of chaincode development. Chaincode interacts with the ledger state to read and write data.
Key-Value Store
The world state in Hyperledger Fabric is a key-value store. Each piece of data is stored with a unique key that can be used to retrieve it later.
Basic operations include: - Put: Write data to the ledger - Get: Read data from the ledger - Delete: Remove data from the ledger
Data Serialization
Data in chaincode is typically serialized to JSON or other formats before being stored in the ledger:
// Go example
assetJSON, err := json.Marshal(asset)
err = ctx.GetStub().PutState(id, assetJSON)
// Node.js example
await ctx.stub.putState(id, Buffer.from(JSON.stringify(asset)));
// Java example
String assetJSON = genson.serialize(asset);
stub.putStringState(id, assetJSON);
Composite Keys
For more complex data models, composite keys can be used to create relationships between data:
// Go example
compositeKey, err := ctx.GetStub().CreateCompositeKey("owner~asset", []string{owner, assetID})
// Node.js example
const compositeKey = ctx.stub.createCompositeKey('owner~asset', [owner, assetID]);
// Java example
String compositeKey = stub.createCompositeKey("owner~asset", owner, assetID);
Range Queries
Range queries allow for retrieving multiple keys within a specified range:
// Go example
resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)
// Node.js example
const iterator = await ctx.stub.getStateByRange(startKey, endKey);
// Java example
QueryResultsIterator<KeyValue> resultsIterator = stub.getStateByRange(startKey, endKey);
Rich Queries
When using CouchDB as the state database, rich queries using JSON queries are possible:
// Go example
queryString := fmt.Sprintf(`{"selector":{"owner":"%s"}}`, owner)
resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
// Node.js example
const queryString = `{"selector":{"owner":"${owner}"}}`;
const iterator = await ctx.stub.getQueryResult(queryString);
// Java example
String queryString = "{\"selector\":{\"owner\":\"" + owner + "\"}}";
QueryResultsIterator<KeyValue> resultsIterator = stub.getQueryResult(queryString);
Chaincode APIs
Chaincode APIs provide the interface between chaincode and the Fabric network.
Transaction Context
The transaction context provides information about the current transaction:
// Go example
txID := ctx.GetStub().GetTxID()
channelID := ctx.GetStub().GetChannelID()
timestamp, err := ctx.GetStub().GetTxTimestamp()
// Node.js example
const txID = ctx.stub.getTxID();
const channelID = ctx.stub.getChannelID();
const timestamp = ctx.stub.getTxTimestamp();
// Java example
String txID = stub.getTxID();
String channelID = stub.getChannelID();
Timestamp timestamp = stub.getTxTimestamp();
Client Identity
The client identity API allows chaincode to get information about the transaction submitter:
// Go example
import "github.com/hyperledger/fabric-chaincode-go/pkg/cid"
id, err := cid.GetID(ctx.GetStub())
mspID, err := cid.GetMSPID(ctx.GetStub())
// Node.js example
const ClientIdentity = require('fabric-shim').ClientIdentity;
const cid = new ClientIdentity(ctx.stub);
const id = cid.getID();
const mspID = cid.getMSPID();
// Java example
ClientIdentity clientIdentity = new ClientIdentity(stub);
String id = clientIdentity.getId();
String mspID = clientIdentity.getMSPID();
Event Emission
Chaincode can emit events that client applications can subscribe to:
// Go example
err = ctx.GetStub().SetEvent("AssetCreated", []byte(id))
// Node.js example
await ctx.stub.setEvent('AssetCreated', Buffer.from(id));
// Java example
stub.setEvent("AssetCreated", id.getBytes());
Cross-Chaincode Invocation
Chaincode can invoke functions in other chaincodes:
// Go example
response := ctx.GetStub().InvokeChaincode("otherChaincode", [][]byte{[]byte("ReadAsset"), []byte(id)}, "mychannel")
// Node.js example
const response = await ctx.stub.invokeChaincode('otherChaincode', ['ReadAsset', id], 'mychannel');
// Java example
Response response = stub.invokeChaincode("otherChaincode", Arrays.asList("ReadAsset", id), "mychannel");
Understanding these chaincode development concepts and APIs is essential for building effective smart contracts on Hyperledger Fabric. In the next sections, we'll explore advanced chaincode concepts and practical development techniques.