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

hyperledger-sawtooth
sawtooth-series
hyperledger

(Varun Raj) #1

Previous Article


In our previous articles, we saw how to setup a sawtooth network and write your first transaction processor. The next obvious thing is to create our client application that can submit transactions to our sawtooth validators to store some data with our transaction processor.

What is a Client Application?

In a blockchain application ecosystem there are two layers one is the blockchain network layer which holds the data and has the distributed architectures. The other is the client application which talks to the blockchain layer from your UI layer. It works more like an middleware and doesn’t holds any major logics other than some data cleansing.

Sawtooth provides a SDK for creating client application which we’ll be using in this article. We pack all the transactions into one batch and submit the batch to the validator via Rest Server. From where based on the familyName defined the each transaction’s header, the corresponding TP is invoked.

Time for Actions

In our scenario, we’re building the client also with NodeJS, thus we’ve to create a new NodeJS project. Create a folder in our projects root folder and name it client.

mkdir client
cd client
npm init

After initializing the npm project, install the libraries.

npm install cbor crypto request sawtooth-sdk --save

Here we’re using the libraries for :

  • cbor library for the encoding the payload.
  • crypto for generating a sha512 hash of payload.
  • request for sending the transaction batch to Rest Server .
  • sawtooth-sdk for obvious reasons which to create the entire client.

The client we’ll be building in this article is fairly simple and will be used from command line as it’s majorly to explain how to package your transactions and submit it to a Validator.

Create a file called sendRequest.js which will be the only file we’ll be having in this article.

touch sendRequest.js

Now we’ve to import the the libraries that we’ll be using for the client.

const { createContext, CryptoFactory } = require('sawtooth-sdk/signing')
const { createHash } = require('crypto')
const cbor = require('cbor')
const { protobuf } = require('sawtooth-sdk')
const request = require('request')

Here we’re importing two functions called createContext and CryptoFactory from sawtooth-sdk/signing, as the name says these are using for creating private key and a signer object for the corresponding key. createHash function is used to create the hash for a specific data, cbor is for encoding and we saw this a lot of times now. protobuf from sawtooth-sdk is used for packaging each and every payload for building the transaction data object. Finally request is used to send http request to API servers.

In this client, we’ll be generating new private keys every time you invoke the functions and create a signer object from the key. But in actual scenario we’ll be storing the private key somewhere and we’ll be using that instead of generating newly everytime.

const context = createContext('secp256k1')
const privateKey = context.newRandomPrivateKey()
const signer = new CryptoFactory(context).newSigner(privateKey)

Now lets define a function called sendRequest which will take the payload object and submit a transaction.

function  sendRequest(payload) {

	// Magic Happens Here

}

This function will have the following steps to build a BatchList which you can send to Rest API for validating and commiting.

Creating Payload Bytes

As the first step, you need to convert your payload (JSON) data to bytes. For this we’ll be using cbor library.

const payloadBytes = cbor.encode(payload)

Create Transaction Header Bytes

const transactionHeaderBytes = protobuf.TransactionHeader.encode({
   familyName: 'simplestore',
   familyVersion: '1.0',
   inputs: ['917479'],
   outputs: ['917479'],
   signerPublicKey: signer.getPublicKey().asHex(),
   batcherPublicKey: signer.getPublicKey().asHex(),
   dependencies: [],
   payloadSha512: createHash('sha512').update(payloadBytes).digest('hex'),
   nonce: (new  Date()).toString()
 }).finish()

Here we’ll create a object with the transactions information which includes:

  • Family Name of the TP,
  • Version of the TP,
  • Input & output address (The input address says which are the addresses that are allowed to update by this transaction and output says which are the addresses that can be read from the ledger in this transaction, is you miss configure this the transaction will not work and fail. If you’re not sure about what to give, better give the TP’s address which makes every data readable and writable)
  • Signer Public Key, who is signing the transaction
  • Batcher Public Key, whois signing this batch of transaction. In our case both are same.
  • Dependencies are any other transactions that current transaction is depending on, so this will be in queue until others are completed. Mostly used in banking transactions.
  • SHA512 Hash of the payload in order to verify if the payload is received properly at TP.
  • Finally nonce, which says the uniqueness of the transaction. Sawtooth will not execute a transaction if there was any other transaction with same header. Thus this will help us to make the transaction unique.

We’re using protobuf.TransactionHeader.encode() to encode this data into a transaction header bytes.

After creating the TxHeaderBytes, we’ve to sign it with the created signer object.

const signature = signer.sign(transactionHeaderBytes)

With the created transaction header, now we’ve to create the actual transaction where we attach the header, header signature & the actual payload. And create a transaction array.

const transaction = protobuf.Transaction.create({
   header: transactionHeaderBytes,
   headerSignature: signature,
   payload: payloadBytes
})

const transactions = [transaction]

Now we’ve the transactions list in a form of array. We’ve to create a batch that contains these transactions.

