Hyperledger Sawtooth Series - 2. Writing Your First Transaction Processor

hyperledger-sawtooth
sawtooth-series
hyperledger

(Varun Raj) #1

Previous Article


Now that you’ve some basic understanding about sawtooth (hope the previous one was clear enough to give some clarity) and also having a sawtooth network up and running with one validator and one Rest server, in this part of the series we’ll create a simple transaction processor that will be able to create and write data to the ledger.

Again as a precautionary, make sure you have some good amount of time to go through this entire article and don’t forget some music :slight_smile:

What is a Transaction Processor?

In Hyperledger Sawtooth, Transaction Processors (Usually called as TPs) are the smart contracts which governs the entire data. It consists of our applications business logics and the validations. Each time when you have to invoke some actions in the transaction processor, you need to submit a transaction with some payload. We’ll use that payload to decide what to be done and how it has to be done.

Ways to write a transaction processor.

Luckily sawtooth provide almost all the programming language support for writing the transaction processors. So you don’t have to start any new programming from scratch.

In this article, we’ll be developing the contract with the most popular language of recent times (NodeJS) for covering up the majority of the crowd. And also the same philosophy is followed in other languages as well so there is not a huge disconnect.

Lets Jump In

In our sawtooth-kickstart folder, create a new folder and name it tp, this will work like the root folder for our Transaction Processor.

mkdir tp
cd tp

Now initialize an npm project with the following command and fill the information during the creation process.

npm init

After creating you can see the package.json file where all the npm package informations are stored.

Now lets install the sawtooth node SDKs for our project. Also we’ll install cbor library for encoding and decoding data.

npm install cbor sawtooth-sdk --save 

Now that the libraries are installed, I’ll explain the file structure so that it’s much clear when we go deeper.

Root Folder

  • index.js - Entry point where we register the TP
  • handler.js - The action based routing happens
  • constants.js - All the constants used in the projects are defined here
  • lib.js - Library functions are written here
  • state.js - This is the important file where we manage all the state based operations. It works more like the model files in MVC applications.

There is no convention to follow, but this pattern is well organized when you’re developing a large scale applications.

index.js

const { TransactionProcessor } = require('sawtooth-sdk/processor')

const SimpleStoreHandler = require('./handler')
const transactionProcessor = new TransactionProcessor("tcp://localhost:4004")

transactionProcessor.addHandler(new SimpleStoreHandler())
transactionProcessor.start()

console.log(`Starting bank transaction processor`)
console.log(`Connecting to Sawtooth validator at tcp://localhost:4004`)

