When starting with Hyperledger Fabric-based applications in our company, we used the Hyperledger Composer project. It uses the term “business network” to define what you want your chaincode to do:
This file is the boundary between declaration and implementation — you can deploy it locally on the back-end based on Docker, using third-party services, or just testing it in Composer Playground. A strong point of Composer is its REST server — it takes a BNA file as a parameter and generates HTTP methods for each asset, participant, and transaction without any effort on the developer’s side.
In September 2018, after reading the news about Composer not being developed further, we needed to find a way how to continue the chaincode development process. All of our developers needed to learn the Go language and we switched to writing pure Go chaincode. We started using a helpful library called cckit which greatly simplifies method routing and tests.
The Go language is inherently static, so it was clear from the beginning that we wouldn’t be able to match the flexibility of BNA + JS. Having no other choice at the time, we persevered and ported our Composer application into Go structs and custom chaincode. The biggest workload increase was with our equivalent REST server — that which was previously generated automatically for us, we now needed to write ourselves. Also, we needed to tell the REST server all the structures that are being exchanged with the chaincode — this was previously handled by its BNA file.
By knowing the shortcomings of the previous attempt and approach from the Composer, I had quite a clear idea of how to do our next chaincode iteration. Here are some goals that were defined:
So, a new chaincode uses a “registry” (a term borrowed from Composer) to store JSON Schema for each asset type that you want to manage. You can update existing registry items with new schema — this will create a new version. Since each asset instance knows which version it is, old assets will continue to work, they will just validate against different JSON Schema for older versions. Chaincode also includes a “migrate” method to change each asset instance’s version by having the client provide a JSON Patch and the target version.
Everything written in the previous paragraph can be done dynamically without any issues in Go. However, defining business logic is a different problem. Since Go is a static language and we do not currently have the resources to develop some domain-specific language, we needed to stick to Go code as the most expressive way to define that logic. This leads to an unfortunate conclusion, that for business logic changes, the chaincode will need to be compiled.
Luckily, Fabric has the ability to “upgrade” existing chaincode, which allows us to deploy a new chaincode with the business logic changes that we aim to achieve. To also allow migrations for business logic, the chaincode just needs to define the version of the specific asset on which it is executed.
As an example, let’s make a model of an author — book relationship. This is M:N relation (the book can have multiple different authors, and the author can contribute to multiple books). In Composer, the definition in the CTO language looks like this:
Assets in CTO
Analogical definition in for our chaincode would look like this:
Assets in Go
When comparing the two, it is clear that Composer CTO is much shorter. On the other hand, JSON Schema is quite common and standardized, which allows for reuse. Also, we are defining where those assets should be stored — dynamic chaincode allows some assets to be stored in a private data collection, instead of a blockchain state (Composer stores everything in state). Dynamic chaincode also uses UUID v4 to identify all assets — you do not need to define these fields in the schema.
JSON Schema by default allows the objects to have some extra keys without breaking the schema. This is something we definitely do not want, so it is required to set additional properties to false (chaincode checks that value and will refuse to accept the schema without this setting).
In Composer CTO, you can define default values directly in the schema (default keyword). In dynamic chaincode, we did not want to clutter the schema, so setting default values was moved to the business logic code.
Both of these approaches will create a functioning code that will properly manage references without any more code required. This is the ultimate goal, to reduce boilerplate code and focus on modeling the business you want to achieve.
By learning from Composer and features of the Go language (it is not that hard to work with dynamic structures in it) we were able to create this hybrid chaincode. It decouples asset declarations from implementation and also provides a strong migration mechanism during runtime.