A New Way of Writing Chaincode  | Centroida

Big businesses and corporations are now starting to experiment with private blockchains, since the technology tends to be a good fit for some of their specific business requirements. Hyperledger Fabric is increasingly establishing itself among those private blockchain technologies, as it’s one of the first choices for most corporate users. Industry leaders, such as Walmart, Coca-cola and Nestlé are relying on Hyperledger Fabric for use cases such as supply chains, asset depositories and others.  

We at Centroida are currently working on several products in the FinTech space (more info here), which leverage Hyperledger Fabric. We got to experience just how hard getting started with Fabric can be. Even though we had prior experience with public blockchain technologies such as Ethereum and Bitcoin, we had to start thinking through the prism of a private blockchain. In this article, we will share our experience with structuring chaincode(s) in the context of a Fabric network with a single organization, as well as lay out the pros and cons of our approach. 

1. Hyperledger Fabric 

Fabric’s popularity comes from the fact that it is an open source platform governed by the Linux Foundation and backed by numerous corporations worldwide. More importantly, it provides several features, which are critical for the needs of most big businesses, such as high throughput, secure channels of communication and permitted access. On top of that, Fabric sports a highly modular architecture, thus allowing businesses to freely customize most of its components, including consensus algorithms. It is also the first distributed ledger platform to support general-purpose languages like JavaScript, GoLang and Java for writing smart-contracts, or chaincode as Fabric calls them. It is exactly Fabric’s versatility and extensibility that make it such a solid candidate for a wide variety of business applications. 

2. Chaincode 

Chaincode is essentially the equivalent of smart contracts inthe world of Fabric. However, in most blockchain platforms smart contracts are very limited in terms of functionality because of various considerations, such as determinism. In contrast, Fabric chaincodes are fully-fledged applications, which can even make HTTP calls and take advantage of multithreading. Aside from that, supporting chaincode written in general-purpose languages greatly reduces the learning curve for developers, since they will have access to vastly more literature compared to languages like Solidity. 

3. Where Things Went Wrong 

In the beginning of our journey with Hyperledger Fabric, we went about writing chaincode as if we were writing Ethereum smart-contracts. Ethereum smart-contracts can call other smart-contracts without any drawbacks performance-wise, so dividing logic in separate chaincodes sounded like the way to go. Unfortunately, at a later stage we realized that the point of having multiple chaincodes in Fabric is to control, via different endorsement policies, what organizations can and cannot do on the ledger. However, we only had one organization, so having multiple chaincodes made no sense, even more so since chaincode invoking another chaincode is an expensive and slow operation. Not only that, but from a development standpoint having multiple chaincodes would greatly complicate writing, testing and debugging our business logic. Eventually we figured out that we need to change our approach, so we came up with something that both does not hinder performance and is easy to write/debug. What we did is we unified all the chaincodes in one and organized it in a layered, modular architecture like that of the well-known and production tested design pattern MVC.

4. Components 

This new “unified” chaincode consists of a four-layer framework, which separates logic into easily testable chunks. The four layers are: 

  • Routers 

Routers are used for defining the endpoints of our chaincode API. They take a Fabric request and route it to the specific controller function the request was meant to go to. Additionally, this layer provides middleware functionality, which can be used to wrap controller functions in order to avoid code duplication. Middleware also simplifies input sanitization and common request operations, which in our case were signature validation and nonce incrementation. 

  • Controllers 

Controllers define functions, which implement a standard Fabric chaincode function interface. They are responsible for input parsing/sanitization, service utilization and coordination, and response formatting. They use the services layer as the base of their logic. 

  • Services 

Each service is what a separate chaincode was before – a standalone logical unit. The core business logic of our Fabric-based products is written in this layer, as separate services. Since the raw string data coming from the Fabric request is now handled by the controllers, services work with concrete types instead. Services use repositories to access the ledger’s state. 

  • Repositories 

Repositories are used for encapsulating access to the ledger. They decouple the ledger read/write logic from the services, thus also greatly facilitating testing. 

