API: Transactions

Note

Before reading this page, you should be familiar with the key concepts of Transactions.

Transaction types

There are two types of transaction in Corda:

  • TransactionType.NotaryChange, used to change the notary for a set of states
  • TransactionType.General, for transactions other than notary-change transactions

Notary-change transactions

A single Corda network will usually have multiple notary services. To commit a transaction, we require a signature from the notary service associated with each input state. If we tried to commit a transaction where the input states were associated with different notary services, the transaction would require a signature from multiple notary services, creating a complicated multi-phase commit scenario. To prevent this, every input state in a transaction must be associated the same notary.

However, we will often need to create a transaction involving input states associated with different notaries. Before we can create this transaction, we will need to change the notary service associated with each state by:

  • Deciding which notary service we want to notarise the transaction
  • For each set of inputs states that point to the same notary service that isn’t the desired notary service, creating a TransactionType.NotaryChange transaction that:
    • Consumes the input states pointing to the old notary
    • Outputs the same states, but that now point to the new notary
  • Using the outputs of the notary-change transactions as inputs to a standard TransactionType.General transaction

In practice, this process is handled automatically by a built-in flow called NotaryChangeFlow. See API: Flows for more details.

Transaction workflow

There are four states the transaction can occupy:

  • TransactionBuilder, a builder for a transaction in construction
  • WireTransaction, an immutable transaction
  • SignedTransaction, an immutable transaction with 1+ associated signatures
  • LedgerTransaction, a transaction that can be checked for validity

Here are the possible transitions between transaction states:

_images/transaction-flow.png

TransactionBuilder

Creating a builder

The first step when creating a transaction is to instantiate a TransactionBuilder. We can create a builder for each transaction type as follows:

val txBuilder: TransactionBuilder = TransactionBuilder(General, specificNotary)
TransactionBuilder txBuilder = new TransactionBuilder(General.INSTANCE, specificNotary);

Transaction components

Once we have a TransactionBuilder, we need to gather together the various transaction components the transaction will include.

Input states

Input states are added to a transaction as StateAndRef instances. A StateAndRef combines:

  • A ContractState representing the input state itself
  • A StateRef pointing to the input among the outputs of the transaction that created it
val ourStateAndRef: StateAndRef<DummyState> = serviceHub.toStateAndRef<DummyState>(ourStateRef)
StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef);

A StateRef uniquely identifies an input state, allowing the notary to mark it as historic. It is made up of:

  • The hash of the transaction that generated the state
  • The state’s index in the outputs of that transaction
val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0)
StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0);

The StateRef create a chain of pointers from the input states back to the transactions that created them. This allows a node to work backwards and verify the entirety of the transaction chain.

Output states

Since a transaction’s output states do not exist until the transaction is committed, they cannot be referenced as the outputs of previous transactions. Instead, we create the desired output states as ContractState instances, and add them to the transaction:

val ourOutput: DummyState = DummyState()
DummyState ourOutput = new DummyState();

In many cases (e.g. when we have a transaction that updates an existing state), we may want to create an output by copying from the input state:

val ourOtherOutput: DummyState = ourOutput.copy(magicNumber = 77)
DummyState ourOtherOutput = ourOutput.copy(77);

Commands

Commands are added to the transaction as Command instances. Command combines:

  • A CommandData instance representing the type of the command
  • A list of the command’s required signers
val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create()
val ourPubKey: PublicKey = serviceHub.legalIdentityKey
val counterpartyPubKey: PublicKey = counterparty.owningKey
val requiredSigners: List<PublicKey> = listOf(ourPubKey, counterpartyPubKey)
val ourCommand: Command<DummyContract.Commands.Create> = Command(commandData, requiredSigners)
DummyContract.Commands.Create commandData = new DummyContract.Commands.Create();
PublicKey ourPubKey = getServiceHub().getLegalIdentityKey();
PublicKey counterpartyPubKey = counterparty.getOwningKey();
List<PublicKey> requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey);
Command<DummyContract.Commands.Create> ourCommand = new Command<>(commandData, requiredSigners);

Attachments

Attachments are identified by their hash. The attachment with the corresponding hash must have been uploaded ahead of time via the node’s RPC interface:

val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment")
SecureHash ourAttachment = SecureHash.sha256("DummyAttachment");

Time-windows

Time windows represent the period of time during which the transaction must be notarised. They can have a start and an end time, or be open at either end:

val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX)
val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN)
val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX)
TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX);
TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN);
TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX);

We can also define a time window as an Instant +/- a time tolerance (e.g. 30 seconds):

val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(Instant.now(), 30.seconds)
TimeWindow ourTimeWindow2 = TimeWindow.withTolerance(Instant.now(), Duration.ofSeconds(30));

Or as a start-time plus a duration:

val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(Instant.now(), 30.seconds)
TimeWindow ourTimeWindow3 = TimeWindow.fromStartAndDuration(Instant.now(), Duration.ofSeconds(30));

Adding items

The transaction builder is mutable. We add items to it using the TransactionBuilder.withItems method:

    /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
    fun withItems(vararg items: Any): TransactionBuilder {
        for (t in items) {
            when (t) {
                is StateAndRef<*> -> addInputState(t)
                is SecureHash -> addAttachment(t)
                is TransactionState<*> -> addOutputState(t)
                is ContractState -> addOutputState(t)
                is Command<*> -> addCommand(t)
                is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.")
                is TimeWindow -> setTimeWindow(t)
                else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
            }
        }
        return this
    }

withItems takes a vararg of objects and adds them to the builder based on their type:

  • StateAndRef objects are added as input states
  • TransactionState and ContractState objects are added as output states
  • Command objects are added as commands

