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