mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Merged in rnicoll-clause-docs (pull request #219)
Add documentation on contract clauses
This commit is contained in:
commit
a914b12a11
@ -39,6 +39,7 @@ Read on to learn:
|
||||
|
||||
where-to-start
|
||||
tutorial-contract
|
||||
tutorial-contract-clauses
|
||||
tutorial-test-dsl
|
||||
protocol-state-machines
|
||||
oracles
|
||||
|
@ -6,7 +6,8 @@ Here are brief summaries of what's changed between each snapshot release.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
There are currently no unreleased changes.
|
||||
* Smart contracts have been redesigned around reusable components, referred to as "clauses". The cash, commercial paper
|
||||
and obligation contracts now share a common issue clause.
|
||||
|
||||
Milestone 1
|
||||
-----------
|
||||
|
257
docs/source/tutorial-contract-clauses.rst
Normal file
257
docs/source/tutorial-contract-clauses.rst
Normal file
@ -0,0 +1,257 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Writing a contract using clauses
|
||||
================================
|
||||
|
||||
This tutorial will take you through restructuring the commercial paper contract to use clauses. You should have
|
||||
already completed ":doc:`tutorial-contract`".
|
||||
|
||||
Clauses are essentially "mini-contracts" which contain verification logic, and are composed together to form
|
||||
a contract. With appropriate design, they can be made to be reusable, for example issuing contract state objects is
|
||||
generally the same for all fungible contracts, so a single issuance clause can be shared. This cuts down on scope for
|
||||
error, and improves consistency of behaviour.
|
||||
|
||||
Clauses can be composed of subclauses, either to combine clauses in different ways, or to apply specialised clauses.
|
||||
In the case of commercial paper, we have a "Grouping" outermost clause, which will contain the "Issue", "Move" and
|
||||
"Redeem" clauses. The result is a contract that looks something like this:
|
||||
|
||||
1. Group input and output states together, and then apply the following clauses on each group:
|
||||
a. If an Issue command is present, run appropriate tests and end processing this group.
|
||||
b. If a Move command is present, run appropriate tests and end processing this group.
|
||||
c. If a Redeem command is present, run appropriate tests and end processing this group.
|
||||
|
||||
Commercial paper class
|
||||
----------------------
|
||||
|
||||
First we need to change the class from implementing ``Contract``, to extend ``ClauseVerifier``. This is an abstract
|
||||
class which provides a verify() function for us, and requires we provide a property (``clauses``) for the clauses to test,
|
||||
and a function (``extractCommands``) to extract the applicable commands from the transaction. This is important because
|
||||
``ClauseVerifier`` checks that no commands applicable to the contract are left unprocessed at the end. The following
|
||||
examples are trimmed to the modified class definition and added elements, for brevity:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class CommercialPaper : ClauseVerifier {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
|
||||
|
||||
override val clauses: List<SingleClause>
|
||||
get() = throw UnsupportedOperationException("not implemented")
|
||||
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class CommercialPaper implements Contract {
|
||||
@Override
|
||||
public SecureHash getLegalContractReference() {
|
||||
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SingleClause> getClauses() {
|
||||
throw UnsupportedOperationException("not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Clauses
|
||||
-------
|
||||
|
||||
We'll tackle the inner clauses that contain the bulk of the verification logic, first, and the clause which handles
|
||||
grouping of input/output states later. The inner clauses need to implement the ``GroupClause`` interface, which defines
|
||||
the verify() function, and properties for key information on how the clause is processed. These properties specify the
|
||||
command(s) which must be present in order for the clause to be matched, and what to do after processing the clause
|
||||
depending on whether it was matched or not.
|
||||
|
||||
The ``verify()`` functions defined in the ``SingleClause`` and ``GroupClause`` interfaces is similar to the conventional
|
||||
``Contract`` verification function, although it adds new parameters and returns the set of commands which it has processed.
|
||||
Normally this returned set is identical to the commands matched in order to trigger the clause, however in some cases the
|
||||
clause may process optional commands which it needs to report that it has handled, or may by designed to only process
|
||||
the first (or otherwise) matched command.
|
||||
|
||||
The Move clause for the commercial paper contract is relatively simple, so lets start there:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class Move: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class Move implements GroupClause<State, State> {
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Move.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = single(inputs);
|
||||
|
||||
requireThat(require -> {
|
||||
require.by("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner()));
|
||||
require.by("the state is propagated", outputs.size() == 1);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
The post-processing ``MatchBehaviour`` options are:
|
||||
* CONTINUE
|
||||
* END
|
||||
* ERROR
|
||||
|
||||
In this case we process commands against each group, until the first matching clause is found, so we ``END`` on a match
|
||||
and ``CONTINUE`` otherwise. ``ERROR`` can be used as a part of a clause which must always/never be matched.
|
||||
|
||||
Group Clause
|
||||
------------
|
||||
|
||||
We need to wrap the move clause (as well as the issue and redeem clauses - see the relevant contract code for their
|
||||
full specifications) in an outer clause. For this we extend the standard ``GroupClauseVerifier`` and specify how to
|
||||
group input/output states, as well as the clauses to run on each group.
|
||||
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val clauses: List<GroupClause<State, Issued<Terms>>>
|
||||
get() = listOf(
|
||||
Clause.Redeem(),
|
||||
Clause.Move(),
|
||||
Clause.Issue()
|
||||
)
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class Group extends GroupClauseVerifier<State, State> {
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.ERROR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.r3corda.core.contracts.clauses.GroupClause<State, State>> getClauses() {
|
||||
final List<GroupClause<State, State>> clauses = new ArrayList<>();
|
||||
|
||||
clauses.add(new Clause.Redeem());
|
||||
clauses.add(new Clause.Move());
|
||||
clauses.add(new Clause.Issue());
|
||||
|
||||
return clauses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
|
||||
return tx.groupStates(State.class, State::withoutOwner);
|
||||
}
|
||||
}
|
||||
|
||||
We then pass this clause into the outer ``ClauseVerifier`` contract by returning it from the ``clauses`` property. We
|
||||
also implement the ``extractCommands()`` function, which filters commands on the transaction down to the set the
|
||||
contained clauses must handle (any unmatched commands at the end of clause verification results in an exception to be
|
||||
thrown).
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@Override
|
||||
public List<SingleClause> getClauses() {
|
||||
return Collections.singletonList(new Clause.Group());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
In summary the top level contract ``CommercialPaper`` specifies a single grouping clause of type
|
||||
``CommercialPaper.Clauses.Group`` which in turn specifies ``GroupClause`` implementations for each type of command
|
||||
(``Redeem``, ``Move`` and ``Issue``). This reflects the flow of verification: In order to verify a ``CommercialPaper``
|
||||
we first group states, check which commands are specified, and run command-specific verification logic accordingly.
|
@ -15,9 +15,10 @@ for how Kotlin syntax works.
|
||||
Starting the commercial paper class
|
||||
-----------------------------------
|
||||
|
||||
A smart contract is a class that implements the ``Contract`` interface. For now, they have to be a part of the main
|
||||
codebase, as dynamic loading of contract code is not yet implemented. Therefore, we start by creating a file named
|
||||
either ``CommercialPaper.kt`` or ``CommercialPaper.java`` in the src/contracts directory with the following contents:
|
||||
A smart contract is a class that implements the ``Contract`` interface. This can be either implemented directly, or
|
||||
via an abstract contract such as ``ClauseVerifier``. For now, contracts have to be a part of the main codebase, as
|
||||
dynamic loading of contract code is not yet implemented. Therefore, we start by creating a file named either
|
||||
``CommercialPaper.kt`` or ``CommercialPaper.java`` in the ``contracts/src/main`` directory with the following contents:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -841,3 +842,9 @@ The logic that implements measurement of the threshold, different signing combin
|
||||
be implemented once in a separate contract, with the controlling data being held in the named state.
|
||||
|
||||
Future versions of the prototype will explore these concepts in more depth.
|
||||
|
||||
Clauses
|
||||
-------
|
||||
|
||||
Instead of structuring contracts as a single entity, they can be broken down into reusable chunks known as clauses.
|
||||
This idea is addressed in the next tutorial, ":doc:`tutorial-contract-clauses`".
|
Loading…
Reference in New Issue
Block a user