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
|
where-to-start
|
||||||
tutorial-contract
|
tutorial-contract
|
||||||
|
tutorial-contract-clauses
|
||||||
tutorial-test-dsl
|
tutorial-test-dsl
|
||||||
protocol-state-machines
|
protocol-state-machines
|
||||||
oracles
|
oracles
|
||||||
|
@ -6,7 +6,8 @@ Here are brief summaries of what's changed between each snapshot release.
|
|||||||
Unreleased
|
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
|
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
|
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
|
A smart contract is a class that implements the ``Contract`` interface. This can be either implemented directly, or
|
||||||
codebase, as dynamic loading of contract code is not yet implemented. Therefore, we start by creating a file named
|
via an abstract contract such as ``ClauseVerifier``. For now, contracts have to be a part of the main codebase, as
|
||||||
either ``CommercialPaper.kt`` or ``CommercialPaper.java`` in the src/contracts directory with the following contents:
|
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
|
.. 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.
|
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.
|
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