From 1943b3633f0840397114828f226be49d2f0bacbb Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 12 Jul 2016 10:24:11 +0100 Subject: [PATCH] Add documentation on contract clauses --- docs/source/index.rst | 1 + docs/source/release-notes.rst | 3 +- docs/source/tutorial-contract-clauses.rst | 257 ++++++++++++++++++++++ docs/source/tutorial-contract.rst | 15 +- 4 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 docs/source/tutorial-contract-clauses.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 03721c65b9..6d0af3e9e5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,6 +39,7 @@ Read on to learn: where-to-start tutorial-contract + tutorial-contract-clauses tutorial-test-dsl protocol-state-machines oracles diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index b78680b575..0cb1df89cb 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -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 ----------- diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst new file mode 100644 index 0000000000..db6e18a0c9 --- /dev/null +++ b/docs/source/tutorial-contract-clauses.rst @@ -0,0 +1,257 @@ +.. highlight:: kotlin +.. raw:: html + + + + +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 + get() = throw UnsupportedOperationException("not implemented") + + override fun extractCommands(tx: TransactionForContract): List> + = tx.commands.select() + + .. 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 getClauses() { + throw UnsupportedOperationException("not implemented"); + } + + @Override + public Collection> extractCommands(@NotNull TransactionForContract tx) { + return tx.getCommands() + .stream() + .filter((AuthenticatedObject 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> { + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.CONTINUE + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.END + override val requiredCommands: Set> + get() = setOf(Commands.Move::class.java) + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: Collection>, + token: Issued): Set { + val command = commands.requireSingleCommand() + 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 { + @Override + public MatchBehaviour getIfNotMatched() { + return MatchBehaviour.CONTINUE; + } + + @Override + public MatchBehaviour getIfMatched() { + return MatchBehaviour.END; + } + + @Override + public Set> getRequiredCommands() { + return Collections.singleton(Commands.Move.class); + } + + @Override + public Set verify(@NotNull TransactionForContract tx, + @NotNull List inputs, + @NotNull List outputs, + @NotNull Collection> commands, + @NotNull State token) { + AuthenticatedObject 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>() { + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.ERROR + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.END + override val clauses: List>> + get() = listOf( + Clause.Redeem(), + Clause.Move(), + Clause.Issue() + ) + + override fun extractGroups(tx: TransactionForContract): List>> + = tx.groupStates> { it.token } + } + + .. sourcecode:: java + + public class Group extends GroupClauseVerifier { + @Override + public MatchBehaviour getIfMatched() { + return MatchBehaviour.END; + } + + @Override + public MatchBehaviour getIfNotMatched() { + return MatchBehaviour.ERROR; + } + + @Override + public List> getClauses() { + final List> clauses = new ArrayList<>(); + + clauses.add(new Clause.Redeem()); + clauses.add(new Clause.Move()); + clauses.add(new Clause.Issue()); + + return clauses; + } + + @Override + public List> 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 + get() = listOf(Clauses.Group()) + + override fun extractCommands(tx: TransactionForContract): List> + = tx.commands.select() + + .. sourcecode:: java + + @Override + public List getClauses() { + return Collections.singletonList(new Clause.Group()); + } + + @Override + public Collection> extractCommands(@NotNull TransactionForContract tx) { + return tx.getCommands() + .stream() + .filter((AuthenticatedObject 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. \ No newline at end of file diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 6ff025760c..aa50b3d689 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -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. \ No newline at end of file +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`". \ No newline at end of file