Building and Submitting Transactions

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. Hyperledger Sawtooth is no different, but the JavaScript SDK does provide client functionality that abstracts away most of these details, and greatly simplifies the process of making changes to the blockchain.

Creating a Private Key

In order to confirm your identity and sign the information you send to the validator, 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, and it is fairly simple to generate this using the SDK’s signer module.

const {signer} = require('sawtooth-sdk')

const privateKey = signer.makePrivateKey()

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.

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)

Note

This process can be simplified somewhat by offloading some of the work to the payload encoder of a TransactionEncoder (see below).

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 an Encoder

A TransactionEncoder stores your private key, and (optionally) default TransactionHeader values and a function to encode each payload. Once instantiated, multiple Transactions can be created using these common elements, and without any explicit hashing or signing. You will never need to specify the nonce, signer pubkey, or payload Sha512 properties of a TransactionHeader, as the SDK will generate these automatically. You will only need to set a batcher pubkey if a different private key will be used to sign Batches containing these Transactions (see below).

const {TransactionEncoder} = require('sawtooth-sdk')

const encoder = new TransactionEncoder(privateKey, {
    // We don't want a batcher pubkey or dependencies for our example,
    // but this is what setting them might look like:
    // batcherPubkey: '02d260a46457a064733153e09840c322bee1dff34445d7d49e19e60abd18fd0758',
    // dependencies: ['540a6803971d1880ec73a96cb97815a95d374cbad5d865925e5aa0432fcf1931539afe10310c122c5eaae15df61236079abbf4f258889359c4d175516934484a'],
    familyName: 'intkey',
    familyVersion: '1.0',
    inputs: ['1cf126'],
    outputs: ['1cf126'],
    payloadEncoding: 'application/cbor',
    payloadEncoder: cbor.encode
})

Note

Remember that a batcher pubkey is the hex public key matching the private key that will later be used to sign a Transaction’s Batch, and dependencies are the header signatures of Transactions that must be committed before this one (see TransactionHeaders in Transactions and Batches!).

Although possible, it would be unusual to set these properties when creating a TransactionEncoder. The default batcher pubkey will be valid as long as the Transactions and Batches are signed by the same key, and dependencies are typically different from Transaction to Transaction.

2. Create the Transaction

If all of the necessary header defaults were set in the TransactionEncoder, a Transaction can be created simply by calling the create method and passing it a payload. If a payload encoder function was set, it will be run with the payload as its one argument. The payload encoder can do any work you like to format the payload, but in the end it what it returns must be binary encoded.

Optionally, you may pass in header properties in order to override any defaults on for an individual Transaction.

const txn = encoder.create(payload, {
    inputs: ['1cf1266e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7'],
    outputs: ['1cf1266e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7']
})

const txn2 = encoder.create({
    Verb: 'inc',
    Name: 'foo',
    Value: 1
})

Note

Remember that inputs and outputs are the state addresses a Transaction is allowed to read from or write to. When initializing our TransactionEncoder we used only the six character IntKey prefix, allowing Transactions which don’t specify inputs/outputs to access any IntKey address. With txn above, we referenced the specific address where the value of 'foo' is stored. Whenever possible, specific addresses should be used, as this will allow the validator to better schedule Transaction processing.

Note that the methods for assigning and validating addresses are entirely up to the Transaction Processor. In the case of IntKey, there are specific rules to generate valid addresses, which must be followed or Transactions will be rejected. You will need to know and follow the addressing rules for whichever Transaction Family you are working with.

3. (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. The JavaScript SDK offers two options for this. One or more Transactions can be combined into a serialized TransactionList using the encode method, or if only serializing a single Transaction, creation and encoding can done in a single step with createEncoded.

const txnBytes = encoder.encode([txn, txn2])

const txnBytes2 = encoder.createEncoded({
    Verb: 'dec',
    Name: 'foo',
    Value: 3
})

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. Create an Encoder

Similar to the TransactionEncoder, there is a BatchEncoder for making Batches. As Batches are much simpler than Transactions, the only argument to pass during instantiation is the private key to sign the Batches with.

const {BatchEncoder} = require('sawtooth-sdk')

const batcher = new BatchEncoder(privateKey)

2. Create the Batch

Using the SDK, creating a Batch is as simple as calling the create method and passing it one or more Transactions. If serialized, there is no need to decode them first. In addition to Transaction instances, the BatchEncoder can handle TransactionLists encoded as both raw binaries and url-safe base64 strings.

const batch = batcher.create(txn)

const batch2 = batcher.create([txn, txn2])

const batch3 = batcher.create(txnBytes)

3. Encode the Batch(es) in a BatchList

Like the TransactionEncoder, BatchEncoders have both encode and createEncoded methods for serializing Batches in a BatchList. If encoding multiple Batches in one BatchList, they must be created individually first, and then encoded. If only wrapping one Batch per BatchList, creating and encoding can happen in one step.

const batchBytes = batcher.encode([batch, batch2, batch3])

const batchBytes2 = batcher.createEncoded(txn)

Note

Note, if the transaction creator is using a different private key than the batcher, the batcher pubkey must have been specified for every Transaction, and must have been generated from the private key being used to sign the Batch, or validation will fail.

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"