From 162d19deeb618dd6efb17c973952dbdd53129e51 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 4 Aug 2016 15:23:58 +0100 Subject: [PATCH] Change how clause verification is called Change away from extending ClauseVerifier for contracts which support clauses, and explicitely call clause verification code in the verify() function. This should make the flow of control easier to understand. --- .../contracts/JavaCommercialPaper.java | 18 +++--- .../com/r3corda/contracts/CommercialPaper.kt | 10 +-- .../main/kotlin/com/r3corda/contracts/IRS.kt | 9 +-- .../com/r3corda/contracts/asset/Cash.kt | 2 +- .../contracts/asset/CommodityContract.kt | 2 +- .../com/r3corda/contracts/asset/Obligation.kt | 9 +-- .../r3corda/contracts/asset/OnLedgerAsset.kt | 9 ++- .../r3corda/core/contracts/clauses/Clause.kt | 44 +++++++++++++ .../core/contracts/clauses/ClauseVerifier.kt | 61 ++----------------- .../contracts/clauses/GroupClauseVerifier.kt | 4 +- docs/source/tutorial-contract-clauses.rst | 33 +++++----- 11 files changed, 98 insertions(+), 103 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 3985966652..6413e88917 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -22,7 +22,7 @@ import static kotlin.collections.CollectionsKt.*; * This is a Java version of the CommercialPaper contract (chosen because it's simple). This demonstrates how the * use of Kotlin for implementation of the framework does not impose the same language choice on contract developers. */ -public class JavaCommercialPaper extends ClauseVerifier { +public class JavaCommercialPaper implements Contract { //public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)"); private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper(); @@ -161,7 +161,7 @@ public class JavaCommercialPaper extends ClauseVerifier { @NotNull @Override - public List> extractGroups(@NotNull TransactionForContract tx) { + public List> groupStates(@NotNull TransactionForContract tx) { return tx.groupStates(State.class, State::withoutOwner); } } @@ -316,20 +316,18 @@ public class JavaCommercialPaper extends ClauseVerifier { } @NotNull - @Override - public List getClauses() { - return Collections.singletonList(new Clause.Group()); - } - - @NotNull - @Override - public Collection> extractCommands(@NotNull TransactionForContract tx) { + private Collection> extractCommands(@NotNull TransactionForContract tx) { return tx.getCommands() .stream() .filter((AuthenticatedObject command) -> command.getValue() instanceof Commands) .collect(Collectors.toList()); } + @Override + public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { + ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx)); + } + @NotNull @Override public SecureHash getLegalContractReference() { diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 712bec598e..56b5c3847e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -40,7 +40,7 @@ import java.util.* val CP_PROGRAM_ID = CommercialPaper() // TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. -class CommercialPaper : ClauseVerifier() { +class CommercialPaper : Contract { // TODO: should reference the content of the legal agreement, not its URI override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") @@ -49,11 +49,11 @@ class CommercialPaper : ClauseVerifier() { val maturityDate: Instant ) - override val clauses = listOf(Clauses.Group()) - - override fun extractCommands(tx: TransactionForContract): List> + private fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() + override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx)) + data class State( val issuance: PartyAndReference, override val owner: PublicKey, @@ -88,7 +88,7 @@ class CommercialPaper : ClauseVerifier() { Issue() ) - override fun extractGroups(tx: TransactionForContract): List>> + override fun groupStates(tx: TransactionForContract): List>> = tx.groupStates> { it.token } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 773b095acd..6573879522 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -183,7 +183,7 @@ class FloatingRatePaymentEvent(date: LocalDate, * Currently, we are not interested (excuse pun) in valuing the swap, calculating the PVs, DFs and all that good stuff (soon though). * This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model. */ -class InterestRateSwap() : ClauseVerifier() { +class InterestRateSwap() : Contract { override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") /** @@ -447,10 +447,11 @@ class InterestRateSwap() : ClauseVerifier() { fixingCalendar, index, indexSource, indexTenor) } - override val clauses: List = listOf(Clause.Timestamped(), Clause.Group()) - override fun extractCommands(tx: TransactionForContract): Collection> + private fun extractCommands(tx: TransactionForContract): Collection> = tx.commands.select() + tx.commands.select() + override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx)) + interface Clause { /** * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides @@ -505,7 +506,7 @@ class InterestRateSwap() : ClauseVerifier() { override val ifMatched = MatchBehaviour.END override val ifNotMatched = MatchBehaviour.ERROR - override fun extractGroups(tx: TransactionForContract): List> + override fun groupStates(tx: TransactionForContract): List> // Group by Trade ID for in / out states = tx.groupStates() { state -> state.common.tradeID } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index fe954a669f..5778afaf57 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -60,7 +60,7 @@ class Cash : OnLedgerAsset() { Issue(), ConserveAmount()) - override fun extractGroups(tx: TransactionForContract): List>> + override fun groupStates(tx: TransactionForContract): List>> = tx.groupStates> { it.issuanceDef } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt index d929a1723c..28e07cdde3 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt @@ -80,7 +80,7 @@ class CommodityContract : OnLedgerAsset() { /** * Group commodity states by issuance definition (issuer and underlying commodity). */ - override fun extractGroups(tx: TransactionForContract) + override fun groupStates(tx: TransactionForContract) = tx.groupStates> { it.issuanceDef } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index 472b2fa10d..2f63ee0921 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -30,7 +30,7 @@ val OBLIGATION_PROGRAM_ID = Obligation() * * @param P the product the obligation is for payment of. */ -class Obligation

: ClauseVerifier() { +class Obligation

: Contract { /** * TODO: @@ -43,7 +43,7 @@ class Obligation

: ClauseVerifier() { * that is inconsistent with the legal contract. */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") - override val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle

(), Clauses.Net

()), + private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle

(), Clauses.Net

()), Clauses.Group

()) interface Clauses { @@ -62,7 +62,7 @@ class Obligation

: ClauseVerifier() { ConserveAmount() ) - override fun extractGroups(tx: TransactionForContract): List, Issued>>> + override fun groupStates(tx: TransactionForContract): List, Issued>>> = tx.groupStates, Issued>> { it.issuanceDef } } @@ -377,8 +377,9 @@ class Obligation