Here we’re importing the TransactionProcessor function from the library and creating a new object by connecting to a specific validator (tcp://localhost:4004)

After that we have to add an handler to the transaction processor with addHandler function on the transaction processor object, here the handler is imported from the handler.js file.

Finally we’re starting the transaction processor with start() function. Running the index.js file will start the tp and keep listening at TCP 4004 port by default.

Before getting into the handler, I’ll explain what all will be there in constants.js and lib.js since handler and state are dependent on it.

lib.js

const crypto = require("crypto");

exports._hash = (x) =>
 crypto.createHash('sha512').update(x).digest('hex').toLowerCase().substring(0, 64)

Here we’re just having a function to hash a specific value. This hash will be used for addressing purposes by other functions.

constants.js

var { _hash } = require("./lib");

const TP_FAMILY = 'simplestore';
exports.TP_FAMILY = TP_FAMILY;
exports.TP_NAMESPACE = _hash(TP_FAMILY).substring(0, 6);

Hash function is imported here and as I said we’ll be using this for generating an address. Every Transaction processor will be belonging to a Family, and this can be anything we want to name. In our case I gave it as simplestore as we’re creating a tp for simply storing data. This family name is exported in the variable called TP_FAMILY which we’ll use in our handler.

Also here we’re creating another value called the TP_NAMESPACE which is the address of the contract or TP. This is generated with the _hash function by hashing the Family name of the TP.

Next up, let see what does state.js does. This is a crucial place since it handles the data layer.

Import the hashing function and the TP_NAMESPACE from lib and constants

var { _hash } = require("./lib");
var { TP_NAMESPACE } = require("./constants");

And create a new class called SimpelStoreState that takes context as constructor

class SimpelStoreState {
 constructor(context) {
   this.context = context;
   this.timeout = 500;
   this.stateEntries = {};
 }

// Other Functions
}

Here the context is the sawtooth data layer object which is used for setting a state value and also to retrieve any state value. For each TP, there will be using context. And the stateEntires is an object which is used like an container to set the state. This will contains the addresses and each addresses will have their data. This will be used to set the state, thus you need to add all the addresses to be modified or created as key and their values as well.

In order to store data for a specific TP, we need to store it in an address that’s prefixed by the TP’s address or namespace. Thus we create function called makeAddress here, that takes an input and hash it to create an address prefixed with TP_NAMESPACE.

const makeAddress = (x, label) => TP_NAMESPACE + _hash(x)

Now lets create a function to set a value to ledger at a specific address.

class SimpelStoreState {
 //  ....
 setValue(value) {
   var address = makeAddress(value);
   var stateEntriesSend = {}
   stateEntriesSend[address] = Buffer.from("Hello! " + value);
   return  this.context.setState(stateEntriesSend, this.timeout).then(function(result) {
     console.log("Success", result)
   }).catch(function(error) {
     console.error("Error", error)
   })
 }
}

As the first step I’m creating an address with makeAddress function by passing the given value itself.

Here we’re creating an empty stateEntries object and then adding the value for the corresponding address. When you store the value, you can only store buffer values and not direct strings. So make sure you encode it to buffer using Buffer.from(string). Finally call the setState of context with stateEntries as one params and timeout as another params. This will return a promise as it’s a async task.

And similarly for getting the state with a specific address, you’ll use context.getState() by passing array of addresses you want to fetch.

getValue(value) {
   var address = makeAddress(value);
   return  this.context.getState([address], this.timeout).then(function(stateEntries) {
     Object.assign(this.stateEntries, stateEntries);
     console.log(this.stateEntries[address].toString())
     return  this.stateEntries;
   }.bind(this))
 }

Finally export the SimpelStoreState class which will be used in the handler.js

Thus finally the state.js looks like below.

var { _hash } = require("./lib");
var { TP_NAMESPACE } = require("./constants");

class SimpelStoreState {
 constructor(context) {
   this.context = context;
   this.timeout = 500;
   this.stateEntries = {};
 }

 setValue(value) {
   var address = makeAddress(value);
   var stateEntriesSend = {}
   stateEntriesSend[address] = Buffer.from("Hello! " + value);
   return  this.context.setState(stateEntriesSend, this.timeout).then(function(result) {
     console.log("Success", result)
   }).catch(function(error) {
     console.error("Error", error)
   })
 }

 getValue(value) {
   var address = makeAddress(value);
   return  this.context.getState([address], this.timeout).then(function(stateEntries) {
     Object.assign(this.stateEntries, stateEntries);
     console.log(this.stateEntries[address].toString())
     return  this.stateEntries;
   }.bind(this))
 }
}

const makeAddress = (x, label) => TP_NAMESPACE + _hash(x)

module.exports = SimpelStoreState;

And the last piece of our cake is the handle.js

The handle is the very important file where the transaction is reached first and decoded to retrieve the payload and corresponding actions are executed.

Handler file creates a class that is extended from TransactionHandler of sawtooth.

Import the libraries

const { TransactionHandler } = require('sawtooth-sdk/processor/handler')
const { InvalidTransaction, InternalError } = require('sawtooth-sdk/processor/exceptions')
const cbor = require('cbor')
const SimpleStoreState = require('./state');
var { TP_FAMILY, TP_NAMESPACE } = require("./constants");

We’re importing the State a SimpleStoreState and TP_FAMILY & TP_NAMESPACE from contants.

Now create the SimpleStoreHandler that extends the TransactionHandler and in the constructor, we’ll call the parent class constructor by passing family name, the version and the address of the TP.

class SimpleStoreHandler extends TransactionHandler {
 constructor() {
   super(TP_FAMILY, ['1.0'], [TP_NAMESPACE])
 }
 apply(transactionProcessRequest, context) {
 }
}

After the constructor we’ve a special function called apply, which the function which is called by the validator when it receives a transaction. This will take two parameters one is the request object which has the information about the transaction request and the second is the context which we use in state.js

In the apply function of our case, we’ve to do the following things.

  1. Extract the payload from the transactionProcessRequest object using cbor library.
  2. Create an instance for our state class and name it simpleStoreState
  3. Based on the payload perform the corresponding state action. In our case we’ve either getState or setState in state class and in payload we’ll send the function as action attribute. Our payload will essentially look like below.
// Transaction Payload for Storing Data
{
 action: 'set',
 data: 'varun'
}
// Transaction Payload for Reading Data
{
 action: 'get',
 data: 'varun'
}

Finally if the action attribute is not a valid one, we’ll throw an InvalidTransaction error which will reject the entire transaction and everything will rollback.

Putting everything together our handler.js will look like.

const { TransactionHandler } = require('sawtooth-sdk/processor/handler')
const { InvalidTransaction, InternalError } = require('sawtooth-sdk/processor/exceptions')
const cbor = require('cbor')
const SimpleStoreState = require('./state');
var { TP_FAMILY, TP_NAMESPACE } = require("./constants");

class SimpleStoreHandler extends TransactionHandler {
 constructor() {
   super(TP_FAMILY, ['1.0'], [TP_NAMESPACE])
 }

 apply(transactionProcessRequest, context) {
   let payload = cbor.decode(transactionProcessRequest.payload);
   let simpleStoreState = new SimpleStoreState(context);

   if (payload.action === 'get') {
     return simpleStoreState.getValue(payload.data)
} else  if (payload.action === 'set') {
     return simpleStoreState.setValue(payload.data)
} else {
     throw  new InvalidTransaction(
       `Action must be create, delete, or take not ${payload.action}`
     )
   }
 }
}

module.exports = SimpleStoreHandler;

Now that we’ve our entire transaction processor ready, it’s time to run it and connect it with our sawtooth validator. To do that just run your TP like a normal NodeJS application

node index.js

Now you’ll be able see that it’s getting connected to our validator.

image

And in the docker logs of our sawtooth validator which the TP is connected to, you can see the below log.

Now we’ve our own first sawtooth Transaction Processor is ready and up and running. In the next part of the series, I’ll be explaining how to code your client application to submit transactions.

Stay Tuned.


Hyperledger Sawtooth Series - 3. Writing The Client Application For Our Transaction Processor