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