mirror of
https://github.com/corda/corda.git
synced 2025-02-24 18:50:57 +00:00
Fix up and improve some docs
This commit is contained in:
parent
b4288cf9dc
commit
492005bcfe
@ -45,6 +45,7 @@ class ResolveTransactionsFlowTest {
|
|||||||
net.stopNodes()
|
net.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
@Test
|
@Test
|
||||||
fun `resolve from two hashes`() {
|
fun `resolve from two hashes`() {
|
||||||
val (stx1, stx2) = makeTransactions()
|
val (stx1, stx2) = makeTransactions()
|
||||||
@ -58,6 +59,7 @@ class ResolveTransactionsFlowTest {
|
|||||||
assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id))
|
assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dependency with an error`() {
|
fun `dependency with an error`() {
|
||||||
@ -145,6 +147,7 @@ class ResolveTransactionsFlowTest {
|
|||||||
assertNotNull(b.services.storageService.attachments.openAttachment(id))
|
assertNotNull(b.services.storageService.attachments.openAttachment(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DOCSTART 2
|
||||||
private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> {
|
private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> {
|
||||||
// Make a chain of custody of dummy states and insert into node A.
|
// Make a chain of custody of dummy states and insert into node A.
|
||||||
val dummy1: SignedTransaction = DummyContract.generateInitial(MEGA_CORP.ref(1), 0, notary).let {
|
val dummy1: SignedTransaction = DummyContract.generateInitial(MEGA_CORP.ref(1), 0, notary).let {
|
||||||
@ -165,4 +168,5 @@ class ResolveTransactionsFlowTest {
|
|||||||
}
|
}
|
||||||
return Pair(dummy1, dummy2)
|
return Pair(dummy1, dummy2)
|
||||||
}
|
}
|
||||||
|
// DOCEND 2
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Flow state machines
|
Writing flows
|
||||||
===================
|
=============
|
||||||
|
|
||||||
This article explains our experimental approach to modelling financial flows in code. It explains how the
|
This article explains our approach to modelling financial flows in code. It explains how the
|
||||||
platform's state machine framework is used, and takes you through the code for a simple 2-party asset trading flow
|
platform's state machine framework is used, and takes you through the code for a simple 2-party asset trading flow
|
||||||
which is included in the source.
|
which is included in the source.
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ Our flow has two parties (B and S for buyer and seller) and will proceed as foll
|
|||||||
it lacks a signature from S authorising movement of the asset.
|
it lacks a signature from S authorising movement of the asset.
|
||||||
3. S signs it and hands the now finalised ``SignedTransaction`` back to B.
|
3. S signs it and hands the now finalised ``SignedTransaction`` back to B.
|
||||||
|
|
||||||
You can find the implementation of this flow in the file ``finance/src/main/kotlin/net.corda.flows/TwoPartyTradeFlow.kt``.
|
You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt``.
|
||||||
|
|
||||||
Assuming no malicious termination, they both end the flow being in posession of a valid, signed transaction that
|
Assuming no malicious termination, they both end the flow being in posession of a valid, signed transaction that
|
||||||
represents an atomic asset swap.
|
represents an atomic asset swap.
|
||||||
@ -120,13 +120,13 @@ each side.
|
|||||||
data class SellerTradeInfo(
|
data class SellerTradeInfo(
|
||||||
val assetForSale: StateAndRef<OwnableState>,
|
val assetForSale: StateAndRef<OwnableState>,
|
||||||
val price: Amount<Currency>,
|
val price: Amount<Currency>,
|
||||||
val sellerOwnerKey: PublicKey
|
val sellerOwnerKey: CompositeKey
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
|
data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
|
||||||
val notarySig: DigitalSignature.LegallyIdentifiable)
|
val notarySig: DigitalSignature.LegallyIdentifiable)
|
||||||
|
|
||||||
open class Seller(val otherSide: Party,
|
open class Seller(val otherParty: Party,
|
||||||
val notaryNode: NodeInfo,
|
val notaryNode: NodeInfo,
|
||||||
val assetToSell: StateAndRef<OwnableState>,
|
val assetToSell: StateAndRef<OwnableState>,
|
||||||
val price: Amount<Currency>,
|
val price: Amount<Currency>,
|
||||||
@ -138,7 +138,7 @@ each side.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Buyer(val otherSide: Party,
|
open class Buyer(val otherParty: Party,
|
||||||
val notary: Party,
|
val notary: Party,
|
||||||
val acceptablePrice: Amount<Currency>,
|
val acceptablePrice: Amount<Currency>,
|
||||||
val typeToBuy: Class<out OwnableState>) : FlowLogic<SignedTransaction>() {
|
val typeToBuy: Class<out OwnableState>) : FlowLogic<SignedTransaction>() {
|
||||||
@ -154,7 +154,7 @@ simply flow messages or exceptions. The other two represent the buyer and seller
|
|||||||
|
|
||||||
Going through the data needed to become a seller, we have:
|
Going through the data needed to become a seller, we have:
|
||||||
|
|
||||||
- ``otherSide: Party`` - the party with which you are trading.
|
- ``otherParty: Party`` - the party with which you are trading.
|
||||||
- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`consensus`" for more
|
- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`consensus`" for more
|
||||||
information on notaries.
|
information on notaries.
|
||||||
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
|
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
|
||||||
@ -197,20 +197,23 @@ when messages arrive. It provides the send/receive/sendAndReceive calls that let
|
|||||||
interaction and it will save/restore serialised versions of the fiber at the right times.
|
interaction and it will save/restore serialised versions of the fiber at the right times.
|
||||||
|
|
||||||
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events,
|
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events,
|
||||||
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered via the HTTP API. Or they can
|
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC
|
||||||
be triggered directly via the Java-level node APIs from your app code.
|
APIs from your app code.
|
||||||
|
|
||||||
You request a flow to be invoked by using the ``ServiceHub.invokeFlowAsync`` method. This takes a
|
You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a
|
||||||
Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``).
|
Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``).
|
||||||
It also takes a set of arguments to pass to the constructor. Because it's possible for flow invocations to
|
It also takes a set of arguments to pass to the constructor. Because it's possible for flow invocations to
|
||||||
be requested by untrusted code (e.g. a state that you have been sent), the types that can be passed into the
|
be requested by untrusted code (e.g. a state that you have been sent), the types that can be passed into the
|
||||||
flow are checked against a whitelist, which can be extended by apps themselves at load time.
|
flow are checked against a whitelist, which can be extended by apps themselves at load time. There are also a series
|
||||||
|
of inlined extension functions of the form ``CordaRPCOps.startFlow`` which help with invoking flows in a type
|
||||||
|
safe manner.
|
||||||
|
|
||||||
The process of starting a flow returns a ``ListenableFuture`` that you can use to either block waiting for
|
The process of starting a flow returns a ``FlowHandle`` that you can use to either observe
|
||||||
the result, or register a callback that will be invoked when the result is ready.
|
the result, observe its progress and which also contains a permanent identifier for the invoked flow in the form
|
||||||
|
of the ``StateMachineRunId``.
|
||||||
|
|
||||||
In a two party flow only one side is to be manually started using ``ServiceHub.invokeFlowAsync``. The other side
|
In a two party flow only one side is to be manually started using ``CordaRPCOps.startFlow``. The other side
|
||||||
has to be registered by its node to respond to the initiating flow via ``ServiceHubInternal.registerFlowInitiator``.
|
has to be registered by its node to respond to the initiating flow via ``PluginServiceHub.registerFlowInitiator``.
|
||||||
In our example it doesn't matter which flow is the initiator and which is the initiated. For example, if we are to
|
In our example it doesn't matter which flow is the initiator and which is the initiated. For example, if we are to
|
||||||
take the seller as the initiator then we would register the buyer as such:
|
take the seller as the initiator then we would register the buyer as such:
|
||||||
|
|
||||||
@ -218,8 +221,7 @@ take the seller as the initiator then we would register the buyer as such:
|
|||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
val services: ServiceHubInternal = TODO()
|
val services: PluginServiceHub = TODO()
|
||||||
|
|
||||||
services.registerFlowInitiator(Seller::class) { otherParty ->
|
services.registerFlowInitiator(Seller::class) { otherParty ->
|
||||||
val notary = services.networkMapCache.notaryNodes[0]
|
val notary = services.networkMapCache.notaryNodes[0]
|
||||||
val acceptablePrice = TODO()
|
val acceptablePrice = TODO()
|
||||||
@ -266,25 +268,21 @@ Let's fill out the ``receiveAndCheckProposedTransaction()`` method.
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||||
// Make the first message we'll send to kick off the flow.
|
// Make the first message we'll send to kick off the flow.
|
||||||
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public)
|
val myPublicKey = myKeyPair.public.composite
|
||||||
|
val hello = SellerTradeInfo(assetToSell, price, myPublicKey)
|
||||||
|
|
||||||
val maybeSTX = sendAndReceive<SignedTransaction>(otherSide, hello)
|
val maybeSTX = sendAndReceive<SignedTransaction>(otherSide, hello)
|
||||||
|
|
||||||
maybeSTX.unwrap {
|
maybeSTX.unwrap {
|
||||||
// Check that the tx proposed by the buyer is valid.
|
// Check that the tx proposed by the buyer is valid.
|
||||||
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
|
val wtx: WireTransaction = it.verifySignatures(myPublicKey, notaryNode.notaryIdentity.owningKey)
|
||||||
val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey)
|
|
||||||
if (missingSigs != expected)
|
|
||||||
throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs ${expected.toStringsShort()}")
|
|
||||||
|
|
||||||
val wtx: WireTransaction = it.tx
|
|
||||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||||
|
|
||||||
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
||||||
// even though it is missing signatures.
|
// even though it is missing signatures.
|
||||||
subFlow(ResolveTransactionsFlow(wtx, otherSide))
|
subFlow(ResolveTransactionsFlow(wtx, otherParty))
|
||||||
|
|
||||||
if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price)
|
if (wtx.outputs.map { it.data }.sumCashBy(myPublicKey).withoutIssuer() != price)
|
||||||
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
||||||
|
|
||||||
return it
|
return it
|
||||||
@ -306,7 +304,9 @@ needs human interaction!
|
|||||||
|
|
||||||
.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
|
.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
|
||||||
The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by
|
The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by
|
||||||
the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to.
|
the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to. You can
|
||||||
|
always use private methods to keep the stack uncluttered with temporary variables, or to avoid objects that
|
||||||
|
Kryo is not able to serialise correctly.
|
||||||
|
|
||||||
The second is that as well as being kept on the heap, objects reachable from the stack will be serialised. The state
|
The second is that as well as being kept on the heap, objects reachable from the stack will be serialised. The state
|
||||||
of the function call may be resurrected much later! Kryo doesn't require objects be marked as serialisable, but even so,
|
of the function call may be resurrected much later! Kryo doesn't require objects be marked as serialisable, but even so,
|
||||||
@ -372,18 +372,18 @@ Here's the rest of the code:
|
|||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
open fun computeOurSignature(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
open fun calculateOurSignature(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.id)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun sendSignatures(allPartySignedTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
private fun sendSignatures(allPartySignedTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||||
notarySignature: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
notarySignature: DigitalSignature.WithKey): SignedTransaction {
|
||||||
val fullySigned = allPartySignedTX + notarySignature
|
val fullySigned = allPartySignedTX + notarySignature
|
||||||
logger.trace { "Built finished transaction, sending back to secondary!" }
|
logger.trace { "Built finished transaction, sending back to secondary!" }
|
||||||
send(otherSide, SignaturesFromSeller(ourSignature, notarySignature))
|
send(otherSide, SignaturesFromSeller(ourSignature, notarySignature))
|
||||||
return fullySigned
|
return fullySigned
|
||||||
}
|
}
|
||||||
|
|
||||||
It's all pretty straightforward from now on. Here ``txBits`` is the raw byte array representing the serialised
|
It's all pretty straightforward from now on. Here ``id`` is the secure hash representing the serialised
|
||||||
transaction, and we just use our private key to calculate a signature over it. As a reminder, in Corda signatures do
|
transaction, and we just use our private key to calculate a signature over it. As a reminder, in Corda signatures do
|
||||||
not cover other signatures: just the core of the transaction data.
|
not cover other signatures: just the core of the transaction data.
|
||||||
|
|
||||||
@ -405,98 +405,15 @@ Implementing the buyer
|
|||||||
|
|
||||||
OK, let's do the same for the buyer side:
|
OK, let's do the same for the buyer side:
|
||||||
|
|
||||||
.. container:: codeset
|
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt
|
||||||
|
:language: kotlin
|
||||||
.. sourcecode:: kotlin
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
@Suspendable
|
|
||||||
override fun call(): SignedTransaction {
|
|
||||||
val tradeRequest = receiveAndValidateTradeRequest()
|
|
||||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
|
||||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
|
||||||
|
|
||||||
val signatures = swapSignaturesWithSeller(stx)
|
|
||||||
|
|
||||||
logger.trace { "Got signatures from seller, verifying ... " }
|
|
||||||
|
|
||||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
|
||||||
fullySigned.verifySignatures()
|
|
||||||
|
|
||||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
|
||||||
return fullySigned
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
|
||||||
// Wait for a trade request to come in from the other side
|
|
||||||
val maybeTradeRequest = receive<SellerTradeInfo>(otherParty)
|
|
||||||
maybeTradeRequest.unwrap {
|
|
||||||
// What is the seller trying to sell us?
|
|
||||||
val asset = it.assetForSale.state.data
|
|
||||||
val assetTypeName = asset.javaClass.name
|
|
||||||
logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }
|
|
||||||
|
|
||||||
if (it.price > acceptablePrice)
|
|
||||||
throw UnacceptablePriceException(it.price)
|
|
||||||
if (!typeToBuy.isInstance(asset))
|
|
||||||
throw AssetMismatchException(typeToBuy.name, assetTypeName)
|
|
||||||
|
|
||||||
// Check the transaction that contains the state which is being resolved.
|
|
||||||
// We only have a hash here, so if we don't know it already, we have to ask for it.
|
|
||||||
subFlow(ResolveTransactionsFlow(setOf(it.assetForSale.ref.txhash), otherSide))
|
|
||||||
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
private fun swapSignaturesWithSeller(stx: SignedTransaction): SignaturesFromSeller {
|
|
||||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
|
||||||
logger.trace { "Sending partially signed transaction to seller" }
|
|
||||||
|
|
||||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
|
||||||
|
|
||||||
return sendAndReceive<SignaturesFromSeller>(otherSide, stx).unwrap { it }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
|
||||||
// Now sign the transaction with whatever keys we need to move the cash.
|
|
||||||
for (k in cashSigningPubKeys) {
|
|
||||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
|
||||||
ptx.signWith(KeyPair(k, priv))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
|
||||||
val ptx = TransactionType.General.Builder(notary)
|
|
||||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
|
|
||||||
val wallet = serviceHub.walletService.currentWallet
|
|
||||||
val cashStates = wallet.statesOfType<Cash.State>()
|
|
||||||
val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
|
|
||||||
// Add inputs/outputs/a command for the movement of the asset.
|
|
||||||
ptx.addInputState(tradeRequest.assetForSale)
|
|
||||||
// Just pick some new public key for now. This won't be linked with our identity in any way, which is what
|
|
||||||
// we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
|
|
||||||
// reveal who the owner actually is. The key management service is expected to derive a unique key from some
|
|
||||||
// initial seed in order to provide privacy protection.
|
|
||||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
|
||||||
val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public)
|
|
||||||
ptx.addOutputState(state, tradeRequest.assetForSale.state.notary)
|
|
||||||
ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner)
|
|
||||||
|
|
||||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
|
||||||
// to have one.
|
|
||||||
val currentTime = serviceHub.clock.instant()
|
|
||||||
ptx.setTime(currentTime, 30.seconds)
|
|
||||||
return Pair(ptx, cashSigningPubKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
This code is longer but no more complicated. Here are some things to pay attention to:
|
This code is longer but no more complicated. Here are some things to pay attention to:
|
||||||
|
|
||||||
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
|
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
|
||||||
2. We create a cash spend in the normal way, by using ``Cash().generateSpend``. See the contracts tutorial if this
|
2. We create a cash spend in the normal way, by using ``VaultService.generateSpend``. See the vault documentation if this
|
||||||
part isn't clear.
|
part isn't clear.
|
||||||
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
|
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
|
||||||
whilst a flow is suspended, things like the wallet or the network map.
|
whilst a flow is suspended, things like the wallet or the network map.
|
||||||
@ -559,108 +476,6 @@ and linked ahead of time.
|
|||||||
In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are
|
In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are
|
||||||
surfaced to human operators for investigation and resolution.
|
surfaced to human operators for investigation and resolution.
|
||||||
|
|
||||||
Unit testing
|
|
||||||
------------
|
|
||||||
|
|
||||||
A flow can be a fairly complex thing that interacts with many services and other parties over the network. That
|
|
||||||
means unit testing one requires some infrastructure to provide lightweight mock implementations. The MockNetwork
|
|
||||||
provides this testing infrastructure layer; you can find this class in the node module
|
|
||||||
|
|
||||||
A good example to examine for learning how to unit test flows is the ``ResolveTransactionsFlow`` tests. This
|
|
||||||
flow takes care of downloading and verifying transaction graphs, with all the needed dependencies. We start
|
|
||||||
with this basic skeleton:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
|
||||||
|
|
||||||
class ResolveTransactionsFlowTest {
|
|
||||||
lateinit var net: MockNetwork
|
|
||||||
lateinit var a: MockNetwork.MockNode
|
|
||||||
lateinit var b: MockNetwork.MockNode
|
|
||||||
lateinit var notary: Party
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
net = MockNetwork()
|
|
||||||
val nodes = net.createSomeNodes()
|
|
||||||
a = nodes.partyNodes[0]
|
|
||||||
b = nodes.partyNodes[1]
|
|
||||||
notary = nodes.notaryNode.info.identity
|
|
||||||
net.runNetwork()
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
net.stopNodes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
We create a mock network in our ``@Before`` setup method and create a couple of nodes. We also record the identity
|
|
||||||
of the notary in our test network, which will come in handy later. We also tidy up when we're done.
|
|
||||||
|
|
||||||
Next, we write a test case:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun resolveFromTwoHashes() {
|
|
||||||
val (stx1, stx2) = makeTransactions()
|
|
||||||
val p = ResolveTransactionsFlow(setOf(stx2.id), a.info.identity)
|
|
||||||
val future = b.services.startFlow("resolve", p)
|
|
||||||
net.runNetwork()
|
|
||||||
val results = future.get()
|
|
||||||
assertEquals(listOf(stx1.id, stx2.id), results.map { it.id })
|
|
||||||
assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id))
|
|
||||||
assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
We'll take a look at the ``makeTransactions`` function in a moment. For now, it's enough to know that it returns two
|
|
||||||
``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by node A
|
|
||||||
but not node B.
|
|
||||||
|
|
||||||
The test logic is simple enough: we create the flow, giving it node A's identity as the target to talk to.
|
|
||||||
Then we start it on node B and use the ``net.runNetwork()`` method to bounce messages around until things have
|
|
||||||
settled (i.e. there are no more messages waiting to be delivered). All this is done using an in memory message
|
|
||||||
routing implementation that is fast to initialise and use. Finally, we obtain the result of the flow and do
|
|
||||||
some tests on it. We also check the contents of node B's database to see that the flow had the intended effect
|
|
||||||
on the node's persistent state.
|
|
||||||
|
|
||||||
Here's what ``makeTransactions`` looks like:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
|
||||||
|
|
||||||
private fun makeTransactions(): Pair<SignedTransaction, SignedTransaction> {
|
|
||||||
// Make a chain of custody of dummy states and insert into node A.
|
|
||||||
val dummy1: SignedTransaction = DummyContract.generateInitial(MEGA_CORP.ref(1), 0, notary).let {
|
|
||||||
it.signWith(MEGA_CORP_KEY)
|
|
||||||
it.signWith(DUMMY_NOTARY_KEY)
|
|
||||||
it.toSignedTransaction(false)
|
|
||||||
}
|
|
||||||
val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP_PUBKEY).let {
|
|
||||||
it.signWith(MEGA_CORP_KEY)
|
|
||||||
it.signWith(DUMMY_NOTARY_KEY)
|
|
||||||
it.toSignedTransaction()
|
|
||||||
}
|
|
||||||
a.services.recordTransactions(dummy1, dummy2)
|
|
||||||
return Pair(dummy1, dummy2)
|
|
||||||
}
|
|
||||||
|
|
||||||
We're using the ``DummyContract``, a simple test smart contract which stores a single number in its states, along
|
|
||||||
with ownership and issuer information. You can issue such states, exit them and re-assign ownership (move them).
|
|
||||||
It doesn't do anything else. This code simply creates a transaction that issues a dummy state (the issuer is
|
|
||||||
``MEGA_CORP``, a pre-defined unit test identity), signs it with the test notary and MegaCorp keys and then
|
|
||||||
converts the builder to the final ``SignedTransaction``. It then does so again, but this time instead of issuing
|
|
||||||
it re-assigns ownership instead. The chain of two transactions is finally committed to node A by sending them
|
|
||||||
directly to the ``a.services.recordTransaction`` method (note that this method doesn't check the transactions are
|
|
||||||
valid).
|
|
||||||
|
|
||||||
And that's it: you can explore the documentation for the `MockNode API <api/net.corda.node.internal.testing/-mock-network/index.html>`_ here.
|
|
||||||
|
|
||||||
Versioning
|
Versioning
|
||||||
----------
|
----------
|
||||||
|
|
||||||
@ -684,10 +499,9 @@ The flow framework is a key part of the platform and will be extended in major w
|
|||||||
the features we have planned:
|
the features we have planned:
|
||||||
|
|
||||||
* Identity based addressing
|
* Identity based addressing
|
||||||
* Exposing progress trackers to local (inside the firewall) clients using message queues and/or WebSockets
|
|
||||||
* Exception propagation and management, with a "flow hospital" tool to manually provide solutions to unavoidable
|
* Exception propagation and management, with a "flow hospital" tool to manually provide solutions to unavoidable
|
||||||
problems (e.g. the other side doesn't know the trade)
|
problems (e.g. the other side doesn't know the trade)
|
||||||
* Being able to interact with internal apps and tools via HTTP and similar
|
* Being able to interact with internal apps and tools via RPC
|
||||||
* Being able to interact with people, either via some sort of external ticketing system, or email, or a custom UI.
|
* Being able to interact with people, either via some sort of external ticketing system, or email, or a custom UI.
|
||||||
For example to implement human transaction authorisations.
|
For example to implement human transaction authorisations.
|
||||||
* A standard library of flows that can be easily sub-classed by local developers in order to integrate internal
|
* A standard library of flows that can be easily sub-classed by local developers in order to integrate internal
|
||||||
|
83
docs/source/flow-testing.rst
Normal file
83
docs/source/flow-testing.rst
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Writing flow tests
|
||||||
|
==================
|
||||||
|
|
||||||
|
A flow can be a fairly complex thing that interacts with many services and other parties over the network. That
|
||||||
|
means unit testing one requires some infrastructure to provide lightweight mock implementations. The MockNetwork
|
||||||
|
provides this testing infrastructure layer; you can find this class in the test-utils module.
|
||||||
|
|
||||||
|
A good example to examine for learning how to unit test flows is the ``ResolveTransactionsFlow`` tests. This
|
||||||
|
flow takes care of downloading and verifying transaction graphs, with all the needed dependencies. We start
|
||||||
|
with this basic skeleton:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
class ResolveTransactionsFlowTest {
|
||||||
|
lateinit var net: MockNetwork
|
||||||
|
lateinit var a: MockNetwork.MockNode
|
||||||
|
lateinit var b: MockNetwork.MockNode
|
||||||
|
lateinit var notary: Party
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
net = MockNetwork()
|
||||||
|
val nodes = net.createSomeNodes()
|
||||||
|
a = nodes.partyNodes[0]
|
||||||
|
b = nodes.partyNodes[1]
|
||||||
|
notary = nodes.notaryNode.info.notaryIdentity
|
||||||
|
net.runNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
net.stopNodes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
We create a mock network in our ``@Before`` setup method and create a couple of nodes. We also record the identity
|
||||||
|
of the notary in our test network, which will come in handy later. We also tidy up when we're done.
|
||||||
|
|
||||||
|
Next, we write a test case:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
|
||||||
|
We'll take a look at the ``makeTransactions`` function in a moment. For now, it's enough to know that it returns two
|
||||||
|
``SignedTransaction`` objects, the second of which spends the first. Both transactions are known by node A
|
||||||
|
but not node B.
|
||||||
|
|
||||||
|
The test logic is simple enough: we create the flow, giving it node A's identity as the target to talk to.
|
||||||
|
Then we start it on node B and use the ``net.runNetwork()`` method to bounce messages around until things have
|
||||||
|
settled (i.e. there are no more messages waiting to be delivered). All this is done using an in memory message
|
||||||
|
routing implementation that is fast to initialise and use. Finally, we obtain the result of the flow and do
|
||||||
|
some tests on it. We also check the contents of node B's database to see that the flow had the intended effect
|
||||||
|
on the node's persistent state.
|
||||||
|
|
||||||
|
Here's what ``makeTransactions`` looks like:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 2
|
||||||
|
:end-before: DOCEND 2
|
||||||
|
|
||||||
|
We're using the ``DummyContract``, a simple test smart contract which stores a single number in its states, along
|
||||||
|
with ownership and issuer information. You can issue such states, exit them and re-assign ownership (move them).
|
||||||
|
It doesn't do anything else. This code simply creates a transaction that issues a dummy state (the issuer is
|
||||||
|
``MEGA_CORP``, a pre-defined unit test identity), signs it with the test notary and MegaCorp keys and then
|
||||||
|
converts the builder to the final ``SignedTransaction``. It then does so again, but this time instead of issuing
|
||||||
|
it re-assigns ownership instead. The chain of two transactions is finally committed to node A by sending them
|
||||||
|
directly to the ``a.services.recordTransaction`` method (note that this method doesn't check the transactions are
|
||||||
|
valid) inside a ``databaseTransaction``. All node flows run within a database transaction in the nodes themselves,
|
||||||
|
but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown
|
||||||
|
here.
|
||||||
|
|
||||||
|
And that's it: you can explore the documentation for the `MockNode API <api/net.corda.node.internal.testing/-mock-network/index.html>`_ here.
|
@ -68,6 +68,7 @@ Read on to learn:
|
|||||||
tutorial-test-dsl
|
tutorial-test-dsl
|
||||||
tutorial-clientrpc-api
|
tutorial-clientrpc-api
|
||||||
flow-state-machines
|
flow-state-machines
|
||||||
|
flow-testing
|
||||||
oracles
|
oracles
|
||||||
tutorial-attachments
|
tutorial-attachments
|
||||||
event-scheduling
|
event-scheduling
|
||||||
|
@ -168,6 +168,7 @@ object TwoPartyTradeFlow {
|
|||||||
|
|
||||||
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val tradeRequest = receiveAndValidateTradeRequest()
|
val tradeRequest = receiveAndValidateTradeRequest()
|
||||||
@ -257,5 +258,6 @@ object TwoPartyTradeFlow {
|
|||||||
tx.setTime(currentTime, 30.seconds)
|
tx.setTime(currentTime, 30.seconds)
|
||||||
return Pair(tx, cashSigningPubKeys)
|
return Pair(tx, cashSigningPubKeys)
|
||||||
}
|
}
|
||||||
|
// DOCEND 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user