Introduction

Are you evaluating blockchains and wondering how to get started? In this technical article I will compare and contrast two approaches that fall under the Hyperledger umbrella: Hyperledger Fabric and Hyperledger Composer.

As their names indicate, both Hyperledger Composer (Composer) and Hyperledger Fabric (Fabric) are top-level projects under the Linux Foundation’s Hyperledger umbrella blockchain project; however, they serve very different purposes:

  • Hyperledger Fabric is a pluggable blockchain implementation. It provides a set of peers with permissioned access to a distributed ledger.
  • Hyperledger Composer is a set of abstractions, tools and APIs to model, build, integrate and deploy a blockchain solution (a business network archive). Composer business network archives may be deployed to Hyperledger Fabric for execution.

So, Composer RUNS ON Fabric. Either can be used to implement a blockchain solution, however the level of abstraction, tools and languages used are quite different. Internally the Composer APIs map down to the underlying Fabric APIs – this access is managed by the Composer runtime however, which can provide a range of services that ease application development (described in more detail below).

Let’s take a look at the differences between the abstractions for Fabric and Composer.

Coding for Fabric

The chaincode for a Fabric solution is written in the Go programming language. The Hello World style example for Fabric is Marbles.

A chaincode Go file must implement two functions:

The Init function is called when the chaincode is initialized by the Fabric. It gives the chaincode an opportunity to create initialization data in the world state.

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

 

The Invoke function is called when the Fabric client API is used to submit a transaction:

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

 

A typical pattern is to use an if statement to dispatch from the Invoke function to a private function, based on calling stub.GetFunctionAndParameters().

Step 1) Data Model

Fundamentally blockchain solutions need to store data on a distributed ledger. The format/shape/schema of the data is therefore critical, particularly if the data will be persisted on the ledger for years, or decades, to come.

The data model for the Go chaincode is defined as a Go struct, with field tags used to specify how the fields in the struct are persisted as JSON.

type marble struct {
   ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
   Name       string `json:"name"`    //the fieldtags are needed to keep case from bouncing around
   Color      string `json:"color"`
   Size       int    `json:"size"`
   Owner      string `json:"owner"`
}

Step 2) Dispatch Incoming Calls

When client submit transactions for processing to the blockchain (typically via the Fabric Node-SDK), they use an async RPC style interface. The RPC calls are then dispatched to individual Go functions for processing. The dispatch logic for Go chaincode is typically a large if statement, that switches on the name of the function that is being invoked.

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
   function, args := stub.GetFunctionAndParameters()
   fmt.Println("invoke is running " + function)
   // Handle different functions
   if function == "initMarble" { //create a new marble
      return t.initMarble(stub, args)
   } else if function == "transferMarble" { //change owner of a specific marble
      return t.transferMarble(stub, args)
   } else if function == "transferMarblesBasedOnColor" { //transfer all marbles of a certain color
      return t.transferMarblesBasedOnColor(stub, args)
   } else if function == "delete" { //delete a marble
      return t.delete(stub, args)
   } else if function == "readMarble" { //read a marble
      return t.readMarble(stub, args)
   } else if function == "queryMarblesByOwner" { //find marbles for owner X using rich query
      return t.queryMarblesByOwner(stub, args)
   } else if function == "queryMarbles" { //find marbles based on an ad hoc rich query
      return t.queryMarbles(stub, args)
   } else if function == "getHistoryForMarble" { //get history of values for a marble
      return t.getHistoryForMarble(stub, args)
   } else if function == "getMarblesByRange" { //get marbles based on range query
      return t.getMarblesByRange(stub, args)
   }

   fmt.Println("invoke did not find func: " + function) //error
   return shim.Error("Received unknown function invocation")
}

Once inside a function, the Go code calls methods on the shim to read and write to the world-state. The shim defines the programmatic interface between the chaincode and the underlying Fabric platform: put/get/delete state, execute queries, emit events etc.

Examples:

err = stub.DelState(colorNameIndexKey)
colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marble.Color, marble.Name})
stub.PutState(colorNameIndexKey, value)
valAsbytes, err := stub.GetState(name) //get the marble from chaincode state

Let’s take a look at one of the functions in more detail.

Step 3) Validate arguments

Parameter validation is typically performed at the start of each function, ensuring that the right number of arguments have been passed and that the arguments are of the correct type.

func (t *SimpleChaincode) transferMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
   //   0       1
   // "name", "bob"

   if len(args) < 2 {
      return shim.Error("Incorrect number of arguments. Expecting 2")
   }

 

Step 4) Lookup Asset

Chaincode can then look up an asset by id, and returns an error if it does not exist:

