mirror of
https://github.com/corda/corda.git
synced 2025-01-18 18:56:28 +00:00
Add support for clause based contract verification
This commit is contained in:
parent
fb2efd8fc1
commit
1ae8ada999
@ -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
|
||||
}
|
||||
|
@ -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