Passing in objects of any other type will cause an IllegalArgumentException to be thrown.

Here’s an example usage of TransactionBuilder.withItems:

txBuilder.withItems(
        // Inputs, as ``StateRef``s that reference the outputs of previous transactions
        ourStateAndRef,
        // Outputs, as ``ContractState``s
        ourOutput,
        // Commands, as ``Command``s
        ourCommand
)
txBuilder.withItems(
        // Inputs, as ``StateRef``s that reference to the outputs of previous transactions
        ourStateAndRef,
        // Outputs, as ``ContractState``s
        ourOutput,
        // Commands, as ``Command``s
        ourCommand
);

You can also pass in objects one-by-one. This is the only way to add attachments:

txBuilder.addInputState(ourStateAndRef)
txBuilder.addOutputState(ourOutput)
txBuilder.addCommand(ourCommand)
txBuilder.addAttachment(ourAttachment)
txBuilder.addInputState(ourStateAndRef);
txBuilder.addOutputState(ourOutput);
txBuilder.addCommand(ourCommand);
txBuilder.addAttachment(ourAttachment);

To set the transaction builder’s time-window, we can either set a time-window directly:

txBuilder.setTimeWindow(ourTimeWindow)
txBuilder.setTimeWindow(ourTimeWindow);

Or define the time-window as a time plus a duration (e.g. 45 seconds):

txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds)
txBuilder.setTimeWindow(Instant.now(), Duration.ofSeconds(45));

Signing the builder

Once the builder is ready, we finalize it by signing it and converting it into a SignedTransaction:

val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder)
SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(txBuilder);

This will sign the transaction with your legal identity key. You can also choose to use another one of your public keys:

val otherKey: PublicKey = serviceHub.keyManagementService.freshKey()
val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherKey)
PublicKey otherKey = getServiceHub().getKeyManagementService().freshKey();
SignedTransaction onceSignedTx2 = getServiceHub().signInitialTransaction(txBuilder, otherKey);

Either way, the outcome of this process is to create a SignedTransaction, which can no longer be modified.

SignedTransaction

A SignedTransaction is a combination of:

  • An immutable WireTransaction
  • A list of signatures over that transaction
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
                             val sigs: List<DigitalSignature.WithKey>
) : NamedByHash {

Before adding our signature to the transaction, we’ll want to verify both the transaction’s contents and the transaction’s signatures.

Verifying the transaction’s contents

To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn’t currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow called ResolveTransactionsFlow. See API: Flows for more details.

When verifying a SignedTransaction, we don’t verify the SignedTransaction per se, but rather the WireTransaction it contains. We extract this WireTransaction as follows:

val wireTx: WireTransaction = twiceSignedTx.tx
WireTransaction wireTx = twiceSignedTx.getTx();

However, this still isn’t enough. The WireTransaction holds its inputs as StateRef instances, and its attachments as hashes. These do not provide enough information to properly validate the transaction’s contents. To resolve these into actual ContractState and Attachment instances, we need to use the ServiceHub to convert the WireTransaction into a LedgerTransaction:

val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub)
LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub());

We can now verify the transaction to ensure that it satisfies the contracts of all the transaction’s input and output states:

ledgerTx.verify()
ledgerTx.verify();

We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in the contract. Here’s an example of how we might do this:

val outputState: DummyState = wireTx.outputsOfType<DummyState>().single()
if (outputState.magicNumber == 777) {
    // ``FlowException`` is a special exception type. It will be
    // propagated back to any counterparty flows waiting for a
    // message from this flow, notifying them that the flow has
    // failed.
    throw FlowException("We expected a magic number of 777.")
}
DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData();
if (outputState.getMagicNumber() != 777) {
    // ``FlowException`` is a special exception type. It will be
    // propagated back to any counterparty flows waiting for a
    // message from this flow, notifying them that the flow has
    // failed.
    throw new FlowException("We expected a magic number of 777.");
}

Verifying the transaction’s signatures

We also need to verify that the transaction has all the required signatures, and that these signatures are valid, to prevent tampering. We do this using SignedTransaction.verifyRequiredSignatures:

fullySignedTx.verifyRequiredSignatures()
    fullySignedTx.verifyRequiredSignatures();

Alternatively, we can use SignedTransaction.verifySignaturesExcept, which takes a vararg of the public keys for which the signatures are allowed to be missing:

onceSignedTx.verifySignaturesExcept(counterpartyPubKey)
    onceSignedTx.verifySignaturesExcept(counterpartyPubKey);

If the transaction is missing any signatures without the corresponding public keys being passed in, a SignaturesMissingException is thrown.

We can also choose to simply verify the signatures that are present:

twiceSignedTx.checkSignaturesAreValid()
    twiceSignedTx.checkSignaturesAreValid();

However, BE VERY CAREFUL - this function provides no guarantees that the signatures are correct, or that none are missing.

Signing the transaction

Once we are satisfied with the contents and existing signatures over the transaction, we can add our signature to the SignedTransaction using:

val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx)
SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx);

As with the TransactionBuilder, we can also choose to sign using another one of our public keys:

val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2)
SignedTransaction twiceSignedTx2 = getServiceHub().addSignature(onceSignedTx, otherKey2);

We can also generate a signature over the transaction without adding it to the transaction directly by using:

val sig: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx)
DigitalSignature.WithKey sig = getServiceHub().createSignature(onceSignedTx);

Or using another one of our public keys, as follows:

val sig2: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx, otherKey2)
DigitalSignature.WithKey sig2 = getServiceHub().createSignature(onceSignedTx, otherKey2);

Notarising and recording

Notarising and recording a transaction is handled by a built-in flow called FinalityFlow. See API: Flows for more details.