marbleName := args[0]
newOwner := strings.ToLower(args[1])
fmt.Println("- start transferMarble ", marbleName, newOwner)
marbleAsBytes, err := stub.GetState(marbleName)

if err != nil {
   return shim.Error("Failed to get marble:" + err.Error())
} else if marbleAsBytes == nil {
   return shim.Error("Marble does not exist")
}

 

Step 5) Deserialize Data

If the asset (marble in this case) exists, the chaincode deserializes the bytes of the JSON document that represents the marble, converting it back into a Go struct.

marbleToTransfer := marble{}
err = json.Unmarshal(marbleAsBytes, &marbleToTransfer) //unmarshal it aka JSON.parse()
if err != nil {
   return shim.Error(err.Error())
}

 

Step 6) Update Data In Memory

The chaincode then updates the state of the struct, based on data in the parameters of the calling function.

marbleToTransfer.Owner = newOwner //change the owner

 

Step 7) Serialize Data and Persist

Then chaincode finally converts the struct back into JSON bytes, and updates world-state.

marbleJSONasBytes, _ := json.Marshal(marbleToTransfer)

err = stub.PutState(marbleName, marbleJSONasBytes) //rewrite the marble

if err != nil {
   return shim.Error(err.Error())
}

fmt.Println("- end transferMarble (success)")
return shim.Success(nil)

 

Step 8) Content Based Query

It is often useful to be able to query JSON data based on the contents of the data, rather than just doing key-based lookups.

queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"marble\",\"owner\":\"%s\"}}", owner)

<snip>

func getQueryResultForQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {

   fmt.Printf("- getQueryResultForQueryString queryString:\n%s\n", queryString)
   resultsIterator, err := stub.GetQueryResult(queryString)

   if err != nil {
      return nil, err
   }

   defer resultsIterator.Close()

   // buffer is a JSON array containing QueryRecords
   var buffer bytes.Buffer
   buffer.WriteString("[")
   bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
      queryResponse, err := resultsIterator.Next()

      if err != nil {
         return nil, err
      }

      // 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("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String())
   return buffer.Bytes(), nil
}

Step 9) Emitting Events

When something of significance has happened to the data on the blockchain an event can be emitted. Fabric places the data for the event on its Event Bus, so that client applications can pick it up for client-side processing.

err = stub.SetEvent("evtsender", []byte(tosend))
if err != nil {
   return shim.Error(err.Error())
}

Coding for Composer

The comparable marbles-network sample for Composer is here.

Step 1) Data Model

The data model for a Composer business network is defined in a CTO file. The Composer Modeling Language defines the structure (schema) for the assets, participants and transactions in a business network.

The data model is Object-Oriented in nature and is powerful enough to model complex domains. The language includes: namespaces and imports, abstract types, enumerations, unary and Nary relationships, inheritance, field validation expressions, optional properties, default values for properties etc.

Let’s take a look at the detailed steps to set up the same “Hello World” Sample in Composer. Here is the code for that network.

/**
 * Defines a data model for a marble trading network
 */
namespace org.hyperledger_composer.marbles

enum MarbleColor {
   o RED
   o GREEN
   o BLUE
   o PURPLE
   o ORANGE
}

enum MarbleSize {
   o SMALL
   o MEDIUM
   o LARGE
}

asset Marble identified by marbleId {
   o String marbleId
   o MarbleSize size
   o MarbleColor color
   --> Player owner
}

participant Player identified by email {
   o String email
   o String firstName
   o String lastName
}

transaction TradeMarble {
   --> Marble marble
   --> Player newOwner
}

Step 2) Dispatch Incoming Calls

Composer dispatches incoming transactions to JavaScript functions by introspecting the decorations (annotations) on the functions themselves. There is therefore no need to explicitly code a dispatch function.

For example, the marble-network contains a single JS function, and the @param and @transaction decorations indicate to the Composer runtime that this function should be called with an instance of org.hyperledger_composer.marbles.TradeMarble is submitted.

/**
 * Trade a marble to a new player
 * @param  {org.hyperledger_composer.marbles.TradeMarble} tradeMarble - the trade marble transaction
 * @transaction
 */
function tradeMarble(tradeMarble)

Step 3) Validate arguments

Composer automatically validates that transactions conform to the data model defined in the CTO file, both within the client API as well as within the runtime. This happens transparent to user code, meaning that functions written by the end user can be sure that the data they are processing conforms to the Composer data model for the business network.

Step 4, 5, 6, 7) Lookup Asset, Deserialize Data, Update Data In Memory, Serialize Data and Persist

The entire business logic for the Composer marbles sample is 5 lines of JavaScript! The runtime automatically resolves the marble asset for the incoming

