mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Added the additional Corda utility code with FSM-like transition contract checking
This commit is contained in:
parent
acefe4261c
commit
3a9fa50799
121
experimental/corda-utils/README.md
Normal file
121
experimental/corda-utils/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Introduction
|
||||
This project holds different Corda-related utility code.
|
||||
|
||||
## Utils
|
||||
Utils.kt contains various extension functions and other short utility code that aid
|
||||
development on Corda. The code is mostly self-explanatory -- the only exception may
|
||||
be `StateRefHere` which can be used in situations where multiple states are produced
|
||||
in one transaction, and one state needs to refer to the others, e.g. something like this:
|
||||
```
|
||||
val tx = TransactionBuilder(//...
|
||||
// ...
|
||||
tx.addOutputState(innerState, contractClassName)
|
||||
val innerStateRef = StateRefHere(null, tx.outputStates().count() - 1)
|
||||
tx.addOutputState(OuterState(innerStateRef = innerStateRef), contractClassName)
|
||||
// ...
|
||||
|
||||
```
|
||||
|
||||
## StatusTransitions
|
||||
StatusTransitions.kt contains utility code related to FSM-style defining possible transactions that can happen
|
||||
with the respect to the contained status and roles of participants. Here's a simple example for illustration.
|
||||
We are going to track package delivery status, so we first define all roles of participants and possible statuses
|
||||
each package could have:
|
||||
```
|
||||
enum class PackageDeliveryRole {
|
||||
Sender,
|
||||
Receiver,
|
||||
Courier
|
||||
}
|
||||
|
||||
enum class DeliveryStatus {
|
||||
InTransit,
|
||||
Delivered,
|
||||
Returned
|
||||
}
|
||||
```
|
||||
|
||||
The information about each package is held in PackageState: it contains its involved parties, status, linearId,
|
||||
current location, and information related to delivery attempts:
|
||||
```
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import java.time.Instant
|
||||
|
||||
data class PackageState(val sender: Party,
|
||||
val receiver: Party,
|
||||
val deliveryCompany: Party,
|
||||
val currentLocation: String,
|
||||
override val status: DeliveryStatus,
|
||||
val deliveryAttempts: Int = 0,
|
||||
val lastDeliveryAttempt: Instant? = null,
|
||||
override val linearId: UniqueIdentifier): LinearState, StatusTrackingContractState<DeliveryStatus, PackageDeliveryRole> {
|
||||
|
||||
override fun roleToParty(role: PackageDeliveryRole): Party {
|
||||
return when (role) {
|
||||
PackageDeliveryRole.Sender -> sender
|
||||
PackageDeliveryRole.Receiver -> receiver
|
||||
PackageDeliveryRole.Courier -> deliveryCompany
|
||||
}
|
||||
}
|
||||
|
||||
override val participants: List<AbstractParty> = listOf(sender, receiver, deliveryCompany)
|
||||
}
|
||||
```
|
||||
We can then define operations one can do with this state, who can do them and under what circumstances (i.e. from what status):
|
||||
```
|
||||
sealed class DeliveryCommand: CommandData {
|
||||
object Send: DeliveryCommand()
|
||||
object Transport: DeliveryCommand()
|
||||
object ConfirmReceipt: DeliveryCommand()
|
||||
object AttemptedDelivery: DeliveryCommand()
|
||||
object Return: DeliveryCommand()
|
||||
}
|
||||
|
||||
class PackageDelivery: Contract {
|
||||
companion object {
|
||||
val transitions = StatusTransitions(PackageState::class,
|
||||
DeliveryCommand.Send.txDef(PackageDeliveryRole.Sender, null, listOf(DeliveryStatus.InTransit)),
|
||||
DeliveryCommand.Transport.txDef(PackageDeliveryRole.Courier, DeliveryStatus.InTransit, listOf(DeliveryStatus.InTransit)),
|
||||
DeliveryCommand.AttemptedDelivery.txDef(PackageDeliveryRole.Courier, DeliveryStatus.InTransit, listOf(DeliveryStatus.InTransit)),
|
||||
DeliveryCommand.ConfirmReceipt.txDef(PackageDeliveryRole.Receiver, DeliveryStatus.InTransit, listOf(DeliveryStatus.Delivered)),
|
||||
DeliveryCommand.Return.txDef(PackageDeliveryRole.Courier, DeliveryStatus.InTransit, listOf(DeliveryStatus.Returned)))
|
||||
}
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
transitions.verify(tx)
|
||||
// ...
|
||||
// other checks -- linearId is preserved, attributes are updated correctly for given commands, return is only allowed after 3 attempts, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
This definition gives us some basic generic verification -- e.g. that package receipt confirmations need to be signed by package receivers.
|
||||
In addition that, we could visualize the defined transitions in a PUML diagram:
|
||||
|
||||
```
|
||||
PackageDelivery.transitions.printGraph().printedPUML
|
||||
```
|
||||
|
||||
Which will result in:
|
||||
```
|
||||
@startuml
|
||||
title PackageState
|
||||
[*] --> InTransit : Send (by Sender)
|
||||
InTransit --> InTransit : Transport (by Courier)
|
||||
InTransit --> InTransit : AttemptedDelivery (by Courier)
|
||||
InTransit --> Delivered : ConfirmReceipt (by Receiver)
|
||||
InTransit --> Returned : Return (by Courier)
|
||||
@enduml
|
||||
```
|
||||
![Generated PlantUML model](http://www.plantuml.com:80/plantuml/png/VSsn2i8m58NXlK-HKOM-W8DKwk8chPiunEOemIGDjoU5lhqIHP12jn_k-RZLG2rCtXMqT50dtJtr0oqrKLmsLrMMEtKCPz5Xi5HRrI8OjRfDEI3hudUSJNF5NfZtTP_4BeCz2Hy9Su2p8sHQWjyDp1lMVRXRyGqwsCYiSezpre19GbQV_FzH8PZatGi0)
|
||||
|
||||
## Future plans
|
||||
Depending on particular use cases, this utility library may be enhanced in different ways. Here are a few ideas:
|
||||
|
||||
* More generic verification (e.g. verifying numbers of produced and consumed states of a particular type)
|
||||
* More convenient syntax, not abusing nulls so much, etc.
|
||||
* ...
|
27
experimental/corda-utils/build.gradle
Normal file
27
experimental/corda-utils/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
compile project(':core')
|
||||
compile project(':node-api')
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':node-driver')
|
||||
|
||||
testCompile "junit:junit:$junit_version"
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package io.cryptoblk.core
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Contract state that records changes of some [status] on the ledger and roles of parties that are participants
|
||||
* in that state using [roleToParty].
|
||||
*/
|
||||
interface StatusTrackingContractState<out S, in R> : ContractState {
|
||||
val status: S
|
||||
fun roleToParty(role: R): Party
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of finite state transition: for a particular command in a TX, it defines what transitions can be done
|
||||
* [from] what status [to] what statuses, and who needs to sign them ([signer]).
|
||||
* If [from] is null, it means there doesn't need to be any input; if [to] is null, it mean there doesn't need to be any output.
|
||||
* If [signer] is null, it means anyone can sign it.
|
||||
*/
|
||||
data class TransitionDef<out S, out R>(val cmd: Class<*>, val signer: R?, val from: S?, val to: List<S?>)
|
||||
|
||||
/**
|
||||
* Holds visualized PUML graph in [printedPUML] and the relevant state class name in [stateClassName].
|
||||
*/
|
||||
data class PrintedTransitionGraph(val stateClassName: String, val printedPUML: String)
|
||||
|
||||
/**
|
||||
* Shorthand for defining transitions directly from the command class
|
||||
*/
|
||||
fun <S, R> CommandData.txDef(signer: R? = null, from: S?, to: List<S?>):
|
||||
TransitionDef<S, R> = TransitionDef(this::class.java, signer, from, to)
|
||||
|
||||
/**
|
||||
* For a given [stateClass] that tracks a status, it holds all possible transitions in [ts].
|
||||
* This can be used for generic [verify] in contract code as well as for visualizing the state transition graph in PUML ([printGraph]).
|
||||
*/
|
||||
class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(private val stateClass: KClass<T>,
|
||||
private vararg val ts: TransitionDef<S, R>) {
|
||||
|
||||
private val allowedCmds = ts.map { it.cmd }.toSet()
|
||||
|
||||
private fun matchingTransitions(input: S?, output: S?, command: CommandData): List<TransitionDef<S, R>> {
|
||||
val options = ts.filter {
|
||||
(it.from == input) && (output in it.to) && (it.cmd == command.javaClass)
|
||||
}
|
||||
if (options.isEmpty()) throw IllegalStateException("Transition [$input -(${command.javaClass.simpleName})-> $output] not allowed")
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic verification based on provided [TransitionDef]s
|
||||
*/
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
val relevantCmds = tx.commands.filter { allowedCmds.contains(it.value.javaClass) }
|
||||
require(relevantCmds.isNotEmpty()) { "Transaction must have at least one Command relevant to its defined transitions" }
|
||||
|
||||
relevantCmds.forEach { cmd ->
|
||||
val ins = tx.inputsOfType(stateClass.java)
|
||||
val inputStates = if (ins.isEmpty()) listOf(null) else ins
|
||||
val outs = tx.outputsOfType(stateClass.java)
|
||||
val outputStates = if (outs.isEmpty()) listOf(null) else outs
|
||||
|
||||
// for each combination of in x out which should normally be at most 1...
|
||||
inputStates.forEach { inp ->
|
||||
outputStates.forEach { outp ->
|
||||
assert((inp != null) || (outp != null))
|
||||
val options = matchingTransitions(inp?.status, outp?.status, cmd.value)
|
||||
|
||||
val signerGroup = options.groupBy { it.signer }.entries.singleOrNull()
|
||||
?: throw IllegalStateException("Cannot have different signers in StatusTransitions for the same command.")
|
||||
val signer = signerGroup.key
|
||||
if (signer != null) {
|
||||
// which state determines who is the signer? by default the input, unless it's the initial transition
|
||||
val state = (inp ?: outp)!!
|
||||
val signerParty = state.roleToParty(signer)
|
||||
if (!cmd.signers.contains(signerParty.owningKey))
|
||||
throw IllegalStateException("Command ${cmd.value.javaClass} must be signed by $signer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun printGraph(): PrintedTransitionGraph {
|
||||
val sb = StringBuilder()
|
||||
sb.append("@startuml\n")
|
||||
if (stateClass.simpleName != null) sb.append("title ${stateClass.simpleName}\n")
|
||||
ts.forEach { txDef ->
|
||||
val fromStatus = txDef.from?.toString() ?: "[*]"
|
||||
txDef.to.forEach { to ->
|
||||
val toStatus = (to ?: "[*]").toString()
|
||||
val cmd = txDef.cmd.simpleName
|
||||
val signer = txDef.signer?.toString() ?: "anyone involved"
|
||||
|
||||
sb.append("$fromStatus --> $toStatus : $cmd (by $signer)\n")
|
||||
}
|
||||
}
|
||||
sb.append("@enduml")
|
||||
return PrintedTransitionGraph(stateClass.simpleName ?: "", sb.toString())
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package io.cryptoblk.core
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
inline fun <reified T : ContractState> ServiceHub.queryStateByRef(ref: StateRef): StateAndRef<T> {
|
||||
val results = vaultService.queryBy<T>(QueryCriteria.VaultQueryCriteria(stateRefs = kotlin.collections.listOf(ref)))
|
||||
return results.states.firstOrNull() ?: throw IllegalArgumentException("State (type=${T::class}) corresponding to the reference $ref not found (or is spent).")
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand when a single party signs a TX and then returns a result that uses the signed TX (e.g. includes the TX id)
|
||||
*/
|
||||
@Suspendable
|
||||
fun <R> FlowLogic<R>.finalize(tx: TransactionBuilder, returnWithSignedTx: (stx: SignedTransaction) -> R): R {
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
subFlow(FinalityFlow(stx)) // it'll send to all participants in the state by default
|
||||
return returnWithSignedTx(stx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Corda fails when it tries to store the same attachment hash twice. And it's convenient to also do nothing if no attachment is provided.
|
||||
* This doesn't fix the same-attachment problem completely but should at least help in testing with the same file.
|
||||
*/
|
||||
fun TransactionBuilder.addAttachmentOnce(att: SecureHash?): TransactionBuilder {
|
||||
if (att == null) return this
|
||||
if (att !in this.attachments())
|
||||
this.addAttachment(att)
|
||||
return this
|
||||
}
|
||||
|
||||
// checks the instance type, so the cast is safe
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : ContractState> List<StateAndRef<ContractState>>.entriesOfType(): List<StateAndRef<T>> = this.mapNotNull {
|
||||
if (T::class.java.isInstance(it.state.data)) it as StateAndRef<T> else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when multiple objects may be created in the same transaction and need to refer to each other. If a state
|
||||
* contains this object as a reference to another object and txhash is null, the same txhash as of the containing/outer state
|
||||
* should be used. If txhash is not null, then this works exactly like StateRef.
|
||||
*
|
||||
* WARNING:
|
||||
* - if the outer state gets updated but its referenced state does not (in the same tx) then
|
||||
* - this reference in parent state must be updated with the real txhash: [StateRefHere.copyWith]
|
||||
* - otherwise it will be unresolvable (could be solved by disallowing copy on this)
|
||||
*/
|
||||
// do not make it a data class
|
||||
@CordaSerializable
|
||||
class StateRefHere(val txhash: SecureHash?, val index: Int) {
|
||||
constructor(ref: StateRef) : this(ref.txhash, ref.index)
|
||||
|
||||
fun toStateRef(parent: SecureHash) = StateRef(txhash ?: parent, index)
|
||||
|
||||
// not standard copy
|
||||
fun copyWith(parent: SecureHash): StateRefHere {
|
||||
return StateRefHere(txhash ?: parent, index)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is StateRefHere) return false
|
||||
return (this.txhash == other.txhash) && (this.index == other.index)
|
||||
}
|
||||
}
|
@ -0,0 +1,301 @@
|
||||
package io.cryptoblk.core
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Test
|
||||
|
||||
private val ALICE_ID = TestIdentity(CordaX500Name.parse("L=London,O=Alice Ltd,OU=Trade,C=GB"))
|
||||
private val BOB_ID = TestIdentity(CordaX500Name.parse("L=London,O=Bob Ltd,OU=Trade,C=GB"))
|
||||
private val BIGCORP_ID = TestIdentity(CordaX500Name.parse("L=New York,O=Bigcorp Ltd,OU=Trade,C=US"))
|
||||
private val ALICE = ALICE_ID.party
|
||||
private val BOB = BOB_ID.party
|
||||
private val BIG_CORP = BIGCORP_ID.party
|
||||
private val ALICE_PUBKEY = ALICE_ID.publicKey
|
||||
private val BOB_PUBKEY = BOB_ID.publicKey
|
||||
private val BIG_CORP_PUBKEY = BIGCORP_ID.publicKey
|
||||
|
||||
private enum class PartyRole {
|
||||
Adder,
|
||||
MultiplierAndRandomiser,
|
||||
Randomiser
|
||||
}
|
||||
|
||||
private class IntegerTestState(override val status: String) : StatusTrackingContractState<String, PartyRole> {
|
||||
override fun roleToParty(role: PartyRole): Party {
|
||||
return when (role) {
|
||||
PartyRole.Adder -> BIG_CORP
|
||||
PartyRole.MultiplierAndRandomiser -> BOB
|
||||
PartyRole.Randomiser -> ALICE
|
||||
}
|
||||
}
|
||||
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(ALICE, BOB, BIG_CORP)
|
||||
}
|
||||
|
||||
private sealed class Operations: TypeOnlyCommandData() {
|
||||
object Add1 : Operations()
|
||||
object Add10 : Operations()
|
||||
object Multiply2 : Operations()
|
||||
object Randomise : Operations()
|
||||
object Close : Operations()
|
||||
object AnotherCommand : Operations()
|
||||
}
|
||||
|
||||
class TestIntegerContract: Contract {
|
||||
companion object {
|
||||
private val fsTransitions = StatusTransitions(IntegerTestState::class,
|
||||
Operations.Add1.txDef(PartyRole.Adder, null, listOf("1")),
|
||||
Operations.Add1.txDef(PartyRole.Adder, "1", listOf("2")),
|
||||
Operations.Add10.txDef(PartyRole.Adder, "1", listOf("11")),
|
||||
Operations.Multiply2.txDef(PartyRole.MultiplierAndRandomiser, "2", listOf("4")),
|
||||
Operations.Multiply2.txDef(PartyRole.MultiplierAndRandomiser, "11", listOf("22")),
|
||||
Operations.Randomise.txDef(PartyRole.Randomiser, "2", listOf("8", "9", "1", "11")),
|
||||
Operations.Randomise.txDef(PartyRole.Randomiser, "11", listOf("2", "11", "4")),
|
||||
Operations.Randomise.txDef(PartyRole.MultiplierAndRandomiser, "11", listOf("22")),
|
||||
Operations.Close.txDef(PartyRole.Randomiser, "9", listOf(null))
|
||||
)
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
fsTransitions.verify(tx)
|
||||
}
|
||||
}
|
||||
|
||||
private class TestOwnedIntegerState(override val status: String): StatusTrackingContractState<String, PartyRole> {
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(ALICE, BOB)
|
||||
|
||||
override fun roleToParty(role: PartyRole): Party {
|
||||
return if (status == "0") ALICE else BOB
|
||||
}
|
||||
}
|
||||
|
||||
class TestOwnedIntegerContract: Contract {
|
||||
companion object {
|
||||
private val fsTransitions = StatusTransitions(TestOwnedIntegerState::class,
|
||||
Operations.Add1.txDef(PartyRole.Adder, null, listOf("0")),
|
||||
Operations.Add1.txDef(PartyRole.Adder, "0", listOf("1")),
|
||||
Operations.Add1.txDef(PartyRole.Adder, "1", listOf("2")),
|
||||
Operations.Multiply2.txDef(PartyRole.MultiplierAndRandomiser, "10", listOf("20")),
|
||||
Operations.Multiply2.txDef(PartyRole.Adder, "10", listOf("20")) // bug for the test
|
||||
)
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
fsTransitions.verify(tx)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusTransitionsTest {
|
||||
|
||||
companion object {
|
||||
private val integerContract = TestIntegerContract::class.qualifiedName!!
|
||||
private val ownedIntegerContract = TestOwnedIntegerContract::class.qualifiedName!!
|
||||
private val ledgerServices = MockServices(ALICE_ID, BOB_ID, BIGCORP_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `basic correct cases`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
output(integerContract, IntegerTestState("1"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Add1)
|
||||
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
output(integerContract, IntegerTestState("2"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Add1)
|
||||
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("2"))
|
||||
output(integerContract, IntegerTestState("9"))
|
||||
command(ALICE_PUBKEY, Operations.Randomise)
|
||||
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("9"))
|
||||
command(ALICE_PUBKEY, Operations.Close)
|
||||
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disallowed output`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
output(integerContract, IntegerTestState("3"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Add1)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disallowed command`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
output(integerContract, IntegerTestState("2"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Multiply2)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disallowed signer`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
output(integerContract, IntegerTestState("2"))
|
||||
command(ALICE_PUBKEY, Operations.Add1)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `irrelevant commands fail`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
output(integerContract, IntegerTestState("8"))
|
||||
command(ALICE_PUBKEY, Operations.AnotherCommand)
|
||||
|
||||
failsWith("at least one Command relevant")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple relevant commands accepted`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("11"))
|
||||
output(integerContract, IntegerTestState("22"))
|
||||
command(BOB_PUBKEY, Operations.Randomise)
|
||||
command(BOB_PUBKEY, Operations.Multiply2)
|
||||
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple relevant commands failed`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("2"))
|
||||
output(integerContract, IntegerTestState("4"))
|
||||
command(BOB_PUBKEY, Operations.Randomise)
|
||||
command(BOB_PUBKEY, Operations.Multiply2)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple inputs failed`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
input(integerContract, IntegerTestState("2"))
|
||||
output(integerContract, IntegerTestState("11"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Add10)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple outputs failed`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
output(integerContract, IntegerTestState("2"))
|
||||
output(integerContract, IntegerTestState("11"))
|
||||
command(BIG_CORP_PUBKEY, Operations.Add10)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `role change signer correct`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
output(ownedIntegerContract, TestOwnedIntegerState("0"))
|
||||
command(ALICE_PUBKEY, Operations.Add1)
|
||||
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
input(ownedIntegerContract, TestOwnedIntegerState("0"))
|
||||
output(ownedIntegerContract, TestOwnedIntegerState("1"))
|
||||
command(ALICE_PUBKEY, Operations.Add1)
|
||||
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
input(ownedIntegerContract, TestOwnedIntegerState("1"))
|
||||
output(ownedIntegerContract, TestOwnedIntegerState("2"))
|
||||
command(ALICE_PUBKEY, Operations.Add1)
|
||||
|
||||
fails()
|
||||
}
|
||||
transaction {
|
||||
input(ownedIntegerContract, TestOwnedIntegerState("1"))
|
||||
output(ownedIntegerContract, TestOwnedIntegerState("2"))
|
||||
command(BOB_PUBKEY, Operations.Add1)
|
||||
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple signers disallowed`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(ownedIntegerContract, TestOwnedIntegerState("10"))
|
||||
output(ownedIntegerContract, TestOwnedIntegerState("20"))
|
||||
command(ALICE_PUBKEY, Operations.Multiply2)
|
||||
|
||||
failsWith("Cannot have different signers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spend disallowed`() {
|
||||
ledgerServices.ledger {
|
||||
transaction {
|
||||
input(integerContract, IntegerTestState("1"))
|
||||
command(ALICE_PUBKEY, Operations.Close)
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ include 'experimental:sandbox'
|
||||
include 'experimental:quasar-hook'
|
||||
include 'experimental:kryo-hook'
|
||||
include 'experimental:blobinspector'
|
||||
include 'experimental:corda-utils'
|
||||
include 'test-common'
|
||||
include 'test-utils'
|
||||
include 'smoke-test-utils'
|
||||
|
Loading…
Reference in New Issue
Block a user