Building and Submitting Transactions (JavaScript)

The process of encoding information to be submitted to a distributed ledger is generally non-trivial. A series of cryptographic safeguards are used to confirm identity and data validity, and Hyperledger Sawtooth is no different. SHA-512 hashes and secp256k1 signatures must be generated. Transaction and Batch Protobufs must be created and serialized. The process can be somewhat daunting, but this document will take Sawtooth client developers step by step through the process using copious JavaScript examples.

Note

This document goes through building and submitting Transactions manually in order to explain the complete process. If your goal is just to write a functioning client, the JavaScript SDK considerably simplifies the task. Check out Building and Submitting Transactions.

Creating Private and Public Keys

In order to sign your Transactions, you will need a 256-bit private key. Sawtooth uses the secp256k1 ECSDA standard for signing, which means that almost any set of 32 bytes is a valid key.

A common way to generate one is to create a random set of bytes, and then use a secp256k1 library to ensure they are valid.

const crypto = require('crypto')
const secp256k1 = require('secp256k1')

let privateKeyBytes

do {
    privateKeyBytes = crypto.randomBytes(32)
} while (!secp256k1.privateKeyVerify(privateKeyBytes))

Note

This key is the only way to prove your identity on the blockchain. Any person possessing it will be able to sign Transactions using your identity, and there is no way to recover it if lost. It is very important that any private key is kept secret and secure.

In addition to a private key, you will need a shareable public key generated from the private key. It should be encoded as a hexadecimal string, to distribute with the Transaction and confirm that its signature is valid.

const publicKeyBytes = secp256k1.publicKeyCreate(privateKeyBytes)

const publicKeyHex = publicKeyBytes.toString('hex')

Encoding your Payload

Transaction payloads are composed of binary-encoded data that is opaque to the validator. The logic for encoding and decoding them rests entirely within the particular Transaction Processor itself. As a result, there are many possible formats, and you will have to look to the definition of the Transaction Processor itself for that information. As an example, the IntKey Transaction Processor uses a payload of three key/value pairs encoded as CBOR. Creating one might look like this:

const cbor = require('cbor')

const payload = {
    Verb: 'set',
    Name: 'foo',
    Value: 42
}

const payloadBytes = cbor.encode(payload)

Building the Transaction

Transactions are the basis for individual changes of state to the Sawtooth blockchain. They are composed of a binary payload, a binary-encoded TransactionHeader with some cryptographic safeguards and metadata about how it should be handled, and a signature of that header. It would be worthwhile to familiarize yourself with the information in Transactions and Batches!, particularly the definition of TransactionHeaders.

1. Create a SHA-512 Payload Hash

However the payload was originally encoded, in order to confirm it has not been tampered with, a hash of it must be included within the Transaction’s header. This hash should be created using the SHA-512 function, and then formatted as a hexadecimal string.

let hasher = crypto.createHash('sha512')

const payloadSha512 = hasher.update(payloadBytes).digest('hex')

2. Create the TransactionHeader

Transactions and their headers are built using Google Protocol Buffer (or Protobuf) format. This allows data to be serialized and deserialzed consistently and efficiently across multiple platforms and multiple languages. The Protobuf definition files are located in the /protos directory at the root level of the sawtooth-core repo. These files will have to first be compiled into usable JavaScript classes. Then, serializing a TransactionHeader is just a matter of plugging the right data into the right keys.

const protobuf = require('protobufjs')

const txnRoot = protobuf.loadSync('sawtooth-core/protos/transaction.proto')
const TransactionHeader = txnRoot.lookup('TransactionHeader')

const txnHeaderBytes = TransactionHeader.encode({
    batcherPubkey: publicKeyHex,
    // This is what setting a dependency might look like:
    // dependencies: ['540a6803971d1880ec73a96cb97815a95d374cbad5d865925e5aa0432fcf1931539afe10310c122c5eaae15df61236079abbf4f258889359c4d175516934484a'],
    familyName: 'intkey',
    familyVersion: '1.0',
    inputs: ['1cf12650d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c'],
    nonce: Math.random().toString(36),
    outputs: ['1cf12650d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c'],
    payloadEncoding: 'application/cbor',
    payloadSha512: payloadSha512,
    signerPubkey: publicKeyHex
}).finish()

Note

Remember that inputs and outputs are state addresses that this Transaction is allowed to read from or write to, and dependencies are the header signatures of Transactions that must be committed before this one (see TransactionHeaders in Transactions and Batches!). The dependencies property will frequently be left empty, but generally at least one input and output must always be set.

3. Sign the Header

Once the TransactionHeader is created and serialized as a Protobuf binary, you can use your private key to create an ECDSA signature. In order to generate a signature the Sawtooth validator will accept, you must:

  • use the secp256k1 elliptic curve
  • sign a SHA-256 hash of the TransactionHeader binary
  • use a compact 64-byte signature
  • format the signature as a hexadecimal string

