mirror of
https://github.com/corda/corda.git
synced 2025-01-20 11:39:09 +00:00
Merged in rnicoll-clause (pull request #210)
Add support for clause based contract verification
This commit is contained in:
commit
0d17e8d98f
@ -60,14 +60,22 @@ inline fun <R> requireThat(body: Requirements.() -> R) = R.body()
|
|||||||
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
|
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/** Filters the command list by type, party and public key all at once. */
|
/** 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) =
|
party: Party? = null) =
|
||||||
filter { it.value is T }.
|
filter { it.value is T }.
|
||||||
filter { if (signer == null) true else it.signers.contains(signer) }.
|
filter { if (signer == null) true else signer in it.signers }.
|
||||||
filter { if (party == null) true else it.signingParties.contains(party) }.
|
filter { if (party == null) true else party in it.signingParties }.
|
||||||
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
|
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()
|
select<T>().single()
|
||||||
} catch (e: NoSuchElementException) {
|
} catch (e: NoSuchElementException) {
|
||||||
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
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)
|
@Throws(IllegalArgumentException::class)
|
||||||
// TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here?
|
// 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) {
|
inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState>,
|
||||||
return verifyMoveCommand<T>(inputs, tx.commands)
|
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.
|
* 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
|
* @param T the type of the move command
|
||||||
*/
|
*/
|
||||||
@Throws(IllegalArgumentException::class)
|
@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
|
// 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
|
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
||||||
// data by the platform before execution.
|
// data by the platform before execution.
|
||||||
val owningPubKeys = inputs.map { it.owner }.toSet()
|
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 {
|
requireThat {
|
||||||
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||||
}
|
}
|
||||||
|
return command.value
|
||||||
}
|
}
|
||||||
|
@ -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." }
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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()) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user