Merged in rnicoll-clause-cleanup (pull request #266)

Clean up how clauses are implemented
This commit is contained in:
Ross Nicoll 2016-08-11 15:51:02 +01:00
commit 84c6989b8b
11 changed files with 98 additions and 103 deletions

View File

@ -22,7 +22,7 @@ import static kotlin.collections.CollectionsKt.*;
* This is a Java version of the CommercialPaper contract (chosen because it's simple). This demonstrates how the
* use of Kotlin for implementation of the framework does not impose the same language choice on contract developers.
*/
public class JavaCommercialPaper extends ClauseVerifier {
public class JavaCommercialPaper implements Contract {
//public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
@ -161,7 +161,7 @@ public class JavaCommercialPaper extends ClauseVerifier {
@NotNull
@Override
public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
public List<InOutGroup<State, State>> groupStates(@NotNull TransactionForContract tx) {
return tx.groupStates(State.class, State::withoutOwner);
}
}
@ -316,20 +316,18 @@ public class JavaCommercialPaper extends ClauseVerifier {
}
@NotNull
@Override
public List<SingleClause> getClauses() {
return Collections.singletonList(new Clause.Group());
}
@NotNull
@Override
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
.stream()
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
.collect(Collectors.toList());
}
@Override
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx));
}
@NotNull
@Override
public SecureHash getLegalContractReference() {

View File

@ -40,7 +40,7 @@ import java.util.*
val CP_PROGRAM_ID = CommercialPaper()
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
class CommercialPaper : ClauseVerifier() {
class CommercialPaper : Contract {
// TODO: should reference the content of the legal agreement, not its URI
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
@ -49,11 +49,11 @@ class CommercialPaper : ClauseVerifier() {
val maturityDate: Instant
)
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
data class State(
val issuance: PartyAndReference,
override val owner: PublicKey,
@ -88,7 +88,7 @@ class CommercialPaper : ClauseVerifier() {
Issue()
)
override fun extractGroups(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 }
}

View File

@ -183,7 +183,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
* Currently, we are not interested (excuse pun) in valuing the swap, calculating the PVs, DFs and all that good stuff (soon though).
* This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model.
*/
class InterestRateSwap() : ClauseVerifier() {
class InterestRateSwap() : Contract {
override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
/**
@ -447,10 +447,11 @@ class InterestRateSwap() : ClauseVerifier() {
fixingCalendar, index, indexSource, indexTenor)
}
override val clauses: List<SingleClause> = listOf(Clause.Timestamped(), Clause.Group())
override fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
private fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>() + tx.commands.select<TimestampCommand>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx))
interface Clause {
/**
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
@ -505,7 +506,7 @@ class InterestRateSwap() : ClauseVerifier() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.ERROR
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
// Group by Trade ID for in / out states
= tx.groupStates() { state -> state.common.tradeID }

View File

@ -60,7 +60,7 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
Issue(),
ConserveAmount())
override fun extractGroups(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 }
}

View File

@ -80,7 +80,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
/**
* Group commodity states by issuance definition (issuer and underlying commodity).
*/
override fun extractGroups(tx: TransactionForContract)
override fun groupStates(tx: TransactionForContract)
= tx.groupStates<State, Issued<Commodity>> { it.issuanceDef }
}

View File

@ -30,7 +30,7 @@ val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
*
* @param P the product the obligation is for payment of.
*/
class Obligation<P> : ClauseVerifier() {
class Obligation<P> : Contract {
/**
* TODO:
@ -43,7 +43,7 @@ class Obligation<P> : ClauseVerifier() {
* 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 clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
Clauses.Group<P>())
interface Clauses {
@ -62,7 +62,7 @@ class Obligation<P> : ClauseVerifier() {
ConserveAmount()
)
override fun extractGroups(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 }
}
@ -377,8 +377,9 @@ class Obligation<P> : ClauseVerifier() {
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
}
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Obligation.Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
/**
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.

View File

@ -2,7 +2,8 @@ package com.r3corda.contracts.asset
import com.r3corda.contracts.clause.AbstractConserveAmount
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.ClauseVerifier
import com.r3corda.core.contracts.clauses.SingleClause
import com.r3corda.core.contracts.clauses.verifyClauses
import com.r3corda.core.crypto.Party
import java.security.PublicKey
@ -24,9 +25,13 @@ import java.security.PublicKey
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
* the issuer/depositRefs and just examine the amount fields.
*/
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : ClauseVerifier() {
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
abstract val clauses: List<SingleClause>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
abstract val conserveClause: AbstractConserveAmount<S, T>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
/**
* Generate an transaction exiting assets from the ledger.
*

View File

@ -0,0 +1,44 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.TransactionForContract
/**
* A clause that can be matched as part of execution of a contract.
*/
// TODO: ifNotMatched/ifMatched should be dropped, and replaced by logic in the calling code that understands
// "or", "and", "single" etc. composition of sets of clauses.
interface Clause {
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */
val requiredCommands: Set<Class<out CommandData>>
/** Behaviour if this clause is matched */
val ifNotMatched: MatchBehaviour
/** Behaviour if this clause is not matches */
val ifMatched: MatchBehaviour
}
enum class MatchBehaviour {
CONTINUE,
END,
ERROR
}
interface SingleVerify {
/**
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
* if any matched.
*
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
* commands that were not required (for example the Exit command for fungible assets is optional).
*/
@Throws(IllegalStateException::class)
fun verify(tx: TransactionForContract,
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
}
interface SingleClause : Clause, SingleVerify

View File

@ -1,64 +1,10 @@
@file:JvmName("ClauseVerifier")
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.*
import java.util.*
interface Clause {
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */
val requiredCommands: Set<Class<out CommandData>>
/** Behaviour if this clause is matched */
val ifNotMatched: MatchBehaviour
/** Behaviour if this clause is not matches */
val ifMatched: MatchBehaviour
}
enum class MatchBehaviour {
CONTINUE,
END,
ERROR
}
interface SingleVerify {
/**
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
* if any matched.
*
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
* commands that were not required (for example the Exit command for fungible assets is optional).
*/
@Throws(IllegalStateException::class)
fun verify(tx: TransactionForContract,
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
}
interface SingleClause : Clause, SingleVerify
/**
* Abstract superclass for clause-based contracts to extend, which provides a verify() function
* that delegates to the supplied list of clauses.
*/
abstract class ClauseVerifier : Contract {
abstract val clauses: List<SingleClause>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
}
/**
* Verify a transaction against the given list of clauses.
*
* @param tx transaction to be verified.
* @param clauses the clauses to verify.
* @param T common supertype of commands to extract from the transaction, which are of relevance to these clauses.
*/
inline fun <reified T : CommandData> verifyClauses(tx: TransactionForContract,
clauses: List<SingleClause>)
= verifyClauses(tx, clauses, tx.commands.select<T>())
// Wrapper object for exposing a JVM friend version of the clause verifier
/**
* Verify a transaction against the given list of clauses.
*
@ -89,4 +35,5 @@ fun verifyClauses(tx: TransactionForContract,
}
require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." }
}
}

View File

@ -26,10 +26,10 @@ abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
abstract fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val groups = extractGroups(tx)
val groups = groupStates(tx)
val matchedCommands = HashSet<CommandData>()
val unmatchedCommands = ArrayList(commands.map { it.value })

View File

@ -10,7 +10,7 @@ Writing a contract using clauses
This tutorial will take you through restructuring the commercial paper contract to use clauses. You should have
already completed ":doc:`tutorial-contract`".
Clauses are essentially "mini-contracts" which contain verification logic, and are composed together to form
Clauses are essentially micro-contracts which contain independent verification logic, and are composed together to form
a contract. With appropriate design, they can be made to be reusable, for example issuing contract state objects is
generally the same for all fungible contracts, so a single issuance clause can be shared. This cuts down on scope for
error, and improves consistency of behaviour.
@ -27,25 +27,24 @@ In the case of commercial paper, we have a "Grouping" outermost clause, which wi
Commercial paper class
----------------------
First we need to change the class from implementing ``Contract``, to extend ``ClauseVerifier``. This is an abstract
class which provides a verify() function for us, and requires we provide a property (``clauses``) for the clauses to test,
and a function (``extractCommands``) to extract the applicable commands from the transaction. This is important because
``ClauseVerifier`` checks that no commands applicable to the contract are left unprocessed at the end. The following
examples are trimmed to the modified class definition and added elements, for brevity:
To use the clause verification logic, the contract needs to call the ``verifyClauses()`` function, passing in the transaction,
a list of clauses to verify, and a collection of commands the clauses are expected to handle all of. This list of
commands is important because ``verifyClauses()`` checks that none of the commands are left unprocessed at the end, and
raises an error if they are. The following examples are trimmed to the modified class definition and added elements, for
brevity:
.. container:: codeset
.. sourcecode:: kotlin
class CommercialPaper : ClauseVerifier {
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
class CommercialPaper : Contract {
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
override val clauses: List<SingleClause>
get() = throw UnsupportedOperationException("not implemented")
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
.. sourcecode:: java
public class CommercialPaper implements Contract {
@ -54,11 +53,6 @@ examples are trimmed to the modified class definition and added elements, for br
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
}
@Override
public List<SingleClause> getClauses() {
throw UnsupportedOperationException("not implemented");
}
@Override
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
@ -67,6 +61,11 @@ examples are trimmed to the modified class definition and added elements, for br
.collect(Collectors.toList());
}
@Override
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx));
}
Clauses
-------