Fig 1. Flowchart diagram of the data flow within our chaincode framework 

5. Implementation 

Let’s visualize the layers better with some example code for adding an entity to the ledger. Since it is our language of choice, all the code snippets below will be in Golang, but the same approach will work just as well in Java or JavaScript (the other two supported languages for chaincode). 

The first code snippet is the entry point of the chaincode. Every request passes through the its Invoke function, where the router name and function name are extracted from the Fabric request. They are then passed, together with the request arguments, to routerMap, which maps the router name to a specific router implementation. InitializeExampleRouter is an auto-generated function by the dependency injection tool Wire.go, which returns reference to the ExampleRouter. 

type ExampleChaincode struct { 
    routerMap RouterMap 
} 

type RouterMap map[string]routers.ExampleRequestRouter 
func (cc *ExampleChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response { 
    return shim.Success(nil) 
} 

func (cc *ExampleChaincode) Invoke(stub shim.ChaincodeStubInterface) (res peer.Response) 
{ 
    functionPath, args := stub.GetFunctionAndParameters() 
    routerName, functionName, err := parseFunctionPath(functionPath) 
    // error handling 
    return cc.routerMap[routerName].RouteInvocationRequest(stub, functionname, args) 
} 

func main() { 
    cc := new(ExampleChaincode) 
    cc.routerMap = initializeRouters() 
    err := shim.Start(cc) 
    // error handling 
} 

func initializeRouters() RouterMap { 
    routerMap := RouterMap{} 
    routerMap["example"] = InitializeExampleRouter() 

    return routerMap 
} 
  • Router 

ExampleRequestRouter is an interface, which all routers implement. RouteInvocationRequest simply calls the controller function, mapped by function name in the funcMap. NewExampleRouter is essentially the “constructor” of the ExampleRouter, called in the wire.go file. It is also where the function mapping is done and where we can wrap the controller functions with middleware functions. 

type ExampleRequestRouter interface { 
    RouteInvocationRequest(stub shim.ChaincodeStubInterface, functionName string, 
args []string) peer.Response 
} 

type ExampleRouter struct { 
    exampleController controllers.ExampleController 
    funcMap           map[string]func(shim.ChaincodeStubInterface, []string) peer.Response 
} 

func (self *ExampleRouter) RouteInvocationRequest(stub shim.ChaincodeStubInterface, 
functionName string, args []string) peer.Response { 
    controllerFunction := self.funcMap[functionName] 
    return controllerFunction(stub, args) 
} 

func NewExampleRouter(middleware Middleware, exampleController 
controllers.ExampleController) *ExampleRouter { 
    funcMap := make(map[string]func(shim.ChaincodeStubInterface, []string) peer.Response) 

    ExampleFunction := exampleController.ExampleFunction 
    ExampleFunction = middleware.ValidateSignature(ExampleFunction, true) 
    ExampleFunction = middleware.IncrementNonce(ExampleFunction) 

    funcMap["exampleFunction"] = ExampleFunction 

    return &ExampleRouter{ 
        exampleController: exampleController, 
        funcMap:           funcMap} 
}
  • Controller 

The controller functions take the arguments passed to them by the router, validate them and parse them to concrete types. Finally, they invoke the service function/s using the parsed data. 

type ExampleController interface { 
    ExampleFunction(stub shim.ChaincodeStubInterface, args []string) peer.Response 
} 

type ExampleControllerImplementation struct { 
    exampleService services.ExampleService 
} 

func (self *ExampleControllerImplementation) ExampleFunction(stub 
shim.ChaincodeStubInterface, args []string) peer.Response { 
    // input validation and sanitization 

    exampleDTOBytes := []byte(request.Args[0]) 
    var exampleDTO dtos.ExampleDTO 
    err = json.Unmarshal(exampleDTOBytes, &exampleDTO) 
    // error handling 

    exampleObjectID, err := self.exampleService.ExampleAddFunction(stub, exampleDTO) 
    // error handling 

    return shim.Success([]byte(exampleObjectID)) 
} 
  • Service 

The service takes the data passed to it from the controllers and executes business logic. In this example the service function takes an object and passes it to the repository to be written in the ledger. Finally, it returns the ID of the object, generated in the repository. 

type ExampleService interface { 
    ExampleAddFunction(stub shim.ChaincodeStubInterface, exampleDTO 
dtos.ExampleDTO) (string, error) 
} 

type ExampleServiceImplementation struct { 
    exampleRepository examplerepository.ExampleRepository 
} 

func (self *ExampleServiceImplementation) ExampleAddFunction(stub 
shim.ChaincodeStubInterface, 
exampleDTO dtos.ExampleDTO) (string, error) { 
    // business logic 
 
   exampleObjectID, err := self.exampleRepository.Create(stub, exampleDTO) 
   // error handling 

    return exampleObjectID, nil 
} 
  • Repository 

The repository functions are usually CRUD operations, including some filtering/ordering. In the current example the repository just takes the object passed to it from the service and saves it in the ledger, returning the object ID back. 

type ExampleRepository interface { 
    Create(stub shim.ChaincodeStubInterface, exampleDTO dtos.ExampleDTO) (string, error) 
} 

type ExampleRepositoryImplementation struct{} 

func (self *ExampleRepositoryImplementation) Create(stub shim.ChaincodeStubInterface, 
exampleDTO dtos.ExampleDTO) (string, error) { 
    // ID generation logic 

    exampleObjectID := "example" 
    // collision check 

    exampleObjectBytes, err := json.Marshal(exampleDTO) 
    // error handling 

    err = stub.PutState(exampleObjectID, exampleObjectBytes) 
     // error handling 

    return exampleObjectID, nil 
} 

 6. The good & the bad  

Although our solution served us well in our specific use cases, it inevitably comes with some tradeoffs. Below we’ve listed the pros and cons of the framework, so that you can decide for yourself if it fits your needs. 

The Good 

  • Performance 

Because we only have one chaincode, we eliminate the performance drawbacks of chaincode invoking another chaincode. 

  • Security 

Utility functions, such as nonce incrementation, are now just services in the unified chaincode instead of separate chaincode altogether. That makes their interaction with the rest of the application’s logic a lot simpler and more secure. The reason is that in the case of separate chaincodes, you have no way of determining whether a chaincode is called directly, or by a different chaincode, which malicious users could exploit.  

  • Less complexity 

Because of the modular architecture of the chaincode new, complex business logic is easily implemented and more comprehensible. 

  • Debugging 

Due to the fact the even the data flow is clearly divided amongst several components, bugs can be quickly isolated and fixed.  

  • Testing 

Mocking an entire chaincode is not an easy. However, logic being separated in different components allows us to easily mock them individually. This way logic like input/output serialization/deserialization and data access can be tested on its own, while mocking it when testing the business logic of a chaincode, allowing us to unit test business flows, rather than having to write big, complex integration tests.

The Bad 

  • Writing more code 

Implementing new logic comes with a lot of boilerplate code and adds unnecessary complexity when writing simple logic, thus this architectural approach is not good for simple chaincodes. 

  • Testing more code 

Because everything is separated in standalone components, we have to write tests for each new one. Even when it’s a mostly trivial task, it’s development overhead nonetheless and has to be taken into account.  

  • Only works with one organization 

For this approach to be applicable, there must only be one organization in the Fabric network. Having a single chaincode in a Fabric network with more than one organization makes no sense because then there would be no adequate way of distributing/managing control over assets between the different organizations. 

Conclusion 

There aren’t many cases when you would have a Fabric network with a single organization, so the “unified chaincode” framework we employed is probably not suitable for the majority of Fabric deployments. However, if you do find that it fits your project’s needs, it can greatly reduce the complexity of your chaincode and vastly simplify testing, while still making full use of Fabric’s high throughput capabilities.  

 

 

Author: Stanislav Klimov

 

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *