From 13b040ecd6c0dd25db39b089bd73546cc367daba Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 10 Aug 2016 17:51:13 +0100 Subject: [PATCH] Rework clauses to use composition Rework clauses so that rather than defining match/no-match behaviour themselves, they are now composed by nesting them within clauses that understand how to match their child clauses. This unifies a lot of the structure of clauses and removes corner cases needed for the first design, as well as moving towards a model which is easier to prove. --- .../contracts/JavaCommercialPaper.java | 75 ++----- .../com/r3corda/contracts/CommercialPaper.kt | 53 ++--- .../main/kotlin/com/r3corda/contracts/IRS.kt | 55 ++--- .../com/r3corda/contracts/asset/Cash.kt | 32 +-- .../contracts/asset/CommodityContract.kt | 42 ++-- .../com/r3corda/contracts/asset/Obligation.kt | 112 ++++----- .../r3corda/contracts/asset/OnLedgerAsset.kt | 11 +- .../clause/AbstractConserveAmount.kt | 38 ++-- .../r3corda/contracts/clause/AbstractIssue.kt | 23 +- .../com/r3corda/contracts/clause/Net.kt | 19 +- .../contracts/clause/NoZeroSizedOutputs.kt | 18 +- .../com/r3corda/contracts/asset/CashTests.kt | 4 +- .../contracts/asset/ObligationTests.kt | 4 +- .../com/r3corda/core/contracts/Structures.kt | 24 +- .../core/contracts/clauses/AllComposition.kt | 38 ++++ .../core/contracts/clauses/AnyComposition.kt | 24 ++ .../r3corda/core/contracts/clauses/Clause.kt | 59 +++-- .../core/contracts/clauses/ClauseVerifier.kt | 32 +-- .../core/contracts/clauses/CompositeClause.kt | 20 ++ .../core/contracts/clauses/ConcreteClause.kt | 17 ++ .../contracts/clauses/FirstComposition.kt | 30 +++ .../contracts/clauses/GroupClauseVerifier.kt | 77 +------ .../contracts/clauses/InterceptorClause.kt | 30 --- .../r3corda/core/testing/DummyLinearState.kt | 18 +- .../contracts/clauses/AllCompositionTests.kt | 31 +++ .../contracts/clauses/AnyCompositionTests.kt | 46 ++++ .../core/contracts/clauses/ClauseTestUtils.kt | 29 +++ .../contracts/clauses/VerifyClausesTests.kt | 83 +------ docs/source/tutorial-contract-clauses.rst | 212 ++++++------------ 29 files changed, 602 insertions(+), 654 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/AllComposition.kt create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/AnyComposition.kt create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/CompositeClause.kt create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/ConcreteClause.kt create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt delete mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/clauses/InterceptorClause.kt create mode 100644 core/src/test/kotlin/com/r3corda/core/contracts/clauses/AllCompositionTests.kt create mode 100644 core/src/test/kotlin/com/r3corda/core/contracts/clauses/AnyCompositionTests.kt create mode 100644 core/src/test/kotlin/com/r3corda/core/contracts/clauses/ClauseTestUtils.kt diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 5cfd3fcb62..d986eb1130 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -125,44 +125,14 @@ public class JavaCommercialPaper implements Contract { } } - public interface Clause { - abstract class AbstractGroup implements GroupClause { - @NotNull - @Override - public MatchBehaviour getIfNotMatched() { - return MatchBehaviour.CONTINUE; - } - - @NotNull - @Override - public MatchBehaviour getIfMatched() { - return MatchBehaviour.END; - } - } - - class Group extends GroupClauseVerifier { - @NotNull - @Override - public MatchBehaviour getIfMatched() { - return MatchBehaviour.END; - } - - @NotNull - @Override - public MatchBehaviour getIfNotMatched() { - return MatchBehaviour.ERROR; - } - - @NotNull - @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; + public interface Clauses { + class Group extends GroupClauseVerifier { + public Group() { + super(new AnyComposition<>( + new Clauses.Redeem(), + new Clauses.Move(), + new Clauses.Issue() + )); } @NotNull @@ -172,7 +142,7 @@ public class JavaCommercialPaper implements Contract { } } - class Move extends AbstractGroup { + class Move extends ConcreteClause { @NotNull @Override public Set> getRequiredCommands() { @@ -181,11 +151,11 @@ public class JavaCommercialPaper implements Contract { @NotNull @Override - public Set verify(@NotNull TransactionForContract tx, + public Set verify(@NotNull TransactionForContract tx, @NotNull List inputs, @NotNull List outputs, - @NotNull Collection> commands, - @NotNull State token) { + @NotNull List> commands, + @NotNull State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above State input = single(inputs); @@ -203,7 +173,7 @@ public class JavaCommercialPaper implements Contract { } } - class Redeem extends AbstractGroup { + class Redeem extends ConcreteClause { @NotNull @Override public Set> getRequiredCommands() { @@ -212,11 +182,11 @@ public class JavaCommercialPaper implements Contract { @NotNull @Override - public Set verify(@NotNull TransactionForContract tx, + public Set verify(@NotNull TransactionForContract tx, @NotNull List inputs, @NotNull List outputs, - @NotNull Collection> commands, - @NotNull State token) { + @NotNull List> commands, + @NotNull State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); // There should be only a single input due to aggregation above @@ -245,7 +215,7 @@ public class JavaCommercialPaper implements Contract { } } - class Issue extends AbstractGroup { + class Issue extends ConcreteClause { @NotNull @Override public Set> getRequiredCommands() { @@ -254,11 +224,11 @@ public class JavaCommercialPaper implements Contract { @NotNull @Override - public Set verify(@NotNull TransactionForContract tx, + public Set verify(@NotNull TransactionForContract tx, @NotNull List inputs, @NotNull List outputs, - @NotNull Collection> commands, - @NotNull State token) { + @NotNull List> commands, + @NotNull State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); State output = single(outputs); Timestamp timestampCommand = tx.getTimestamp(); @@ -298,16 +268,17 @@ public class JavaCommercialPaper implements Contract { } @NotNull - private Collection> extractCommands(@NotNull TransactionForContract tx) { + private List> extractCommands(@NotNull TransactionForContract tx) { return tx.getCommands() .stream() .filter((AuthenticatedObject command) -> command.getValue() instanceof Commands) + .map((AuthenticatedObject command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue())) .collect(Collectors.toList()); } @Override public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { - ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx)); + ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx)); } @NotNull diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index a1fba8c589..a224844b11 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -1,14 +1,12 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.Cash +import com.r3corda.contracts.asset.FungibleAsset import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.GroupClause -import com.r3corda.core.contracts.clauses.GroupClauseVerifier -import com.r3corda.core.contracts.clauses.MatchBehaviour -import com.r3corda.core.contracts.clauses.verifyClauses +import com.r3corda.core.contracts.clauses.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -52,10 +50,7 @@ class CommercialPaper : Contract { val maturityDate: Instant ) - private fun extractCommands(tx: TransactionForContract): List> - = tx.commands.select() - - override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx)) + override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select()) data class State( val issuance: PartyAndReference, @@ -82,25 +77,16 @@ class CommercialPaper : Contract { } interface Clauses { - class Group : GroupClauseVerifier>() { - override val ifNotMatched = MatchBehaviour.ERROR - override val ifMatched = MatchBehaviour.END - override val clauses = listOf( - Redeem(), - Move(), - Issue() - ) - + class Group : GroupClauseVerifier>( + AnyComposition( + Redeem(), + Move(), + Issue())) { override fun groupStates(tx: TransactionForContract): List>> = tx.groupStates> { it.token } } - abstract class AbstractGroupClause: GroupClause> { - override val ifNotMatched = MatchBehaviour.CONTINUE - override val ifMatched = MatchBehaviour.END - } - - class Issue : AbstractIssue( + class Issue : AbstractIssue( { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() }, { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) { override val requiredCommands: Set> = setOf(Commands.Issue::class.java) @@ -108,8 +94,8 @@ class CommercialPaper : Contract { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + token: Issued?): Set { val consumedCommands = super.verify(tx, inputs, outputs, commands, token) commands.requireSingleCommand() val timestamp = tx.timestamp @@ -121,14 +107,14 @@ class CommercialPaper : Contract { } } - class Move: AbstractGroupClause() { + class Move: ConcreteClause>() { override val requiredCommands: Set> = setOf(Commands.Move::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + groupingKey: Issued?): Set { val command = commands.requireSingleCommand() val input = inputs.single() requireThat { @@ -141,15 +127,14 @@ class CommercialPaper : Contract { } } - class Redeem(): AbstractGroupClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Redeem::class.java) + class Redeem(): ConcreteClause>() { + override val requiredCommands: Set> = setOf(Commands.Redeem::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + groupingKey: Issued?): Set { // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) // before requiring a single command val command = commands.requireSingleCommand() @@ -172,7 +157,7 @@ class CommercialPaper : Contract { } interface Commands : CommandData { - class Move : TypeOnlyCommandData(), Commands + data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands class Redeem : TypeOnlyCommandData(), Commands data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 11ec5119b5..af3d33c827 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -447,24 +447,14 @@ class InterestRateSwap() : Contract { fixingCalendar, index, indexSource, indexTenor) } - fun extractCommands(tx: TransactionForContract): Collection> - = tx.commands.select() + override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) - override fun verify(tx: TransactionForContract) { - verifyClauses(tx, - listOf(Clause.Timestamped(), Clause.Group(), LinearState.ClauseVerifier(State::class.java)), - extractCommands(tx)) - } - - interface Clause { + interface Clauses { /** * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides * helper functions for the clauses. */ - abstract class AbstractIRSClause : GroupClause { - override val ifMatched = MatchBehaviour.END - override val ifNotMatched = MatchBehaviour.CONTINUE - + abstract class AbstractIRSClause : ConcreteClause() { // These functions may make more sense to use for basket types, but for now let's leave them here fun checkLegDates(legs: List) { requireThat { @@ -506,19 +496,18 @@ class InterestRateSwap() : Contract { } } - class Group : GroupClauseVerifier() { - override val ifMatched = MatchBehaviour.END - override val ifNotMatched = MatchBehaviour.ERROR - + class Group : GroupClauseVerifier(AnyComposition(Agree(), Fix(), Pay(), Mature())) { override fun groupStates(tx: TransactionForContract): List> // Group by Trade ID for in / out states = tx.groupStates() { state -> state.linearId } - - override val clauses = listOf(Agree(), Fix(), Pay(), Mature()) } - class Timestamped : SingleClause() { - override fun verify(tx: TransactionForContract, commands: Collection>): Set { + class Timestamped : ConcreteClause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { require(tx.timestamp?.midpoint != null) { "must be timestamped" } // We return an empty set because we don't process any commands return emptySet() @@ -526,13 +515,13 @@ class InterestRateSwap() : Contract { } class Agree : AbstractIRSClause() { - override val requiredCommands = setOf(Commands.Agree::class.java) + override val requiredCommands: Set> = setOf(Commands.Agree::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: UniqueIdentifier): Set { + commands: List>, + groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() val irs = outputs.filterIsInstance().single() requireThat { @@ -562,13 +551,13 @@ class InterestRateSwap() : Contract { } class Fix : AbstractIRSClause() { - override val requiredCommands = setOf(Commands.Refix::class.java) + override val requiredCommands: Set> = setOf(Commands.Refix::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: UniqueIdentifier): Set { + commands: List>, + groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() val irs = outputs.filterIsInstance().single() val prevIrs = inputs.filterIsInstance().single() @@ -607,13 +596,13 @@ class InterestRateSwap() : Contract { } class Pay : AbstractIRSClause() { - override val requiredCommands = setOf(Commands.Pay::class.java) + override val requiredCommands: Set> = setOf(Commands.Pay::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: UniqueIdentifier): Set { + commands: List>, + groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() requireThat { "Payments not supported / verifiable yet" by false @@ -623,13 +612,13 @@ class InterestRateSwap() : Contract { } class Mature : AbstractIRSClause() { - override val requiredCommands = setOf(Commands.Mature::class.java) + override val requiredCommands: Set> = setOf(Commands.Mature::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: UniqueIdentifier): Set { + commands: List>, + groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() val irs = inputs.filterIsInstance().single() requireThat { 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 c753afeb9f..891b6c829c 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -4,8 +4,7 @@ import com.r3corda.contracts.clause.AbstractConserveAmount import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.GroupClauseVerifier -import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.* import com.r3corda.core.crypto.* import com.r3corda.core.node.services.Wallet import com.r3corda.core.utilities.Emoji @@ -34,7 +33,7 @@ val CASH_PROGRAM_ID = Cash() * At the same time, other contracts that just want money and don't care much who is currently holding it in their * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ -class Cash : OnLedgerAsset() { +class Cash : OnLedgerAsset() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -46,32 +45,30 @@ class Cash : OnLedgerAsset() { * that is inconsistent with the legal contract. */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() - override val clauses = listOf(Clauses.Group()) - override fun extractCommands(tx: TransactionForContract): List> - = tx.commands.select() + override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() + override fun extractCommands(commands: Collection>): List> + = commands.select() interface Clauses { - class Group : GroupClauseVerifier>() { - override val ifMatched: MatchBehaviour = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR - override val clauses = listOf( - NoZeroSizedOutputs(), + class Group : GroupClauseVerifier>(AllComposition>( + NoZeroSizedOutputs(), + FirstComposition>( Issue(), ConserveAmount()) - + ) + ) { override fun groupStates(tx: TransactionForContract): List>> = tx.groupStates> { it.issuanceDef } } - class Issue : AbstractIssue( + class Issue : AbstractIssue( sum = { sumCash() }, sumOrZero = { sumCashOrZero(it) } ) { - override val requiredCommands = setOf(Commands.Issue::class.java) + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) } - class ConserveAmount : AbstractConserveAmount() + class ConserveAmount : AbstractConserveAmount() } /** A state representing a cash claim against some party */ @@ -144,6 +141,9 @@ class Cash : OnLedgerAsset() { override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() + + override fun verify(tx: TransactionForContract) + = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands)) } // Small DSL extensions. 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 f576930108..fcc83f18ef 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt @@ -5,7 +5,8 @@ import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.GroupClauseVerifier -import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.AnyComposition +import com.r3corda.core.contracts.clauses.verifyClause import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.newSecureRandom @@ -33,7 +34,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract() * in future. */ // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. -class CommodityContract : OnLedgerAsset() { +class CommodityContract : OnLedgerAsset() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -46,7 +47,7 @@ class CommodityContract : OnLedgerAsset() { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") - override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() + override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() /** * The clauses for this contract are essentially: @@ -62,24 +63,10 @@ class CommodityContract : OnLedgerAsset() { * Grouping clause to extract input and output states into matched groups and then run a set of clauses over * each group. */ - class Group : GroupClauseVerifier>() { - /** - * The group clause does not depend on any commands being present, so something has gone terribly wrong if - * it doesn't match. - */ - override val ifNotMatched = MatchBehaviour.ERROR - /** - * The group clause is the only top level clause, so end after processing it. If there are any commands left - * after this clause has run, the clause verifier will trigger an error. - */ - override val ifMatched = MatchBehaviour.END - // Subclauses to run on each group - override val clauses = listOf( - NoZeroSizedOutputs(), - Issue(), - ConserveAmount() - ) - + class Group : GroupClauseVerifier>(AnyComposition( + NoZeroSizedOutputs(), + Issue(), + ConserveAmount())) { /** * Group commodity states by issuance definition (issuer and underlying commodity). */ @@ -90,17 +77,17 @@ class CommodityContract : OnLedgerAsset() { /** * Standard issue clause, specialised to match the commodity issue command. */ - class Issue : AbstractIssue( + class Issue : AbstractIssue( sum = { sumCommodities() }, sumOrZero = { sumCommoditiesOrZero(it) } ) { - override val requiredCommands = setOf(Commands.Issue::class.java) + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) } /** * Standard clause for conserving the amount from input to output. */ - class ConserveAmount : AbstractConserveAmount() + class ConserveAmount : AbstractConserveAmount() } /** A state representing a commodity claim against some party */ @@ -150,9 +137,10 @@ class CommodityContract : OnLedgerAsset() { */ data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit } - override val clauses = listOf(Clauses.Group()) - override fun extractCommands(tx: TransactionForContract): List> - = tx.commands.select() + override fun verify(tx: TransactionForContract) + = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands)) + override fun extractCommands(commands: Collection>): List> + = commands.select() /** * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. 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 1f8b56a293..f355bae09e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -43,25 +43,27 @@ class Obligation

: Contract { * 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") - private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle

(), Clauses.Net

()), - Clauses.Group

()) interface Clauses { /** * Parent clause for clauses that operate on grouped states (those which are fungible). */ - class Group

: GroupClauseVerifier, Issued>>() { - override val ifMatched: MatchBehaviour = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR - override val clauses = listOf( - NoZeroSizedOutputs, Terms

>(), - SetLifecycle

(), - VerifyLifecycle

(), - Settle

(), - Issue(), - ConserveAmount() - ) - + class Group

: GroupClauseVerifier, Commands, Issued>>( + AllComposition( + NoZeroSizedOutputs, Commands, Terms

>(), + FirstComposition( + SetLifecycle

(), + AllComposition( + VerifyLifecycle, Commands, Issued>, P>(), + FirstComposition( + Settle

(), + Issue(), + ConserveAmount() + ) + ) + ) + ) + ) { override fun groupStates(tx: TransactionForContract): List, Issued>>> = tx.groupStates, Issued>> { it.issuanceDef } } @@ -69,58 +71,64 @@ class Obligation

: Contract { /** * Generic issuance clause */ - class Issue

: AbstractIssue, Terms

>({ -> sumObligations() }, { token: Issued> -> sumObligationsOrZero(token) }) { - override val requiredCommands = setOf(Obligation.Commands.Issue::class.java) + class Issue

: AbstractIssue, Commands, Terms

>({ -> sumObligations() }, { token: Issued> -> sumObligationsOrZero(token) }) { + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) } /** * Generic move/exit clause for fungible assets */ - class ConserveAmount

: AbstractConserveAmount, Terms

>() + class ConserveAmount

: AbstractConserveAmount, Commands, Terms

>() /** * Clause for supporting netting of obligations. */ - class Net

: NetClause

() + class Net : NetClause() { + val lifecycleClause = Clauses.VerifyLifecycle() + override fun toString(): String = "Net obligations" + + override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: Unit?): Set { + lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey) + return super.verify(tx, inputs, outputs, commands, groupingKey) + } + } /** * Obligation-specific clause for changing the lifecycle of one or more states. */ - class SetLifecycle

: GroupClause, Issued>> { - override val requiredCommands = setOf(Commands.SetLifecycle::class.java) - override val ifMatched: MatchBehaviour = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE + class SetLifecycle

: ConcreteClause, Commands, Issued>>() { + override val requiredCommands: Set> = setOf(Commands.SetLifecycle::class.java) override fun verify(tx: TransactionForContract, inputs: List>, outputs: List>, - commands: Collection>, - token: Issued>): Set { + commands: List>, + groupingKey: Issued>?): Set { val command = commands.requireSingleCommand() Obligation

().verifySetLifecycleCommand(inputs, outputs, tx, command) return setOf(command.value) } + + override fun toString(): String = "Set obligation lifecycle" } /** * Obligation-specific clause for settling an outstanding obligation by witnessing * change of ownership of other states to fulfil */ - class Settle

: GroupClause, Issued>> { - override val requiredCommands = setOf(Commands.Settle::class.java) - override val ifMatched: MatchBehaviour = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE - + class Settle

: ConcreteClause, Commands, Issued>>() { + override val requiredCommands: Set> = setOf(Commands.Settle::class.java) override fun verify(tx: TransactionForContract, inputs: List>, outputs: List>, - commands: Collection>, - token: Issued>): Set { + commands: List>, + groupingKey: Issued>?): Set { + require(groupingKey != null) val command = commands.requireSingleCommand>() - val obligor = token.issuer.party - val template = token.product + val obligor = groupingKey!!.issuer.party + val template = groupingKey.product val inputAmount: Amount>> = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount: Amount>> = outputs.sumObligationsOrZero(token) + val outputAmount: Amount>> = outputs.sumObligationsOrZero(groupingKey) // Sum up all asset state objects that are moving and fulfil our requirements @@ -166,7 +174,7 @@ class Obligation

: Contract { for ((beneficiary, obligations) in inputs.groupBy { it.owner }) { val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() if (settled != null) { - val debt = obligations.sumObligationsOrZero(token) + val debt = obligations.sumObligationsOrZero(groupingKey) require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } totalPenniesSettled += settled.quantity } @@ -185,7 +193,7 @@ class Obligation

: Contract { "signatures are present from all obligors" by command.signers.containsAll(requiredSigners) "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } "at obligor ${obligor.name} the obligations after settlement balance" by - (inputAmount == outputAmount + Amount(totalPenniesSettled, token)) + (inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey)) } return setOf(command.value) } @@ -197,22 +205,15 @@ class Obligation

: Contract { * any lifecycle change clause, which is the only clause that involve * non-standard lifecycle states on input/output. */ - class VerifyLifecycle

: SingleClause(), GroupClause, Issued>> { - override fun verify(tx: TransactionForContract, commands: Collection>): Set - = verify( - tx.inputs.filterIsInstance>(), - tx.outputs.filterIsInstance>() - ) - + class VerifyLifecycle : ConcreteClause() { override fun verify(tx: TransactionForContract, - inputs: List>, - outputs: List>, - commands: Collection>, - token: Issued>): Set - = verify(inputs, outputs) - - fun verify(inputs: List>, - outputs: List>): Set { + inputs: List, + outputs: List, + commands: List>, + groupingKey: T?): Set + = verify(inputs.filterIsInstance>(), outputs.filterIsInstance>()) + private fun verify(inputs: List>, + outputs: List>): Set { requireThat { "all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } @@ -330,7 +331,7 @@ class Obligation

: Contract { * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting * as only the beneficiary (not the obligor) needs to sign. */ - data class Net(val type: NetType) : Obligation.Commands + data class Net(val type: NetType) : Commands /** * A command stating that a debt has been moved, optionally to fulfil another contract. @@ -373,9 +374,10 @@ class Obligation

: Contract { data class Exit

(override val amount: Amount>>) : Commands, FungibleAsset.Commands.Exit> } - private fun extractCommands(tx: TransactionForContract): List> - = tx.commands.select() - override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) + override fun verify(tx: TransactionForContract) = verifyClause(tx, FirstComposition( + Clauses.Net(), + Clauses.Group

() + ), tx.commands.select()) /** * 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 550f45b264..9378cc7b7e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -2,8 +2,6 @@ package com.r3corda.contracts.asset import com.r3corda.contracts.clause.AbstractConserveAmount import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.SingleClause -import com.r3corda.core.contracts.clauses.verifyClauses import com.r3corda.core.crypto.Party import java.security.PublicKey @@ -25,12 +23,9 @@ 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> : 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)) +abstract class OnLedgerAsset> : Contract { + abstract fun extractCommands(commands: Collection>): Collection> + abstract val conserveClause: AbstractConserveAmount /** * Generate an transaction exiting assets from the ledger. diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index e5f4d1ab95..963e44ff27 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.sumFungibleOrNull import com.r3corda.contracts.asset.sumFungibleOrZero import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.GroupClause -import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.ConcreteClause import com.r3corda.core.crypto.Party import java.security.PublicKey import java.util.* @@ -16,14 +15,7 @@ import java.util.* * Move command is provided, and errors if absent. Must be the last clause under a grouping clause; * errors on no-match, ends on match. */ -abstract class AbstractConserveAmount, T: Any> : GroupClause> { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val requiredCommands: Set> - get() = emptySet() - +abstract class AbstractConserveAmount, C : CommandData, T : Any> : ConcreteClause>() { /** * Gather assets from the given list of states, sufficient to match or exceed the given amount. * @@ -177,16 +169,18 @@ abstract class AbstractConserveAmount, T: Any> : GroupClause override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { - val inputAmount: Amount> = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for group $token") - val deposit = token.issuer - val outputAmount: Amount> = outputs.sumFungibleOrZero(token) + commands: List>, + groupingKey: Issued?): Set { + require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" } + val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> } + val inputAmount: Amount> = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey") + val deposit = groupingKey!!.issuer + val outputAmount: Amount> = outputs.sumFungibleOrZero(groupingKey) // If we want to remove assets from the ledger, that must be signed for by the issuer and owner. val exitKeys: Set = inputs.flatMap { it.exitKeys }.toSet() - val exitCommand = tx.commands.select>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull() - val amountExitingLedger: Amount> = exitCommand?.value?.amount ?: Amount(0, token) + val exitCommand = matchedCommands.select>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull() + val amountExitingLedger: Amount> = exitCommand?.value?.amount ?: Amount(0, groupingKey) requireThat { "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } @@ -194,8 +188,12 @@ abstract class AbstractConserveAmount, T: Any> : GroupClause (inputAmount == outputAmount + amountExitingLedger) } - return listOf(exitCommand?.value, verifyMoveCommand(inputs, tx)) - .filter { it != null } - .requireNoNulls().toSet() + verifyMoveCommand(inputs, commands) + + // This is safe because we've taken the commands from a collection of C objects at the start + @Suppress("UNCHECKED_CAST") + return matchedCommands.map { it.value }.toSet() } + + override fun toString(): String = "Conserve amount between inputs and outputs" } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt index f0b05d0a60..ee5b28089a 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt @@ -1,8 +1,7 @@ package com.r3corda.contracts.clause import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.GroupClause -import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.ConcreteClause /** * Standard issue clause for contracts that issue fungible assets. @@ -14,18 +13,16 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour * @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are * no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed. */ -abstract class AbstractIssue( +abstract class AbstractIssue( val sum: List.() -> Amount>, val sumOrZero: List.(token: Issued) -> Amount> -) : GroupClause> { - override val ifMatched = MatchBehaviour.END - override val ifNotMatched = MatchBehaviour.CONTINUE - +) : ConcreteClause>() { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + groupingKey: Issued?): Set { + require(groupingKey != null) // TODO: Take in matched commands as a parameter val issueCommand = commands.requireSingleCommand() @@ -40,8 +37,8 @@ abstract class AbstractIssue( // external mechanism (such as locally defined rules on which parties are trustworthy). // The grouping already ensures that all outputs have the same deposit reference and token. - val issuer = token.issuer.party - val inputAmount = inputs.sumOrZero(token) + val issuer = groupingKey!!.issuer.party + val inputAmount = inputs.sumOrZero(groupingKey) val outputAmount = outputs.sum() requireThat { "the issue command has a nonce" by (issueCommand.value.nonce != 0L) @@ -51,6 +48,8 @@ abstract class AbstractIssue( "output values sum to more than the inputs" by (outputAmount > inputAmount) } - return setOf(issueCommand.value) + // This is safe because we've taken the command from a collection of C objects at the start + @Suppress("UNCHECKED_CAST") + return setOf(issueCommand.value as C) } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt index 5566aa38a9..d67f7be52e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt @@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.Obligation import com.r3corda.contracts.asset.extractAmountsDue import com.r3corda.contracts.asset.sumAmountsDue import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.MatchBehaviour -import com.r3corda.core.contracts.clauses.SingleClause +import com.r3corda.core.contracts.clauses.ConcreteClause import java.security.PublicKey /** @@ -43,22 +42,24 @@ data class MultilateralNetState

( * Clause for netting contract states. Currently only supports obligation contract. */ // TODO: Make this usable for any nettable contract states -open class NetClause

: SingleClause() { - override val ifMatched: MatchBehaviour = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE +open class NetClause : ConcreteClause() { override val requiredCommands: Set> = setOf(Obligation.Commands.Net::class.java) @Suppress("ConvertLambdaToReference") - override fun verify(tx: TransactionForContract, commands: Collection>): Set { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { val command = commands.requireSingleCommand() val groups = when (command.value.type) { NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State

-> it.bilateralNetState } NetType.PAYMENT -> tx.groupStates { it: Obligation.State

-> it.multilateralNetState } } - for ((inputs, outputs, key) in groups) { - verifyNetCommand(inputs, outputs, command, key) + for ((groupInputs, groupOutputs, key) in groups) { + verifyNetCommand(groupInputs, groupOutputs, command, key) } - return setOf(command.value) + return setOf(command.value as C) } /** diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt index 59cd8d5688..ef59c4bd9c 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt @@ -2,29 +2,23 @@ package com.r3corda.contracts.clause import com.r3corda.contracts.asset.FungibleAsset import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.GroupClause -import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.ConcreteClause /** * Clause for fungible asset contracts, which enforces that no output state should have * a balance of zero. */ -open class NoZeroSizedOutputs, T: Any> : GroupClause> { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val requiredCommands: Set> - get() = emptySet() - +open class NoZeroSizedOutputs, C : CommandData, T : Any> : ConcreteClause>() { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + groupingKey: Issued?): Set { requireThat { "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } } return emptySet() } + + override fun toString(): String = "No zero sized outputs" } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index 6301acfd41..5d2c76d20b 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -172,11 +172,11 @@ class CashTests { } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this `fails with` "All commands must be matched at end of execution." + this `fails with` "The following commands were not matched at the end of execution" } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) } - this `fails with` "All commands must be matched at end of execution." + this `fails with` "The following commands were not matched at the end of execution" } this.verifies() } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index eb35995ab7..b1185bf537 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -180,11 +180,11 @@ class ObligationTests { } tweak { command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() } - this `fails with` "All commands must be matched at end of execution." + this `fails with` "The following commands were not matched at the end of execution" } tweak { command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) } - this `fails with` "All commands must be matched at end of execution." + this `fails with` "The following commands were not matched at the end of execution" } this.verifies() } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 8305ffcda7..34b5fc60af 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -1,7 +1,7 @@ package com.r3corda.core.contracts -import com.r3corda.core.contracts.clauses.MatchBehaviour -import com.r3corda.core.contracts.clauses.SingleClause +import com.r3corda.core.contracts.clauses.ConcreteClause +import com.r3corda.core.contracts.clauses.Clause import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -218,14 +218,18 @@ interface LinearState: ContractState { /** * Standard clause to verify the LinearState safety properties. */ - class ClauseVerifier(val stateClass: Class) : SingleClause() { - override fun verify(tx: TransactionForContract, commands: Collection>): Set { - val inputs = tx.inputs.filterIsInstance(stateClass) - val inputIds = inputs.map { it.linearId }.distinct() - require(inputIds.count() == inputs.count()) { "LinearStates cannot be merged" } - val outputs = tx.outputs.filterIsInstance(stateClass) - val outputIds = outputs.map { it.linearId }.distinct() - require(outputIds.count() == outputs.count()) { "LinearStates cannot be split" } + class ClauseVerifier(val stateClass: Class) : ConcreteClause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { + val filteredInputs = inputs.filterIsInstance(stateClass) + val inputIds = filteredInputs.map { it.linearId }.distinct() + require(inputIds.count() == filteredInputs.count()) { "LinearStates cannot be merged" } + val filteredOutputs = outputs.filterIsInstance(stateClass) + val outputIds = filteredOutputs.map { it.linearId }.distinct() + require(outputIds.count() == filteredOutputs.count()) { "LinearStates cannot be split" } return emptySet() } } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AllComposition.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AllComposition.kt new file mode 100644 index 0000000000..1deea44fdd --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AllComposition.kt @@ -0,0 +1,38 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionForContract +import java.util.* + +/** + * Compose a number of clauses, such that all of the clauses must run for verification to pass. + */ +class AllComposition(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() { + override val clauses = ArrayList>() + + init { + clauses.add(firstClause) + clauses.addAll(remainingClauses) + } + + override fun matchedClauses(commands: List>): List> { + clauses.forEach { clause -> + check(clause.matches(commands)) { "Failed to match clause ${clause}" } + } + return clauses + } + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: K?): Set { + return matchedClauses(commands).flatMapTo(HashSet()) { clause -> + clause.verify(tx, inputs, outputs, commands, groupingKey) + } + } + + override fun toString() = "All: $clauses.toList()" +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AnyComposition.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AnyComposition.kt new file mode 100644 index 0000000000..9e9ebe9fa2 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/AnyComposition.kt @@ -0,0 +1,24 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionForContract +import java.util.* + +/** + * Compose a number of clauses, such that any number of the clauses can run. + */ +class AnyComposition(vararg val rawClauses: Clause) : CompositeClause() { + override val clauses: List> = rawClauses.asList() + + override fun matchedClauses(commands: List>): List> = clauses.filter { it.matches(commands) } + + override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set { + return matchedClauses(commands).flatMapTo(HashSet()) { clause -> + clause.verify(tx, inputs, outputs, commands, groupingKey) + } + } + + override fun toString(): String = "Or: ${clauses.toList()}" +} \ No newline at end of file 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 index b7b542525a..3b3072f695 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/Clause.kt @@ -2,34 +2,43 @@ package com.r3corda.core.contracts.clauses import com.r3corda.core.contracts.AuthenticatedObject import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.utilities.loggerFor /** - * A clause that can be matched as part of execution of a contract. + * @param S the type of contract state this clause operates on. + * @param C a common supertype of commands this clause operates on. + * @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable. */ -// 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 */ +interface Clause { + companion object { + val log = loggerFor>() + } + + /** Determine whether this clause runs or not */ 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 -} + /** + * Determine the subclauses which will be verified as a result of verifying this clause. + */ + fun getExecutionPath(commands: List>): List> -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. * + * @param tx the full transaction being verified. This is provided for cases where clauses need to access + * states or commands outside of their normal scope. + * @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause], + * but may be further reduced by clauses such as [GroupClauseVerifier]. + * @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause], + * but may be further reduced by clauses such as [GroupClauseVerifier]. + * @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause], + * but may be further reduced by clauses such as [GroupClauseVerifier]. + * @param groupingKey a grouping key applied to states and commands, where applicable. Taken from + * [TransactionForContract.InOutGroup]. * @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 @@ -37,16 +46,18 @@ interface SingleVerify { */ @Throws(IllegalStateException::class) fun verify(tx: TransactionForContract, - commands: Collection>): Set - + inputs: List, + outputs: List, + commands: List>, + groupingKey: K?): Set } /** - * A single verifiable clause. By default always matches, continues to the next clause when matched and errors - * if not matched. + * Determine if the given list of commands matches the required commands for a clause to trigger. */ -abstract class SingleClause : Clause, SingleVerify { - override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE - override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR - override val requiredCommands: Set> = emptySet() +fun Clause<*, C, *>.matches(commands: List>): Boolean { + return if (requiredCommands.isEmpty()) + true + else + commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands) } \ 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 eaa4331c3d..37dcd55c33 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 @@ -2,9 +2,7 @@ package com.r3corda.core.contracts.clauses import com.r3corda.core.contracts.* -import java.util.* -// Wrapper object for exposing a JVM friend version of the clause verifier /** * Verify a transaction against the given list of clauses. * @@ -13,27 +11,15 @@ import java.util.* * @param commands commands extracted from the transaction, which are relevant to the * clauses. */ -fun verifyClauses(tx: TransactionForContract, - clauses: List, - commands: Collection>) { - val unmatchedCommands = ArrayList(commands.map { it.value }) - - verify@ for (clause in clauses) { - val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) { - unmatchedCommands.removeAll(clause.verify(tx, commands)) - clause.ifMatched - } else { - clause.ifNotMatched - } - - when (matchBehaviour) { - MatchBehaviour.ERROR -> throw IllegalStateException("Error due to matching/not matching ${clause}") - MatchBehaviour.CONTINUE -> { - } - MatchBehaviour.END -> break@verify +fun verifyClause(tx: TransactionForContract, + clause: Clause, + commands: List>) { + if (Clause.log.isTraceEnabled) { + clause.getExecutionPath(commands).forEach { + Clause.log.trace("Tx ${tx.origHash} clause: ${clause}") } } + val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null) - require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." } -} - + check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/CompositeClause.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/CompositeClause.kt new file mode 100644 index 0000000000..90e2a3cb8f --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/CompositeClause.kt @@ -0,0 +1,20 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState + +/** + * Abstract supertype for clauses which compose other clauses together in some logical manner. + * + * @see ConcreteClause + */ +abstract class CompositeClause: Clause { + /** List of clauses under this composite clause */ + abstract val clauses: List> + override val requiredCommands: Set> = emptySet() + override fun getExecutionPath(commands: List>): List> + = matchedClauses(commands).flatMap { it.getExecutionPath(commands) } + /** Determine which clauses are matched by the supplied commands */ + abstract fun matchedClauses(commands: List>): List> +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ConcreteClause.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ConcreteClause.kt new file mode 100644 index 0000000000..65d85a3a85 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/ConcreteClause.kt @@ -0,0 +1,17 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState + +/** + * Abstract supertype for clauses which provide their own verification logic, rather than delegating to subclauses. + * By default these clauses are always matched (they have no required commands). + * + * @see CompositeClause + */ +abstract class ConcreteClause: Clause { + override fun getExecutionPath(commands: List>): List> + = listOf(this) + override val requiredCommands: Set> = emptySet() +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt new file mode 100644 index 0000000000..49101c6f84 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/FirstComposition.kt @@ -0,0 +1,30 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.utilities.loggerFor +import java.util.* + +/** + * Compose a number of clauses, such that the first match is run, and it errors if none is run. + */ +class FirstComposition(val firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() { + companion object { + val logger = loggerFor>() + } + + override val clauses = ArrayList>() + override fun matchedClauses(commands: List>): List> = listOf(clauses.first { it.matches(commands) }) + + init { + clauses.add(firstClause) + clauses.addAll(remainingClauses) + } + + override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set + = matchedClauses(commands).single().verify(tx, inputs, outputs, commands, groupingKey) + + override fun toString() = "First: ${clauses.toList()}" +} \ 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 cf1b4a2007..750233c7f1 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 @@ -6,77 +6,24 @@ import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.TransactionForContract import java.util.* -interface GroupVerify { - /** - * - * @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a - * later clause. - */ - fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: Collection>, - token: T): Set -} +abstract class GroupClauseVerifier(val clause: Clause) : ConcreteClause() { + abstract fun groupStates(tx: TransactionForContract): List> -interface GroupClause : Clause, GroupVerify + override fun getExecutionPath(commands: List>): List> + = clause.getExecutionPath(commands) -abstract class GroupClauseVerifier : SingleClause() { - abstract val clauses: List> - override val requiredCommands: Set> - get() = emptySet() - - abstract fun groupStates(tx: TransactionForContract): List> - - override fun verify(tx: TransactionForContract, commands: Collection>): Set { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, + groupingKey: Unit?): Set { val groups = groupStates(tx) - val matchedCommands = HashSet() - val unmatchedCommands = ArrayList(commands.map { it.value }) + val matchedCommands = HashSet() - for ((inputs, outputs, token) in groups) { - val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands) - matchedCommands.addAll(temp) - unmatchedCommands.removeAll(temp) + for ((groupInputs, groupOutputs, groupToken) in groups) { + matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken)) } return matchedCommands } - - /** - * Verify a subset of a transaction's inputs and outputs matches the conditions from this clause. For example, a - * "no zero amount output" clause would check each of the output states within the group, looking for a zero amount, - * and throw IllegalStateException if any matched. - * - * @param commands the full set of commands which apply to this contract. - * @param inputs input states within this group. - * @param outputs output states within this group. - * @param token the object used as a key when grouping states. - * @param unmatchedCommands commands which have not yet been matched within this group. - * @return matchedCommands commands which are matched during the verification process. - */ - @Throws(IllegalStateException::class) - private fun verifyGroup(commands: Collection>, - inputs: List, - outputs: List, - token: T, - tx: TransactionForContract, - unmatchedCommands: List): Set { - val matchedCommands = HashSet() - verify@ for (clause in clauses) { - val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) { - matchedCommands.addAll(clause.verify(tx, inputs, outputs, commands, token)) - clause.ifMatched - } else { - clause.ifNotMatched - } - - when (matchBehaviour) { - MatchBehaviour.ERROR -> throw IllegalStateException() - MatchBehaviour.CONTINUE -> { - } - MatchBehaviour.END -> break@verify - } - } - return matchedCommands - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/InterceptorClause.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/InterceptorClause.kt deleted file mode 100644 index e1a64d58d6..0000000000 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/InterceptorClause.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.r3corda.core.contracts.clauses - -import com.r3corda.core.contracts.AuthenticatedObject -import com.r3corda.core.contracts.CommandData -import com.r3corda.core.contracts.TransactionForContract -import java.util.* - -/** - * A clause which intercepts calls to a wrapped clause, and passes them through verification - * only from a pre-clause. This is similar to an inceptor in aspect orientated programming. - */ -class InterceptorClause( - val preclause: SingleVerify, - val clause: SingleClause -) : SingleClause() { - override val ifNotMatched: MatchBehaviour - get() = clause.ifNotMatched - override val ifMatched: MatchBehaviour - get() = clause.ifMatched - override val requiredCommands: Set> - get() = clause.requiredCommands - - override fun verify(tx: TransactionForContract, commands: Collection>): Set { - val consumed = HashSet(preclause.verify(tx, commands)) - consumed.addAll(clause.verify(tx, commands)) - return consumed - } - - override fun toString(): String = "Interceptor clause [${clause}]" -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt index 31021fda65..b2910fcf52 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt @@ -1,22 +1,18 @@ package com.r3corda.core.testing -import com.r3corda.core.contracts.Contract -import com.r3corda.core.contracts.LinearState -import com.r3corda.core.contracts.UniqueIdentifier -import com.r3corda.core.contracts.TransactionForContract -import com.r3corda.core.contracts.clauses.verifyClauses +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.Clause +import com.r3corda.core.contracts.clauses.verifyClause import com.r3corda.core.crypto.SecureHash import java.security.PublicKey class DummyLinearContract: Contract { override val legalContractReference: SecureHash = SecureHash.sha256("Test") - override fun verify(tx: TransactionForContract) { - verifyClauses(tx, - listOf(LinearState.ClauseVerifier(State::class.java)), - emptyList()) - } - + val clause: Clause = LinearState.ClauseVerifier(State::class.java) + override fun verify(tx: TransactionForContract) = verifyClause(tx, + clause, + emptyList()) class State( override val linearId: UniqueIdentifier = UniqueIdentifier(), diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AllCompositionTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AllCompositionTests.kt new file mode 100644 index 0000000000..a9bc875c0e --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AllCompositionTests.kt @@ -0,0 +1,31 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.crypto.SecureHash +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class AllCompositionTests { + + @Test + fun minimal() { + val counter = AtomicInteger(0) + val clause = AllComposition(matchedClause(counter), matchedClause(counter)) + val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) + verifyClause(tx, clause, emptyList>()) + + // Check that we've run the verify() function of two clauses + assertEquals(2, counter.get()) + } + + @Test + fun `not all match`() { + val clause = AllComposition(matchedClause(), unmatchedClause()) + val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) + assertFailsWith { verifyClause(tx, clause, emptyList>()) } + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AnyCompositionTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AnyCompositionTests.kt new file mode 100644 index 0000000000..30459db5fd --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/AnyCompositionTests.kt @@ -0,0 +1,46 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.crypto.SecureHash +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class AnyCompositionTests { + @Test + fun minimal() { + val counter = AtomicInteger(0) + val clause = AnyComposition(matchedClause(counter), matchedClause(counter)) + val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) + verifyClause(tx, clause, emptyList>()) + + // Check that we've run the verify() function of two clauses + assertEquals(2, counter.get()) + } + + @Test + fun `not all match`() { + val counter = AtomicInteger(0) + val clause = AnyComposition(matchedClause(counter), unmatchedClause(counter)) + val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) + verifyClause(tx, clause, emptyList>()) + + // Check that we've run the verify() function of one clause + assertEquals(1, counter.get()) + } + + @Test + fun `none match`() { + val counter = AtomicInteger(0) + val clause = AnyComposition(unmatchedClause(counter), unmatchedClause(counter)) + val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) + verifyClause(tx, clause, emptyList>()) + + // Check that we've run the verify() function of neither clause + assertEquals(0, counter.get()) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/clauses/ClauseTestUtils.kt b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/ClauseTestUtils.kt new file mode 100644 index 0000000000..ae8e4618ed --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/ClauseTestUtils.kt @@ -0,0 +1,29 @@ +package com.r3corda.core.contracts.clauses + +import com.r3corda.core.contracts.AuthenticatedObject +import com.r3corda.core.contracts.CommandData +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.TransactionForContract +import java.util.concurrent.atomic.AtomicInteger + +internal fun matchedClause(counter: AtomicInteger? = null) = object : ConcreteClause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, groupingKey: Unit?): Set { + counter?.incrementAndGet() + return emptySet() + } +} + +/** A clause that can never be matched */ +internal fun unmatchedClause(counter: AtomicInteger? = null) = object : ConcreteClause() { + override val requiredCommands: Set> = setOf(object: CommandData {}.javaClass) + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, groupingKey: Unit?): Set { + counter?.incrementAndGet() + return emptySet() + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/clauses/VerifyClausesTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/VerifyClausesTests.kt index 271a1588f2..e8b108b980 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/clauses/VerifyClausesTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/clauses/VerifyClausesTests.kt @@ -9,89 +9,30 @@ import kotlin.test.assertFailsWith * Tests for the clause verifier. */ class VerifyClausesTests { - /** Check that if there's no clauses, verification passes. */ - @Test - fun `passes empty clauses`() { - val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) - verifyClauses(tx, emptyList(), emptyList>()) - } - /** Very simple check that the function doesn't error when given any clause */ @Test fun minimal() { - val clause = object : SingleClause() { - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - - override fun verify(tx: TransactionForContract, commands: Collection>): Set = emptySet() + val clause = object : ConcreteClause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, groupingKey: Unit?): Set = emptySet() } val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) - verifyClauses(tx, listOf(clause), emptyList>()) - } - - /** Check that when there are no required commands, a clause always matches */ - @Test - fun emptyAlwaysMatches() { - val clause = object : SingleClause() { - override fun verify(tx: TransactionForContract, commands: Collection>): Set = emptySet() - } - val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) - // This would error if it wasn't matched - verifyClauses(tx, listOf(clause), emptyList>()) + verifyClause(tx, clause, emptyList>()) } @Test fun errorSuperfluousCommands() { - val clause = object : SingleClause() { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - - override fun verify(tx: TransactionForContract, commands: Collection>): Set - = emptySet() + val clause = object : ConcreteClause() { + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: List>, groupingKey: Unit?): Set = emptySet() } val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create()) val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256()) // The clause is matched, but doesn't mark the command as consumed, so this should error - assertFailsWith { verifyClauses(tx, listOf(clause), listOf(command)) } - } - - /** Check triggering of error if matched */ - @Test - fun errorMatched() { - val clause = object : SingleClause() { - override val requiredCommands: Set> - get() = setOf(DummyContract.Commands.Create::class.java) - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - - override fun verify(tx: TransactionForContract, commands: Collection>): Set - = commands.select().map { it.value }.toSet() - } - var tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) - - // This should pass as it doesn't match - verifyClauses(tx, listOf(clause), emptyList()) - - // This matches and should throw an error - val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create()) - tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256()) - assertFailsWith { verifyClauses(tx, listOf(clause), listOf(command)) } - } - - /** Check triggering of error if unmatched */ - @Test - fun errorUnmatched() { - val clause = object : SingleClause() { - override val requiredCommands: Set> - get() = setOf(DummyContract.Commands.Create::class.java) - - override fun verify(tx: TransactionForContract, commands: Collection>): Set = emptySet() - } - val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) - assertFailsWith { verifyClauses(tx, listOf(clause), emptyList()) } + assertFailsWith { verifyClause(tx, clause, listOf(command)) } } } \ No newline at end of file diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index eb1ee343c0..325fcc437c 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -10,14 +10,18 @@ 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 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. +Clauses are essentially micro-contracts which contain independent verification logic, and can be logically composed +together to form a contract. Clauses are designed to enable re-use of common logic, for example issuing state objects +is generally the same for all fungible contracts, so a common issuance clause can be inherited for each contract's +issue clause. This cuts down on scope for error, and improves consistency of behaviour. By splitting verification logic +into smaller chunks, they can also be readily tested in isolation. -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 ``Group`` outermost clause, which will contain the ``Issue``, ``Move`` and -``Redeem`` clauses. The result is a contract that looks something like this: +Clauses can be composed of subclauses, for example the ``AllClause`` or ``AnyClause`` clauses take list of clauses +that they delegate to. Clauses can also change the scope of states and commands being verified, for example grouping +together fungible state objects and running a clause against each distinct group. + +The commercial paper contract has a ``Group`` outermost clause, which contains 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. @@ -27,11 +31,12 @@ In the case of commercial paper, we have a ``Group`` outermost clause, which wil Commercial paper class ---------------------- -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: +To use the clause verification logic, the contract needs to call the ``verifyClause`` function, passing in the +transaction, a clause to verify, and a collection of commands the clauses are expected to handle all of. This list of +commands is important because ``verifyClause`` checks that none of the commands are left unprocessed at the end, and +raises an error if they are. The top level clause would normally be a composite clause (such as ``AnyComposition``, +``AllComposition``, etc.) which contains further clauses. The following examples are trimmed to the modified class +definition and added elements, for brevity: .. container:: codeset @@ -40,10 +45,7 @@ brevity: class CommercialPaper : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - private fun extractCommands(tx: TransactionForContract): List> - = tx.commands.select() - - override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx)) + override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select()) .. sourcecode:: java @@ -53,53 +55,40 @@ brevity: return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } - @Override - public Collection> extractCommands(@NotNull TransactionForContract tx) { - return tx.getCommands() - .stream() - .filter((AuthenticatedObject command) -> { return 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)); + ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx)); } 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 (``ifMatched``, ``ifNotMatched`` and ``requiredCommands``) defining 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. +grouping of input/output states later. The clauses must implement the ``Clause`` interface, which defines +the ``verify`` function, and the ``requiredCommands`` property used to determine the conditions under which a clause +is triggered. Normally clauses would extend ``ConcreteClause`` which provides defaults suitable for a clause which +verifies transactions, rather than delegating to other clauses. -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 ``verify`` function defined in the ``Clause`` interface 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 ``requiredCommands`` used to trigger the clause, however in some cases the clause may process +further optional commands which it needs to report that it has handled. -The Move clause for the commercial paper contract is relatively simple, so lets start there: +The ``Move`` clause for the commercial paper contract is relatively simple, so we will start there: .. container:: codeset .. sourcecode:: kotlin - class Move: GroupClause> { - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END + class Move: ConcreteClause>() { override val requiredCommands: Set> get() = setOf(Commands.Move::class.java) override fun verify(tx: TransactionForContract, inputs: List, outputs: List, - commands: Collection>, - token: Issued): Set { + commands: List>, + groupingKey: Issued?): Set { val command = commands.requireSingleCommand() val input = inputs.single() requireThat { @@ -114,140 +103,79 @@ The Move clause for the commercial paper contract is relatively simple, so lets .. sourcecode:: java - public class Move implements GroupClause { - @Override - public MatchBehaviour getIfNotMatched() { - return MatchBehaviour.CONTINUE; - } - - @Override - public MatchBehaviour getIfMatched() { - return MatchBehaviour.END; - } - + class Move extends ConcreteClause { + @NotNull @Override public Set> getRequiredCommands() { return Collections.singleton(Commands.Move.class); } + @NotNull @Override - public Set verify(@NotNull TransactionForContract tx, + 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); + @NotNull List> commands, + @NotNull State groupingKey) { + AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), 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; - }); + if (!cmd.getSigners().contains(input.getOwner())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + // Check the output CP state is the same as the input state, ignoring the owner field. + if (outputs.size() != 1) { + throw new IllegalStateException("the state is propagated"); + } // 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. By default -clauses are always matched (``requiredCommands`` is an empty set), execution continues after a clause is matched, and an -error is raised if a clause is not 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. +full specifications) in an outer clause that understands how to group contract states and objects. For this we extend +the standard ``GroupClauseVerifier`` and specify how to group input/output states, as well as the top-level to run on +each group. As with the top level clause on a contract, this is normally a composite clause that delegates to subclauses. .. 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>> + class Group : GroupClauseVerifier>( + AnyComposition( + Redeem(), + Move(), + Issue())) { + override fun groupStates(tx: TransactionForContract): List>> = tx.groupStates> { it.token } } .. sourcecode:: java - public class Group extends GroupClauseVerifier { - @Override - public MatchBehaviour getIfMatched() { - return MatchBehaviour.END; + class Group extends GroupClauseVerifier { + public Group() { + super(new AnyComposition<>( + new Clauses.Redeem(), + new Clauses.Move(), + new Clauses.Issue() + )); } + @NotNull @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) { + public List> groupStates(@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()); - } +For the ``CommercialPaper`` contract, this is the top level clause for the contract, and is passed directly into +``verifyClause`` (see the example code at the top of this tutorial). Summary ------- @@ -255,4 +183,12 @@ 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 +we first group states, check which commands are specified, and run command-specific verification logic accordingly. + +Debugging +--------- + +Debugging clauses which have been composed together can be complicated due to the difficulty in knowing which clauses +have been matched, whether specific clauses failed to match or passed verification, etc. There is "trace" level +logging code in the clause verifier which evaluates which clauses will be matched and logs them, before actually +performing the validation. To enable this, ensure trace level logging is enabled on the ``Clause`` interface.