forked from corda/corda
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added the additional Corda utility code with FSM-like transition cont…
…ract checking
- Loading branch information
Tomas Tauber
authored and
Mike Hearn
committed
May 21, 2018
1 parent
acefe42
commit 3a9fa50
Showing
6 changed files
with
630 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
105 changes: 105 additions & 0 deletions
105
experimental/corda-utils/src/main/kotlin/io/cryptoblk/core/StatusTransitions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
experimental/corda-utils/src/main/kotlin/io/cryptoblk/core/Utils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Oops, something went wrong.