mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
Rework clauses to use composition
Rework clauses so that rather than defining match/no-match behaviour themselves, they are now composed by nesting them within clauses that understand how to match their child clauses. This unifies a lot of the structure of clauses and removes corner cases needed for the first design, as well as moving towards a model which is easier to prove.
This commit is contained in:
parent
d6c595da37
commit
13b040ecd6
@ -125,44 +125,14 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
public interface Clause {
|
||||
abstract class AbstractGroup implements GroupClause<State, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.CONTINUE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
}
|
||||
|
||||
class Group extends GroupClauseVerifier<State, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.ERROR;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<GroupClause<State, State>> getClauses() {
|
||||
final List<GroupClause<State, State>> clauses = new ArrayList<>();
|
||||
|
||||
clauses.add(new Clause.Redeem());
|
||||
clauses.add(new Clause.Move());
|
||||
clauses.add(new Clause.Issue());
|
||||
|
||||
return clauses;
|
||||
public interface Clauses {
|
||||
class Group extends GroupClauseVerifier<State, Commands, State> {
|
||||
public Group() {
|
||||
super(new AnyComposition<>(
|
||||
new Clauses.Redeem(),
|
||||
new Clauses.Move(),
|
||||
new Clauses.Issue()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -172,7 +142,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Move extends AbstractGroup {
|
||||
class Move extends ConcreteClause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -181,11 +151,11 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = single(inputs);
|
||||
@ -203,7 +173,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Redeem extends AbstractGroup {
|
||||
class Redeem extends ConcreteClause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -212,11 +182,11 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||
|
||||
// There should be only a single input due to aggregation above
|
||||
@ -245,7 +215,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Issue extends AbstractGroup {
|
||||
class Issue extends ConcreteClause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -254,11 +224,11 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||
State output = single(outputs);
|
||||
Timestamp timestampCommand = tx.getTimestamp();
|
||||
@ -298,16 +268,17 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
|
||||
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
|
||||
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx));
|
||||
ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
@ -1,14 +1,12 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.FungibleAsset
|
||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.verifyClauses
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
@ -52,10 +50,7 @@ class CommercialPaper : Contract {
|
||||
val maturityDate: Instant
|
||||
)
|
||||
|
||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
||||
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
@ -82,25 +77,16 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val clauses = listOf(
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
||||
AnyComposition(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue()
|
||||
)
|
||||
|
||||
Issue())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Terms>(
|
||||
class Issue : AbstractIssue<State, Commands, Terms>(
|
||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
@ -108,8 +94,8 @@ class CommercialPaper : Contract {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
token: Issued<Terms>?): Set<Commands> {
|
||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
|
||||
commands.requireSingleCommand<Commands.Issue>()
|
||||
val timestamp = tx.timestamp
|
||||
@ -121,14 +107,14 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Move: AbstractGroupClause() {
|
||||
class Move: ConcreteClause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
@ -141,15 +127,14 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Redeem(): AbstractGroupClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Redeem::class.java)
|
||||
class Redeem(): ConcreteClause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
|
||||
// before requiring a single command
|
||||
val command = commands.requireSingleCommand<Commands.Redeem>()
|
||||
@ -172,7 +157,7 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||
class Redeem : TypeOnlyCommandData(), Commands
|
||||
data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
||||
}
|
||||
|
@ -447,24 +447,14 @@ class InterestRateSwap() : Contract {
|
||||
fixingCalendar, index, indexSource, indexTenor)
|
||||
}
|
||||
|
||||
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
verifyClauses(tx,
|
||||
listOf(Clause.Timestamped(), Clause.Group(), LinearState.ClauseVerifier(State::class.java)),
|
||||
extractCommands(tx))
|
||||
}
|
||||
|
||||
interface Clause {
|
||||
interface Clauses {
|
||||
/**
|
||||
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
||||
* helper functions for the clauses.
|
||||
*/
|
||||
abstract class AbstractIRSClause : GroupClause<State, UniqueIdentifier> {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
|
||||
abstract class AbstractIRSClause : ConcreteClause<State, Commands, UniqueIdentifier>() {
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
fun checkLegDates(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
@ -506,19 +496,18 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, UniqueIdentifier>() {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
|
||||
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
|
||||
// Group by Trade ID for in / out states
|
||||
= tx.groupStates() { state -> state.linearId }
|
||||
|
||||
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
|
||||
}
|
||||
|
||||
class Timestamped : SingleClause() {
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
class Timestamped : ConcreteClause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
@ -526,13 +515,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
class Agree : AbstractIRSClause() {
|
||||
override val requiredCommands = setOf(Commands.Agree::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
@ -562,13 +551,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
class Fix : AbstractIRSClause() {
|
||||
override val requiredCommands = setOf(Commands.Refix::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
@ -607,13 +596,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
class Pay : AbstractIRSClause() {
|
||||
override val requiredCommands = setOf(Commands.Pay::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" by false
|
||||
@ -623,13 +612,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
class Mature : AbstractIRSClause() {
|
||||
override val requiredCommands = setOf(Commands.Mature::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Mature::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||
val irs = inputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
|
@ -4,8 +4,7 @@ import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
@ -34,7 +33,7 @@ val CASH_PROGRAM_ID = Cash()
|
||||
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
|
||||
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
||||
class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
@ -46,32 +45,30 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
||||
* that is inconsistent with the legal contract.
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||
override val conserveClause: AbstractConserveAmount<State, Currency> = Clauses.ConserveAmount()
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Cash.Commands>()
|
||||
override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||
= commands.select<Cash.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State, Currency>(),
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
|
||||
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||
FirstComposition<State, Commands, Issued<Currency>>(
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Currency>(
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
sum = { sumCash() },
|
||||
sumOrZero = { sumCashOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
class ConserveAmount : AbstractConserveAmount<State, Currency>()
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||
}
|
||||
|
||||
/** A state representing a cash claim against some party */
|
||||
@ -144,6 +141,9 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
||||
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: TransactionForContract)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
@ -5,7 +5,8 @@ import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.AnyComposition
|
||||
import com.r3corda.core.contracts.clauses.verifyClause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
@ -33,7 +34,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
|
||||
* in future.
|
||||
*/
|
||||
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
|
||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
@ -46,7 +47,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
|
||||
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
|
||||
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
|
||||
|
||||
/**
|
||||
* The clauses for this contract are essentially:
|
||||
@ -62,24 +63,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
|
||||
* each group.
|
||||
*/
|
||||
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
|
||||
/**
|
||||
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
|
||||
* it doesn't match.
|
||||
*/
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
/**
|
||||
* The group clause is the only top level clause, so end after processing it. If there are any commands left
|
||||
* after this clause has run, the clause verifier will trigger an error.
|
||||
*/
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
// Subclauses to run on each group
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State, Commodity>(),
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
|
||||
NoZeroSizedOutputs<State, Commands, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
ConserveAmount())) {
|
||||
/**
|
||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||
*/
|
||||
@ -90,17 +77,17 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
/**
|
||||
* Standard issue clause, specialised to match the commodity issue command.
|
||||
*/
|
||||
class Issue : AbstractIssue<State, Commodity>(
|
||||
class Issue : AbstractIssue<State, Commands, Commodity>(
|
||||
sum = { sumCommodities() },
|
||||
sumOrZero = { sumCommoditiesOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard clause for conserving the amount from input to output.
|
||||
*/
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
|
||||
}
|
||||
|
||||
/** A state representing a commodity claim against some party */
|
||||
@ -150,9 +137,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
*/
|
||||
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||
}
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<CommodityContract.Commands>()
|
||||
override fun verify(tx: TransactionForContract)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
|
||||
= commands.select<CommodityContract.Commands>()
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||
|
@ -43,25 +43,27 @@ class Obligation<P> : Contract {
|
||||
* that is inconsistent with the legal contract.
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
|
||||
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
|
||||
Clauses.Group<P>())
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
||||
*/
|
||||
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State<P>, Terms<P>>(),
|
||||
class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
|
||||
AllComposition(
|
||||
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
|
||||
FirstComposition(
|
||||
SetLifecycle<P>(),
|
||||
VerifyLifecycle<P>(),
|
||||
AllComposition(
|
||||
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
|
||||
FirstComposition(
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||
}
|
||||
@ -69,58 +71,64 @@ class Obligation<P> : Contract {
|
||||
/**
|
||||
* Generic issuance clause
|
||||
*/
|
||||
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
|
||||
class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic move/exit clause for fungible assets
|
||||
*/
|
||||
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Terms<P>>()
|
||||
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
|
||||
|
||||
/**
|
||||
* Clause for supporting netting of obligations.
|
||||
*/
|
||||
class Net<P> : NetClause<P>()
|
||||
class Net<C: CommandData, P> : NetClause<C, P>() {
|
||||
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
|
||||
override fun toString(): String = "Net obligations"
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
|
||||
lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
return super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for changing the lifecycle of one or more states.
|
||||
*/
|
||||
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
class SetLifecycle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
||||
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Set obligation lifecycle"
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
||||
* change of ownership of other states to fulfil
|
||||
*/
|
||||
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands = setOf(Commands.Settle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
|
||||
class Settle<P> : ConcreteClause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
require(groupingKey != null)
|
||||
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
||||
val obligor = token.issuer.party
|
||||
val template = token.product
|
||||
val obligor = groupingKey!!.issuer.party
|
||||
val template = groupingKey.product
|
||||
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(token)
|
||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
|
||||
|
||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||
|
||||
@ -166,7 +174,7 @@ class Obligation<P> : Contract {
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
if (settled != null) {
|
||||
val debt = obligations.sumObligationsOrZero(token)
|
||||
val debt = obligations.sumObligationsOrZero(groupingKey)
|
||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
@ -185,7 +193,7 @@ class Obligation<P> : Contract {
|
||||
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, token))
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
@ -197,22 +205,15 @@ class Obligation<P> : Contract {
|
||||
* any lifecycle change clause, which is the only clause that involve
|
||||
* non-standard lifecycle states on input/output.
|
||||
*/
|
||||
class VerifyLifecycle<P> : SingleClause(), GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
= verify(
|
||||
tx.inputs.filterIsInstance<State<P>>(),
|
||||
tx.outputs.filterIsInstance<State<P>>()
|
||||
)
|
||||
|
||||
class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : ConcreteClause<S, C, T>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData>
|
||||
= verify(inputs, outputs)
|
||||
|
||||
fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<CommandData> {
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: T?): Set<C>
|
||||
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
|
||||
private fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<C> {
|
||||
requireThat {
|
||||
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
@ -330,7 +331,7 @@ class Obligation<P> : Contract {
|
||||
* Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
|
||||
* as only the beneficiary (not the obligor) needs to sign.
|
||||
*/
|
||||
data class Net(val type: NetType) : Obligation.Commands
|
||||
data class Net(val type: NetType) : Commands
|
||||
|
||||
/**
|
||||
* A command stating that a debt has been moved, optionally to fulfil another contract.
|
||||
@ -373,9 +374,10 @@ class Obligation<P> : Contract {
|
||||
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
|
||||
}
|
||||
|
||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Obligation.Commands>()
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
||||
override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
|
||||
Clauses.Net<Commands, P>(),
|
||||
Clauses.Group<P>()
|
||||
), tx.commands.select<Obligation.Commands>())
|
||||
|
||||
/**
|
||||
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||
|
@ -2,8 +2,6 @@ package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.SingleClause
|
||||
import com.r3corda.core.contracts.clauses.verifyClauses
|
||||
import com.r3corda.core.crypto.Party
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -25,12 +23,9 @@ import java.security.PublicKey
|
||||
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
|
||||
* the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
|
||||
abstract val clauses: List<SingleClause>
|
||||
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
||||
abstract val conserveClause: AbstractConserveAmount<S, T>
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
||||
abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
|
||||
abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
|
||||
abstract val conserveClause: AbstractConserveAmount<S, C, T>
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting assets from the ledger.
|
||||
|
@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumFungibleOrNull
|
||||
import com.r3corda.contracts.asset.sumFungibleOrZero
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.ConcreteClause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -16,14 +15,7 @@ import java.util.*
|
||||
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
|
||||
* errors on no-match, ends on match.
|
||||
*/
|
||||
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, Issued<T>>() {
|
||||
/**
|
||||
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
|
||||
*
|
||||
@ -177,16 +169,18 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token")
|
||||
val deposit = token.issuer
|
||||
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token)
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
|
||||
val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
|
||||
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
|
||||
val deposit = groupingKey!!.issuer
|
||||
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(groupingKey)
|
||||
|
||||
// If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = tx.commands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull()
|
||||
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token)
|
||||
val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
|
||||
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, groupingKey)
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
@ -194,8 +188,12 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx))
|
||||
.filter { it != null }
|
||||
.requireNoNulls().toSet()
|
||||
verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
|
||||
|
||||
// This is safe because we've taken the commands from a collection of C objects at the start
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return matchedCommands.map { it.value }.toSet()
|
||||
}
|
||||
|
||||
override fun toString(): String = "Conserve amount between inputs and outputs"
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package com.r3corda.contracts.clause
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.ConcreteClause
|
||||
|
||||
/**
|
||||
* Standard issue clause for contracts that issue fungible assets.
|
||||
@ -14,18 +13,16 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
|
||||
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
|
||||
*/
|
||||
abstract class AbstractIssue<in S: ContractState, T: Any>(
|
||||
abstract class AbstractIssue<in S: ContractState, C: CommandData, T: Any>(
|
||||
val sum: List<S>.() -> Amount<Issued<T>>,
|
||||
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
|
||||
) : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
|
||||
) : ConcreteClause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null)
|
||||
// TODO: Take in matched commands as a parameter
|
||||
val issueCommand = commands.requireSingleCommand<IssueCommand>()
|
||||
|
||||
@ -40,8 +37,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
|
||||
// external mechanism (such as locally defined rules on which parties are trustworthy).
|
||||
|
||||
// The grouping already ensures that all outputs have the same deposit reference and token.
|
||||
val issuer = token.issuer.party
|
||||
val inputAmount = inputs.sumOrZero(token)
|
||||
val issuer = groupingKey!!.issuer.party
|
||||
val inputAmount = inputs.sumOrZero(groupingKey)
|
||||
val outputAmount = outputs.sum()
|
||||
requireThat {
|
||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||
@ -51,6 +48,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
|
||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
||||
}
|
||||
|
||||
return setOf(issueCommand.value)
|
||||
// This is safe because we've taken the command from a collection of C objects at the start
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return setOf(issueCommand.value as C)
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.Obligation
|
||||
import com.r3corda.contracts.asset.extractAmountsDue
|
||||
import com.r3corda.contracts.asset.sumAmountsDue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.SingleClause
|
||||
import com.r3corda.core.contracts.clauses.ConcreteClause
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -43,22 +42,24 @@ data class MultilateralNetState<P>(
|
||||
* Clause for netting contract states. Currently only supports obligation contract.
|
||||
*/
|
||||
// TODO: Make this usable for any nettable contract states
|
||||
open class NetClause<P> : SingleClause() {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
open class NetClause<C: CommandData, P> : ConcreteClause<ContractState, C, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
|
||||
|
||||
@Suppress("ConvertLambdaToReference")
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
|
||||
val groups = when (command.value.type) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
verifyNetCommand(inputs, outputs, command, key)
|
||||
for ((groupInputs, groupOutputs, key) in groups) {
|
||||
verifyNetCommand(groupInputs, groupOutputs, command, key)
|
||||
}
|
||||
return setOf(command.value)
|
||||
return setOf(command.value as C)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,29 +2,23 @@ package com.r3corda.contracts.clause
|
||||
|
||||
import com.r3corda.contracts.asset.FungibleAsset
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.ConcreteClause
|
||||
|
||||
/**
|
||||
* Clause for fungible asset contracts, which enforces that no output state should have
|
||||
* a balance of zero.
|
||||
*/
|
||||
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : ConcreteClause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
requireThat {
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
override fun toString(): String = "No zero sized outputs"
|
||||
}
|
||||
|
@ -172,11 +172,11 @@ class CashTests {
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
|
@ -180,11 +180,11 @@ class ObligationTests {
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.SingleClause
|
||||
import com.r3corda.core.contracts.clauses.ConcreteClause
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
@ -218,14 +218,18 @@ interface LinearState: ContractState {
|
||||
/**
|
||||
* Standard clause to verify the LinearState safety properties.
|
||||
*/
|
||||
class ClauseVerifier<S: LinearState>(val stateClass: Class<S>) : SingleClause() {
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
val inputs = tx.inputs.filterIsInstance(stateClass)
|
||||
val inputIds = inputs.map { it.linearId }.distinct()
|
||||
require(inputIds.count() == inputs.count()) { "LinearStates cannot be merged" }
|
||||
val outputs = tx.outputs.filterIsInstance(stateClass)
|
||||
val outputIds = outputs.map { it.linearId }.distinct()
|
||||
require(outputIds.count() == outputs.count()) { "LinearStates cannot be split" }
|
||||
class ClauseVerifier<S : LinearState>(val stateClass: Class<S>) : ConcreteClause<ContractState, CommandData, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>,
|
||||
groupingKey: Unit?): Set<CommandData> {
|
||||
val filteredInputs = inputs.filterIsInstance(stateClass)
|
||||
val inputIds = filteredInputs.map { it.linearId }.distinct()
|
||||
require(inputIds.count() == filteredInputs.count()) { "LinearStates cannot be merged" }
|
||||
val filteredOutputs = outputs.filterIsInstance(stateClass)
|
||||
val outputIds = filteredOutputs.map { it.linearId }.distinct()
|
||||
require(outputIds.count() == filteredOutputs.count()) { "LinearStates cannot be split" }
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
@ -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()"
|
||||
}
|
@ -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()}"
|
||||
}
|
@ -2,34 +2,43 @@ package com.r3corda.core.contracts.clauses
|
||||
|
||||
import com.r3corda.core.contracts.AuthenticatedObject
|
||||
import com.r3corda.core.contracts.CommandData
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.TransactionForContract
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
|
||||
/**
|
||||
* A clause that can be matched as part of execution of a contract.
|
||||
* @param S the type of contract state this clause operates on.
|
||||
* @param C a common supertype of commands this clause operates on.
|
||||
* @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable.
|
||||
*/
|
||||
// TODO: ifNotMatched/ifMatched should be dropped, and replaced by logic in the calling code that understands
|
||||
// "or", "and", "single" etc. composition of sets of clauses.
|
||||
interface Clause {
|
||||
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */
|
||||
interface Clause<in S : ContractState, C: CommandData, in K : Any> {
|
||||
companion object {
|
||||
val log = loggerFor<Clause<*, *, *>>()
|
||||
}
|
||||
|
||||
/** Determine whether this clause runs or not */
|
||||
val requiredCommands: Set<Class<out CommandData>>
|
||||
/** Behaviour if this clause is matched */
|
||||
val ifNotMatched: MatchBehaviour
|
||||
/** Behaviour if this clause is not matches */
|
||||
val ifMatched: MatchBehaviour
|
||||
}
|
||||
|
||||
enum class MatchBehaviour {
|
||||
CONTINUE,
|
||||
END,
|
||||
ERROR
|
||||
}
|
||||
/**
|
||||
* Determine the subclauses which will be verified as a result of verifying this clause.
|
||||
*/
|
||||
fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
|
||||
interface SingleVerify {
|
||||
/**
|
||||
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
|
||||
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
|
||||
* if any matched.
|
||||
*
|
||||
* @param tx the full transaction being verified. This is provided for cases where clauses need to access
|
||||
* states or commands outside of their normal scope.
|
||||
* @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||
* @param groupingKey a grouping key applied to states and commands, where applicable. Taken from
|
||||
* [TransactionForContract.InOutGroup].
|
||||
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
||||
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
|
||||
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
|
||||
@ -37,16 +46,18 @@ interface SingleVerify {
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
fun verify(tx: TransactionForContract,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* A single verifiable clause. By default always matches, continues to the next clause when matched and errors
|
||||
* if not matched.
|
||||
* Determine if the given list of commands matches the required commands for a clause to trigger.
|
||||
*/
|
||||
abstract class SingleClause : Clause, SingleVerify {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
|
||||
return if (requiredCommands.isEmpty())
|
||||
true
|
||||
else
|
||||
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
package com.r3corda.core.contracts.clauses
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import java.util.*
|
||||
|
||||
// Wrapper object for exposing a JVM friend version of the clause verifier
|
||||
/**
|
||||
* Verify a transaction against the given list of clauses.
|
||||
*
|
||||
@ -13,27 +11,15 @@ import java.util.*
|
||||
* @param commands commands extracted from the transaction, which are relevant to the
|
||||
* clauses.
|
||||
*/
|
||||
fun verifyClauses(tx: TransactionForContract,
|
||||
clauses: List<SingleClause>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>) {
|
||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
||||
fun <C: CommandData> verifyClause(tx: TransactionForContract,
|
||||
clause: Clause<ContractState, C, Unit>,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||
|
||||
verify@ for (clause in clauses) {
|
||||
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
|
||||
unmatchedCommands.removeAll(clause.verify(tx, commands))
|
||||
clause.ifMatched
|
||||
} else {
|
||||
clause.ifNotMatched
|
||||
check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) }
|
||||
}
|
||||
|
||||
when (matchBehaviour) {
|
||||
MatchBehaviour.ERROR -> throw IllegalStateException("Error due to matching/not matching ${clause}")
|
||||
MatchBehaviour.CONTINUE -> {
|
||||
}
|
||||
MatchBehaviour.END -> break@verify
|
||||
}
|
||||
}
|
||||
|
||||
require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." }
|
||||
}
|
||||
|
||||
|
@ -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>>
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()}"
|
||||
}
|
@ -6,77 +6,24 @@ import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
interface GroupVerify<in S, in T : Any> {
|
||||
/**
|
||||
*
|
||||
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
||||
* later clause.
|
||||
*/
|
||||
fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: T): Set<CommandData>
|
||||
}
|
||||
abstract class GroupClauseVerifier<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>) : ConcreteClause<ContractState, C, Unit>() {
|
||||
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, K>>
|
||||
|
||||
interface GroupClause<in S : ContractState, in T : Any> : Clause, GroupVerify<S, T>
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= clause.getExecutionPath(commands)
|
||||
|
||||
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause() {
|
||||
abstract val clauses: List<GroupClause<S, T>>
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val groups = groupStates(tx)
|
||||
val matchedCommands = HashSet<CommandData>()
|
||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
||||
val matchedCommands = HashSet<C>()
|
||||
|
||||
for ((inputs, outputs, token) in groups) {
|
||||
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands)
|
||||
matchedCommands.addAll(temp)
|
||||
unmatchedCommands.removeAll(temp)
|
||||
for ((groupInputs, groupOutputs, groupToken) in groups) {
|
||||
matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
|
||||
}
|
||||
|
||||
return matchedCommands
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a subset of a transaction's inputs and outputs matches the conditions from this clause. For example, a
|
||||
* "no zero amount output" clause would check each of the output states within the group, looking for a zero amount,
|
||||
* and throw IllegalStateException if any matched.
|
||||
*
|
||||
* @param commands the full set of commands which apply to this contract.
|
||||
* @param inputs input states within this group.
|
||||
* @param outputs output states within this group.
|
||||
* @param token the object used as a key when grouping states.
|
||||
* @param unmatchedCommands commands which have not yet been matched within this group.
|
||||
* @return matchedCommands commands which are matched during the verification process.
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
private fun verifyGroup(commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
token: T,
|
||||
tx: TransactionForContract,
|
||||
unmatchedCommands: List<CommandData>): Set<CommandData> {
|
||||
val matchedCommands = HashSet<CommandData>()
|
||||
verify@ for (clause in clauses) {
|
||||
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
|
||||
matchedCommands.addAll(clause.verify(tx, inputs, outputs, commands, token))
|
||||
clause.ifMatched
|
||||
} else {
|
||||
clause.ifNotMatched
|
||||
}
|
||||
|
||||
when (matchBehaviour) {
|
||||
MatchBehaviour.ERROR -> throw IllegalStateException()
|
||||
MatchBehaviour.CONTINUE -> {
|
||||
}
|
||||
MatchBehaviour.END -> break@verify
|
||||
}
|
||||
}
|
||||
return matchedCommands
|
||||
}
|
||||
}
|
@ -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}]"
|
||||
}
|
@ -1,22 +1,18 @@
|
||||
package com.r3corda.core.testing
|
||||
|
||||
import com.r3corda.core.contracts.Contract
|
||||
import com.r3corda.core.contracts.LinearState
|
||||
import com.r3corda.core.contracts.UniqueIdentifier
|
||||
import com.r3corda.core.contracts.TransactionForContract
|
||||
import com.r3corda.core.contracts.clauses.verifyClauses
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import com.r3corda.core.contracts.clauses.verifyClause
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import java.security.PublicKey
|
||||
|
||||
class DummyLinearContract: Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Test")
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
verifyClauses(tx,
|
||||
listOf(LinearState.ClauseVerifier(State::class.java)),
|
||||
val clause: Clause<ContractState, CommandData, Unit> = LinearState.ClauseVerifier(State::class.java)
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx,
|
||||
clause,
|
||||
emptyList())
|
||||
}
|
||||
|
||||
|
||||
class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
|
@ -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>>()) }
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -9,89 +9,30 @@ import kotlin.test.assertFailsWith
|
||||
* Tests for the clause verifier.
|
||||
*/
|
||||
class VerifyClausesTests {
|
||||
/** Check that if there's no clauses, verification passes. */
|
||||
@Test
|
||||
fun `passes empty clauses`() {
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClauses(tx, emptyList<SingleClause>(), emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
|
||||
/** Very simple check that the function doesn't error when given any clause */
|
||||
@Test
|
||||
fun minimal() {
|
||||
val clause = object : SingleClause() {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
|
||||
val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
|
||||
}
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
|
||||
/** Check that when there are no required commands, a clause always matches */
|
||||
@Test
|
||||
fun emptyAlwaysMatches() {
|
||||
val clause = object : SingleClause() {
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
|
||||
}
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
// This would error if it wasn't matched
|
||||
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorSuperfluousCommands() {
|
||||
val clause = object : SingleClause() {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
= emptySet()
|
||||
val clause = object : ConcreteClause<ContractState, CommandData, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
|
||||
}
|
||||
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256())
|
||||
// The clause is matched, but doesn't mark the command as consumed, so this should error
|
||||
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
|
||||
}
|
||||
|
||||
/** Check triggering of error if matched */
|
||||
@Test
|
||||
fun errorMatched() {
|
||||
val clause = object : SingleClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(DummyContract.Commands.Create::class.java)
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
= commands.select<DummyContract.Commands.Create>().map { it.value }.toSet()
|
||||
}
|
||||
var tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
|
||||
// This should pass as it doesn't match
|
||||
verifyClauses(tx, listOf(clause), emptyList())
|
||||
|
||||
// This matches and should throw an error
|
||||
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
|
||||
tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256())
|
||||
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
|
||||
}
|
||||
|
||||
/** Check triggering of error if unmatched */
|
||||
@Test
|
||||
fun errorUnmatched() {
|
||||
val clause = object : SingleClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(DummyContract.Commands.Create::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
|
||||
}
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), emptyList()) }
|
||||
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, listOf(command)) }
|
||||
}
|
||||
}
|
@ -10,14 +10,18 @@ Writing a contract using clauses
|
||||
This tutorial will take you through restructuring the commercial paper contract to use clauses. You should have
|
||||
already completed ":doc:`tutorial-contract`".
|
||||
|
||||
Clauses are essentially micro-contracts which contain independent verification logic, and are composed together to form
|
||||
a contract. With appropriate design, they can be made to be reusable, for example issuing contract state objects is
|
||||
generally the same for all fungible contracts, so a single issuance clause can be shared. This cuts down on scope for
|
||||
error, and improves consistency of behaviour.
|
||||
Clauses are essentially micro-contracts which contain independent verification logic, and can be logically composed
|
||||
together to form a contract. Clauses are designed to enable re-use of common logic, for example issuing state objects
|
||||
is generally the same for all fungible contracts, so a common issuance clause can be inherited for each contract's
|
||||
issue clause. This cuts down on scope for error, and improves consistency of behaviour. By splitting verification logic
|
||||
into smaller chunks, they can also be readily tested in isolation.
|
||||
|
||||
Clauses can be composed of subclauses, either to combine clauses in different ways, or to apply specialised clauses.
|
||||
In the case of commercial paper, we have a ``Group`` outermost clause, which will contain the ``Issue``, ``Move`` and
|
||||
``Redeem`` clauses. The result is a contract that looks something like this:
|
||||
Clauses can be composed of subclauses, for example the ``AllClause`` or ``AnyClause`` clauses take list of clauses
|
||||
that they delegate to. Clauses can also change the scope of states and commands being verified, for example grouping
|
||||
together fungible state objects and running a clause against each distinct group.
|
||||
|
||||
The commercial paper contract has a ``Group`` outermost clause, which contains the ``Issue``, ``Move`` and ``Redeem``
|
||||
clauses. The result is a contract that looks something like this:
|
||||
|
||||
1. Group input and output states together, and then apply the following clauses on each group:
|
||||
a. If an ``Issue`` command is present, run appropriate tests and end processing this group.
|
||||
@ -27,11 +31,12 @@ In the case of commercial paper, we have a ``Group`` outermost clause, which wil
|
||||
Commercial paper class
|
||||
----------------------
|
||||
|
||||
To use the clause verification logic, the contract needs to call the ``verifyClauses()`` function, passing in the transaction,
|
||||
a list of clauses to verify, and a collection of commands the clauses are expected to handle all of. This list of
|
||||
commands is important because ``verifyClauses()`` checks that none of the commands are left unprocessed at the end, and
|
||||
raises an error if they are. The following examples are trimmed to the modified class definition and added elements, for
|
||||
brevity:
|
||||
To use the clause verification logic, the contract needs to call the ``verifyClause`` function, passing in the
|
||||
transaction, a clause to verify, and a collection of commands the clauses are expected to handle all of. This list of
|
||||
commands is important because ``verifyClause`` checks that none of the commands are left unprocessed at the end, and
|
||||
raises an error if they are. The top level clause would normally be a composite clause (such as ``AnyComposition``,
|
||||
``AllComposition``, etc.) which contains further clauses. The following examples are trimmed to the modified class
|
||||
definition and added elements, for brevity:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -40,10 +45,7 @@ brevity:
|
||||
class CommercialPaper : Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
||||
|
||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@ -53,53 +55,40 @@ brevity:
|
||||
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
|
||||
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx));
|
||||
ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx));
|
||||
}
|
||||
|
||||
Clauses
|
||||
-------
|
||||
|
||||
We'll tackle the inner clauses that contain the bulk of the verification logic, first, and the clause which handles
|
||||
grouping of input/output states later. The inner clauses need to implement the ``GroupClause`` interface, which defines
|
||||
the verify() function, and properties (``ifMatched``, ``ifNotMatched`` and ``requiredCommands``) defining how the clause
|
||||
is processed. These properties specify the command(s) which must be present in order for the clause to be matched,
|
||||
and what to do after processing the clause depending on whether it was matched or not.
|
||||
grouping of input/output states later. The clauses must implement the ``Clause`` interface, which defines
|
||||
the ``verify`` function, and the ``requiredCommands`` property used to determine the conditions under which a clause
|
||||
is triggered. Normally clauses would extend ``ConcreteClause`` which provides defaults suitable for a clause which
|
||||
verifies transactions, rather than delegating to other clauses.
|
||||
|
||||
The ``verify()`` functions defined in the ``SingleClause`` and ``GroupClause`` interfaces is similar to the conventional
|
||||
``Contract`` verification function, although it adds new parameters and returns the set of commands which it has processed.
|
||||
Normally this returned set is identical to the commands matched in order to trigger the clause, however in some cases the
|
||||
clause may process optional commands which it needs to report that it has handled, or may by designed to only process
|
||||
the first (or otherwise) matched command.
|
||||
The ``verify`` function defined in the ``Clause`` interface is similar to the conventional ``Contract`` verification
|
||||
function, although it adds new parameters and returns the set of commands which it has processed. Normally this returned
|
||||
set is identical to the ``requiredCommands`` used to trigger the clause, however in some cases the clause may process
|
||||
further optional commands which it needs to report that it has handled.
|
||||
|
||||
The Move clause for the commercial paper contract is relatively simple, so lets start there:
|
||||
The ``Move`` clause for the commercial paper contract is relatively simple, so we will start there:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class Move: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
class Move: ConcreteClause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
@ -114,140 +103,79 @@ The Move clause for the commercial paper contract is relatively simple, so lets
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class Move implements GroupClause<State, State> {
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
|
||||
class Move extends ConcreteClause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Move.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.Move.class);
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = single(inputs);
|
||||
|
||||
requireThat(require -> {
|
||||
require.by("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner()));
|
||||
require.by("the state is propagated", outputs.size() == 1);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
if (!cmd.getSigners().contains(input.getOwner()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
// Check the output CP state is the same as the input state, ignoring the owner field.
|
||||
if (outputs.size() != 1) {
|
||||
throw new IllegalStateException("the state is propagated");
|
||||
}
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
The post-processing ``MatchBehaviour`` options are:
|
||||
* CONTINUE
|
||||
* END
|
||||
* ERROR
|
||||
|
||||
In this case we process commands against each group, until the first matching clause is found, so we ``END`` on a match
|
||||
and ``CONTINUE`` otherwise. ``ERROR`` can be used as a part of a clause which must always/never be matched. By default
|
||||
clauses are always matched (``requiredCommands`` is an empty set), execution continues after a clause is matched, and an
|
||||
error is raised if a clause is not matched.
|
||||
|
||||
Group Clause
|
||||
------------
|
||||
|
||||
We need to wrap the move clause (as well as the issue and redeem clauses - see the relevant contract code for their
|
||||
full specifications) in an outer clause. For this we extend the standard ``GroupClauseVerifier`` and specify how to
|
||||
group input/output states, as well as the clauses to run on each group.
|
||||
full specifications) in an outer clause that understands how to group contract states and objects. For this we extend
|
||||
the standard ``GroupClauseVerifier`` and specify how to group input/output states, as well as the top-level to run on
|
||||
each group. As with the top level clause on a contract, this is normally a composite clause that delegates to subclauses.
|
||||
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val clauses: List<GroupClause<State, Issued<Terms>>>
|
||||
get() = listOf(
|
||||
Clause.Redeem(),
|
||||
Clause.Move(),
|
||||
Clause.Issue()
|
||||
)
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
||||
AnyComposition(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class Group extends GroupClauseVerifier<State, State> {
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
class Group extends GroupClauseVerifier<State, Commands, State> {
|
||||
public Group() {
|
||||
super(new AnyComposition<>(
|
||||
new Clauses.Redeem(),
|
||||
new Clauses.Move(),
|
||||
new Clauses.Issue()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.ERROR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.r3corda.core.contracts.clauses.GroupClause<State, State>> getClauses() {
|
||||
final List<GroupClause<State, State>> clauses = new ArrayList<>();
|
||||
|
||||
clauses.add(new Clause.Redeem());
|
||||
clauses.add(new Clause.Move());
|
||||
clauses.add(new Clause.Issue());
|
||||
|
||||
return clauses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
|
||||
public List<InOutGroup<State, State>> groupStates(@NotNull TransactionForContract tx) {
|
||||
return tx.groupStates(State.class, State::withoutOwner);
|
||||
}
|
||||
}
|
||||
|
||||
We then pass this clause into the outer ``ClauseVerifier`` contract by returning it from the ``clauses`` property. We
|
||||
also implement the ``extractCommands()`` function, which filters commands on the transaction down to the set the
|
||||
contained clauses must handle (any unmatched commands at the end of clause verification results in an exception to be
|
||||
thrown).
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@Override
|
||||
public List<SingleClause> getClauses() {
|
||||
return Collections.singletonList(new Clause.Group());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
For the ``CommercialPaper`` contract, this is the top level clause for the contract, and is passed directly into
|
||||
``verifyClause`` (see the example code at the top of this tutorial).
|
||||
|
||||
Summary
|
||||
-------
|
||||
@ -256,3 +184,11 @@ In summary the top level contract ``CommercialPaper`` specifies a single groupin
|
||||
``CommercialPaper.Clauses.Group`` which in turn specifies ``GroupClause`` implementations for each type of command
|
||||
(``Redeem``, ``Move`` and ``Issue``). This reflects the flow of verification: In order to verify a ``CommercialPaper``
|
||||
we first group states, check which commands are specified, and run command-specific verification logic accordingly.
|
||||
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user