Merged in rnicoll-clause-composition (pull request #294)

Rework clauses to use composition
This commit is contained in:
Ross Nicoll 2016-08-26 17:48:04 +01:00
commit 478e4bb75c
29 changed files with 602 additions and 654 deletions

View File

@ -125,44 +125,14 @@ public class JavaCommercialPaper implements Contract {
} }
} }
public interface Clause { public interface Clauses {
abstract class AbstractGroup implements GroupClause<State, State> { class Group extends GroupClauseVerifier<State, Commands, State> {
@NotNull public Group() {
@Override super(new AnyComposition<>(
public MatchBehaviour getIfNotMatched() { new Clauses.Redeem(),
return MatchBehaviour.CONTINUE; new Clauses.Move(),
} new Clauses.Issue()
));
@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;
} }
@NotNull @NotNull
@ -172,7 +142,7 @@ public class JavaCommercialPaper implements Contract {
} }
} }
class Move extends AbstractGroup { class Move extends ConcreteClause<State, Commands, State> {
@NotNull @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -181,11 +151,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
// There should be only a single input due to aggregation above // There should be only a single input due to aggregation above
State input = single(inputs); 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 @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -212,11 +182,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
// There should be only a single input due to aggregation above // 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 @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -254,11 +224,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
State output = single(outputs); State output = single(outputs);
Timestamp timestampCommand = tx.getTimestamp(); Timestamp timestampCommand = tx.getTimestamp();
@ -298,16 +268,17 @@ public class JavaCommercialPaper implements Contract {
} }
@NotNull @NotNull
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) { private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands() return tx.getCommands()
.stream() .stream()
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands) .filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { 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 @NotNull

View File

@ -1,14 +1,12 @@
package com.r3corda.contracts package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.*
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.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
@ -52,10 +50,7 @@ class CommercialPaper : Contract {
val maturityDate: Instant val maturityDate: Instant
) )
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>> override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
data class State( data class State(
val issuance: PartyAndReference, val issuance: PartyAndReference,
@ -82,25 +77,16 @@ class CommercialPaper : Contract {
} }
interface Clauses { interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Terms>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
override val ifNotMatched = MatchBehaviour.ERROR AnyComposition(
override val ifMatched = MatchBehaviour.END Redeem(),
override val clauses = listOf( Move(),
Redeem(), Issue())) {
Move(),
Issue()
)
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token } = tx.groupStates<State, Issued<Terms>> { it.token }
} }
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> { class Issue : AbstractIssue<State, Commands, Terms>(
override val ifNotMatched = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
}
class Issue : AbstractIssue<State, Terms>(
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() }, { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) { { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
@ -108,8 +94,8 @@ class CommercialPaper : Contract {
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { token: Issued<Terms>?): Set<Commands> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, token) val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
commands.requireSingleCommand<Commands.Issue>() commands.requireSingleCommand<Commands.Issue>()
val timestamp = tx.timestamp 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 val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>() val command = commands.requireSingleCommand<Commands.Move>()
val input = inputs.single() val input = inputs.single()
requireThat { requireThat {
@ -141,15 +127,14 @@ class CommercialPaper : Contract {
} }
} }
class Redeem(): AbstractGroupClause() { class Redeem(): ConcreteClause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
get() = setOf(Commands.Redeem::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
// before requiring a single command // before requiring a single command
val command = commands.requireSingleCommand<Commands.Redeem>() val command = commands.requireSingleCommand<Commands.Redeem>()
@ -172,7 +157,7 @@ class CommercialPaper : Contract {
} }
interface Commands : CommandData { interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
class Redeem : TypeOnlyCommandData(), Commands class Redeem : TypeOnlyCommandData(), Commands
data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands
} }

View File

@ -447,24 +447,14 @@ class InterestRateSwap() : Contract {
fixingCalendar, index, indexSource, indexTenor) fixingCalendar, index, indexSource, indexTenor)
} }
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) { interface Clauses {
verifyClauses(tx,
listOf(Clause.Timestamped(), Clause.Group(), LinearState.ClauseVerifier(State::class.java)),
extractCommands(tx))
}
interface Clause {
/** /**
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
* helper functions for the clauses. * helper functions for the clauses.
*/ */
abstract class AbstractIRSClause : GroupClause<State, UniqueIdentifier> { abstract class AbstractIRSClause : ConcreteClause<State, Commands, UniqueIdentifier>() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
// These functions may make more sense to use for basket types, but for now let's leave them here // These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: List<CommonLeg>) { fun checkLegDates(legs: List<CommonLeg>) {
requireThat { requireThat {
@ -506,19 +496,18 @@ class InterestRateSwap() : Contract {
} }
} }
class Group : GroupClauseVerifier<State, UniqueIdentifier>() { class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.ERROR
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
// Group by Trade ID for in / out states // Group by Trade ID for in / out states
= tx.groupStates() { state -> state.linearId } = tx.groupStates() { state -> state.linearId }
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
} }
class Timestamped : SingleClause() { class Timestamped : ConcreteClause<ContractState, Commands, Unit>() {
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<Commands>>,
groupingKey: Unit?): Set<Commands> {
require(tx.timestamp?.midpoint != null) { "must be timestamped" } require(tx.timestamp?.midpoint != null) { "must be timestamped" }
// We return an empty set because we don't process any commands // We return an empty set because we don't process any commands
return emptySet() return emptySet()
@ -526,13 +515,13 @@ class InterestRateSwap() : Contract {
} }
class Agree : AbstractIRSClause() { 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, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: UniqueIdentifier): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Agree>() val command = tx.commands.requireSingleCommand<Commands.Agree>()
val irs = outputs.filterIsInstance<State>().single() val irs = outputs.filterIsInstance<State>().single()
requireThat { requireThat {
@ -562,13 +551,13 @@ class InterestRateSwap() : Contract {
} }
class Fix : AbstractIRSClause() { 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, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: UniqueIdentifier): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Refix>() val command = tx.commands.requireSingleCommand<Commands.Refix>()
val irs = outputs.filterIsInstance<State>().single() val irs = outputs.filterIsInstance<State>().single()
val prevIrs = inputs.filterIsInstance<State>().single() val prevIrs = inputs.filterIsInstance<State>().single()
@ -607,13 +596,13 @@ class InterestRateSwap() : Contract {
} }
class Pay : AbstractIRSClause() { 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, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: UniqueIdentifier): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Pay>() val command = tx.commands.requireSingleCommand<Commands.Pay>()
requireThat { requireThat {
"Payments not supported / verifiable yet" by false "Payments not supported / verifiable yet" by false
@ -623,13 +612,13 @@ class InterestRateSwap() : Contract {
} }
class Mature : AbstractIRSClause() { 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, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: UniqueIdentifier): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Mature>() val command = tx.commands.requireSingleCommand<Commands.Mature>()
val irs = inputs.filterIsInstance<State>().single() val irs = inputs.filterIsInstance<State>().single()
requireThat { requireThat {

View File

@ -4,8 +4,7 @@ import com.r3corda.contracts.clause.AbstractConserveAmount
import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.contracts.clause.NoZeroSizedOutputs
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClauseVerifier import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.crypto.* import com.r3corda.core.crypto.*
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.utilities.Emoji 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 * 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. * 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: * TODO:
* 1) hash should be of the contents, not the URI * 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. * 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 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 conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
override val clauses = listOf(Clauses.Group()) override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>> = commands.select<Cash.Commands>()
= tx.commands.select<Cash.Commands>()
interface Clauses { interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Currency>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
override val ifMatched: MatchBehaviour = MatchBehaviour.END NoZeroSizedOutputs<State, Commands, Currency>(),
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR FirstComposition<State, Commands, Issued<Currency>>(
override val clauses = listOf(
NoZeroSizedOutputs<State, Currency>(),
Issue(), Issue(),
ConserveAmount()) ConserveAmount())
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef } = tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
} }
class Issue : AbstractIssue<State, Currency>( class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() }, sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) } 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 */ /** 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 generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateIssueCommand() = Commands.Issue() override fun generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move() override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: TransactionForContract)
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
} }
// Small DSL extensions. // Small DSL extensions.

View File

@ -5,7 +5,8 @@ import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.contracts.clause.NoZeroSizedOutputs
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClauseVerifier 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.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.crypto.newSecureRandom
@ -33,7 +34,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
* in future. * in future.
*/ */
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. // 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: * TODO:
* 1) hash should be of the contents, not the URI * 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 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: * 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 * Grouping clause to extract input and output states into matched groups and then run a set of clauses over
* each group. * each group.
*/ */
class Group : GroupClauseVerifier<State, Issued<Commodity>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
/** NoZeroSizedOutputs<State, Commands, Commodity>(),
* The group clause does not depend on any commands being present, so something has gone terribly wrong if Issue(),
* it doesn't match. ConserveAmount())) {
*/
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()
)
/** /**
* Group commodity states by issuance definition (issuer and underlying commodity). * 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. * Standard issue clause, specialised to match the commodity issue command.
*/ */
class Issue : AbstractIssue<State, Commodity>( class Issue : AbstractIssue<State, Commands, Commodity>(
sum = { sumCommodities() }, sum = { sumCommodities() },
sumOrZero = { sumCommoditiesOrZero(it) } 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. * 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 */ /** 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> data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
} }
override val clauses = listOf(Clauses.Group()) override fun verify(tx: TransactionForContract)
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>> = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
= tx.commands.select<CommodityContract.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. * 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. * that is inconsistent with the legal contract.
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") override val 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 { interface Clauses {
/** /**
* Parent clause for clauses that operate on grouped states (those which are fungible). * Parent clause for clauses that operate on grouped states (those which are fungible).
*/ */
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() { class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
override val ifMatched: MatchBehaviour = MatchBehaviour.END AllComposition(
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
override val clauses = listOf( FirstComposition(
NoZeroSizedOutputs<State<P>, Terms<P>>(), SetLifecycle<P>(),
SetLifecycle<P>(), AllComposition(
VerifyLifecycle<P>(), VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
Settle<P>(), FirstComposition(
Issue(), Settle<P>(),
ConserveAmount() Issue(),
) ConserveAmount()
)
)
)
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef } = tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
} }
@ -69,58 +71,64 @@ class Obligation<P> : Contract {
/** /**
* Generic issuance clause * Generic issuance clause
*/ */
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) { class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
} }
/** /**
* Generic move/exit clause for fungible assets * 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. * 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. * Obligation-specific clause for changing the lifecycle of one or more states.
*/ */
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> { class SetLifecycle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands = setOf(Commands.SetLifecycle::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<State<P>>,
outputs: List<State<P>>, outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms<P>>): Set<CommandData> { groupingKey: Issued<Terms<P>>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.SetLifecycle>() val command = commands.requireSingleCommand<Commands.SetLifecycle>()
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command) Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
return setOf(command.value) return setOf(command.value)
} }
override fun toString(): String = "Set obligation lifecycle"
} }
/** /**
* Obligation-specific clause for settling an outstanding obligation by witnessing * Obligation-specific clause for settling an outstanding obligation by witnessing
* change of ownership of other states to fulfil * change of ownership of other states to fulfil
*/ */
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> { class Settle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands = setOf(Commands.Settle::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<State<P>>,
outputs: List<State<P>>, outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms<P>>): Set<CommandData> { groupingKey: Issued<Terms<P>>?): Set<Commands> {
require(groupingKey != null)
val command = commands.requireSingleCommand<Commands.Settle<P>>() val command = commands.requireSingleCommand<Commands.Settle<P>>()
val obligor = token.issuer.party val obligor = groupingKey!!.issuer.party
val template = token.product 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 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 // 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 }) { for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>() val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
if (settled != null) { 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" } require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
totalPenniesSettled += settled.quantity totalPenniesSettled += settled.quantity
} }
@ -185,7 +193,7 @@ class Obligation<P> : Contract {
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners) "signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"at obligor ${obligor.name} the obligations after settlement balance" by "at obligor ${obligor.name} the obligations after settlement balance" by
(inputAmount == outputAmount + Amount(totalPenniesSettled, token)) (inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
} }
return setOf(command.value) return setOf(command.value)
} }
@ -197,22 +205,15 @@ class Obligation<P> : Contract {
* any lifecycle change clause, which is the only clause that involve * any lifecycle change clause, which is the only clause that involve
* non-standard lifecycle states on input/output. * non-standard lifecycle states on input/output.
*/ */
class VerifyLifecycle<P> : SingleClause(), GroupClause<State<P>, Issued<Terms<P>>> { class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : ConcreteClause<S, C, T>() {
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= verify(
tx.inputs.filterIsInstance<State<P>>(),
tx.outputs.filterIsInstance<State<P>>()
)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<S>,
outputs: List<State<P>>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<Terms<P>>): Set<CommandData> groupingKey: T?): Set<C>
= verify(inputs, outputs) = verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
private fun verify(inputs: List<State<P>>,
fun verify(inputs: List<State<P>>, outputs: List<State<P>>): Set<C> {
outputs: List<State<P>>): Set<CommandData> {
requireThat { requireThat {
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } "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 } "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 * 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. * 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. * 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>> 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>> override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
= tx.commands.select<Obligation.Commands>() Clauses.Net<Commands, P>(),
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) Clauses.Group<P>()
), tx.commands.select<Obligation.Commands>())
/** /**
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes. * 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.contracts.clause.AbstractConserveAmount
import com.r3corda.core.contracts.* 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 com.r3corda.core.crypto.Party
import java.security.PublicKey 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 * 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. * the issuer/depositRefs and just examine the amount fields.
*/ */
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract { abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
abstract val clauses: List<SingleClause> abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> abstract val conserveClause: AbstractConserveAmount<S, C, T>
abstract val conserveClause: AbstractConserveAmount<S, T>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
/** /**
* Generate an transaction exiting assets from the ledger. * 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.sumFungibleOrNull
import com.r3corda.contracts.asset.sumFungibleOrZero import com.r3corda.contracts.asset.sumFungibleOrZero
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.ConcreteClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import java.security.PublicKey import java.security.PublicKey
import java.util.* 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; * Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
* errors on no-match, ends on match. * errors on no-match, ends on match.
*/ */
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> { abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, 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()
/** /**
* Gather assets from the given list of states, sufficient to match or exceed the given amount. * 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, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token") require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
val deposit = token.issuer val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token) 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. // 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 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 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, token) val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, groupingKey)
requireThat { requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } "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) (inputAmount == outputAmount + amountExitingLedger)
} }
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx)) verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
.filter { it != null }
.requireNoNulls().toSet() // 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 package com.r3corda.contracts.clause
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.ConcreteClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
/** /**
* Standard issue clause for contracts that issue fungible assets. * 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 * @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. * 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 sum: List<S>.() -> Amount<Issued<T>>,
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>> val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
) : GroupClause<S, Issued<T>> { ) : ConcreteClause<S, C, Issued<T>>() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null)
// TODO: Take in matched commands as a parameter // TODO: Take in matched commands as a parameter
val issueCommand = commands.requireSingleCommand<IssueCommand>() 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). // 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. // The grouping already ensures that all outputs have the same deposit reference and token.
val issuer = token.issuer.party val issuer = groupingKey!!.issuer.party
val inputAmount = inputs.sumOrZero(token) val inputAmount = inputs.sumOrZero(groupingKey)
val outputAmount = outputs.sum() val outputAmount = outputs.sum()
requireThat { requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L) "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) "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.extractAmountsDue
import com.r3corda.contracts.asset.sumAmountsDue import com.r3corda.contracts.asset.sumAmountsDue
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.MatchBehaviour import com.r3corda.core.contracts.clauses.ConcreteClause
import com.r3corda.core.contracts.clauses.SingleClause
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -43,22 +42,24 @@ data class MultilateralNetState<P>(
* Clause for netting contract states. Currently only supports obligation contract. * Clause for netting contract states. Currently only supports obligation contract.
*/ */
// TODO: Make this usable for any nettable contract states // TODO: Make this usable for any nettable contract states
open class NetClause<P> : SingleClause() { open class NetClause<C: CommandData, P> : ConcreteClause<ContractState, C, Unit>() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
@Suppress("ConvertLambdaToReference") @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 command = commands.requireSingleCommand<Obligation.Commands.Net>()
val groups = when (command.value.type) { val groups = when (command.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState } NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState } NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
} }
for ((inputs, outputs, key) in groups) { for ((groupInputs, groupOutputs, key) in groups) {
verifyNetCommand(inputs, outputs, command, key) 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.contracts.asset.FungibleAsset
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.ConcreteClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
/** /**
* Clause for fungible asset contracts, which enforces that no output state should have * Clause for fungible asset contracts, which enforces that no output state should have
* a balance of zero. * a balance of zero.
*/ */
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> { open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, Issued<T>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<CommandData>>
get() = emptySet()
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
requireThat { requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
} }
return emptySet() return emptySet()
} }
override fun toString(): String = "No zero sized outputs"
} }

