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 statesTransactionType.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 constructionWireTransaction
, an immutable transactionSignedTransaction
, an immutable transaction with 1+ associated signaturesLedgerTransaction
, a transaction that can be checked for validity
Here are the possible transitions between transaction states:
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 statesTransactionState
andContractState
objects are added as output statesCommand
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.