2015-12-14 17:22:21 +00:00
|
|
|
.. highlight:: kotlin
|
|
|
|
.. raw:: html
|
|
|
|
|
|
|
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
|
|
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
|
|
|
|
|
|
|
Protocol state machines
|
|
|
|
=======================
|
|
|
|
|
|
|
|
This article explains our experimental approach to modelling financial protocols 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 protocol
|
|
|
|
which is included in the source.
|
|
|
|
|
|
|
|
Introduction
|
|
|
|
------------
|
|
|
|
|
|
|
|
Shared distributed ledgers are interesting because they allow many different, mutually distrusting parties to
|
|
|
|
share a single source of truth about the ownership of assets. Digitally signed transactions are used to update that
|
|
|
|
shared ledger, and transactions may alter many states simultaneously and atomically.
|
|
|
|
|
|
|
|
Blockchain systems such as Bitcoin support the idea of building up a finished, signed transaction by passing around
|
|
|
|
partially signed invalid transactions outside of the main network, and by doing this you can implement
|
|
|
|
*delivery versus payment* such that there is no chance of settlement failure, because the movement of cash and the
|
|
|
|
traded asset are performed atomically by the same transaction. To perform such a trade involves a multi-step protocol
|
|
|
|
in which messages are passed back and forth privately between parties, checked, signed and so on.
|
|
|
|
|
|
|
|
Despite how useful these protocols are, platforms such as Bitcoin and Ethereum do not assist the developer with the rather
|
|
|
|
tricky task of actually building them. That is unfortunate. There are many awkward problems in their implementation
|
|
|
|
that a good platform would take care of for you, problems like:
|
|
|
|
|
|
|
|
* Avoiding "callback hell" in which code that should ideally be sequential is turned into an unreadable mess due to the
|
|
|
|
desire to avoid using up a thread for every protocol instantiation.
|
|
|
|
* Surviving node shutdowns/restarts that may occur in the middle of the protocol without complicating things. This
|
|
|
|
implies that the state of the protocol must be persisted to disk.
|
|
|
|
* Error handling.
|
|
|
|
* Message routing.
|
|
|
|
* Serialisation.
|
|
|
|
* Catching type errors, in which the developer gets temporarily confused and expects to receive/send one type of message
|
|
|
|
when actually they need to receive/send another.
|
|
|
|
* Unit testing of the finished protocol.
|
|
|
|
|
|
|
|
Actor frameworks can solve some of the above but they are often tightly bound to a particular messaging layer, and
|
|
|
|
we would like to keep a clean separation. Additionally, they are typically not type safe, and don't make persistence or
|
|
|
|
writing sequential code much easier.
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
To put these problems in perspective, the *payment channel protocol* in the bitcoinj library, which allows bitcoins to
|
2015-12-14 17:22:21 +00:00
|
|
|
be temporarily moved off-chain and traded at high speed between two parties in private, consists of about 7000 lines of
|
|
|
|
Java and took over a month of full time work to develop. Most of that code is concerned with the details of persistence,
|
|
|
|
message passing, lifecycle management, error handling and callback management. Because the business logic is quite
|
|
|
|
spread out the code can be difficult to read and debug.
|
|
|
|
|
|
|
|
As small contract-specific trading protocols are a common occurence in finance, we provide a framework for the
|
|
|
|
construction of them that automatically handles many of the concerns outlined above.
|
|
|
|
|
|
|
|
Theory
|
|
|
|
------
|
|
|
|
|
|
|
|
A *continuation* is a suspended stack frame stored in a regular object that can be passed around, serialised,
|
2016-01-13 12:54:10 +00:00
|
|
|
unserialised and resumed from where it was suspended. This concept is sometimes referred to as "fibers". This may
|
|
|
|
sound abstract but don't worry, the examples below will make it clearer. The JVM does not natively support
|
|
|
|
continuations, so we implement them using a library called Quasar which works through behind-the-scenes
|
|
|
|
bytecode rewriting. You don't have to know how this works to benefit from it, however.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
We use continuations for the following reasons:
|
|
|
|
|
|
|
|
* It allows us to write code that is free of callbacks, that looks like ordinary sequential code.
|
|
|
|
* A suspended continuation takes far less memory than a suspended thread. It can be as low as a few hundred bytes.
|
2016-02-18 16:47:05 +00:00
|
|
|
In contrast a suspended Java thread stack can easily be 1mb in size.
|
2015-12-14 17:22:21 +00:00
|
|
|
* It frees the developer from thinking (much) about persistence and serialisation.
|
|
|
|
|
|
|
|
A *state machine* is a piece of code that moves through various *states*. These are not the same as states in the data
|
|
|
|
model (that represent facts about the world on the ledger), but rather indicate different stages in the progression
|
|
|
|
of a multi-stage protocol. Typically writing a state machine would require the use of a big switch statement and some
|
|
|
|
explicit variables to keep track of where you're up to. The use of continuations avoids this hassle.
|
|
|
|
|
|
|
|
A two party trading protocol
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
We would like to implement the "hello world" of shared transaction building protocols: a seller wishes to sell some
|
|
|
|
*asset* (e.g. some commercial paper) in return for *cash*. The buyer wishes to purchase the asset using his cash. They
|
|
|
|
want the trade to be atomic so neither side is exposed to the risk of settlement failure. We assume that the buyer
|
|
|
|
and seller have found each other and arranged the details on some exchange, or over the counter. The details of how
|
|
|
|
the trade is arranged isn't covered in this article.
|
|
|
|
|
|
|
|
Our protocol has two parties (B and S for buyer and seller) and will proceed as follows:
|
|
|
|
|
|
|
|
1. S sends a ``StateAndRef`` pointing to the state they want to sell to B, along with info about the price they require
|
|
|
|
B to pay.
|
2016-02-12 14:42:25 +00:00
|
|
|
2. B sends to S a ``SignedTransaction`` that includes the state as input, B's cash as input, the state with the new
|
2015-12-14 17:22:21 +00:00
|
|
|
owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
|
|
|
|
it lacks a signature from S authorising movement of the asset.
|
2016-02-12 14:42:25 +00:00
|
|
|
3. S signs it and hands the now finalised ``SignedTransaction`` back to B.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
You can find the implementation of this protocol in the file ``contracts/protocols/TwoPartyTradeProtocol.kt``.
|
|
|
|
|
|
|
|
Assuming no malicious termination, they both end the protocol being in posession of a valid, signed transaction that
|
|
|
|
represents an atomic asset swap.
|
|
|
|
|
|
|
|
Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine.
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
We start by defining a wrapper that namespaces the protocol code, two functions to start either the buy or sell side
|
|
|
|
of the protocol, and two classes that will contain the protocol definition. We also pick what data will be used by
|
|
|
|
each side.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
object TwoPartyTradeProtocol {
|
|
|
|
val TRADE_TOPIC = "platform.trade"
|
|
|
|
|
|
|
|
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
|
|
|
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
2016-02-12 14:49:18 +00:00
|
|
|
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
2016-01-13 12:54:10 +00:00
|
|
|
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
|
|
|
|
smm.add("$TRADE_TOPIC.seller", seller)
|
|
|
|
return seller.resultFuture
|
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
|
|
|
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
2016-02-12 14:49:18 +00:00
|
|
|
sessionID: Long): ListenableFuture<SignedTransaction> {
|
2016-01-13 12:54:10 +00:00
|
|
|
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
|
|
|
|
smm.add("$TRADE_TOPIC.buyer", buyer)
|
|
|
|
return buyer.resultFuture
|
|
|
|
}
|
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
|
|
|
class SellerTradeInfo(
|
|
|
|
val assetForSale: StateAndRef<OwnableState>,
|
|
|
|
val price: Amount,
|
|
|
|
val sellerOwnerKey: PublicKey,
|
|
|
|
val sessionID: Long
|
|
|
|
)
|
|
|
|
|
|
|
|
class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
class Seller(val otherSide: SingleMessageRecipient,
|
|
|
|
val timestampingAuthority: LegallyIdentifiableNode,
|
|
|
|
val assetToSell: StateAndRef<OwnableState>,
|
|
|
|
val price: Amount,
|
|
|
|
val myKeyPair: KeyPair,
|
2016-02-18 16:47:05 +00:00
|
|
|
val buyerSessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
2016-01-13 12:54:10 +00:00
|
|
|
@Suspendable
|
2016-02-12 14:49:18 +00:00
|
|
|
override fun call(): SignedTransaction {
|
2016-01-13 12:54:10 +00:00
|
|
|
TODO()
|
|
|
|
}
|
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
class UnacceptablePriceException(val givenPrice: Amount) : Exception()
|
|
|
|
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
|
|
|
|
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
|
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
class Buyer(val otherSide: SingleMessageRecipient,
|
|
|
|
val timestampingAuthority: Party,
|
|
|
|
val acceptablePrice: Amount,
|
|
|
|
val typeToBuy: Class<out OwnableState>,
|
2016-02-18 16:47:05 +00:00
|
|
|
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
2016-01-13 12:54:10 +00:00
|
|
|
@Suspendable
|
2016-02-12 14:49:18 +00:00
|
|
|
override fun call(): SignedTransaction {
|
2016-01-13 12:54:10 +00:00
|
|
|
TODO()
|
2015-12-14 17:22:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Let's unpack what this code does:
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one
|
2016-02-09 14:11:17 +00:00
|
|
|
to run the buyer side of the protocol and one to run the seller side. Some of the classes are simply protocol messages.
|
2016-01-13 12:54:10 +00:00
|
|
|
- It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved
|
|
|
|
by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation.
|
|
|
|
- The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run,
|
|
|
|
use them to construct a ``Buyer`` or ``Seller`` object respectively, and then add the new instances to the
|
|
|
|
``StateMachineManager``. The purpose of this class is described below. The ``smm.add`` method takes a logger name as
|
|
|
|
the first parameter, this is just a standard JDK logging identifier string, and the instance to add.
|
|
|
|
|
|
|
|
Going through the data needed to become a seller, we have:
|
|
|
|
|
|
|
|
- ``timestampingAuthority: LegallyIdentifiableNode`` - a reference to a node on the P2P network that acts as a trusted
|
|
|
|
timestamper. The use of timestamping is described in :doc:`data-model`.
|
|
|
|
- ``otherSide: SingleMessageRecipient`` - the network address of the node with which you are trading.
|
|
|
|
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
|
|
|
|
- ``price: Amount`` - the agreed on price that the asset is being sold for.
|
|
|
|
- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction.
|
|
|
|
- ``buyerSessionID: Long`` - a unique number that identifies this trade to the buyer. It is expected that the buyer
|
|
|
|
knows that the trade is going to take place and has sent you such a number already. (This field may go away in a future
|
|
|
|
iteration of the framework)
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2015-12-15 12:16:13 +00:00
|
|
|
.. note:: Session IDs keep different traffic streams separated, so for security they must be large and random enough
|
2015-12-15 14:52:07 +00:00
|
|
|
to be unguessable. 63 bits is good enough.
|
2015-12-15 12:16:13 +00:00
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
And for the buyer:
|
|
|
|
|
|
|
|
- ``acceptablePrice: Amount`` - the price that was agreed upon out of band. If the seller specifies a price less than
|
|
|
|
or equal to this, then the trade will go ahead.
|
|
|
|
- ``typeToBuy: Class<out OwnableState>`` - the type of state that is being purchased. This is used to check that the
|
|
|
|
sell side of the protocol isn't trying to sell us the wrong thing, whether by accident or on purpose.
|
|
|
|
- ``sessionID: Long`` - the session ID that was handed to the seller in order to start the protocol.
|
|
|
|
|
|
|
|
The run methods return a ``ListenableFuture`` that will complete when the protocol has finished.
|
|
|
|
|
2015-12-14 17:22:21 +00:00
|
|
|
Alright, so using this protocol shouldn't be too hard: in the simplest case we can just pass in the details of the trade
|
2016-01-13 12:54:10 +00:00
|
|
|
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on resulting object to
|
2015-12-15 14:52:07 +00:00
|
|
|
block the calling thread until the protocol has finished. Or we could register a callback on the returned future that
|
|
|
|
will be invoked when it's done, where we could e.g. update a user interface.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
Finally, we define a couple of exceptions, and two classes that will be used as a protocol message called
|
|
|
|
``SellerTradeInfo`` and ``SignaturesFromSeller``.
|
2016-01-13 12:54:10 +00:00
|
|
|
|
|
|
|
Suspendable methods
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
The ``call`` method of the buyer/seller classes is marked with the ``@Suspendable`` annotation. What does this mean?
|
|
|
|
|
|
|
|
As mentioned above, our protocol framework will at points suspend the code and serialise it to disk. For this to work,
|
|
|
|
any methods on the call stack must have been pre-marked as ``@Suspendable`` so the bytecode rewriter knows to modify
|
|
|
|
the underlying code to support this new feature. A protocol is suspended when calling either ``receive``, ``send`` or
|
|
|
|
``sendAndReceive`` which we will learn more about below. For now, just be aware that when one of these methods is
|
|
|
|
invoked, all methods on the stack must have been marked. If you forget, then in the unit test environment you will
|
|
|
|
get a useful error message telling you which methods you didn't mark. The fix is simple enough: just add the annotation
|
|
|
|
and try again.
|
|
|
|
|
|
|
|
.. note:: A future version of Java is likely to remove this pre-marking requirement completely.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
The state machine manager
|
|
|
|
-------------------------
|
|
|
|
|
|
|
|
The SMM is a class responsible for taking care of all running protocols in a node. It knows how to register handlers
|
2016-01-13 12:54:10 +00:00
|
|
|
with a ``MessagingService`` and iterate the right state machine when messages arrive. It provides the
|
2015-12-14 17:22:21 +00:00
|
|
|
send/receive/sendAndReceive calls that let the code request network interaction and it will store a serialised copy of
|
|
|
|
each state machine before it's suspended to wait for the network.
|
|
|
|
|
|
|
|
To get a ``StateMachineManager``, you currently have to build one by passing in a ``ServiceHub`` and a thread or thread
|
|
|
|
pool which it can use. This will change in future so don't worry about the details of this too much: just check the
|
|
|
|
unit tests to see how it's done.
|
|
|
|
|
|
|
|
Implementing the seller
|
|
|
|
-----------------------
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
Let's implement the ``Seller.call`` method. This will be invoked by the platform when the protocol is started by the
|
|
|
|
``StateMachineManager``.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
2016-02-12 14:42:25 +00:00
|
|
|
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
|
|
|
val ourSignature = signWithOurKey(partialTX)
|
2016-02-18 16:47:05 +00:00
|
|
|
val tsaSig = subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits))
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-12 14:49:18 +00:00
|
|
|
val stx: SignedTransaction = sendSignatures(partialTX, ourSignature, tsaSig)
|
2016-02-09 14:11:17 +00:00
|
|
|
|
2016-02-12 14:49:18 +00:00
|
|
|
return stx
|
2016-02-09 14:11:17 +00:00
|
|
|
|
|
|
|
Here we see the outline of the procedure. We receive a proposed trade transaction from the buyer and check that it's
|
|
|
|
valid. Then we sign with our own key, request a timestamping authority to assert with another signature that the
|
|
|
|
timestamp in the transaction (if any) is valid, and finally we send back both our signature and the TSA's signature.
|
|
|
|
Finally, we hand back to the code that invoked the protocol the finished transaction in a couple of different forms.
|
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
.. note:: ``ProtocolLogic`` classes can be composed together. Here, we see the use of the ``subProtocol`` method, which
|
|
|
|
is given an instance of ``TimestampingProtocol``. This protocol will run to completion and yield a result, almost
|
|
|
|
as if it's a regular method call. In fact, under the hood, all the ``subProtocol`` method does is pass the current
|
|
|
|
fiber object into the newly created object and then run ``call()`` on it ... so it basically _is_ just a method call.
|
|
|
|
This is where we can see the benefits of using continuations/fibers as a programming model.
|
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
Let's fill out the ``receiveAndCheckProposedTransaction()`` method.
|
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
|
|
|
@Suspendable
|
2016-02-12 14:42:25 +00:00
|
|
|
open fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
2016-02-09 14:11:17 +00:00
|
|
|
val sessionID = random63BitValue()
|
|
|
|
|
|
|
|
// Make the first message we'll send to kick off the protocol.
|
|
|
|
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
|
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
val maybeSTX = sendAndReceive<SignedTransaction>(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello)
|
|
|
|
|
|
|
|
maybeSTX.validate {
|
|
|
|
// Check that the tx proposed by the buyer is valid.
|
|
|
|
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
|
|
|
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
|
|
|
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
|
|
|
|
|
|
|
val wtx: WireTransaction = it.tx
|
|
|
|
logger.trace { "Received partially signed transaction: ${it.id}" }
|
|
|
|
|
|
|
|
checkDependencies(it)
|
|
|
|
|
|
|
|
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
|
|
|
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService))
|
|
|
|
|
|
|
|
if (wtx.outputs.sumCashBy(myKeyPair.public) != price)
|
|
|
|
throw IllegalArgumentException("Transaction is not sending us the right amounnt of cash")
|
|
|
|
|
|
|
|
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
|
|
|
//
|
|
|
|
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
|
|
|
// we're reusing keys! So don't reuse keys!
|
|
|
|
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
|
|
|
// once we implement state pairing.
|
|
|
|
//
|
|
|
|
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
|
|
|
// express protocol state machines on top of the messaging layer.
|
|
|
|
|
|
|
|
return it
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2015-12-15 14:52:07 +00:00
|
|
|
That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out
|
2015-12-14 17:22:21 +00:00
|
|
|
the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments:
|
|
|
|
|
|
|
|
- The topic string that ensures the message is routed to the right bit of code in the other side's node.
|
|
|
|
- The session IDs that ensure the messages don't get mixed up with other simultaneous trades.
|
2016-01-13 12:54:10 +00:00
|
|
|
- The thing to send. It'll be serialised and sent automatically.
|
|
|
|
- Finally a type argument, which is the kind of object we're expecting to receive from the other side.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
Once ``sendAndReceive`` is called, the call method will be suspended into a continuation. When it gets back we'll do a log
|
2015-12-14 17:22:21 +00:00
|
|
|
message. The buyer is supposed to send us a transaction with all the right inputs/outputs/commands in return, with their
|
|
|
|
cash put into the transaction and their signature on it authorising the movement of the cash.
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
|
2015-12-14 17:22:21 +00:00
|
|
|
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 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,
|
|
|
|
doing things like creating threads from inside these calls would be a bad idea. They should only contain business
|
|
|
|
logic.
|
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
You get back a simple wrapper class, ``UntrustworthyData<SignedTransaction>``, which is just a marker class that reminds
|
|
|
|
us that the data came from a potentially malicious external source and may have been tampered with or be unexpected in
|
|
|
|
other ways. It doesn't add any functionality, but acts as a reminder to "scrub" the data before use. Here, our scrubbing
|
|
|
|
simply involves checking the signatures on it. Then we go ahead and check all the dependencies of this partial
|
|
|
|
transaction for validity. Here's the code to do that:
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
@Suspendable
|
2016-02-18 16:47:05 +00:00
|
|
|
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()
|
|
|
|
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
2015-12-14 17:22:21 +00:00
|
|
|
}
|
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
This is simple enough: we mark the method as ``@Suspendable`` because we're going to invoke a sub-protocol, extract the
|
|
|
|
IDs of the transactions the proposed transaction depends on, and then uses a protocol provided by the system to download
|
|
|
|
and check them all. This protocol does a breadth-first search over the dependency graph, bottoming out at issuance
|
|
|
|
transactions that don't have any inputs themselves. Once the node has audited the transaction history, all the dependencies
|
|
|
|
are committed to the node's local database so they won't be checked again next time.
|
|
|
|
|
|
|
|
.. note:: Transaction dependency resolution assumes that the peer you got the transaction from has all of the
|
|
|
|
dependencies itself. It must do, otherwise it could not have convinced itself that the dependencies were themselves
|
|
|
|
valid. It's important to realise that requesting only the transactions we require is a privacy leak, because if
|
|
|
|
we don't download a transaction from the peer, they know we must have already seen it before. Fixing this privacy
|
|
|
|
leak will come later.
|
|
|
|
|
|
|
|
After the dependencies, we check the proposed trading transaction for validity by running the contracts for that as
|
|
|
|
well (but having handled the fact that some signatures are missing ourselves).
|
|
|
|
|
|
|
|
Here's the rest of the code:
|
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
|
|
|
open fun signWithOurKey(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
@Suspendable
|
2016-02-12 14:42:25 +00:00
|
|
|
open fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
2016-02-12 14:49:18 +00:00
|
|
|
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
2016-02-09 14:11:17 +00:00
|
|
|
val fullySigned = partialTX + tsaSig + ourSignature
|
|
|
|
|
|
|
|
logger.trace { "Built finished transaction, sending back to secondary!" }
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature))
|
2016-02-12 14:49:18 +00:00
|
|
|
return fullySigned
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
It's should be all pretty straightforward: here, ``txBits`` is the raw byte array representing the transaction.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
In ``sendSignatures``, we take the two signatures we calculated, then add them to the partial transaction we were sent.
|
|
|
|
We provide an overload for the + operator so signatures can be added to a SignedTransaction easily. Finally, we wrap the
|
2016-02-09 14:11:17 +00:00
|
|
|
two signatures in a simple wrapper message class and send it back. The send won't block waiting for an acknowledgement,
|
|
|
|
but the underlying message queue software will retry delivery if the other side has gone away temporarily.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
.. warning:: This code is **not secure**. Other than not checking for all possible invalid constructions, if the
|
|
|
|
seller stops before sending the finalised transaction to the buyer, the seller is left with a valid transaction
|
|
|
|
but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing will be fixed in a
|
|
|
|
future version of the code.
|
|
|
|
|
|
|
|
Implementing the buyer
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
OK, let's do the same for the buyer side:
|
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
@Suspendable
|
2016-02-12 14:49:18 +00:00
|
|
|
override fun call(): SignedTransaction {
|
2016-02-09 14:11:17 +00:00
|
|
|
val tradeRequest = receiveAndValidateTradeRequest()
|
|
|
|
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
|
|
|
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
|
|
|
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
|
|
|
|
|
|
|
|
logger.trace { "Got signatures from seller, verifying ... "}
|
|
|
|
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
2016-02-12 14:49:18 +00:00
|
|
|
fullySigned.verify()
|
2016-02-09 14:11:17 +00:00
|
|
|
|
|
|
|
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
2016-02-12 14:49:18 +00:00
|
|
|
return fullySigned
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Suspendable
|
|
|
|
open fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
2016-01-13 12:54:10 +00:00
|
|
|
// Wait for a trade request to come in on our pre-provided session ID.
|
2016-02-18 16:47:05 +00:00
|
|
|
val maybeTradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, sessionID)
|
2016-02-09 14:11:17 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
maybeTradeRequest.validate {
|
2016-02-09 14:11:17 +00:00
|
|
|
// What is the seller trying to sell us?
|
2016-02-18 16:47:05 +00:00
|
|
|
val asset = it.assetForSale.state
|
|
|
|
val assetTypeName = asset.javaClass.name
|
|
|
|
logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }
|
2016-02-09 14:11:17 +00:00
|
|
|
|
|
|
|
// Check the start message for acceptability.
|
|
|
|
check(it.sessionID > 0)
|
|
|
|
if (it.price > acceptablePrice)
|
|
|
|
throw UnacceptablePriceException(it.price)
|
2016-02-18 16:47:05 +00:00
|
|
|
if (!typeToBuy.isInstance(asset))
|
2016-02-09 14:11:17 +00:00
|
|
|
throw AssetMismatchException(typeToBuy.name, assetTypeName)
|
2016-01-13 12:54:10 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
// 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.
|
|
|
|
subProtocol(ResolveTransactionsProtocol(setOf(it.assetForSale.ref.txhash), otherSide))
|
|
|
|
|
|
|
|
return it
|
|
|
|
}
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
2016-01-13 12:54:10 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
@Suspendable
|
2016-02-12 14:42:25 +00:00
|
|
|
open fun swapSignaturesWithSeller(stx: SignedTransaction, theirSessionID: Long): SignaturesFromSeller {
|
2016-02-09 14:11:17 +00:00
|
|
|
logger.trace { "Sending partially signed transaction to seller" }
|
2016-01-13 12:54:10 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
2016-01-13 12:54:10 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate { it }
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
2016-01-13 12:54:10 +00:00
|
|
|
|
2016-02-12 14:42:25 +00:00
|
|
|
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
2016-01-13 12:54:10 +00:00
|
|
|
// 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))
|
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-18 16:47:05 +00:00
|
|
|
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
2016-02-09 14:11:17 +00:00
|
|
|
}
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
|
|
|
val ptx = TransactionBuilder()
|
|
|
|
// 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.ref)
|
|
|
|
// 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.withNewOwner(freshKey.public)
|
|
|
|
ptx.addOutputState(state)
|
|
|
|
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
|
2015-12-14 17:22:21 +00:00
|
|
|
|
2016-02-09 14:11:17 +00:00
|
|
|
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
|
|
|
// to have one.
|
|
|
|
ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds)
|
|
|
|
return Pair(ptx, cashSigningPubKeys)
|
2015-12-14 17:22:21 +00:00
|
|
|
}
|
|
|
|
|
2016-01-13 12:54:10 +00:00
|
|
|
This code is longer but still fairly straightforward. Here are some things to pay attention to:
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
|
2016-02-09 14:11:17 +00:00
|
|
|
2. We create a cash spend in the normal way, by using ``Cash().generateSpend``. See the contracts tutorial if this isn't
|
2015-12-15 14:52:07 +00:00
|
|
|
clear.
|
2015-12-14 17:22:21 +00:00
|
|
|
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
|
|
|
|
whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may
|
|
|
|
be suspended when it waits to receive a message across node or computer restarts, so objects representing a service
|
|
|
|
or data which may frequently change should be accessed 'just in time'.
|
2016-02-09 14:11:17 +00:00
|
|
|
4. Finally, we send the unfinished, invalid transaction to the seller so they can sign it. They are expected to send
|
|
|
|
back to us a ``SignaturesFromSeller``, which once we verify it, should be the final outcome of the trade.
|
2015-12-14 17:22:21 +00:00
|
|
|
|
|
|
|
As you can see, the protocol logic is straightforward and does not contain any callbacks or network glue code, despite
|
|
|
|
the fact that it takes minimal resources and can survive node restarts.
|
|
|
|
|
|
|
|
.. warning:: When accessing things via the ``serviceHub`` field, avoid the temptation to stuff a reference into a local variable.
|
|
|
|
If you do this then next time your protocol waits to receive an object, the system will try and serialise all your
|
|
|
|
local variables and end up trying to serialise, e.g. the timestamping service, which doesn't make any conceptual
|
|
|
|
sense. The ``serviceHub`` field is defined by the ``ProtocolStateMachine`` superclass and is marked transient so
|
2015-12-15 14:52:07 +00:00
|
|
|
this problem doesn't occur. It's also restored for you when a protocol state machine is restored after a node
|
2015-12-14 17:22:21 +00:00
|
|
|
restart.
|
|
|
|
|
2016-02-23 17:17:46 +00:00
|
|
|
Progress tracking
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
Not shown in the code snippets above is the usage of the ``ProgressTracker`` API. Progress tracking exports information
|
|
|
|
from a protocol about where it's got up to in such a way that observers can render it in a useful manner to humans who
|
|
|
|
may need to be informed. It may be rendered via an API, in a GUI, onto a terminal window, etc.
|
|
|
|
|
|
|
|
A ``ProgressTracker`` is constructed with a series of ``Step`` objects, where each step is an object representing a
|
|
|
|
stage in a piece of work. It is therefore typical to use singletons that subclass ``Step``, which may be defined easily
|
|
|
|
in one line when using Kotlin. Typical steps might be "Waiting for response from peer", "Waiting for signature to be
|
|
|
|
approved", "Downloading and verifying data" etc.
|
|
|
|
|
|
|
|
Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep``
|
|
|
|
you can make a step that can update its label on the fly. That's useful for steps that want to expose non-structured
|
|
|
|
progress information like the current file being downloaded. By defining your own step types, you can export progress
|
|
|
|
in a way that's both human readable and machine readable.
|
|
|
|
|
|
|
|
Progress trackers are hierarchical. Each step can be the parent for another tracker. By altering the
|
|
|
|
``ProgressTracker.childrenFor[step] = tracker`` map, a tree of steps can be created. It's allowed to alter the hierarchy
|
|
|
|
at runtime, on the fly, and the progress renderers will adapt to that properly. This can be helpful when you don't
|
|
|
|
fully know ahead of time what steps will be required. If you _do_ know what is required, configuring as much of the
|
|
|
|
hierarchy ahead of time is a good idea, as that will help the users see what is coming up.
|
|
|
|
|
|
|
|
Every tracker has not only the steps given to it at construction time, but also the singleton
|
|
|
|
``ProgressTracker.UNSTARTED`` step and the ``ProgressTracker.DONE`` step. Once a tracker has become ``DONE`` its
|
|
|
|
position may not be modified again (because e.g. the UI may have been removed/cleaned up), but until that point, the
|
|
|
|
position can be set to any arbitrary set both forwards and backwards. Steps may be skipped, repeated, etc. Note that
|
|
|
|
rolling the current step backwards will delete any progress trackers that are children of the steps being reversed, on
|
|
|
|
the assumption that those subtasks will have to be repeated.
|
|
|
|
|
|
|
|
Trackers provide an `Rx observable <http://reactivex.io/>`_ which streams changes to the hierarchy. The top level
|
|
|
|
observable exposes all the events generated by its children as well. The changes are represented by objects indicating
|
|
|
|
whether the change is one of position (i.e. progress), structure (i.e. new subtasks being added/removed) or some other
|
|
|
|
aspect of rendering (i.e. a step has changed in some way and is requesting a re-render).
|
|
|
|
|
|
|
|
The protocol framework is somewhat integrated with this API. Each ``ProtocolLogic`` may optionally provide a tracker by
|
|
|
|
overriding the ``protocolTracker`` property (``getProtocolTracker`` method in Java). If the
|
|
|
|
``ProtocolLogic.subProtocol`` method is used, then the tracker of the sub-protocol will be made a child of the current
|
|
|
|
step in the parent protocol automatically, if the parent is using tracking in the first place. The framework will also
|
|
|
|
automatically set the current step to ``DONE`` for you, when the protocol is finished.
|
|
|
|
|
|
|
|
Because a protocol may sometimes wish to configure the children in its progress hierarchy _before_ the sub-protocol
|
|
|
|
is constructed, for sub-protocols that always follow the same outline regardless of their parameters it's conventional
|
|
|
|
to define a companion object/static method (for Kotlin/Java respectively) that constructs a tracker, and then allow
|
|
|
|
the sub-protocol to have the tracker it will use be passed in as a parameter. This allows all trackers to be built
|
|
|
|
and linked ahead of time.
|
|
|
|
|
|
|
|
In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are
|
2016-08-11 15:22:17 +00:00
|
|
|
surfaced to human operators for investigation and resolution.
|
|
|
|
|
|
|
|
Unit testing
|
|
|
|
------------
|
|
|
|
|
|
|
|
A protocol 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 protocols is the ``ResolveTransactionsProtocol`` tests. This
|
|
|
|
protocol takes care of downloading and verifying transaction graphs, with all the needed dependencies. We start
|
|
|
|
with this basic skeleton:
|
|
|
|
|
|
|
|
.. container:: codeset
|
|
|
|
|
|
|
|
.. sourcecode:: kotlin
|
|
|
|
|
|
|
|
class ResolveTransactionsProtocolTest {
|
|
|
|
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 = ResolveTransactionsProtocol(setOf(stx2.id), a.info.identity)
|
|
|
|
val future = b.services.startProtocol("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 protocol, 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 protocol and do
|
|
|
|
some tests on it. We also check the contents of node B's database to see that the protocol 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/com.r3corda.node.internal.testing/-mock-network/index.html>`_ here.
|