Building and Submitting Transactions


In the Sawtooth distributed ledger, Transactions are the method by which changes to state are declared and formatted. These Transactions are wrapped in Batches (the atomic unit of change in Sawtooth’s blockchain) to be submitted to a validator. Each step in constructing these Transactions and Batches involves various cryptographic safeguards (SHA-256 and SHA-512 hashes, secp256k1 signatures), which can be daunting to those not already knowledgable of the core concepts. This document will take developers step by step through the process of building and submitting Transactions. Before beginning, make sure you are familiar with structure of Transactions and Batches!.

Note

This document includes example code in a few different languages, as well as Sawtooth’s SDKs. If available, it is recommended app developers use the SDK of their chosen language to build and submit Transactions, as it greatly simplifies the process.

If you need to build Transactions manually, the non-SDK code provides concrete examples of each step. Use the drop down above to select the language you would prefer to see these examples in.

Creating Private and Public Keys

In order to sign your Transactions, you will need a 256-bit 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 under your identity. It is very important that it is kept secret and secure.

In addition to a private key, you will need a public key encoded as a hexadecimal string to distribute with the Transaction and confirm its signature. This key must must be generated from the same private key you used in signing.

const publicKeyBytes = secp256k1.publicKeyCreate(privateKeyBytes)

const publicKeyHex = publicKeyBytes.toString('hex')

Encoding your Payload

Transaction payloads are composed of some sort of raw binary that is opaque to the validator. The logic for 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 TP for that information. But 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

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.

const crypto = require('crypto')
const sha512 = crypto.createHash('sha512')

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

2. Build 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 language-specific Protobuf 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('protos/transactions.proto')
const TransactionHeader = txnRoot.lookup('TransactionHeader')

const txnHeaderBytes = TransactionHeader.encode({
    batcherPubkey: publicKeyHex,
    dependencies: [],
    familyName: 'intkey',
    familyVersion: '1.0',
    inputs: ['1cf126'],
    nonce: Math.random().toString(36),
    outputs: ['1cf126'],
    payloadEncoding: 'application/cbor',
    payloadSha512: payloadSha512,
    signerPubkey: publicKeyHex
}).finish()

Remember from Transactions and Batches!, that inputs and outputs are state addresses that the Transaction is allowed to read from or write to. In this case we used a six character prefix rather than full 70-character addresses, allowing us to work with any address in the IntKey subtree. Also remember that dependencies are the header signatures of Transactions that must be committed before ours (none in this case).

3. Sign the Header

Once the TransactionHeader is created and serialized as a Protobuf binary, you can use your private key to create a secp256k1 signature. If not handled automatically by your signing library, you may have to manually generate a SHA-256 hash of the header bytes to be signed. The signature should be formatted as a hexedecimal string for transmission.

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

const txnSigBytes = secp256k1.sign(txnHeaderHash, privateKey).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 serialize those Transactions for transmission. However, in the use case where Transactions are being batched externally, they must be encoded before transmitting to the batcher. Since the batcher is likely something you have control over, any encoding scheme could technically be used, but Sawtooth does provide a TransactionList Protobuf for this purpose. Simply wrap a set of Transactions in the transactions property of a TransactionList, and then 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. Remember that the 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 needs are simple, all Transactions must be wrapped in one or more Batches before submitting.

1. (optional) Decode the Transaction(s)

If the batcher is on a separate machine than the Transaction creator, the Transaction will likely to have been encoded as a binary and transmitted. If so, it 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 batches.proto file, and then instantiate the appropriate class with the appropriate values. In this case, 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('protos/batches.proto')
const BatchHeader = batchRoot.lookup('BatchHeader')

const batchHeaderBytes = BatchHeader.encode({
    signerPubkey: publicKey,
    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 if necessary, and then use your private key to create a secp256k1 signature.

const batchHeaderHash = sha256.update(batchHeaderBytes).digest()
const batchSignature = secp256k1.sign(batchHeaderHash, privateKey)

Note

Remember that the batcher pubkey specified in every TransactionHeader must have been genertated 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 Batch class to instaniate a new Batch with the proper data.

const Batch = batchRoot.lookup('Batch')

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

5. Encode the Batch(es)

In order to transmit one or more Batches to a validator for submission, they must be serialized, and BatchList is the protobuf message class for that purpose. It has one batches property, 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 using 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 the Content-Type header set to “application/octet-stream”, and the body of the request a serialized BatchList.

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

const request = require('request')

request.post({
    url: '127.0.0.1:8080/batches',
    body: batchBytes,
    headers: {'Content-Type': 'application/octet-stream'}
}, (err, response) => {
    . . .
})

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

const fs = require('fs')

const fileStream = fs.createWriteStream('intkey.batches')
fileStream.write(batchBytes)
fileStream.end()
% curl -X POST
    -H "Content-Type: application/octet-stream" \
    --data-binary "intkey.batches" \
    http://127.0.0.1:8080/batches