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.
This commit is contained in:
Ross Nicoll 2016-08-10 17:51:13 +01:00
parent d6c595da37
commit 13b040ecd6
29 changed files with 602 additions and 654 deletions

View File

@ -125,44 +125,14 @@ public class JavaCommercialPaper implements Contract {
}
}
public interface Clause {
abstract class AbstractGroup implements GroupClause<State, State> {
@NotNull
@Override
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.CONTINUE;
}
@NotNull
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
}
}
class Group extends GroupClauseVerifier<State, State> {
@NotNull
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
}
@NotNull
@Override
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.ERROR;
}
@NotNull
@Override
public List<GroupClause<State, State>> getClauses() {
final List<GroupClause<State, State>> clauses = new ArrayList<>();
clauses.add(new Clause.Redeem());
clauses.add(new Clause.Move());
clauses.add(new Clause.Issue());
return clauses;
public interface Clauses {
class Group extends GroupClauseVerifier<State, Commands, State> {
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<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -181,11 +151,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Move> 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<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -212,11 +182,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Redeem> 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<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -254,11 +224,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Issue> 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<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
.stream()
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
.map((AuthenticatedObject<CommandData> 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

View File

@ -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<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
data class State(
val issuance: PartyAndReference,
@ -82,25 +77,16 @@ class CommercialPaper : Contract {
}
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
override val ifNotMatched = MatchBehaviour.ERROR
override val ifMatched = MatchBehaviour.END
override val clauses = listOf(
Redeem(),
Move(),
Issue()
)
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
AnyComposition(
Redeem(),
Move(),
Issue())) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
}
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
override val ifNotMatched = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
}
class Issue : AbstractIssue<State, Terms>(
class Issue : AbstractIssue<State, Commands, Terms>(
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
@ -108,8 +94,8 @@ class CommercialPaper : Contract {
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>?): Set<Commands> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
commands.requireSingleCommand<Commands.Issue>()
val timestamp = tx.timestamp
@ -121,14 +107,14 @@ class CommercialPaper : Contract {
}
}
class Move: AbstractGroupClause() {
class Move: ConcreteClause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>()
val input = inputs.single()
requireThat {
@ -141,15 +127,14 @@ class CommercialPaper : Contract {
}
}
class Redeem(): AbstractGroupClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Redeem::class.java)
class Redeem(): ConcreteClause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
// 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<Commands.Redeem>()
@ -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
}

View File

@ -447,24 +447,14 @@ class InterestRateSwap() : Contract {
fixingCalendar, index, indexSource, indexTenor)
}
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
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<State, UniqueIdentifier> {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
abstract class AbstractIRSClause : ConcreteClause<State, Commands, UniqueIdentifier>() {
// These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: List<CommonLeg>) {
requireThat {
@ -506,19 +496,18 @@ class InterestRateSwap() : Contract {
}
}
class Group : GroupClauseVerifier<State, UniqueIdentifier>() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.ERROR
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
// 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<AuthenticatedObject<CommandData>>): Set<CommandData> {
class Timestamped : ConcreteClause<ContractState, Commands, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Unit?): Set<Commands> {
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<Class<out CommandData>> = setOf(Commands.Agree::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: UniqueIdentifier): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Agree>()
val irs = outputs.filterIsInstance<State>().single()
requireThat {
@ -562,13 +551,13 @@ class InterestRateSwap() : Contract {
}
class Fix : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Refix::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: UniqueIdentifier): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Refix>()
val irs = outputs.filterIsInstance<State>().single()
val prevIrs = inputs.filterIsInstance<State>().single()
@ -607,13 +596,13 @@ class InterestRateSwap() : Contract {
}
class Pay : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Pay::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: UniqueIdentifier): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Pay>()
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<Class<out CommandData>> = setOf(Commands.Mature::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: UniqueIdentifier): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Mature>()
val irs = inputs.filterIsInstance<State>().single()
requireThat {

View File

@ -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<Currency, Cash.State>() {
class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
/**
* TODO:
* 1) hash should be of the contents, not the URI
@ -46,32 +45,30 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
* 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<State, Currency> = Clauses.ConserveAmount()
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Cash.Commands>()
override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
= commands.select<Cash.Commands>()
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State, Currency>(),
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
NoZeroSizedOutputs<State, Commands, Currency>(),
FirstComposition<State, Commands, Issued<Currency>>(
Issue(),
ConserveAmount())
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
}
class Issue : AbstractIssue<State, Currency>(
class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) }
) {
override val requiredCommands = setOf(Commands.Issue::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
class ConserveAmount : AbstractConserveAmount<State, Currency>()
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
}
/** A state representing a cash claim against some party */
@ -144,6 +141,9 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = 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.

View File

@ -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<Commodity, CommodityContract.State>() {
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
/**
* TODO:
* 1) hash should be of the contents, not the URI
@ -46,7 +47,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
/**
* The clauses for this contract are essentially:
@ -62,24 +63,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
* each group.
*/
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
/**
* 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<State, Commodity>(),
Issue(),
ConserveAmount()
)
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
NoZeroSizedOutputs<State, Commands, Commodity>(),
Issue(),
ConserveAmount())) {
/**
* Group commodity states by issuance definition (issuer and underlying commodity).
*/
@ -90,17 +77,17 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
/**
* Standard issue clause, specialised to match the commodity issue command.
*/
class Issue : AbstractIssue<State, Commodity>(
class Issue : AbstractIssue<State, Commands, Commodity>(
sum = { sumCommodities() },
sumOrZero = { sumCommoditiesOrZero(it) }
) {
override val requiredCommands = setOf(Commands.Issue::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
/**
* Standard clause for conserving the amount from input to output.
*/
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
}
/** A state representing a commodity claim against some party */
@ -150,9 +137,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
*/
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
}
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<CommodityContract.Commands>()
override fun verify(tx: TransactionForContract)
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
= commands.select<CommodityContract.Commands>()
/**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.

View File

@ -43,25 +43,27 @@ class Obligation<P> : 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<P>(), Clauses.Net<P>()),
Clauses.Group<P>())
interface Clauses {
/**
* Parent clause for clauses that operate on grouped states (those which are fungible).
*/
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State<P>, Terms<P>>(),
SetLifecycle<P>(),
VerifyLifecycle<P>(),
Settle<P>(),
Issue(),
ConserveAmount()
)
class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
AllComposition(
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
FirstComposition(
SetLifecycle<P>(),
AllComposition(
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
FirstComposition(
Settle<P>(),
Issue(),
ConserveAmount()
)
)
)
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
}
@ -69,58 +71,64 @@ class Obligation<P> : Contract {
/**
* Generic issuance clause
*/
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
/**
* Generic move/exit clause for fungible assets
*/
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Terms<P>>()
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
/**
* Clause for supporting netting of obligations.
*/
class Net<P> : NetClause<P>()
class Net<C: CommandData, P> : NetClause<C, P>() {
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
override fun toString(): String = "Net obligations"
override fun verify(tx: TransactionForContract, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
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<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
class SetLifecycle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
Obligation<P>().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<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
class Settle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
require(groupingKey != null)
val command = commands.requireSingleCommand<Commands.Settle<P>>()
val obligor = token.issuer.party
val template = token.product
val obligor = groupingKey!!.issuer.party
val template = groupingKey.product
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(token)
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
// Sum up all asset state objects that are moving and fulfil our requirements
@ -166,7 +174,7 @@ class Obligation<P> : Contract {
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
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<P> : 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<P> : Contract {
* any lifecycle change clause, which is the only clause that involve
* non-standard lifecycle states on input/output.
*/
class VerifyLifecycle<P> : SingleClause(), GroupClause<State<P>, Issued<Terms<P>>> {
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= verify(
tx.inputs.filterIsInstance<State<P>>(),
tx.outputs.filterIsInstance<State<P>>()
)
class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : ConcreteClause<S, C, T>() {
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData>
= verify(inputs, outputs)
fun verify(inputs: List<State<P>>,
outputs: List<State<P>>): Set<CommandData> {
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: T?): Set<C>
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
private fun verify(inputs: List<State<P>>,
outputs: List<State<P>>): Set<C> {
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<P> : 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<P> : Contract {
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
}
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Obligation.Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
Clauses.Net<Commands, P>(),
Clauses.Group<P>()
), tx.commands.select<Obligation.Commands>())
/**
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.

View File

@ -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<T : Any, S : FungibleAsset<T>> : Contract {
abstract val clauses: List<SingleClause>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
abstract val conserveClause: AbstractConserveAmount<S, T>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
abstract val conserveClause: AbstractConserveAmount<S, C, T>
/**
* Generate an transaction exiting assets from the ledger.

View File

@ -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<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, Issued<T>>() {
/**
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
*
@ -177,16 +169,18 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token")
val deposit = token.issuer
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token)
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
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<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
val deposit = groupingKey!!.issuer
val outputAmount: Amount<Issued<T>> = 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<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull()
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token)
val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
val amountExitingLedger: Amount<Issued<T>> = 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<S: FungibleAsset<T>, T: Any> : GroupClause
(inputAmount == outputAmount + amountExitingLedger)
}
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx))
.filter { it != null }
.requireNoNulls().toSet()
verifyMoveCommand<FungibleAsset.Commands.Move>(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"
}

View File

@ -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<in S: ContractState, T: Any>(
abstract class AbstractIssue<in S: ContractState, C: CommandData, T: Any>(
val sum: List<S>.() -> Amount<Issued<T>>,
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
) : GroupClause<S, Issued<T>> {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
) : ConcreteClause<S, C, Issued<T>>() {
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null)
// TODO: Take in matched commands as a parameter
val issueCommand = commands.requireSingleCommand<IssueCommand>()
@ -40,8 +37,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
// 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<in S: ContractState, T: Any>(
"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)
}
}

View File

@ -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<P>(
* Clause for netting contract states. Currently only supports obligation contract.
*/
// TODO: Make this usable for any nettable contract states
open class NetClause<P> : SingleClause() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
open class NetClause<C: CommandData, P> : ConcreteClause<ContractState, C, Unit>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
@Suppress("ConvertLambdaToReference")
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
val groups = when (command.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> 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)
}
/**

View File

@ -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<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<CommandData>>
get() = emptySet()
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, Issued<T>>() {
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
return emptySet()
}
override fun toString(): String = "No zero sized outputs"
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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<S: LinearState>(val stateClass: Class<S>) : SingleClause() {
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
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<S : LinearState>(val stateClass: Class<S>) : ConcreteClause<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>,
groupingKey: Unit?): Set<CommandData> {
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()
}
}

View File

@ -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<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses = ArrayList<Clause<S, C, K>>()
init {
clauses.add(firstClause)
clauses.addAll(remainingClauses)
}
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
clauses.forEach { clause ->
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
}
return clauses
}
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: K?): Set<C> {
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
clause.verify(tx, inputs, outputs, commands, groupingKey)
}
}
override fun toString() = "All: $clauses.toList()"
}

View File

@ -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<in S : ContractState, C : CommandData, in K : Any>(vararg val rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses: List<Clause<S, C, K>> = rawClauses.asList()
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = clauses.filter { it.matches(commands) }
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
clause.verify(tx, inputs, outputs, commands, groupingKey)
}
}
override fun toString(): String = "Or: ${clauses.toList()}"
}

View File

@ -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<in S : ContractState, C: CommandData, in K : Any> {
companion object {
val log = loggerFor<Clause<*, *, *>>()
}
/** Determine whether this clause runs or not */
val requiredCommands: Set<Class<out CommandData>>
/** 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<AuthenticatedObject<C>>): List<Clause<*, *, *>>
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<AuthenticatedObject<CommandData>>): Set<CommandData>
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: K?): Set<C>
}
/**
* 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<Class<out CommandData>> = emptySet()
fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
return if (requiredCommands.isEmpty())
true
else
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
}

View File

@ -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<SingleClause>,
commands: Collection<AuthenticatedObject<CommandData>>) {
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 <C: CommandData> verifyClause(tx: TransactionForContract,
clause: Clause<ContractState, C, Unit>,
commands: List<AuthenticatedObject<C>>) {
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) }
}

View File

@ -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<in S : ContractState, C: CommandData, in K : Any>: Clause<S, C, K> {
/** List of clauses under this composite clause */
abstract val clauses: List<Clause<S, C, K>>
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
/** Determine which clauses are matched by the supplied commands */
abstract fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>>
}

View File

@ -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<in S : ContractState, C: CommandData, in T : Any>: Clause<S, C, T> {
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= listOf(this)
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
}

View File

@ -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<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
companion object {
val logger = loggerFor<FirstComposition<*, *, *>>()
}
override val clauses = ArrayList<Clause<S, C, K>>()
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })
init {
clauses.add(firstClause)
clauses.addAll(remainingClauses)
}
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C>
= matchedClauses(commands).single().verify(tx, inputs, outputs, commands, groupingKey)
override fun toString() = "First: ${clauses.toList()}"
}

View File

@ -6,77 +6,24 @@ import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract
import java.util.*
interface GroupVerify<in S, in T : Any> {
/**
*
* @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<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: T): Set<CommandData>
}
abstract class GroupClauseVerifier<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>) : ConcreteClause<ContractState, C, Unit>() {
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, K>>
interface GroupClause<in S : ContractState, in T : Any> : Clause, GroupVerify<S, T>
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= clause.getExecutionPath(commands)
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause() {
abstract val clauses: List<GroupClause<S, T>>
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
val groups = groupStates(tx)
val matchedCommands = HashSet<CommandData>()
val unmatchedCommands = ArrayList(commands.map { it.value })
val matchedCommands = HashSet<C>()
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<AuthenticatedObject<CommandData>>,
inputs: List<S>,
outputs: List<S>,
token: T,
tx: TransactionForContract,
unmatchedCommands: List<CommandData>): Set<CommandData> {
val matchedCommands = HashSet<CommandData>()
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
}
}

View File

@ -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<Class<out CommandData>>
get() = clause.requiredCommands
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val consumed = HashSet(preclause.verify(tx, commands))
consumed.addAll(clause.verify(tx, commands))
return consumed
}
override fun toString(): String = "Interceptor clause [${clause}]"
}

View File

@ -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<ContractState, CommandData, Unit> = LinearState.ClauseVerifier(State::class.java)
override fun verify(tx: TransactionForContract) = verifyClause(tx,
clause,
emptyList())
class State(
override val linearId: UniqueIdentifier = UniqueIdentifier(),

View File

@ -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<AuthenticatedObject<CommandData>>())
// 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<IllegalStateException> { verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>()) }
}
}

View File

@ -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<AuthenticatedObject<CommandData>>())
// 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<AuthenticatedObject<CommandData>>())
// 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<AuthenticatedObject<CommandData>>())
// Check that we've run the verify() function of neither clause
assertEquals(0, counter.get())
}
}

View File

@ -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<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> {
counter?.incrementAndGet()
return emptySet()
}
}
/** A clause that can never be matched */
internal fun unmatchedClause(counter: AtomicInteger? = null) = object : ConcreteClause<ContractState, CommandData, Unit>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(object: CommandData {}.javaClass)
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> {
counter?.incrementAndGet()
return emptySet()
}
}

View File

@ -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<SingleClause>(), emptyList<AuthenticatedObject<CommandData>>())
}
/** 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<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
}
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>())
}
/** 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<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
}
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
// This would error if it wasn't matched
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>())
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
}
@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<AuthenticatedObject<CommandData>>): Set<CommandData>
= emptySet()
val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = 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<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
}
/** Check triggering of error if matched */
@Test
fun errorMatched() {
val clause = object : SingleClause() {
override val requiredCommands: Set<Class<out CommandData>>
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<AuthenticatedObject<CommandData>>): Set<CommandData>
= commands.select<DummyContract.Commands.Create>().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<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
}
/** Check triggering of error if unmatched */
@Test
fun errorUnmatched() {
val clause = object : SingleClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(DummyContract.Commands.Create::class.java)
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
}
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), emptyList()) }
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, listOf(command)) }
}
}