View File

@ -172,11 +172,11 @@ class CashTests {
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } 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 { tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) } 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() this.verifies()
} }

View File

@ -180,11 +180,11 @@ class ObligationTests {
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() } 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 { tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) } 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() this.verifies()
} }

View File

@ -1,7 +1,7 @@
package com.r3corda.core.contracts package com.r3corda.core.contracts
import com.r3corda.core.contracts.clauses.MatchBehaviour import com.r3corda.core.contracts.clauses.ConcreteClause
import com.r3corda.core.contracts.clauses.SingleClause import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
@ -218,14 +218,18 @@ interface LinearState: ContractState {
/** /**
* Standard clause to verify the LinearState safety properties. * Standard clause to verify the LinearState safety properties.
*/ */
class ClauseVerifier<S: LinearState>(val stateClass: Class<S>) : SingleClause() { class ClauseVerifier<S : LinearState>(val stateClass: Class<S>) : ConcreteClause<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> { override fun verify(tx: TransactionForContract,
val inputs = tx.inputs.filterIsInstance(stateClass) inputs: List<ContractState>,
val inputIds = inputs.map { it.linearId }.distinct() outputs: List<ContractState>,
require(inputIds.count() == inputs.count()) { "LinearStates cannot be merged" } commands: List<AuthenticatedObject<CommandData>>,
val outputs = tx.outputs.filterIsInstance(stateClass) groupingKey: Unit?): Set<CommandData> {
val outputIds = outputs.map { it.linearId }.distinct() val filteredInputs = inputs.filterIsInstance(stateClass)
require(outputIds.count() == outputs.count()) { "LinearStates cannot be split" } 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() 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.AuthenticatedObject
import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract 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 interface Clause<in S : ContractState, C: CommandData, in K : Any> {
// "or", "and", "single" etc. composition of sets of clauses. companion object {
interface Clause { val log = loggerFor<Clause<*, *, *>>()
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */ }
/** Determine whether this clause runs or not */
val requiredCommands: Set<Class<out CommandData>> 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, * Determine the subclauses which will be verified as a result of verifying this clause.
END, */
ERROR 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 * 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 * would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
* if any matched. * 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 * @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 * 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 * 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) @Throws(IllegalStateException::class)
fun verify(tx: TransactionForContract, 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 * Determine if the given list of commands matches the required commands for a clause to trigger.
* if not matched.
*/ */
abstract class SingleClause : Clause, SingleVerify { fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE return if (requiredCommands.isEmpty())
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR true
override val requiredCommands: Set<Class<out CommandData>> = emptySet() else
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
} }

View File

@ -2,9 +2,7 @@
package com.r3corda.core.contracts.clauses package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.* 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. * 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 * @param commands commands extracted from the transaction, which are relevant to the
* clauses. * clauses.
*/ */
fun verifyClauses(tx: TransactionForContract, fun <C: CommandData> verifyClause(tx: TransactionForContract,
clauses: List<SingleClause>, clause: Clause<ContractState, C, Unit>,
commands: Collection<AuthenticatedObject<CommandData>>) { commands: List<AuthenticatedObject<C>>) {
val unmatchedCommands = ArrayList(commands.map { it.value }) if (Clause.log.isTraceEnabled) {
clause.getExecutionPath(commands).forEach {
verify@ for (clause in clauses) { Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
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
} }
} }
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 com.r3corda.core.contracts.TransactionForContract
import java.util.* import java.util.*
interface GroupVerify<in S, in T : Any> { 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>>
*
* @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>
}
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() { override fun verify(tx: TransactionForContract,
abstract val clauses: List<GroupClause<S, T>> inputs: List<ContractState>,
override val requiredCommands: Set<Class<out CommandData>> outputs: List<ContractState>,
get() = emptySet() commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val groups = groupStates(tx) val groups = groupStates(tx)
val matchedCommands = HashSet<CommandData>() val matchedCommands = HashSet<C>()
val unmatchedCommands = ArrayList(commands.map { it.value })
for ((inputs, outputs, token) in groups) { for ((groupInputs, groupOutputs, groupToken) in groups) {
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands) matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
matchedCommands.addAll(temp)
unmatchedCommands.removeAll(temp)
} }
return matchedCommands 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 package com.r3corda.core.testing
import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.LinearState import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.contracts.UniqueIdentifier import com.r3corda.core.contracts.clauses.verifyClause
import com.r3corda.core.contracts.TransactionForContract
import com.r3corda.core.contracts.clauses.verifyClauses
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey import java.security.PublicKey
class DummyLinearContract: Contract { class DummyLinearContract: Contract {
override val legalContractReference: SecureHash = SecureHash.sha256("Test") override val legalContractReference: SecureHash = SecureHash.sha256("Test")
override fun verify(tx: TransactionForContract) { val clause: Clause<ContractState, CommandData, Unit> = LinearState.ClauseVerifier(State::class.java)
verifyClauses(tx, override fun verify(tx: TransactionForContract) = verifyClause(tx,
listOf(LinearState.ClauseVerifier(State::class.java)), clause,
emptyList()) emptyList())
}
class State( class State(
override val linearId: UniqueIdentifier = UniqueIdentifier(), 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. * Tests for the clause verifier.
*/ */
class VerifyClausesTests { 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 */ /** Very simple check that the function doesn't error when given any clause */
@Test @Test
fun minimal() { fun minimal() {
val clause = object : SingleClause() { val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
override val ifNotMatched: MatchBehaviour override fun verify(tx: TransactionForContract,
get() = MatchBehaviour.CONTINUE inputs: List<ContractState>,
outputs: List<ContractState>,
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet() commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
} }
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256()) val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>()) verifyClause(tx, 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>>())
} }
@Test @Test
fun errorSuperfluousCommands() { fun errorSuperfluousCommands() {
val clause = object : SingleClause() { val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
override val ifMatched: MatchBehaviour override fun verify(tx: TransactionForContract,
get() = MatchBehaviour.ERROR inputs: List<ContractState>,
override val ifNotMatched: MatchBehaviour outputs: List<ContractState>,
get() = MatchBehaviour.CONTINUE commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= emptySet()
} }
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create()) val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256()) 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 // The clause is matched, but doesn't mark the command as consumed, so this should error
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) } assertFailsWith<IllegalStateException> { verifyClause(tx, 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()) }
} }
} }

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 This tutorial will take you through restructuring the commercial paper contract to use clauses. You should have
already completed ":doc:`tutorial-contract`". already completed ":doc:`tutorial-contract`".
Clauses are essentially micro-contracts which contain independent verification logic, and are composed together to form Clauses are essentially micro-contracts which contain independent verification logic, and can be logically composed
a contract. With appropriate design, they can be made to be reusable, for example issuing contract state objects is together to form a contract. Clauses are designed to enable re-use of common logic, for example issuing state objects
generally the same for all fungible contracts, so a single issuance clause can be shared. This cuts down on scope for is generally the same for all fungible contracts, so a common issuance clause can be inherited for each contract's
error, and improves consistency of behaviour. 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. Clauses can be composed of subclauses, for example the ``AllClause`` or ``AnyClause`` clauses take list of clauses
In the case of commercial paper, we have a ``Group`` outermost clause, which will contain the ``Issue``, ``Move`` and that they delegate to. Clauses can also change the scope of states and commands being verified, for example grouping
``Redeem`` clauses. The result is a contract that looks something like this: 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: 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. 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 Commercial paper class
---------------------- ----------------------
To use the clause verification logic, the contract needs to call the ``verifyClauses()`` function, passing in the transaction, To use the clause verification logic, the contract needs to call the ``verifyClause`` function, passing in the
a list of clauses to verify, and a collection of commands the clauses are expected to handle all of. This list of 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 ``verifyClauses()`` checks that none of the commands are left unprocessed at the end, and 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 following examples are trimmed to the modified class definition and added elements, for raises an error if they are. The top level clause would normally be a composite clause (such as ``AnyComposition``,
brevity: ``AllComposition``, etc.) which contains further clauses. The following examples are trimmed to the modified class
definition and added elements, for brevity:
.. container:: codeset .. container:: codeset
@ -40,10 +45,7 @@ brevity:
class CommercialPaper : Contract { class CommercialPaper : Contract {
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>> override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
.. sourcecode:: java .. sourcecode:: java
@ -53,53 +55,40 @@ brevity:
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); 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 @Override
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { 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 Clauses
------- -------
We'll tackle the inner clauses that contain the bulk of the verification logic, first, and the clause which handles 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 grouping of input/output states later. The clauses must implement the ``Clause`` interface, which defines
the verify() function, and properties (``ifMatched``, ``ifNotMatched`` and ``requiredCommands``) defining how the clause the ``verify`` function, and the ``requiredCommands`` property used to determine the conditions under which a clause
is processed. These properties specify the command(s) which must be present in order for the clause to be matched, is triggered. Normally clauses would extend ``ConcreteClause`` which provides defaults suitable for a clause which
and what to do after processing the clause depending on whether it was matched or not. verifies transactions, rather than delegating to other clauses.
The ``verify()`` functions defined in the ``SingleClause`` and ``GroupClause`` interfaces is similar to the conventional The ``verify`` function defined in the ``Clause`` interface is similar to the conventional ``Contract`` verification
``Contract`` verification function, although it adds new parameters and returns the set of commands which it has processed. function, although it adds new parameters and returns the set of commands which it has processed. Normally this returned
Normally this returned set is identical to the commands matched in order to trigger the clause, however in some cases the set is identical to the ``requiredCommands`` used to trigger the clause, however in some cases the clause may process
clause may process optional commands which it needs to report that it has handled, or may by designed to only process further optional commands which it needs to report that it has handled.
the first (or otherwise) matched command.
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 .. container:: codeset
.. sourcecode:: kotlin .. sourcecode:: kotlin
class Move: GroupClause<State, Issued<Terms>> { class Move: ConcreteClause<State, Commands, Issued<Terms>>() {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Move::class.java) get() = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>() val command = commands.requireSingleCommand<Commands.Move>()
val input = inputs.single() val input = inputs.single()
requireThat { requireThat {
@ -114,140 +103,79 @@ The Move clause for the commercial paper contract is relatively simple, so lets
.. sourcecode:: java .. sourcecode:: java
public class Move implements GroupClause<State, State> { class Move extends ConcreteClause<State, Commands, State> {
@Override @NotNull
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.CONTINUE;
}
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
}
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Move.class); return Collections.singleton(Commands.Move.class);
} }
@NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.Move.class); AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
// There should be only a single input due to aggregation above // There should be only a single input due to aggregation above
State input = single(inputs); State input = single(inputs);
requireThat(require -> { if (!cmd.getSigners().contains(input.getOwner()))
require.by("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner())); throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
require.by("the state is propagated", outputs.size() == 1);
return Unit.INSTANCE; // 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 // 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. // the input ignoring the owner field due to the grouping.
return Collections.singleton(cmd.getValue()); 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 Group Clause
------------ ------------
We need to wrap the move clause (as well as the issue and redeem clauses - see the relevant contract code for their 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 full specifications) in an outer clause that understands how to group contract states and objects. For this we extend
group input/output states, as well as the clauses to run on each group. 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 .. container:: codeset
.. sourcecode:: kotlin .. sourcecode:: kotlin
class Group : GroupClauseVerifier<State, Issued<Terms>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
override val ifNotMatched: MatchBehaviour AnyComposition(
get() = MatchBehaviour.ERROR Redeem(),
override val ifMatched: MatchBehaviour Move(),
get() = MatchBehaviour.END Issue())) {
override val clauses: List<GroupClause<State, Issued<Terms>>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
get() = listOf(
Clause.Redeem(),
Clause.Move(),
Clause.Issue()
)
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token } = tx.groupStates<State, Issued<Terms>> { it.token }
} }
.. sourcecode:: java .. sourcecode:: java
public class Group extends GroupClauseVerifier<State, State> { class Group extends GroupClauseVerifier<State, Commands, State> {
@Override public Group() {
public MatchBehaviour getIfMatched() { super(new AnyComposition<>(
return MatchBehaviour.END; new Clauses.Redeem(),
new Clauses.Move(),
new Clauses.Issue()
));
} }
@NotNull
@Override @Override
public MatchBehaviour getIfNotMatched() { public List<InOutGroup<State, State>> groupStates(@NotNull TransactionForContract tx) {
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) {
return tx.groupStates(State.class, State::withoutOwner); 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 For the ``CommercialPaper`` contract, this is the top level clause for the contract, and is passed directly into
also implement the ``extractCommands()`` function, which filters commands on the transaction down to the set the ``verifyClause`` (see the example code at the top of this tutorial).
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());
}
Summary Summary
------- -------
@ -255,4 +183,12 @@ Summary
In summary the top level contract ``CommercialPaper`` specifies a single grouping clause of type 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 ``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`` (``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.