Fix minor doc error Get rid of @deprecated method that was only present for technical reasons. Minor fix and include in changelog
11 KiB
API: Contracts
Note
Before reading this page, you should be familiar with the key
concepts of key-concepts-contracts
.
All Corda contracts are JVM classes that implement
net.corda.core.contracts.Contract
.
The Contract
interface is defined as follows:
../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt
Where:
*
verify(tx: LedgerTransaction)
determines whether transactions involving states which reference this contract type are valid
legalContractReference
is the hash of the legal prose contract thatverify
seeks to express in code
verify()
verify()
is a method that doesn't return anything and
takes a LedgerTransaction
as a parameter. It either throws
an exception if the transaction is considered invalid, or returns
normally if the transaction is considered valid.
verify()
is executed in a sandbox. It does not have
access to the enclosing scope, and is not able to access the network or
perform any other I/O. It only has access to the properties defined on
LedgerTransaction
when establishing whether a transaction
is valid.
The two simplest verify
functions are the one that
accepts all transactions, and the one that rejects all transactions.
Here is the verify
that accepts all transactions:
override fun verify(tx: LedgerTransaction) {
// Always accepts!
}
@Override
public void verify(LedgerTransaction tx) {
// Always accepts!
}
And here is the verify
that rejects all
transactions:
override fun verify(tx: LedgerTransaction) {
throw IllegalArgumentException("Always rejects!")
}
@Override
public void verify(LedgerTransaction tx) {
throw new IllegalArgumentException("Always rejects!");
}
LedgerTransaction
The LedgerTransaction
object passed into
verify()
represents the full set of information available
to verify()
when deciding whether to accept or reject the
transaction. It has the following properties:
../../core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
Where:
inputs
is a list of the transaction's inputs'
outputs
is a list of the transaction's outputs'attachments
is a list of the transaction's attachments'commands
is a list of the transaction's commands, and their associated signatures'id
is the transaction's merkle root hash'notary
is the transaction's notary. If there are inputs these must have the same notary on their source transactions.timeWindow
is the transaction's timestamp and defines the acceptable delay for notarisation.type
is the class of Transaction. Normal ledger data transactions areTransactionType.General
, but migration of states to a new notary usesTransactionType.NotaryChange
.
requireThat()
Instead of throwing exceptions manually to reject a transaction, we
can use the requireThat
DSL:
{
requireThat "No inputs should be consumed when issuing an X." using (tx.inputs.isEmpty())
"Only one output state should be created." using (tx.outputs.size == 1)
val out = tx.outputs.single() as XState
"The sender and the recipient cannot be the same entity." using (out.sender != out.recipient)
"All of the participants must be signers." using (command.signers.containsAll(out.participants))
"The X's value must be non-negative." using (out.x.value > 0)
}
requireThat(require -> {
.using("No inputs should be consumed when issuing an X.", tx.getInputs().isEmpty());
require.using("Only one output state should be created.", tx.getOutputs().size() == 1);
requirefinal XState out = (XState) tx.getOutputs().get(0);
.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient());
require.using("All of the participants must be signers.", command.getSigners().containsAll(out.getParticipants()));
require.using("The X's value must be non-negative.", out.getX().getValue() > 0);
requirereturn null;
});
For each <String
, Boolean
> pair
within requireThat
, if the boolean condition is false, an
IllegalArgumentException
is thrown with the corresponding
string as the exception message. In turn, this exception will cause the
transaction to be rejected.
Commands
LedgerTransaction
contains the commands as a list of
AuthenticatedObject
instances.
AuthenticatedObject
pairs an object with a list of signers.
In this case, AuthenticatedObject
pairs a command with a
list of the entities that are required to sign a transaction where this
command is present:
../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt
Where:
signers
is the list of each signer'sPublicKey
signingParties
is the list of the signer's identities, if knownvalue
is the object being signed (a command, in this case)
Extracting commands
You can use the requireSingleCommand()
helper method to
extract commands.
<C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>)
asserts that the transaction contains exactly one command of type
T
, and returns it. If there is not exactly one command of
this type in the transaction, an exception is thrown, rejecting the
transaction.
For requireSingleCommand
to work, all the commands that
we wish to match against must be grouped using the same marker
interface.
Here is an example of using requireSingleCommand
to
extract a transaction's command and using it to fork the execution of
verify()
:
class XContract : Contract {
interface Commands : CommandData {
class Issue : TypeOnlyCommandData(), Commands
class Transfer : TypeOnlyCommandData(), Commands
}
override fun verify(tx: LedgerTransaction) {
val command = tx.commands.requireSingleCommand<Commands>()
when (command.value) {
is Commands.Issue -> {
// Issuance verification logic.
}
is Commands.Transfer -> {
// Transfer verification logic.
}
}
}
override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash")
}
public class XContract implements Contract {
public interface Commands extends CommandData {
class Issue extends TypeOnlyCommandData implements Commands {}
class Transfer extends TypeOnlyCommandData implements Commands {}
}
@Override
public void verify(LedgerTransaction tx) {
final AuthenticatedObject<Commands> command = requireSingleCommand(tx.getCommands(), Commands.class);
if (command.getValue() instanceof Commands.Issue) {
// Issuance verification logic.
} else if (command.getValue() instanceof Commands.Transfer) {
// Transfer verification logic.
}
}
private final SecureHash legalContractReference = SecureHash.sha256("X contract hash");
@Override public final SecureHash getLegalContractReference() { return legalContractReference; }
}
Grouping states
Suppose we have the following transaction, where 15 USD is being exchanged for 10 GBP:
We can imagine that we would like to verify the USD states and the GBP states separately:
LedgerTransaction
provides a groupStates
method to allow you to group states in this way:
../../core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt
Where InOutGroup
is defined as:
../../core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt
For example, we could group the states in the transaction above by
currency (i.e. by amount.token
):
val groups: List<InOutGroup<Cash.State, Issued<Currency>>> = tx.groupStates(Cash.State::class.java) {
-> it.amount.token
it }
final List<InOutGroup<Cash.State, Issued<Currency>>> groups = tx.groupStates(
.State.class,
Cash-> it.getAmount().getToken()
it );
This would produce the following InOutGroups:
We can now verify these groups individually:
for ((in_, out, key) in groups) {
when (key) {
is GBP -> {
// GBP verification logic.
}
is USD -> {
// USD verification logic.
}
}
}
for (InOutGroup group : groups) {
if (group.getGroupingKey() == USD) {
// USD verification logic.
} else if (group.getGroupingKey() == GBP) {
// GBP verification logic.
}
}
Legal prose
Current, legalContractReference
is simply the SHA-256
hash of a contract:
../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt
In the future, a contract's legal prose will be included as an attachment instead.