View File

@ -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<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
.. sourcecode:: java
@ -53,53 +55,40 @@ brevity:
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
}
@Override
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
.stream()
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
.collect(Collectors.toList());
}
@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<State, Issued<Terms>> {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
class Move: ConcreteClause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>()
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<State, State> {
@Override
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.CONTINUE;
}
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
}
class Move extends ConcreteClause<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Move.class);
}
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.Move.class);
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Move> 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<State, Issued<Terms>>() {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val clauses: List<GroupClause<State, Issued<Terms>>>
get() = listOf(
Clause.Redeem(),
Clause.Move(),
Clause.Issue()
)
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
AnyComposition(
Redeem(),
Move(),
Issue())) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
}
.. sourcecode:: java
public class Group extends GroupClauseVerifier<State, State> {
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
class Group extends GroupClauseVerifier<State, Commands, State> {
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<com.r3corda.core.contracts.clauses.GroupClause<State, State>> getClauses() {
final List<GroupClause<State, State>> clauses = new ArrayList<>();
clauses.add(new Clause.Redeem());
clauses.add(new Clause.Move());
clauses.add(new Clause.Issue());
return clauses;
}
@Override
public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
public List<InOutGroup<State, State>> 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<SingleClause>
get() = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
.. sourcecode:: java
@Override
public List<SingleClause> getClauses() {
return Collections.singletonList(new Clause.Group());
}
@Override
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
.stream()
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
.collect(Collectors.toList());
}
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.
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.