tradeMarble transaction. If the marble does not exist then the transaction will fail.

The application developer then sets the owner attribute based on the newOwner attribute of the incoming transaction and updates the marble in an asset registry to persist it.

Dealing at a higher level of abstraction radically reduces the amount of boilerplate code, reducing risk and complexity, as the business intent of the transaction processor function is much clearer.

function tradeMarble(tradeMarble) {
   tradeMarble.marble.owner = tradeMarble.newOwner;
   return getAssetRegistry('org.hyperledger_composer.marbles.Marble')
   .then(function (assetRegistry) {
      return assetRegistry.update(tradeMarble.marble);
   });
}

Step 8) Content Based Query

Composer supports content-based queries, using the Composer Query Language. This SQL-like language allows developers to select assets, participants or transactions based on their properties.

JavaScript function can call the query API to run content based queries:

return query('SELECT org.hyperledger_composer.marbles.Marble
   WHERE (color == "green")')
.then(function (results) {
   // process the results
});

Again, by using a query syntax that should be familiar to most developers, Composer eliminates a large amount of boilerplate code and makes queries easier to write, understand, debug and test.

Step 9) Emitting Events

JavaScript functions can call the emit API to emit instances of events. Events are modelled types, just like assets, participants or transactions.

// Emit an event for the modified asset.
var event = getFactory().newEvent('org.acme.sample', 'SampleEvent');
event.asset = tx.asset;
event.oldValue = oldValue;
event.newValue = tx.newValue;
emit(event);

Prior to emitting event data it will be validated against the Composer model, ensuring that invalid data cannot be emitted to clients.

Testing

Both Go and JavaScript have sophisticated unit testing capabilities. However, Composer has a secret weapon in that it actually supports 4 different runtimes, two of which are particularly useful for testing:

  1. A Node.js runtime that simulates a blockchain
  2. A Web runtime that executes within a web browser (used by the Composer Playground)
  3. Hyperledger Fabric v0.6 runtime
  4. Hyperledger Fabric v1.0 runtime

Runtime (1) is ideal for unit testing, because a unit test can run in a single Node.js process making it extremely quick to unit test transaction processor logic, perform step-by-step debugging, or code coverage analysis. Runtime (2) is great for interactive testing of solutions, right from the web-browser. Runtimes (2) and (3) are uses for integration, system and performance testing.

I cannot over-emphasize how import testing is when developing blockchain solutions. They involve testing fairly complex state-machine logic running on a distributed network, accessed by different parties, with different levels of access. Choosing a framework and runtime that makes testing easy is critical to success.

Summary

It isn’t possible to make a complete apples-to-apples comparison, because the Go version of Marbles includes queries, whereas the Composer version does not, however it is clear that the Composer version is far shorter and includes much less boilerplate code:

  • marbles_chaincode.go : 627 lines
  • Composer marbles-network : 63 lines total, 26 lines (logic), 37 lines (model)

This ±10x reduction in the number of lines of code when Go and Composer solutions are compared is fairly consistent across several samples.

Composer includes several other major features and productivity enhancements:

  • business networks are automatically exposed as OpenAPI (Swagger) REST APIs via the composer-rest-server. The composer-rest-server uses passport.js to support pluggable end-user authentication schemes.
  • Composer includes a declarative Access Control Language, allowing developers to define which participants have access to which assets and under which circumstances. Composer ACLs drastically reduce the amount of procedural access control checks required in business logic.
  • Use the Composer Node-RED nodes to integrate Composer with IoT, analytics, dashboards etc.
  • VSCode extension to validate Composer model, ACL and query files
  • Integrate Composer with industry leading BPM and Integration tools, via OpenAPI and the LoopBack connector
  • Unit test using the Node.js embedded Composer runtime using standard JS tools like Mocha, Chai, Sinon, Istambul etc
  • Develop and test on the web interactively using the Composer Playground, simulating the blockchain in the browser, or connected to a Fabric.
  • Generate skeleton Angular web application from a business network definition
  • Publish and reuse models across business networks

There are of course some advantages to coding in Go to the Fabric APIs directly:

  • follow the absolute latest evolutions in the Fabric APIs and capabilities
  • possibly better raw performance, although for most application chaincode performance is unlikely to be the determining factor
  • type-safety
  • single language for both business logic and model
  • can incorporate third-party C and Go libraries easily

In summary I believe that for 90% of business developers Hyperledger Composer is the right choice to get started with blockchain development. It allows them to focus on the business logic, and avoids a lot of error-prone boilerplate code. They will also benefit from many of the higher-level tools and abstractions that Composer has to offer.