mirror of
https://github.com/corda/corda.git
synced 2025-06-23 01:19:00 +00:00
Added the additional Corda utility code with FSM-like transition contract checking
This commit is contained in:
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
|
||||||
|
```
|
||||||
|

|
||||||
|
|
||||||
|
## 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:quasar-hook'
|
||||||
include 'experimental:kryo-hook'
|
include 'experimental:kryo-hook'
|
||||||
include 'experimental:blobinspector'
|
include 'experimental:blobinspector'
|
||||||
|
include 'experimental:corda-utils'
|
||||||
include 'test-common'
|
include 'test-common'
|
||||||
include 'test-utils'
|
include 'test-utils'
|
||||||
include 'smoke-test-utils'
|
include 'smoke-test-utils'
|
||||||
|
Reference in New Issue
Block a user