corda/docs/source/api-transactions.rst
2017-06-05 13:37:23 +01:00

11 KiB

API: Transactions

Note

Before reading this page, you should be familiar with the key concepts of key-concepts-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 mutable transaction-in-construction
  • WireTransaction, an immutable transaction
  • SignedTransaction, a WireTransaction with 1+ associated signatures
  • LedgerTransaction, a resolved WireTransaction that can be checked for contract validity

Here are the possible transitions between transaction states:

image

TransactionBuilder

Creating a builder

The first step when building a transaction is to create a TransactionBuilder:

// A general transaction builder.
val generalTxBuilder = TransactionType.General.Builder()

// A notary-change transaction builder.
val notaryChangeTxBuilder = TransactionType.NotaryChange.Builder()
// A general transaction builder.
final TransactionBuilder generalTxBuilder = new TransactionType.General.Builder();

// A notary-change transaction builder.
final TransactionBuilder notaryChangeTxBuilder = new TransactionType.NotaryChange.Builder();

Adding items

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

../../core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt

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.

You can also add the following items to the transaction:

  • TimeWindow objects, using TransactionBuilder.setTime
  • SecureHash objects referencing the hash of an attachment stored on the node, using TransactionBuilder.addAttachment

Input states

Input states are added to a transaction as StateAndRef instances, rather than as ContractState instances.

A StateAndRef combines a ContractState with a pointer to the transaction that created it. This series of pointers from the input states back to the transactions that created them is what allows a node to work backwards and verify the entirety of the transaction chain. It is defined as:

../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt

Where StateRef is defined as:

../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt

StateRef.index is the state's position in the outputs of the transaction that created it. In this way, a StateRef allows a notary service to uniquely identify the existing states that a transaction is marking as historic.

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.

Commands

Commands are added to the transaction as Command instances. Command combines a CommandData instance representing the type of the command with a list of the command's required signers. It is defined as:

../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt

Signing the builder

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

// Finalizes the builder by signing it with our primary signing key.
val signedTx1 = serviceHub.signInitialTransaction(unsignedTx)

// Finalizes the builder by signing it with a different key.
val signedTx2 = serviceHub.signInitialTransaction(unsignedTx, otherKey)

// Finalizes the builder by signing it with a set of keys.
val signedTx3 = serviceHub.signInitialTransaction(unsignedTx, otherKeys)
// Finalizes the builder by signing it with our primary signing key.
final SignedTransaction signedTx1 = getServiceHub().signInitialTransaction(unsignedTx);

// Finalizes the builder by signing it with a different key.
final SignedTransaction signedTx2 = getServiceHub().signInitialTransaction(unsignedTx, otherKey);

// Finalizes the builder by signing it with a set of keys.
final SignedTransaction signedTx3 = getServiceHub().signInitialTransaction(unsignedTx, otherKeys);

SignedTransaction

A SignedTransaction is a combination of an immutable WireTransaction and a list of signatures over that transaction:

../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt

Verifying the signatures

The signatures on a SignedTransaction have not necessarily been checked for validity. We check them using SignedTransaction.verifySignatures:

../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt

verifySignatures takes a vararg of the public keys for which the signatures are allowed to be missing. If the transaction is missing any signatures without the corresponding public keys being passed in, a SignaturesMissingException is thrown.

Verifying the transaction

Verifying a transaction is a multi-step process:

  • We check the transaction's signatures:
subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain))
subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain));
  • Before verifying the transaction, we need to retrieve from the proposer(s) of the transaction any parts of the transaction chain that our node doesn't currently have in its local storage:
subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain))
subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain));
  • To verify the transaction, we first need to resolve any state references and attachment hashes by converting the SignedTransaction into a LedgerTransaction. We can then verify the fully-resolved transaction:
partSignedTx.tx.toLedgerTransaction(serviceHub).verify()
partSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify();
  • We will generally also want to conduct some custom validation of the transaction, beyond what is provided for in the contract:
val ledgerTransaction = partSignedTx.tx.toLedgerTransaction(serviceHub)
val inputStateAndRef = ledgerTransaction.inputs.single()
val input = inputStateAndRef.state.data as MyState
if (input.value > 1000000) {
    throw FlowException("Proposed input value too high!")
}
final LedgerTransaction ledgerTransaction = partSignedTx.getTx().toLedgerTransaction(getServiceHub());
final StateAndRef inputStateAndRef = ledgerTransaction.getInputs().get(0);
final MyState input = (MyState) inputStateAndRef.getState().getData();
if (input.getValue() > 1000000) {
    throw new FlowException("Proposed input value too high!");
}

Signing the transaction

We add an additional signature to an existing SignedTransaction using:

val fullySignedTx = serviceHub.addSignature(partSignedTx)
SignedTransaction fullySignedTx = getServiceHub().addSignature(partSignedTx);

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

val signature = serviceHub.createSignature(partSignedTx)
DigitalSignature.WithKey signature = getServiceHub().createSignature(partSignedTx);

Notarising and recording

Notarising and recording a transaction is handled by a built-in flow called FinalityFlow. See api-flows for more details.