Create a batch header, similarly to how we created the transaction header and sign it with the signer object.

const batchHeaderBytes = protobuf.BatchHeader.encode({
   signerPublicKey: signer.getPublicKey().asHex(),
   transactionIds: transactions.map((txn) => txn.headerSignature),
}).finish()

headerSignature = signer.sign(batchHeaderBytes)

The batch header will have the ids of all the transactions it holds. Now we’ve to create the batch data which contains the header, header signature and the transactions. And pack the batch into batch list and encode it with sawtooth’s protobuf.

 const batch = protobuf.Batch.create({
   header: batchHeaderBytes,
   headerSignature: headerSignature,
   transactions: transactions
 })

 const batchListBytes = protobuf.BatchList.encode({
   batches: [batch]
 }).finish()

Finally we’ve to post the batchListBytes to the RestAPI for submitting the batch of transactions. For posting the data to Rest Server, we are using the request library. In the body we’ll be attaching the batchListBytes and set the content type to application/octet-stream

request.post({
   url: 'http://localhost:8008/batches',
   body: batchListBytes,
   headers: { 'Content-Type': 'application/octet-stream' }
 }, (err, response) => {
   if (err) return  console.log(err)
   console.log(response.body)
 })

Once you post the data to the Rest API, it forwards the transaction to the validator and validates it. This will return a link to check the status of the batch which can be used to verify if it’s been committed in the ledger.

As we mentioned that the client is a cli based tool, we’ve to read the payload from the command line and call the sendRequest function. Here I’ll be sending the payload as JSON.

var args = process.argv;
var payload = JSON.parse(args[2]);
sendRequest(payload)

Putting it all together looks like

const { createContext, CryptoFactory } = require('sawtooth-sdk/signing')
const { createHash } = require('crypto')
const cbor = require('cbor')
const { protobuf } = require('sawtooth-sdk')
const request = require('request')

const context = createContext('secp256k1')
const privateKey = context.newRandomPrivateKey()
const signer = new CryptoFactory(context).newSigner(privateKey)

function  sendRequest(payload) {
 const payloadBytes = cbor.encode(payload)
 const transactionHeaderBytes = protobuf.TransactionHeader.encode({
   familyName: 'simplestore',
   familyVersion: '1.0',
   inputs: ['917479'],
   outputs: ['917479'],
   signerPublicKey: signer.getPublicKey().asHex(),
   batcherPublicKey: signer.getPublicKey().asHex(),
   dependencies: [],
   payloadSha512: createHash('sha512').update(payloadBytes).digest('hex'),
   nonce: (new  Date()).toString()
 }).finish()

 const signature = signer.sign(transactionHeaderBytes)

 const transaction = protobuf.Transaction.create({
   header: transactionHeaderBytes,
   headerSignature: signature,
   payload: payloadBytes
 })

 const transactions = [transaction]

 const batchHeaderBytes = protobuf.BatchHeader.encode({
   signerPublicKey: signer.getPublicKey().asHex(),
   transactionIds: transactions.map((txn) => txn.headerSignature),
 }).finish()

 headerSignature = signer.sign(batchHeaderBytes)

 const batch = protobuf.Batch.create({
   header: batchHeaderBytes,
   headerSignature: headerSignature,
   transactions: transactions
 })

 const batchListBytes = protobuf.BatchList.encode({
   batches: [batch]
 }).finish()

 request.post({
   url: 'http://localhost:8008/batches',
   body: batchListBytes,
   headers: { 'Content-Type': 'application/octet-stream' }
 }, (err, response) => {
   if (err) return  console.log(err)
   console.log(response.body)
 })
}

var args = process.argv;
var payload = JSON.parse(args[2]);
sendRequest(payload)

Time for the most awaited moment

Let’s now try executing it. We have two actions in our TP one is to set and one is to get. You can call them by passing the below data payloads.

// For Set
'{"action": "set", "data": "Varun"}'

// For Get
'{"action": "get", "data": "Varun"}'

I assume that you’ve the TP and the network running, if not please start them before proceeding to the client.

Now execute the set action with the following command.

node sendRequest.js  '{"action": "set", "data": "Varun"}'

On executing it, you’ll be able to see Success message with an Hash the TP’s logs. This hash is the hash where our data has been stored. Also In the client you’ll see a link that’s returned. This is used to verify if the data is committed to ledger.

Similarly, let’s now see what happens when we run get action with following command.

node sendRequest.js  '{"action": "get", "data": "a9"}'

Again in the client, you’ll see a similar link as result like before and in the TP you’ll see “Hello! Varun” getting printed. So this is basically reading within the TP and doesn’t function like Querying from Client.

And also if you notice, in the TP the logs are printed twice, this is because one time the TP executes for validation and second time for committing.

Thus I hope I made good sense with this article on how to write a simple client application for your sawtooth application. In the following parts we’ll be seeing how to make the client into an API driving application.

Until then signing off!