: ClauseVerifier() { data class Exit

(override val amount: Amount>>) : Commands, FungibleAsset.Commands.Exit> } - override fun extractCommands(tx: TransactionForContract): List> + private fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() + override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) /** * A default command mutates inputs and produces identical outputs, except that the lifecycle changes. diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt index 677d24df23..c50df04deb 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -2,7 +2,8 @@ package com.r3corda.contracts.asset import com.r3corda.contracts.clause.AbstractConserveAmount import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.ClauseVerifier +import com.r3corda.core.contracts.clauses.SingleClause +import com.r3corda.core.contracts.clauses.verifyClauses import com.r3corda.core.crypto.Party import java.security.PublicKey @@ -24,9 +25,13 @@ import java.security.PublicKey * At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore * the issuer/depositRefs and just examine the amount fields. */ -abstract class OnLedgerAsset> : ClauseVerifier() { +abstract class OnLedgerAsset> : Contract { + abstract val clauses: List + abstract fun extractCommands(tx: TransactionForContract): Collection> abstract val conserveClause: AbstractConserveAmount + override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) + /** * Generate an transaction exiting assets from the ledger. * diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt new file mode 100644 index 0000000000..1c61161efa --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt @@ -0,0 +1,44 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.TransactionForContract + +/** + * A clause that can be matched as part of execution of a contract. + */ +// TODO: ifNotMatched/ifMatched should be dropped, and replaced by logic in the calling code that understands +// "or", "and", "single" etc. composition of sets of clauses. +interface Clause { + /** Classes for commands which must ALL be present in transaction for this clause to be triggered */ + val requiredCommands: Set> + /** Behaviour if this clause is matched */ + val ifNotMatched: MatchBehaviour + /** Behaviour if this clause is not matches */ + val ifMatched: MatchBehaviour +} + +enum class MatchBehaviour { + CONTINUE, + END, + ERROR +} + +interface SingleVerify { + /** + * Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause + * would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException + * if any matched. + * + * @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a + * later clause. This would normally be all commands matching "requiredCommands" for this clause, but some + * verify() functions may do further filtering on possible matches, and return a subset. This may also include + * commands that were not required (for example the Exit command for fungible assets is optional). + */ + @Throws(IllegalStateException::class) + fun verify(tx: TransactionForContract, + commands: Collection>): Set + +} + +interface SingleClause : Clause, SingleVerify \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ClauseVerifier.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ClauseVerifier.kt index 009115ed25..d734fcd43f 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ClauseVerifier.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ClauseVerifier.kt @@ -1,64 +1,10 @@ +@file:JvmName("ClauseVerifier") package com.r3corda.core.contracts.clauses import com.r3corda.core.contracts.* import java.util.* -interface Clause { - /** Classes for commands which must ALL be present in transaction for this clause to be triggered */ - val requiredCommands: Set> - /** Behaviour if this clause is matched */ - val ifNotMatched: MatchBehaviour - /** Behaviour if this clause is not matches */ - val ifMatched: MatchBehaviour -} - -enum class MatchBehaviour { - CONTINUE, - END, - ERROR -} - -interface SingleVerify { - /** - * Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause - * would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException - * if any matched. - * - * @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a - * later clause. This would normally be all commands matching "requiredCommands" for this clause, but some - * verify() functions may do further filtering on possible matches, and return a subset. This may also include - * commands that were not required (for example the Exit command for fungible assets is optional). - */ - @Throws(IllegalStateException::class) - fun verify(tx: TransactionForContract, - commands: Collection>): Set - -} - - -interface SingleClause : Clause, SingleVerify - -/** - * Abstract superclass for clause-based contracts to extend, which provides a verify() function - * that delegates to the supplied list of clauses. - */ -abstract class ClauseVerifier : Contract { - abstract val clauses: List - abstract fun extractCommands(tx: TransactionForContract): Collection> - override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) -} - -/** - * Verify a transaction against the given list of clauses. - * - * @param tx transaction to be verified. - * @param clauses the clauses to verify. - * @param T common supertype of commands to extract from the transaction, which are of relevance to these clauses. - */ -inline fun verifyClauses(tx: TransactionForContract, - clauses: List) - = verifyClauses(tx, clauses, tx.commands.select()) - +// Wrapper object for exposing a JVM friend version of the clause verifier /** * Verify a transaction against the given list of clauses. * @@ -89,4 +35,5 @@ fun verifyClauses(tx: TransactionForContract, } require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." } -} \ No newline at end of file +} + diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt index db5d958416..3478fe8027 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt @@ -26,10 +26,10 @@ abstract class GroupClauseVerifier : SingleClause { override val requiredCommands: Set> get() = emptySet() - abstract fun extractGroups(tx: TransactionForContract): List> + abstract fun groupStates(tx: TransactionForContract): List> override fun verify(tx: TransactionForContract, commands: Collection>): Set { - val groups = extractGroups(tx) + val groups = groupStates(tx) val matchedCommands = HashSet() val unmatchedCommands = ArrayList(commands.map { it.value }) diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index db6e18a0c9..47948b0aef 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -10,7 +10,7 @@ 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 +Clauses are essentially micro-contracts which contain independent 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. @@ -27,25 +27,24 @@ In the case of commercial paper, we have a "Grouping" outermost clause, which wi 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: +To use the clause verification logic, the contract needs to call the ``verifyClauses()`` function, passing in the transaction, +a list of clauses to verify, and a collection of commands the clauses are expected to handle all of. This list of +commands is important because ``verifyClauses()`` checks that none of the commands are left unprocessed at the end, and +raises an error if they are. 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"); + class CommercialPaper : Contract { + 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> + private fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() + override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx)) + .. sourcecode:: java public class CommercialPaper implements Contract { @@ -54,11 +53,6 @@ examples are trimmed to the modified class definition and added elements, for br 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() @@ -67,6 +61,11 @@ examples are trimmed to the modified class definition and added elements, for br .collect(Collectors.toList()); } + @Override + public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { + ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx)); + } + Clauses -------