Merged in rnicoll-clause-docs (pull request #219)

Add documentation on contract clauses
This commit is contained in:
Ross Nicoll 2016-07-12 17:49:02 +01:00
commit a914b12a11
4 changed files with 271 additions and 5 deletions

View File

@ -39,6 +39,7 @@ Read on to learn:
where-to-start
tutorial-contract
tutorial-contract-clauses
tutorial-test-dsl
protocol-state-machines
oracles

View File

@ -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
-----------

View 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.

View File

@ -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
@ -840,4 +841,10 @@ The CP contract then needs to be extended only to verify that a state with the r
The logic that implements measurement of the threshold, different signing combinations that may be allowed etc can then
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`".