mirror of
https://github.com/corda/corda.git
synced 2025-01-18 18:56:28 +00:00
Merged in mnesbit-building-transactions (pull request #571)
Created some examples to include in tutorial on building transactions.
This commit is contained in:
commit
df6fc69cc6
@ -1,6 +1,7 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
@ -0,0 +1,271 @@
|
||||
package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.signWithECDSA
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.flows.ResolveTransactionsFlow
|
||||
import java.util.*
|
||||
|
||||
object FxTransactionDemoTutorial {
|
||||
// Would normally be called by custom service init in a CorDapp
|
||||
fun registerFxProtocols(pluginHub: PluginServiceHub) {
|
||||
pluginHub.registerFlowInitiator(ForeignExchangeFlow::class, ::ForeignExchangeRemoteFlow)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FxRequest(val tradeId: String,
|
||||
val amount: Amount<Issued<Currency>>,
|
||||
val owner: Party,
|
||||
val counterparty: Party,
|
||||
val notary: Party? = null)
|
||||
|
||||
private data class FxResponse(val inputs: List<StateAndRef<Cash.State>>,
|
||||
val outputs: List<Cash.State>)
|
||||
|
||||
// DOCSTART 1
|
||||
// This is equivalent to the VaultService.generateSpend
|
||||
// Which is brought here to make the filtering logic more visible in the example
|
||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// Collect cash type inputs
|
||||
val cashStates = serviceHub.vaultService.currentVault.statesOfType<Cash.State>()
|
||||
// extract our key identity for convenience
|
||||
val ourKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
// Filter down to our own cash states with right currency and issuer
|
||||
val suitableCashStates = cashStates.filter {
|
||||
val state = it.state.data
|
||||
(state.owner == ourKey)
|
||||
&& (state.amount.token == amountRequired.token)
|
||||
}
|
||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
||||
var remaining = amountRequired.quantity
|
||||
// We will need all of the inputs to be on the same notary.
|
||||
// For simplicity we just filter on the first notary encountered
|
||||
// A production quality flow would need to migrate notary if the
|
||||
// the amounts were not sufficient in any one notary
|
||||
val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary
|
||||
|
||||
val inputsList = mutableListOf<StateAndRef<Cash.State>>()
|
||||
// Iterate over filtered cash states to gather enough to pay
|
||||
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) {
|
||||
inputsList += cash
|
||||
if (remaining <= cash.state.data.amount.quantity) {
|
||||
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
|
||||
}
|
||||
remaining -= cash.state.data.amount.quantity
|
||||
}
|
||||
throw IllegalStateException("Insufficient funds")
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): FxResponse {
|
||||
// Create amount with correct issuer details
|
||||
val sellAmount = request.amount
|
||||
|
||||
// DOCSTART 2
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevents issues when
|
||||
// the flow is suspended.
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
|
||||
|
||||
// Build and an output state for the counterparty
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey, null)
|
||||
|
||||
if (residual > 0L) {
|
||||
// Build an output state for the residual change back to us
|
||||
val residualAmount = Amount(residual, sellAmount.token)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey, null)
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
|
||||
} else {
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput))
|
||||
}
|
||||
// DOCEND 2
|
||||
}
|
||||
|
||||
// A flow representing creating a transaction that
|
||||
// carries out exchange of cash assets.
|
||||
class ForeignExchangeFlow(val tradeId: String,
|
||||
val baseCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val quoteCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val baseCurrencyBuyer: Party,
|
||||
val baseCurrencySeller: Party) : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
// Select correct sides of the Fx exchange to query for.
|
||||
// Specifically we own the assets we wish to sell.
|
||||
// Also prepare the other side query
|
||||
val (localRequest, remoteRequest) = if (baseCurrencySeller == serviceHub.myInfo.legalIdentity) {
|
||||
val local = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
val remote = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
Pair(local, remote)
|
||||
} else if (baseCurrencyBuyer == serviceHub.myInfo.legalIdentity) {
|
||||
val local = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
val remote = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
Pair(local, remote)
|
||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val ourStates = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
||||
|
||||
// identify the notary for our states
|
||||
val notary = ourStates.inputs.first().state.notary
|
||||
// ensure request to other side is for a consistent notary
|
||||
val remoteRequestWithNotary = remoteRequest.copy(notary = notary)
|
||||
|
||||
// Send the request to the counterparty to verify and call their version of prepareOurInputsAndOutputs
|
||||
// Then they can return their candidate states
|
||||
val theirStates = sendAndReceive<FxResponse>(remoteRequestWithNotary.owner, remoteRequestWithNotary).unwrap {
|
||||
require(it.inputs.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner.owningKey }) {
|
||||
"The inputs are not owned by the correct counterparty"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Inputs not of the correct currency"
|
||||
}
|
||||
require(it.outputs.all { it.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Outputs not of the correct currency"
|
||||
}
|
||||
require(it.inputs.map { it.state.data.amount.quantity }.sum()
|
||||
>= remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided inputs don't provide sufficient funds"
|
||||
}
|
||||
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity.owningKey }.
|
||||
map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided outputs don't provide the request quantity"
|
||||
}
|
||||
// Download their inputs chains to validate that they are OK
|
||||
val dependencyTxIDs = it.inputs.map { it.ref.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, remoteRequestWithNotary.owner))
|
||||
|
||||
it // return validated response
|
||||
}
|
||||
|
||||
// having collated the data create the full transaction.
|
||||
val signedTransaction = buildTradeProposal(ourStates, theirStates)
|
||||
|
||||
// pass transaction details to the counterparty to revalidate and confirm with a signature
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(remoteRequestWithNotary.owner, signedTransaction).unwrap {
|
||||
val withNewSignature = signedTransaction + it
|
||||
// check all signatures are present except the notary
|
||||
withNewSignature.verifySignatures(withNewSignature.tx.notary!!.owningKey)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
withNewSignature.tx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
withNewSignature // return the almost complete transaction
|
||||
}
|
||||
|
||||
// Initiate the standard protocol to notarise and distribute to the involved parties
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(baseCurrencyBuyer, baseCurrencySeller)))
|
||||
|
||||
return allPartySignedTx.id
|
||||
}
|
||||
|
||||
// DOCSTART 3
|
||||
private fun buildTradeProposal(ourStates: FxResponse, theirStates: FxResponse): SignedTransaction {
|
||||
// This is the correct way to create a TransactionBuilder,
|
||||
// do not construct directly.
|
||||
// We also set the notary to match the input notary
|
||||
val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary)
|
||||
|
||||
// Add the move commands and key to indicate all the respective owners and need to sign
|
||||
val ourSigners = ourStates.inputs.map { it.state.data.owner }.toSet()
|
||||
val theirSigners = theirStates.inputs.map { it.state.data.owner }.toSet()
|
||||
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
|
||||
|
||||
// Build and add the inputs and outputs
|
||||
builder.withItems(*ourStates.inputs.toTypedArray())
|
||||
builder.withItems(*theirStates.inputs.toTypedArray())
|
||||
builder.withItems(*ourStates.outputs.toTypedArray())
|
||||
builder.withItems(*theirStates.outputs.toTypedArray())
|
||||
|
||||
// We have already validated their response and trust our own data
|
||||
// so we can sign
|
||||
builder.signWith(serviceHub.legalIdentityKey)
|
||||
// create a signed transaction, but pass false as parameter, because we know it is not fully signed
|
||||
val signedTransaction = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
return signedTransaction
|
||||
}
|
||||
// DOCEND 3
|
||||
}
|
||||
|
||||
class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Initial receive from remote party
|
||||
val request = receive<FxRequest>(source).unwrap {
|
||||
// We would need to check that this is a known trade ID here!
|
||||
// Also that the amounts and source are correct with the trade details.
|
||||
// In a production system there would be other Corda contracts tracking
|
||||
// the lifecycle of the Fx trades which would be included in the transaction
|
||||
|
||||
// Check request is for us
|
||||
require(serviceHub.myInfo.legalIdentity == it.owner) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
require(source == it.counterparty) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
it // return validated request
|
||||
}
|
||||
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// the flow is suspended.
|
||||
val ourResponse = prepareOurInputsAndOutputs(serviceHub, request)
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val proposedTrade = sendAndReceive<SignedTransaction>(source, ourResponse).unwrap {
|
||||
val wtx = it.tx
|
||||
// check all signatures are present except our own and the notary
|
||||
it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, wtx.notary!!.owningKey)
|
||||
|
||||
// We need to fetch their complete input states and dependencies so that verify can operate
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
it // return the SignedTransaction
|
||||
}
|
||||
|
||||
// assuming we have completed state and business level validation we can sign the trade
|
||||
val ourSignature = serviceHub.legalIdentityKey.signWithECDSA(proposedTrade.id)
|
||||
|
||||
// send the other side our signature.
|
||||
send(source, ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, source))
|
||||
}
|
||||
}
|
@ -0,0 +1,265 @@
|
||||
package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.FinalityFlow
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
|
||||
object WorkflowTransactionBuildTutorial {
|
||||
// Would normally be called by custom service init in a CorDapp
|
||||
fun registerWorkflowProtocols(pluginHub: PluginServiceHub) {
|
||||
pluginHub.registerFlowInitiator(SubmitCompletionFlow::class, ::RecordCompletionFlow)
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
// Helper method to access the StorageService and expand a StateRef into a StateAndRef
|
||||
fun <T : ContractState> ServiceHub.toStateAndRef(ref: StateRef): StateAndRef<T> {
|
||||
return storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
||||
}
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
||||
inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||
val original = toStateAndRef<T>(ref)
|
||||
return linearHeads.get(original.state.data.linearId)!!
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// Minimal state model of a manual approval process
|
||||
enum class WorkflowState {
|
||||
NEW,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
||||
* It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it.
|
||||
*/
|
||||
data class TradeApprovalContract(override val legalContractReference: SecureHash = SecureHash.sha256("Example of workflow type transaction")) : Contract {
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details
|
||||
class Completed : TypeOnlyCommandData(), Commands // Record match
|
||||
}
|
||||
|
||||
/**
|
||||
* Truly minimal state that just records a tradeId string and the parties involved.
|
||||
*/
|
||||
data class State(val tradeId: String,
|
||||
val source: Party,
|
||||
val counterparty: Party,
|
||||
val state: WorkflowState = WorkflowState.NEW,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(tradeId),
|
||||
override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState {
|
||||
|
||||
val parties: List<Party> get() = listOf(source, counterparty)
|
||||
override val participants: List<CompositeKey> get() = parties.map { it.owningKey }
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.containsAny(ourKeys) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The verify method locks down the allowed transactions to contain just a single proposal being
|
||||
* created/modified and the only modification allowed is to the state field.
|
||||
*/
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<TradeApprovalContract.Commands>()
|
||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||
when (command.value) {
|
||||
is Commands.Issue -> {
|
||||
requireThat {
|
||||
"Issue of new WorkflowContract must not include any inputs" by (tx.inputs.isEmpty())
|
||||
"Issue of new WorkflowContract must be in a unique transaction" by (tx.outputs.size == 1)
|
||||
}
|
||||
val issued = tx.outputs.get(0) as TradeApprovalContract.State
|
||||
requireThat {
|
||||
"Issue requires the source Party as signer" by (command.signers.contains(issued.source.owningKey))
|
||||
"Initial Issue state must be NEW" by (issued.state == WorkflowState.NEW)
|
||||
}
|
||||
}
|
||||
is Commands.Completed -> {
|
||||
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
|
||||
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
|
||||
for (group in stateGroups) {
|
||||
val before = group.inputs.single()
|
||||
val after = group.outputs.single()
|
||||
requireThat {
|
||||
"Only a non-final trade can be modified" by (before.state == WorkflowState.NEW)
|
||||
"Output must be a final state" by (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
|
||||
"Completed command can only change state" by (before == after.copy(state = before.state))
|
||||
"Completed command requires the source Party as signer" by (command.signers.contains(before.source.owningKey))
|
||||
"Completed command requires the counterparty as signer" by (command.signers.contains(before.counterparty.owningKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unrecognised Command $command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to create a workflow state, sign and notarise it.
|
||||
* The protocol then sends a copy to the other node. We don't require the other party to sign
|
||||
* as their approval/rejection is to follow.
|
||||
*/
|
||||
class SubmitTradeApprovalFlow(val tradeId: String,
|
||||
val counterparty: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// Manufacture an initial state
|
||||
val tradeProposal = TradeApprovalContract.State(
|
||||
tradeId,
|
||||
serviceHub.myInfo.legalIdentity,
|
||||
counterparty)
|
||||
// identify a notary. This might also be done external to the flow
|
||||
val notary = serviceHub.networkMapCache.getAnyNotary()
|
||||
// Create the TransactionBuilder and populate with the new state.
|
||||
val tx = TransactionType.
|
||||
General.
|
||||
Builder(notary).
|
||||
withItems(tradeProposal,
|
||||
Command(TradeApprovalContract.Commands.Issue(),
|
||||
listOf(tradeProposal.source.owningKey)))
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to a SignedTransaction that we can send to the notary
|
||||
val signedTx = tx.toSignedTransaction(false)
|
||||
// Run the FinalityFlow to notarise and distribute the SignedTransaction to the counterparty
|
||||
subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty)))
|
||||
// Return the initial state
|
||||
return signedTx.tx.outRef<TradeApprovalContract.State>(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to complete a proposal submitted by another party and ensure both nodes
|
||||
* end up with a fully signed copy of the state either as APPROVED, or REJECTED
|
||||
*/
|
||||
class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
init {
|
||||
require(verdict in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED)) {
|
||||
"Verdict must be a final state"
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// Pull in the latest Vault version of the StateRef as a full StateAndRef
|
||||
val latestRecord = serviceHub.latest<TradeApprovalContract.State>(ref)
|
||||
// Check the protocol hasn't already been run
|
||||
require(latestRecord.ref == ref) {
|
||||
"Input trade $ref is not latest version $latestRecord"
|
||||
}
|
||||
// Require that the state is still modifiable
|
||||
require(latestRecord.state.data.state == WorkflowState.NEW) {
|
||||
"Input trade not modifiable ${latestRecord.state.data.state}"
|
||||
}
|
||||
// Check we are the correct Party to run the protocol. Note they will counter check this too.
|
||||
require(latestRecord.state.data.counterparty == serviceHub.myInfo.legalIdentity) {
|
||||
"The counterparty must give the verdict"
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
// Modify the state field for new output. We use copy, to ensure no other modifications.
|
||||
// It is especially important for a LinearState that the linearId is copied across,
|
||||
// not accidentally assigned a new random id.
|
||||
val newState = latestRecord.state.data.copy(state = verdict)
|
||||
|
||||
// We have to use the original notary for the new transaction
|
||||
val notary = latestRecord.state.notary
|
||||
|
||||
// Get and populate the new TransactionBuilder
|
||||
// To destroy the old proposal state and replace with the new completion state.
|
||||
// Also add the Completed command with keys of all parties to signal the Tx purpose
|
||||
// to the Contract verify method.
|
||||
val tx = TransactionType.
|
||||
General.
|
||||
Builder(notary).
|
||||
withItems(
|
||||
latestRecord,
|
||||
newState,
|
||||
Command(TradeApprovalContract.Commands.Completed(),
|
||||
listOf(serviceHub.myInfo.legalIdentity.owningKey, latestRecord.state.data.source.owningKey)))
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can sign this transaction immediately as we have already checked all the fields and the decision
|
||||
// is ultimately a manual one from the caller.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to SignedTransaction we can pass around certain that it cannot be modified.
|
||||
val selfSignedTx = tx.toSignedTransaction(false)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(newState.source, selfSignedTx).unwrap {
|
||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||
val agreedTx = selfSignedTx + it
|
||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||
// Also that the only missing signature is from teh Notary
|
||||
agreedTx.verifySignatures(notary.owningKey)
|
||||
// Recheck the data of the transaction. Note we run toLedgerTransaction on the WireTransaction
|
||||
// as we do not have all the signature.
|
||||
agreedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// return the SignedTransaction to notarise
|
||||
agreedTx
|
||||
}
|
||||
// DOCSTART 4
|
||||
// Run the FinalityFlow to notarise and distribute the completed transaction.
|
||||
subFlow(FinalityFlow(allPartySignedTx,
|
||||
setOf(latestRecord.state.data.source, latestRecord.state.data.counterparty)))
|
||||
|
||||
// DOCEND 4
|
||||
// Return back the details of the completed state/transaction.
|
||||
return allPartySignedTx.tx.outRef<TradeApprovalContract.State>(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to receive the final decision on a proposal.
|
||||
* Then after checking to sign it and eventually store the fully notarised
|
||||
* transaction to the ledger.
|
||||
*/
|
||||
class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
// DOCSTART 3
|
||||
// First we receive the verdict transaction signed by their single key
|
||||
val completeTx = receive<SignedTransaction>(source).unwrap {
|
||||
// Check the transaction is signed apart from our own key and the notary
|
||||
val wtx = it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, it.tx.notary!!.owningKey)
|
||||
// Check the transaction data is correctly formed
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
// Confirm that this is the expected type of transaction
|
||||
require(wtx.commands.single().value is TradeApprovalContract.Commands.Completed) {
|
||||
"Transaction must represent a workflow completion"
|
||||
}
|
||||
// Check the context dependent parts of the transaction as the
|
||||
// Contract verify method must not use serviceHub queries.
|
||||
val state = wtx.outRef<TradeApprovalContract.State>(0)
|
||||
require(state.state.data.source == serviceHub.myInfo.legalIdentity) {
|
||||
"Proposal not one of our original proposals"
|
||||
}
|
||||
require(state.state.data.counterparty == source) {
|
||||
"Proposal not for sent from correct source"
|
||||
}
|
||||
it
|
||||
}
|
||||
// DOCEND 3
|
||||
// Having verified the SignedTransaction passed to us we can sign it too
|
||||
val ourSignature = serviceHub.legalIdentityKey.signWithECDSA(completeTx.tx.id)
|
||||
// Send our signature to the other party.
|
||||
send(source, ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.corda.docs
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FxTransactionBuildTutorialTest {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var nodeA: MockNetwork.MockNode
|
||||
lateinit var nodeB: MockNetwork.MockNode
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
net = MockNetwork(threadPerNode = true)
|
||||
notaryNode = net.createNode(
|
||||
legalName = DUMMY_NOTARY.name,
|
||||
keyPair = DUMMY_NOTARY_KEY,
|
||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
println("Close DB")
|
||||
net.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
val flowHandle1 = nodeA.services.startFlow(CashFlow(CashCommand.IssueCash(DOLLARS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Using NodeB as Issuer create some pounds.
|
||||
val flowHandle2 = nodeB.services.startFlow(CashFlow(CashCommand.IssueCash(POUNDS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeB.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
// Wait for flow to come to an end and print
|
||||
flowHandle2.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
||||
val done2 = SettableFuture.create<Unit>()
|
||||
val done3 = SettableFuture.create<Unit>()
|
||||
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
||||
done2.set(Unit)
|
||||
}
|
||||
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
||||
done3.set(Unit)
|
||||
}
|
||||
// Now run the actual Fx exchange
|
||||
val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1",
|
||||
POUNDS(100).issuedBy(nodeB.info.legalIdentity.ref(0x01)),
|
||||
DOLLARS(200).issuedBy(nodeA.info.legalIdentity.ref(0x01)),
|
||||
nodeA.info.legalIdentity,
|
||||
nodeB.info.legalIdentity))
|
||||
// wait for the flow to finish and the vault updates to be done
|
||||
doIt.resultFuture.getOrThrow()
|
||||
done2.get()
|
||||
done3.get()
|
||||
subs2.unsubscribe()
|
||||
subs3.unsubscribe()
|
||||
// Check the final balances
|
||||
val balancesA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.vaultService.cashBalances
|
||||
}
|
||||
val balancesB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.vaultService.cashBalances
|
||||
}
|
||||
println("BalanceA\n" + balancesA)
|
||||
println("BalanceB\n" + balancesB)
|
||||
// Verify the transfers occurred as expected
|
||||
assertEquals(POUNDS(100), balancesA[GBP])
|
||||
assertEquals(DOLLARS(1000 - 200), balancesA[USD])
|
||||
assertEquals(POUNDS(1000 - 100), balancesB[GBP])
|
||||
assertEquals(DOLLARS(200), balancesB[USD])
|
||||
}
|
||||
|
||||
private fun printBalances() {
|
||||
// Print out the balances
|
||||
databaseTransaction(nodeA.database) {
|
||||
println("BalanceA\n" + nodeA.services.vaultService.cashBalances)
|
||||
}
|
||||
databaseTransaction(nodeB.database) {
|
||||
println("BalanceB\n" + nodeB.services.vaultService.cashBalances)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package net.corda.docs
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WorkflowTransactionBuildTutorialTest {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var nodeA: MockNetwork.MockNode
|
||||
lateinit var nodeB: MockNetwork.MockNode
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
||||
private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||
val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
||||
return linearHeads.get(original.state.data.linearId)!!
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
net = MockNetwork(threadPerNode = true)
|
||||
notaryNode = net.createNode(
|
||||
legalName = DUMMY_NOTARY.name,
|
||||
keyPair = DUMMY_NOTARY_KEY,
|
||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
println("Close DB")
|
||||
net.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run workflow to completion`() {
|
||||
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
||||
val done1 = SettableFuture.create<Unit>()
|
||||
val subs1 = nodeB.services.vaultService.updates.subscribe {
|
||||
done1.set(Unit)
|
||||
}
|
||||
// Kick of the proposal flow
|
||||
val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.legalIdentity))
|
||||
// Wait for the flow to finish
|
||||
val proposalRef = flow1.resultFuture.getOrThrow()
|
||||
// Wait for NodeB to include it's copy in the vault
|
||||
done1.get()
|
||||
subs1.unsubscribe()
|
||||
// Fetch the latest copy of the state from both nodes
|
||||
val latestFromA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
val latestFromB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
// Confirm the state as as expected
|
||||
assertEquals(WorkflowState.NEW, proposalRef.state.data.state)
|
||||
assertEquals("1234", proposalRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.legalIdentity, proposalRef.state.data.source)
|
||||
assertEquals(nodeB.info.legalIdentity, proposalRef.state.data.counterparty)
|
||||
assertEquals(proposalRef, latestFromA)
|
||||
assertEquals(proposalRef, latestFromB)
|
||||
// Setup a vault subscriber to pause until the final update is in NodeA and NodeB
|
||||
val done2 = SettableFuture.create<Unit>()
|
||||
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
||||
done2.set(Unit)
|
||||
}
|
||||
val done3 = SettableFuture.create<Unit>()
|
||||
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
||||
done3.set(Unit)
|
||||
}
|
||||
// Run the manual completion flow from NodeB
|
||||
val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
// wait for the flow to end
|
||||
val completedRef = flow2.resultFuture.getOrThrow()
|
||||
// wait for the vault updates to stabilise
|
||||
done2.get()
|
||||
done3.get()
|
||||
subs2.unsubscribe()
|
||||
subs3.unsubscribe()
|
||||
// Fetch the latest copies from the vault
|
||||
val finalFromA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
val finalFromB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
// Confirm the state is as expected
|
||||
assertEquals(WorkflowState.APPROVED, completedRef.state.data.state)
|
||||
assertEquals("1234", completedRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.legalIdentity, completedRef.state.data.source)
|
||||
assertEquals(nodeB.info.legalIdentity, completedRef.state.data.counterparty)
|
||||
assertEquals(completedRef, finalFromA)
|
||||
assertEquals(completedRef, finalFromB)
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ Read on to learn:
|
||||
tutorial-test-dsl
|
||||
tutorial-integration-testing
|
||||
tutorial-clientrpc-api
|
||||
tutorial-building-transactions
|
||||
flow-state-machines
|
||||
flow-testing
|
||||
running-a-notary
|
||||
|
321
docs/source/tutorial-building-transactions.rst
Normal file
321
docs/source/tutorial-building-transactions.rst
Normal file
@ -0,0 +1,321 @@
|
||||
Building Transactions
|
||||
=====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Understanding and implementing transactions in Corda is key to building
|
||||
and implementing real world smart contracts. It is only through
|
||||
construction of valid Corda transactions containing appropriate data
|
||||
that nodes on the ledger can map real world business objects into a
|
||||
shared digital view of the data in the Corda ledger. More importantly as
|
||||
the developer of new smart contracts it is the code which determines
|
||||
what data is well formed and what data should be rejected as mistakes,
|
||||
or to prevent malicious activity. This document details some of the
|
||||
considerations and APIs used to when constructing transactions as part
|
||||
of a flow.
|
||||
|
||||
The Basic Lifecycle Of Transactions
|
||||
-----------------------------------
|
||||
|
||||
Transactions in Corda are constructed in stages and contain a number of
|
||||
elements. In particular a transaction’s core data structure is the
|
||||
``net.corda.core.transactions.WireTransaction``, which is usually
|
||||
manipulated via a
|
||||
``net.corda.core.contracts.General.TransactionBuilder`` and contains:
|
||||
|
||||
1. A set of Input state references that will be consumed by the final
|
||||
accepted transaction.
|
||||
|
||||
2. A set of Output states to create/replace the consumed states and thus
|
||||
become the new latest versions of data on the ledger.
|
||||
|
||||
3. A set of ``Attachment`` items which can contain legal documents, contract
|
||||
code, or private encrypted sections as an extension beyond the native
|
||||
contract states.
|
||||
|
||||
4. A set of ``Command`` items which give a context to the type of ledger
|
||||
transition that is encoded in the transaction. Also each command has an
|
||||
associated set of signer keys, which will be required to sign the
|
||||
transaction.
|
||||
|
||||
5. A signers list, which is populated by the ``TransactionBuilder`` to
|
||||
be the union of the signers on the individual Command objects.
|
||||
|
||||
6. A notary identity to specify the Notary node which is tracking the
|
||||
state consumption. (If the input states are registered with different
|
||||
notary nodes the flow will have to insert additional ``NotaryChange``
|
||||
transactions to migrate the states across to a consistent notary node,
|
||||
before being allowed to mutate any states.)
|
||||
|
||||
7. Optionally a timestamp that can used in the Notary to time bound the
|
||||
period in which the proposed transaction stays valid.
|
||||
|
||||
Typically, the ``WireTransaction`` should be regarded as a proposal and
|
||||
may need to be exchanged back and forth between parties before it can be
|
||||
fully populated. This is an immediate consequence of the Corda privacy
|
||||
model, which means that the input states are likely to be unknown to the
|
||||
other node.
|
||||
|
||||
Once the proposed data is fully populated the flow code should freeze
|
||||
the ``WireTransaction`` and form a ``SignedTransaction``. This is key to
|
||||
the ledger agreement process, as once a flow has attached a node’s
|
||||
signature it has stated that all details of the transaction are
|
||||
acceptable to it. A flow should take care not to attach signatures to
|
||||
intermediate data, which might be maliciously used to construct a
|
||||
different ``SignedTransaction``. For instance in a foreign exchange
|
||||
scenario we shouldn't send a ``SignedTransaction`` with only our sell
|
||||
side populated as that could be used to take the money without the
|
||||
expected return of the other currency. Also, it is best practice for
|
||||
flows to receive back the ``DigitalSignature.WithKey`` of other parties
|
||||
rather than a full ``SignedTransaction`` objects, because otherwise we
|
||||
have to separately check that this is still the same
|
||||
``SignedTransaction`` and not a malicious substitute.
|
||||
|
||||
The final stage of committing the transaction to the ledger is to
|
||||
notarise the ``SignedTransaction``, distribute this to all appropriate
|
||||
parties and record the data into the ledger. These actions are best
|
||||
delegated to the ``FinalityFlow``, rather than calling the inidividual
|
||||
steps manually. However, do note that the final broadcast to the other
|
||||
nodes is asynchronous, so care must be used in unit testing to
|
||||
correctly await the Vault updates.
|
||||
|
||||
Gathering Inputs
|
||||
----------------
|
||||
|
||||
One of the first steps to forming a transaction is gathering the set of
|
||||
input references. This process will clearly vary according to the nature
|
||||
of the business process being captured by the smart contract and the
|
||||
parameterised details of the request. However, it will generally involve
|
||||
searching the Vault via the ``VaultService`` interface on the
|
||||
``ServiceHub`` to locate the input states.
|
||||
|
||||
To give a few more specific details consider two simplified real world
|
||||
scenarios. First, a basic foreign exchange Cash transaction. This
|
||||
transaction needs to locate a set of funds to exchange. A flow
|
||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``.
|
||||
Second, a simple business model in which parties manually accept, or
|
||||
reject each other's trade proposals which is implemented in
|
||||
``WorkflowTransactionBuildTutorial.kt``. To run and explore these
|
||||
examples using the IntelliJ IDE one can run/step the respective unit
|
||||
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
||||
``WorkflowTransactionBuildTutorialTest.kt``, which drive the flows as
|
||||
part of a simulated in-memory network of nodes. When creating the
|
||||
IntelliJ run configuration for these unit test set the workspace
|
||||
points to the root ``r3prototyping`` folder and add
|
||||
``-javaagent:lib/quasar.jar`` to the VM options, so that the ``Quasar``
|
||||
instrumentation is correctly configured.
|
||||
|
||||
For the Cash transaction let’s assume the cash resources are using the
|
||||
standard ``CashState`` in the ``:financial`` Gradle module. The Cash
|
||||
contract uses ``FungibleAsset`` states to model holdings of
|
||||
interchangeable assets and allow the split/merge and summing of
|
||||
states to meet a contractual obligation. We would normally use the
|
||||
``generateSpend`` method on the ``VaultService`` to gather the required
|
||||
amount of cash into a ``TransactionBuilder``, set the outputs and move
|
||||
command. However, to elucidate more clearly example flow code is shown
|
||||
here that will manually carry out the inputs queries using the lower
|
||||
level ``VaultService``.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
As a foreign exchange transaction we expect an exchange of two
|
||||
currencies, so we will also require a set of input states from the other
|
||||
counterparty. However, the Corda privacy model means we do not know the
|
||||
other node’s states. Our flow must therefore negotiate with the other
|
||||
node for them to carry out a similar query and populate the inputs (See
|
||||
the ``ForeignExchangeFlow`` for more details of the exchange). Having
|
||||
identified a set of Input ``StateRef`` items we can then create the
|
||||
output as discussed below.
|
||||
|
||||
For the trade approval flow we need to implement a simple workflow
|
||||
pattern. We start by recording the unconfirmed trade details in a state
|
||||
object implementing the ``LinearState`` interface. One field of this
|
||||
record is used to map the business workflow to an enumerated state.
|
||||
Initially the initiator creates a new state object which receives a new
|
||||
``UniqueIdentifier`` in its ``linearId`` property and a starting
|
||||
workflow state of ``NEW``. The ``Contract.verify`` method is written to
|
||||
allow the initiator to sign this initial transaction and send it to the
|
||||
other party. This pattern ensures that a permanent copy is recorded on
|
||||
both ledgers for audit purposes, but the state is prevented from being
|
||||
maliciously put in an approved state. The subsequent workflow steps then
|
||||
follow with transactions that consume the state as inputs on one side
|
||||
and output a new version with whatever state updates, or amendments
|
||||
match to the business process, the ``linearId`` being preserved across
|
||||
the changes. Attached ``Command`` objects help the verify method
|
||||
restrict changes to appropriate fields and signers at each step in the
|
||||
workflow. In this it is typical to have both parties sign the change
|
||||
transactions, but it can be valid to allow unilateral signing, if for instance
|
||||
one side could block a rejection. Commonly the manual initiator of these
|
||||
workflows will query the Vault for states of the right contract type and
|
||||
in the right workflow state over the RPC interface. The RPC will then
|
||||
initiate the relevant flow using ``StateRef``, or ``linearId`` values as
|
||||
parameters to the flow to identify the states being operated upon. Thus
|
||||
code to gather the latest input state would be:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// Pull in the latest Vault version of the StateRef as a full StateAndRef
|
||||
val latestRecord = serviceHub.latest<TradeApprovalContract.State>(ref)
|
||||
|
||||
|
||||
Generating Commands
|
||||
-------------------
|
||||
|
||||
For the commands that will be added to the transaction, these will need
|
||||
to correctly reflect the task at hand. These must match because inside
|
||||
the ``Contract.verify`` method the command will be used to select the
|
||||
validation code path. The ``Contract.verify`` method will then restrict
|
||||
the allowed contents of the transaction to reflect this context. Typical
|
||||
restrictions might include that the input cash amount must equal the
|
||||
output cash amount, or that a workflow step is only allowed to change
|
||||
the status field. Sometimes, the command may capture some data too e.g.
|
||||
the foreign exchange rate, or the identity of one party, or the StateRef
|
||||
of the specific input that originates the command in a bulk operation.
|
||||
This data will be used to further aid the ``Contract.verify``, because
|
||||
to ensure consistent, secure and reproducible behaviour in a distributed
|
||||
environment the ``Contract.verify``, transaction is the only allowed to
|
||||
use the content of the transaction to decide validity.
|
||||
|
||||
Another essential requirement for commands is that the correct set of
|
||||
``CompositeKeys`` are added to the Command on the builder, which will be
|
||||
used to form the set of required signers on the final validated
|
||||
transaction. These must correctly align with the expectations of the
|
||||
``Contract.verify`` method, which should be written to defensively check
|
||||
this. In particular, it is expected that at minimum the owner of an
|
||||
asset would have to be signing to permission transfer of that asset. In
|
||||
addition, other signatories will often be required e.g. an Oracle
|
||||
identity for an Oracle command, or both parties when there is an
|
||||
exchange of assets.
|
||||
|
||||
Generating Outputs
|
||||
------------------
|
||||
|
||||
Having located a set of ``StateAndRefs`` as the transaction inputs, the
|
||||
flow has to generate the output states. Typically, this is a simple call
|
||||
to the Kotlin ``copy`` method to modify the few fields that will
|
||||
transitioned in the transaction. The contract code may provide a
|
||||
``generateXXX`` method to help with this process if the task is more
|
||||
complicated. With a workflow state a slightly modified copy state is
|
||||
usually sufficient, especially as it is expected that we wish to preserve
|
||||
the ``linearId`` between state revisions, so that Vault queries can find
|
||||
the latest revision.
|
||||
|
||||
For fungible contract states such as ``Cash`` it is common to distribute
|
||||
and split the total amount e.g. to produce a remaining balance output
|
||||
state for the original owner when breaking up a large amount input
|
||||
state. Remember that the result of a successful transaction is always to
|
||||
fully consume/spend the input states, so this is required to conserve
|
||||
the total cash. For example from the demo code:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
Building the WireTransaction
|
||||
----------------------------
|
||||
|
||||
Having gathered all the ingredients for the transaction we now need to
|
||||
use a ``TransactionBuilder`` to construct the full ``WireTransaction``.
|
||||
The initial ``TransactionBuilder`` should be created by calling the
|
||||
``TransactionType.General.Builder`` method. (The other
|
||||
``TransactionBuilder`` implementation is only used for the ``NotaryChange`` flow where
|
||||
``ContractStates`` need moving to a different Notary.) At this point the
|
||||
Notary to associate with the states should be recorded. Then we keep
|
||||
adding inputs, outputs, commands and attachments to fill the
|
||||
transaction. Examples of this process are:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
|
||||
Completing the SignedTransaction
|
||||
--------------------------------
|
||||
|
||||
Having created an initial ``WireTransaction`` and converted this to an
|
||||
initial ``SignedTransaction`` the process of verifying and forming a
|
||||
full ``SignedTransaction`` begins and then completes with the
|
||||
notarisation. In practice this is a relatively stereotypical process,
|
||||
because assuming the ``WireTransaction`` is correctly constructed the
|
||||
verification should be immediate. However, it is also important to
|
||||
recheck the business details of any data received back from an external
|
||||
node, because a malicious party could always modify the contents before
|
||||
returning the transaction. Each remote flow should therefore check as
|
||||
much as possible of the initial ``SignedTransaction`` inside the ``unwrap`` of
|
||||
the receive before agreeing to sign. Any issues should immediately throw
|
||||
an exception to abort the flow. Similarly the originator, should always
|
||||
apply any new signatures to its original proposal to ensure the contents
|
||||
of the transaction has not been altered by the remote parties.
|
||||
|
||||
The typical code therefore checks the received ``SignedTransaction``
|
||||
using the ``verifySignatures`` method, but excluding itself, the notary
|
||||
and any other parties yet to apply their signature. The contents of the
|
||||
``WireTransaction`` inside the ``SignedTransaction`` should be fully
|
||||
verified further by expanding with ``toLedgerTransaction`` and calling
|
||||
``verify``. Further context specific and business checks should then be
|
||||
made, because the ``Contract.verify`` is not allowed to access external
|
||||
context. For example the flow may need to check that the parties are the
|
||||
right ones, or that the ``Command`` present on the transaction is as
|
||||
expected for this specific flow. An example of this from the demo code is:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
|
||||
After verification the remote flow will return its signature to the
|
||||
originator. The originator should apply that signature to the starting
|
||||
``SignedTransaction`` and recheck the signatures match.
|
||||
|
||||
Committing the Transaction
|
||||
--------------------------
|
||||
|
||||
Once all the party signatures are applied to the SignedTransaction the
|
||||
final step is notarisation. This involves calling ``NotaryFlow.Client``
|
||||
to confirm the transaction, consume the inputs and return its confirming
|
||||
signature. Then the flow should ensure that all nodes end with all
|
||||
signatures and that they call ``ServiceHub.recordTransactions``. The
|
||||
code for this is standardised in the ``FinalityFlow``, or more explictly
|
||||
an example is:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 4
|
||||
:end-before: DOCEND 4
|
||||
|
||||
Partially Visible Transactions
|
||||
------------------------------
|
||||
|
||||
The discussion so far has assumed that the parties need full visibility
|
||||
of the transaction to sign. However, there may be situations where each
|
||||
party needs to store private data for audit purposes, or for evidence to
|
||||
a regulator, but does not wish to share that with the other trading
|
||||
partner. The tear-off/Merkle tree support in Corda allows flows to send
|
||||
portions of the full transaction to restrict visibility to remote
|
||||
parties. To do this one can use the
|
||||
``WireTransaction.buildFilteredTransaction`` extension method to produce
|
||||
a ``FilteredTransaction``. The elements of the ``SignedTransaction``
|
||||
which we wish to be hide will be replaced with their secure hash. The
|
||||
overall transaction txid is still provable from the
|
||||
``FilteredTransaction`` preventing change of the private data, but we do
|
||||
not expose that data to the other node directly. A full example of this
|
||||
can be found in the ``NodeInterestRates`` Oracle code from the
|
||||
``irs-demo`` project which interacts with the ``RatesFixFlow`` flow.
|
||||
Also, refer to the :doc:`merkle-trees` documentation.
|
Loading…
Reference in New Issue
Block a user