Add support for clause based contract verification

This commit is contained in:
Ross Nicoll 2016-07-07 18:10:05 +01:00
parent fb2efd8fc1
commit 1ae8ada999
5 changed files with 338 additions and 9 deletions

View File

@ -60,14 +60,22 @@ inline fun <R> requireThat(body: Requirements.() -> R) = R.body()
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
/** Filters the command list by type, party and public key all at once. */
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
party: Party? = null) =
filter { it.value is T }.
filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (party == null) true else it.signingParties.contains(party) }.
filter { if (signer == null) true else signer in it.signers }.
filter { if (party == null) true else party in it.signingParties }.
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
/** Filters the command list by type, parties and public keys all at once. */
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<PublicKey>?,
parties: Collection<Party>?) =
filter { it.value is T }.
filter { if (signers == null) true else it.signers.containsAll(signers)}.
filter { if (parties == null) true else it.signingParties.containsAll(parties) }.
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
select<T>().single()
} catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
@ -106,9 +114,10 @@ fun List<AuthenticatedObject<CommandData>>.getTimestampByName(vararg names: Stri
*/
@Throws(IllegalArgumentException::class)
// TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here?
inline fun <reified T : CommandData> verifyMoveCommand(inputs: List<OwnableState>, tx: TransactionForContract) {
return verifyMoveCommand<T>(inputs, tx.commands)
}
inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState>,
tx: TransactionForContract)
: MoveCommand
= verifyMoveCommand<T>(inputs, tx.commands)
/**
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
@ -116,13 +125,17 @@ inline fun <reified T : CommandData> verifyMoveCommand(inputs: List<OwnableState
* @param T the type of the move command
*/
@Throws(IllegalArgumentException::class)
inline fun <reified T : CommandData> verifyMoveCommand(inputs: List<OwnableState>, commands: List<AuthenticatedObject<CommandData>>) {
inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState>,
commands: List<AuthenticatedObject<CommandData>>)
: MoveCommand {
// Now check the digital signatures on the move command. Every input has an owning public key, and we must
// see a signature from each of those keys. The actual signatures have been verified against the transaction
// data by the platform before execution.
val owningPubKeys = inputs.map { it.owner }.toSet()
val keysThatSigned = commands.requireSingleCommand<T>().signers.toSet()
val command = commands.requireSingleCommand<T>()
val keysThatSigned = command.signers.toSet()
requireThat {
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
}
return command.value
}

View File

@ -0,0 +1,92 @@
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>())
/**
* Verify a transaction against the given list of clauses.
*
* @param tx transaction to be verified.
* @param clauses the clauses to verify.
* @param commands commands extracted from the transaction, which are relevant to the
* clauses.
*/
fun verifyClauses(tx: TransactionForContract,
clauses: List<SingleClause>,
commands: Collection<AuthenticatedObject<CommandData>>) {
val unmatchedCommands = ArrayList(commands.map { it.value })
verify@ for (clause in clauses) {
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
unmatchedCommands.removeAll(clause.verify(tx, commands))
clause.ifMatched
} else {
clause.ifNotMatched
}
when (matchBehaviour) {
MatchBehaviour.ERROR -> throw IllegalStateException()
MatchBehaviour.CONTINUE -> {
}
MatchBehaviour.END -> break@verify
}
}
require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." }
}

View File

@ -0,0 +1,82 @@
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.*
interface GroupVerify<S, T : Any> {
/**
*
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
* later clause.
*/
fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: T): Set<CommandData>
}
interface GroupClause<S : ContractState, T : Any> : Clause, GroupVerify<S, T>
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause {
abstract val clauses: List<GroupClause<S, T>>
override val requiredCommands: Set<Class<CommandData>>
get() = emptySet()
abstract fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<out S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val groups = extractGroups(tx)
val matchedCommands = HashSet<CommandData>()
val unmatchedCommands = ArrayList(commands.map { it.value })
for ((inputs, outputs, token) in groups) {
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands)
matchedCommands.addAll(temp)
unmatchedCommands.removeAll(temp)
}
return matchedCommands
}
/**
* Verify a subset of a transaction's inputs and outputs matches the conditions from this clause. For example, a
* "no zero amount output" clause would check each of the output states within the group, looking for a zero amount,
* and throw IllegalStateException if any matched.
*
* @param commands the full set of commands which apply to this contract.
* @param inputs input states within this group.
* @param outputs output states within this group.
* @param token the object used as a key when grouping states.
* @param unmatchedCommands commands which have not yet been matched within this group.
* @return matchedCommands commands which are matched during the verification process.
*/
@Throws(IllegalStateException::class)
private fun verifyGroup(commands: Collection<AuthenticatedObject<CommandData>>,
inputs: List<S>,
outputs: List<S>,
token: T,
tx: TransactionForContract,
unmatchedCommands: List<CommandData>): Set<CommandData> {
val matchedCommands = HashSet<CommandData>()
verify@ for (clause in clauses) {
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
matchedCommands.addAll(clause.verify(tx, inputs, outputs, commands, token))
clause.ifMatched
} else {
clause.ifNotMatched
}
when (matchBehaviour) {
MatchBehaviour.ERROR -> throw IllegalStateException()
MatchBehaviour.CONTINUE -> {
}
MatchBehaviour.END -> break@verify
}
}
return matchedCommands
}
}

View File

@ -0,0 +1,28 @@
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.
*/
data 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
}
}

View File

@ -0,0 +1,114 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import org.junit.Test
import kotlin.test.assertFailsWith
/**
* Tests for the clause verifier.
*/
class VerifyClausesTests {
/** Check that if there's no clauses, verification passes. */
@Test
fun `passes empty clauses`() {
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
verifyClauses(tx, emptyList<SingleClause>(), emptyList<AuthenticatedObject<CommandData>>())
}
/** Very simple check that the function doesn't error when given any clause */
@Test
fun minimal() {
val clause = object : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> = emptySet()
}
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
verifyClauses(tx, listOf(clause), emptyList<AuthenticatedObject<CommandData>>())
}
/** Check that when there are no required commands, a clause always matches */
@Test
fun emptyAlwaysMatches() {
val clause = object : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
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
fun errorSuperfluousCommands() {
val clause = object : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= emptySet()
}
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256())
// The clause is matched, but doesn't mark the command as consumed, so this should error
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
}
/** Check triggering of error if matched */
@Test
fun errorMatched() {
val clause = object : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(DummyContract.Commands.Create::class.java)
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= commands.select<DummyContract.Commands.Create>().map { it.value }.toSet()
}
var tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
// This should pass as it doesn't match
verifyClauses(tx, listOf(clause), emptyList())
// This matches and should throw an error
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
tx = TransactionForContract(emptyList(), emptyList(), emptyList(), listOf(command), SecureHash.randomSHA256())
assertFailsWith<IllegalStateException> { verifyClauses(tx, listOf(clause), listOf(command)) }
}
/** Check triggering of error if unmatched */
@Test
fun errorUnmatched() {
val clause = object : SingleClause {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(DummyContract.Commands.Create::class.java)
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
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()) }
}
}