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.
It is fairly simple to generate this using the 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 under your identity. It is very important that it is kept secret and secure.
As the SDK automatically generates and sets a public key, there is no need to explictly create one.
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 an Encoder¶
A TransactionEncoder stores your private key, and (optionally) default TransactionHeader values and/or a function to encode each payload. Once instansiated, multiple Transactions can be created using these common elements, and without any explicit hashing or signing.
const {TransactionEncoder} = require('sawtooth-sdk')
const encoder = new TransactionEncoder(privateKey, {
familyName: 'intkey',
familyVersion: '1.0',
inputs: ['1cf126'],
outputs: ['1cf126'],
payloadEncoding: 'application/cbor'
payloadEncoder: cbor.encode
})
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).
2. Create the Transaction¶
The only additional arguments required to create a Transaction is the payload itself, and if a payload encoder was set, you need not even worry about encoding it as a binary. Optionally, you may override some of the default elements set in the TransactionEncoder on a Transaction by Transaction basis.
const txn = encoder.create(payload, {
inputs: ['1cf12632a292f6ddf757a0a59e9c2284c08cab235aa068b19f85c460f71485540368eec98c3f95af23b0c8cda4790c118238a3b97f2fba2bbff72f15987f00b41e7caf'],
outputs ['1cf12632a292f6ddf757a0a59e9c2284c08cab235aa068b19f85c460f71485540368eec98c3f95af23b0c8cda4790c118238a3b97f2fba2bbff72f15987f00b41e7caf']
})
const txn2 = encoder.create({
Verb: 'inc',
Name: 'foo',
Value: 1
})
3. (optional) Encode the Transaction(s)¶
If serializing your Transaction(s) to be sent to an external batcher, the Sawtooth SDK offers two options. One or more Transactions can be combined using the encode method, or if only serializing a single Transaction, creation and encoding can be combined into a single step with createEncoded.
const txnBytes = encoder.encode([txn1, 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. 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. 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 use to sign Batches.
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, as the BatchEncoder can handle TransactionLists encoded as both raw binaries and url-safe base64 strings.
const batch = batcher.create(txnBytes)
const batch2 = batcher.create(txn)
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])
const batchBytes2 = batcher.createEncoded(txnBytes)
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