Understanding transactions
Transaction lifecycle
Between its creation and its final inclusion on the ledger, a transaction will generally occupy one of three states:
TransactionBuilder
. A transaction’s initial state. This is the only state during which the transaction is mutable, so we must add all the required components before moving on.SignedTransaction
. The transaction now has one or more digital signatures, making it immutable. This is the transaction type that is passed around to collect additional signatures and that is recorded on the ledger.LedgerTransaction
. The transaction has been “resolved” - for example, its inputs have been converted from references to actual states - allowing the transaction to be fully inspected.
Transaction components
A transaction consists of six types of components:
1+ states:
- 0+ input states
- 0+ output states
- 0+ reference input states
1+ commands
0+ attachments
0 or 1 time-window
- A transaction with a time-window must also have a notary
Each component corresponds to a specific class in the Corda API. The following section describes each component class, and how it is created.
Input states
An input state is added to a transaction as a StateAndRef
, which combines:
- The
ContractState
itself - A
StateRef
identifying thisContractState
as the output of a specific transaction
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
links an input state back to the transaction that created it. This means that transactions form
“chains” linking each input back to an original issuance transaction. This allows nodes verifying the transaction
to “walk the chain” and verify that each input was generated through a valid sequence of transactions.
Reference input states
A reference input state is added to a transaction as a ReferencedStateAndRef
. A ReferencedStateAndRef
can be
obtained from a StateAndRef
by calling the StateAndRef.referenced()
method which returns a ReferencedStateAndRef
.
val referenceState: ReferencedStateAndRef<DummyState> = ourStateAndRef.referenced()
ReferencedStateAndRef referenceState = ourStateAndRef.referenced();
Handling of update races:
When using reference states in a transaction, it may be the case that a notarisation failure occurs. This is most likely because the creator of the state (being used as a reference state in your transaction), has just updated it.
Typically, the creator of such reference data will have implemented flows for syndicating the updates out to users. However it is inevitable that there will be a delay between the state being used as a reference being consumed, and the nodes using it receiving the update.
This is where the WithReferencedStatesFlow
comes in. Given a flow which uses reference states, the
WithReferencedStatesFlow
will execute the the flow as a subFlow. If the flow fails due to a NotaryError.Conflict
for a reference state, then it will be suspended until the state refs for the reference states are consumed. In this
case, a consumption means that:
- the owner of the reference state has updated the state with a valid, notarised transaction
- the owner of the reference state has shared the update with the node attempting to run the flow which uses the reference state
- The node has successfully committed the transaction updating the reference state (and all the dependencies), and added the updated reference state to the vault.
At the point where the transaction updating the state being used as a reference is committed to storage and the vault
update occurs, then the WithReferencedStatesFlow
will wake up and re-execute the provided flow.
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 directly:
val ourOutputState: DummyState = DummyState()
DummyState ourOutputState = new DummyState();
In cases where an output state represents an update of an input state, we may want to create the output state by basing it on the input state:
val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77)
DummyState ourOtherOutputState = ourOutputState.copy(77);
Before our output state can be added to a transaction, we need to associate it with a contract. We can do this by
wrapping the output state in a StateAndContract
, which combines:
- The
ContractState
representing the output states - A
String
identifying the contract governing the state
val ourOutput: StateAndContract = StateAndContract(ourOutputState, DummyContract.PROGRAM_ID)
StateAndContract ourOutput = new StateAndContract(ourOutputState, DummyContract.PROGRAM_ID);
Commands
A command is added to the transaction as a Command
, which combines:
- A
CommandData
instance indicating the command’s type - A
List<PublicKey>
representing the command’s required signers
val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create()
val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey
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().getMyInfo().getLegalIdentitiesAndCerts().get(0).getOwningKey();
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:
val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment")
SecureHash ourAttachment = SecureHash.sha256("DummyAttachment");
The attachment with the corresponding hash must have been uploaded ahead of time via the node’s RPC interface.
Time-windows
Time windows represent the period 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
plus/minus a time tolerance (e.g. 30 seconds):
val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds)
TimeWindow ourTimeWindow2 = TimeWindow.withTolerance(getServiceHub().getClock().instant(), Duration.ofSeconds(30));
Or as a start-time plus a duration:
val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds)
TimeWindow ourTimeWindow3 = TimeWindow.fromStartAndDuration(getServiceHub().getClock().instant(), Duration.ofSeconds(30));
TransactionBuilder
Creating a builder
The first step when creating a transaction proposal is to instantiate a TransactionBuilder
.
If the transaction has input states or a time-window, we need to instantiate the builder with a reference to the notary that will notarise the inputs and verify the time-window:
val txBuilder: TransactionBuilder = TransactionBuilder(specificNotary)
TransactionBuilder txBuilder = new TransactionBuilder(specificNotary);
We discuss the selection of a notary in Writing CorDapp Flows.
If the transaction does not have any input states or a time-window, it does not require a notary, and can be instantiated without one:
val txBuilderNoNotary: TransactionBuilder = TransactionBuilder()
TransactionBuilder txBuilderNoNotary = new TransactionBuilder();
Adding items
The next step is to build up the transaction proposal by adding the desired components.
We can add components to the builder 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) = apply {
for (t in items) {
when (t) {
is StateAndRef<*> -> addInputState(t)
is ReferencedStateAndRef<*> -> addReferenceState(t)
is AttachmentId -> addAttachment(t)
is TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract)
is ContractState -> throw UnsupportedOperationException("Removed as of V1: please use a StateAndContract instead")
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)
is PrivacySalt -> setPrivacySalt(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
}
}
}
withItems
takes a vararg
of objects and adds them to the builder based on their type:
StateAndRef
objects are added as input statesReferencedStateAndRef
objects are added as reference input statesTransactionState
andStateAndContract
objects are added as output states- Both
TransactionState
andStateAndContract
are wrappers around aContractState
output that link the output to a specific contract
- Both
Command
objects are added as commandsSecureHash
objects are added as attachmentsA
TimeWindow
object replaces the transaction’s existingTimeWindow
, if any
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 ``StateAndRef``s that reference the outputs of previous transactions
ourStateAndRef,
// Outputs, as ``StateAndContract``s
ourOutput,
// Commands, as ``Command``s
ourCommand,
// Attachments, as ``SecureHash``es
ourAttachment,
// A time-window, as ``TimeWindow``
ourTimeWindow
)
txBuilder.withItems(
// Inputs, as ``StateAndRef``s that reference to the outputs of previous transactions
ourStateAndRef,
// Outputs, as ``StateAndContract``s
ourOutput,
// Commands, as ``Command``s
ourCommand,
// Attachments, as ``SecureHash``es
ourAttachment,
// A time-window, as ``TimeWindow``
ourTimeWindow
);
There are also individual methods for adding components.
Here are the methods for adding inputs and attachments:
txBuilder.addInputState(ourStateAndRef)
txBuilder.addAttachment(ourAttachment)
txBuilder.addInputState(ourStateAndRef);
txBuilder.addAttachment(ourAttachment);
An output state can be added as a ContractState
, contract class name and notary:
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary)
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary);
We can also leave the notary field blank, in which case the transaction’s default notary is used:
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID)
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID);
Or we can add the output state as a TransactionState
, which already specifies the output’s contract and notary:
val txState: TransactionState<DummyState> = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary)
TransactionState txState = new TransactionState(ourOutputState, DummyContract.PROGRAM_ID, specificNotary);
Commands can be added as a Command
:
txBuilder.addCommand(ourCommand)
txBuilder.addCommand(ourCommand);
Or as CommandData
and a vararg PublicKey
:
txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey)
txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey);
For the time-window, we can 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(getServiceHub().getClock().instant(), Duration.ofSeconds(45));
Signing the builder
Once the builder is ready, we finalize it by signing it and converting it into a SignedTransaction
.
We can either sign with our legal identity key:
val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder)
SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(txBuilder);
Or we can also choose to use another one of our public keys:
val otherIdentity: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherIdentity.owningKey)
PartyAndCertificate otherIdentity = getServiceHub().getKeyManagementService().freshKeyAndCert(getOurIdentityAndCert(), false);
SignedTransaction onceSignedTx2 = getServiceHub().signInitialTransaction(txBuilder, otherIdentity.getOwningKey());
Either way, the outcome of this process is to create an immutable SignedTransaction
with our signature over it.
SignedTransaction
A SignedTransaction
is a combination of:
- An immutable transaction
- A list of signatures over that transaction
@KeepForDJVM
@CordaSerializable
data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
override val sigs: List<TransactionSignature>
) : TransactionWithSignatures {
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
If a transaction has inputs, we need to retrieve all the states in the transaction’s dependency chain before we can
verify the transaction’s contents. This is because the transaction is only valid if its dependency chain is also valid.
We do this by requesting any states in the 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 ReceiveTransactionFlow
.
See Writing CorDapp Flows for more details.
We can now verify the transaction’s contents to ensure that it satisfies the contracts of all the transaction’s input and output states:
twiceSignedTx.verify(serviceHub)
twiceSignedTx.verify(getServiceHub());
Checking that the transaction meets the contract constraints is only part of verifying the transaction’s contents. We will usually also want to perform our own additional validation of the transaction contents before signing, to ensure that the transaction proposal represents an agreement we wish to enter into.
However, the SignedTransaction
holds its inputs as StateRef
instances, and its attachments as SecureHash
instances, which do not provide enough information to properly validate the transaction’s contents. We first need to
resolve the StateRef
and SecureHash
instances into actual ContractState
and Attachment
instances, which
we can then inspect.
We achieve this by using the ServiceHub
to convert the SignedTransaction
into a LedgerTransaction
:
val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub)
LedgerTransaction ledgerTx = twiceSignedTx.toLedgerTransaction(getServiceHub());
We can now perform our additional verification. Here’s a simple example:
val outputState: DummyState = ledgerTx.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 = ledgerTx.outputsOfType(DummyState.class).get(0);
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
Aside from verifying that the transaction’s contents are valid, we also need to check that the signatures are valid. A valid signature over the hash of the transaction prevents tampering.
We can verify that all the transaction’s required signatures are present and valid as follows:
fullySignedTx.verifyRequiredSignatures()
fullySignedTx.verifyRequiredSignatures();
However, we’ll often want to verify the transaction’s existing signatures before all of them have been collected. For
this 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);
There is also an overload of SignedTransaction.verifySignaturesExcept
, which takes a Collection
of the
public keys for which the signatures are allowed to be missing:
onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey))
onceSignedTx.verifySignaturesExcept(singletonList(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();
Be very careful, however - this function neither guarantees that the signatures that are present are required, nor checks whether any signatures are missing.
Signing the transaction
Once we are satisfied with the contents and existing signatures over the transaction, we add our signature to the
SignedTransaction
to indicate that we approve the transaction.
We can sign using our legal identity key, as follows:
val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx)
SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx);
Or we can choose to sign using another one of our public keys:
val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherIdentity2.owningKey)
SignedTransaction twiceSignedTx2 = getServiceHub().addSignature(onceSignedTx, otherIdentity2.getOwningKey());
We can also generate a signature over the transaction without adding it to the transaction directly.
We can do this with our legal identity key:
val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx)
TransactionSignature sig = getServiceHub().createSignature(onceSignedTx);
Or using another one of our public keys:
val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherIdentity2.owningKey)
TransactionSignature sig2 = getServiceHub().createSignature(onceSignedTx, otherIdentity2.getOwningKey());
Notarising and recording
Notarising and recording a transaction is handled by a built-in flow called FinalityFlow
. See Writing CorDapp Flows for
more details.
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.