This is a fairly typical way to sign data, so depending on the language and library you are using, some of these steps may be handled automatically.

hasher = crypto.createHash('sha256')
const txnHeaderHash = hasher.update(txnHeaderBytes).digest()

// secp256k1.sign generates compact 64-byte signature by default
const txnSigBytes = secp256k1.sign(txnHeaderHash, privateKeyBytes).signature
const txnSignatureHex = txnSigBytes.toString('hex')

4. Create the Transaction

With the other pieces in place, constructing the Transaction instance should be fairly straightforward. Create a Transaction class and use it to instantiate the Transaction.

const Transaction = txnRoot.lookup('Transaction')

const txn = Transaction.create({
    header: txnHeaderBytes,
    headerSignature: txnSignatureHex,
    payload: payloadBytes
})

5. (optional) Encode the Transaction(s)

If the same machine is creating Transactions and Batches there is no need to encode the Transaction instances. However, in the use case where Transactions are being batched externally, they must be serialized before being transmitted to the batcher. Technically any encoding scheme could be used so long as the batcher knows how to decode it, but Sawtooth does provide a TransactionList Protobuf for this purpose. Simply wrap a set of Transactions in the transactions property of a TransactionList and serialize it.

const TransactionList = txnRoot.lookup('TransactionList')

const txnBytes = TransactionList.encode({
    transactions: [txn]
}).finish()

Building the Batch

Once you have one or more Transaction instances ready, they must be wrapped in a Batch. Batches are the atomic unit of change in Sawtooth’s state. When a Batch is submitted to a validator, each Transaction in it will be applied (in order) or no Transactions will be applied. Even if your Transactions are not dependent on any others, they cannot be submitted directly to the validator. They must all be wrapped in a Batch.

1. (optional) Decode the Transaction(s)

If the batcher is on a separate machine than the Transaction creator, any Transactions will have been encoded as a binary and transmitted. If so, they must be decoded before being wrapped in a batch.

const txnList = TransactionList.decode(txnBytes)

const txn = txnList.transactions[0]

2. Create the BatchHeader

The process for creating a BatchHeader is very similar to a TransactionHeader. Compile the batch.proto file, and then instantiate the appropriate JavaScript class with the appropriate values. This time, there are just two properties: a signer pubkey, and a set of Transaction ids. Just like with a TransactionHeader, the signer pubkey must have been generated from the private key used to sign the Batch. The Transaction ids are a list of the header signatures from the Transactions to be batched. They must be in the same order as the Transactions themselves.

const batchRoot = protobuf.loadSync('sawtooth-core/protos/batch.proto')
const BatchHeader = batchRoot.lookup('BatchHeader')

const batchHeaderBytes = BatchHeader.encode({
    signerPubkey: publicKeyHex,
    transactionIds: [txn.headerSignature]
}).finish()

3. Sign the Header

The process for signing a BatchHeader is identical to signing the TransactionHeader. Create a SHA-256 hash of the the header binary, use your private key to create a 64-byte secp256k1 signature, and format that signature as a hexadecimal string. As with signing a TransactionHeader, some of these steps may be handled automatically by the library you are using.

hasher = crypto.createHash('sha256')
const batchHeaderHash = hasher.update(batchHeaderBytes).digest()

const batchSigBytes = secp256k1.sign(batchHeaderHash, privateKeyBytes).signature
const batchSignatureHex = batchSigBytes.toString('hex')

Note

The batcher pubkey specified in every TransactionHeader must have been generated from the private key being used to sign the Batch, or validation will fail.

4. Create the Batch

Creating a Batch also looks a lot like creating a Transaction. Just use the compiled class to instantiate a new Batch with the proper data.

const Batch = batchRoot.lookup('Batch')

const batch = Batch.create({
    header: batchHeaderBytes,
    headerSignature: batchSignatureHex,
    transactions: [txn]
})

5. Encode the Batch(es)

In order to submit one or more Batches to a validator, they must be serialized in a BatchList Protobuf. BatchLists have a single property, batches, which should be set to one or more Batches.

const BatchList = batchRoot.lookup('BatchList')

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

Submitting Batches to the Validator

The prescribed way to submit Batches to the validator is via the REST API. This is an independent process that runs alongside a validator, allowing clients to communicate using HTTP/JSON standards. Simply send a POST request to the /batches endpoint, with a “Content-Type” header of “application/octet-stream”, and the body as a serialized BatchList.

There are a many ways to make an HTTP request, and hopefully the submission process is fairly straightforward from here, but as an example, this is what it might look if you sent the request from the same JavaScript process that prepared the BatchList:

const request = require('request')

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

And here is what it would look like if you saved the binary to a file, and then sent it from the command-line with curl:

const fs = require('fs')

const fileStream = fs.createWriteStream('intkey.batches')
fileStream.write(batchBytes)
fileStream.end()
% curl --request POST \
    --header "Content-Type: application/octet-stream" \
    --data-binary @intkey.batches \
    "http://rest.api.domain/batches"