mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
commit
1bb729d329
21
build.gradle
21
build.gradle
@ -24,6 +24,9 @@ buildscript {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
}
|
||||
|
||||
@ -33,11 +36,29 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "com.google.guava:guava:18.0"
|
||||
compile "com.esotericsoftware:kryo:3.0.3"
|
||||
compile "de.javakaffee:kryo-serializers:0.37"
|
||||
compile "com.google.code.findbugs:jsr305:3.0.1"
|
||||
|
||||
// Logging
|
||||
compile "org.slf4j:slf4j-jdk14:1.7.13"
|
||||
|
||||
// For the continuations in the state machine tests. Note: JavaFlow is old and unmaintained but still seems to work
|
||||
// just fine, once the patch here is applied to update it to a Java8 compatible asm:
|
||||
//
|
||||
// https://github.com/playframework/play1/commit/e0e28e6780a48c000e7ed536962f1f284cef9437
|
||||
//
|
||||
// Obviously using this year-old upload to Maven Central by the Maven Play Plugin team is a short term hack for
|
||||
// experimenting. Using this for real would mean forking JavaFlow and taking over maintenance (luckily it's small
|
||||
// and Java is stable, so this is unlikely to be a big burden). We have to manually force an in-place upgrade to
|
||||
// asm 5.0.3 here (javaflow wants 5.0.2) in order to avoid version conflicts. This is also something that should be
|
||||
// fixed in any fork. Sadly, even Jigsaw doesn't solve this problem out of the box.
|
||||
compile "com.google.code.maven-play-plugin.org.apache.commons:commons-javaflow:1590792-patched-play-1.3.0"
|
||||
compile "org.ow2.asm:asm:5.0.3"
|
||||
compile "org.ow2.asm:asm-analysis:5.0.3"
|
||||
compile "org.ow2.asm:asm-tree:5.0.3"
|
||||
compile "org.ow2.asm:asm-commons:5.0.3"
|
||||
compile "org.ow2.asm:asm-util:5.0.3"
|
||||
|
||||
// For visualisation
|
||||
compile "org.graphstream:gs-core:1.3"
|
||||
compile "org.graphstream:gs-ui:1.3"
|
||||
|
2
docs/build/html/_sources/index.txt
vendored
2
docs/build/html/_sources/index.txt
vendored
@ -26,6 +26,8 @@ Read on to learn:
|
||||
overview
|
||||
getting-set-up
|
||||
tutorial
|
||||
messaging
|
||||
protocol-state-machines
|
||||
visualiser
|
||||
roadmap
|
||||
|
||||
|
97
docs/build/html/_sources/messaging.txt
vendored
Normal file
97
docs/build/html/_sources/messaging.txt
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
Networking and messaging
|
||||
========================
|
||||
|
||||
Although the platform does not currently provide a network backend, some preliminary interfaces are defined along with
|
||||
an in-memory implementation provided for use by unit tests and other exploratory code. An implementation based on Apache
|
||||
Kafka is also being developed, which should be sufficient for real use cases to be implemented in the short run, even
|
||||
though in the long run a fully peer to peer protocol will be required.
|
||||
|
||||
This article quickly explains the basic networking interfaces in the code.
|
||||
|
||||
Messaging vs networking
|
||||
-----------------------
|
||||
|
||||
It is important to understand that the code expects any networking module to provide the following services:
|
||||
|
||||
- Persistent, reliable and secure delivery of complete messages. The module is expected to retry delivery if initial
|
||||
attempts fail.
|
||||
- Ability to send messages both 1:1 and 1:many, where 'many' may mean the entire group of network users.
|
||||
|
||||
The details of how this is achieved are not exposed to the rest of the code.
|
||||
|
||||
Interfaces
|
||||
----------
|
||||
|
||||
The most important interface is called ``MessagingService`` and is defined in the ``core/messaging/Messaging.kt`` file.
|
||||
It declares an interface with the following operations:
|
||||
|
||||
- ``addMessageHandler(topic: String, executor: Executor, callback: (Message, MessageHandlerRegistration) -> Unit)``
|
||||
- ``createMessage(topic: String, data: ByteArray): Message``
|
||||
- ``send(message: Message, targetRecipients: MessageRecipients)``
|
||||
- ``stop()``
|
||||
|
||||
along with a few misc others that are not important enough to discuss here.
|
||||
|
||||
A *topic* is simply a string that identifies the kind of message that is being sent. When a message is received, the
|
||||
topic is compared exactly to the list of registered message handlers and if it matches, the callback is invoked.
|
||||
Adding a handler returns a ``MessageHandlerRegistration`` object that can be used to remove the handler, and that
|
||||
registration object is also passed to each invocation to simplify the case where a handler wishes to remove itself.
|
||||
|
||||
Some helper functions are also provided that simplify the process of sending a message by using Kryo serialisation, and
|
||||
registering one-shot handlers that remove themselves once they finished running, but those don't need to be implemented
|
||||
by network module authors themselves.
|
||||
|
||||
Destinations are represented using opaque classes (i.e. their contents are defined by the implementation). The
|
||||
``MessageRecipients`` interface represents any possible set of recipients: it's used when a piece of code doesn't
|
||||
care who is going to get a message, just that someone does. The ``SingleMessageRecipient`` interface inherits from
|
||||
``MessageRecipients`` and represents a handle to some specific individual receiver on the network. Whether they are
|
||||
identified by IP address, public key, message router ID or some other kind of address is not exposed at this level.
|
||||
``MessageRecipientGroup`` is not used anywhere at the moment but represents multiple simultaneous recipients. And
|
||||
finally ``AllPossibleRecipients`` is used for network wide broadcast. It's also unused right now, outside of unit tests.
|
||||
|
||||
In memory implementation
|
||||
------------------------
|
||||
|
||||
To ease unit testing of business logic, a simple in-memory messaging service is provided. To access this you can inherit
|
||||
your test case class from the ``TestWithInMemoryNetwork`` class. This provides a few utility methods to help test
|
||||
code that involves message passing.
|
||||
|
||||
You can run a mock network session in one of two modes:
|
||||
|
||||
- Manually "pumped"
|
||||
- Automatically pumped with background threads
|
||||
|
||||
"Pumping" is the act of telling a mock network node to pop a message off its queue and process it. Typically you want
|
||||
unit tests to be fast, repeatable and you want to be able to insert your own changes into the middle of any given
|
||||
message sequence. This is what the manual mode is for. In this mode, all logic runs on the same thread (the thread
|
||||
running the unit tests). You can create and use a node like this:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val (aliceAddr, aliceNode) = makeNode(inBackground = false)
|
||||
val (bobAddr, bobNode) = makeNode(false)
|
||||
|
||||
aliceNode.send("test.topic", aliceAddr, "foo")
|
||||
bobNode.pump(blocking = false)
|
||||
|
||||
.. note:: Currently only Kotlin examples are available for networking and protocol state machines. Java examples may
|
||||
follow later. Naming arguments in Kotlin like above is optional but sometimes useful to make code examples clearer.
|
||||
|
||||
The above code won't actually do anything because no message handler is registered for "test.topic" so the message will
|
||||
go into a holding area. If/when we add a handler that can accept test.topic, the message will be delivered then.
|
||||
|
||||
Sometimes you don't want to have to call the pump method over and over again. You can use the ``runNetwork { .. }``
|
||||
construct to fix this: any code inside the block will be run, and then all nodes you created will be pumped over and
|
||||
over until all of them have reported that they have no work left to do. This means any ping-pongs of messages will
|
||||
be run until everything settles.
|
||||
|
||||
You can see more examples of how to use this in the file ``InMemoryMessagingTests.kt``.
|
||||
|
||||
If you specify ``inBackground = true`` to ``makeNode`` then each node will create its own background thread which will
|
||||
sit around waiting for messages to be delivered. Handlers will then be invoked on that background thread. This is a
|
||||
more difficult style of programming that can be used to increase the realism of the unit tests by ensuring multiple
|
||||
nodes run in parallel, just as they would on a real network spread over multiple machines.
|
||||
|
||||
|
416
docs/build/html/_sources/protocol-state-machines.txt
vendored
Normal file
416
docs/build/html/_sources/protocol-state-machines.txt
vendored
Normal file
@ -0,0 +1,416 @@
|
||||
.. 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.
|
||||
|
||||
To put these problems in perspective the *payment channel protocol* in the bitcoinj library, which allows bitcoins to
|
||||
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,
|
||||
unserialised and resumed from where it was suspended. 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 a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don't have to know how this works to benefit
|
||||
from it, however.
|
||||
|
||||
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.
|
||||
In contrast a suspended Java stack can easily be 1mb in size.
|
||||
* 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.
|
||||
2. B sends to S a ``SignedWireTransaction`` that includes the state as input, B's cash as input, the state with the new
|
||||
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.
|
||||
3. S signs it and hands the now finalised ``SignedWireTransaction`` back to B.
|
||||
|
||||
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.
|
||||
|
||||
We start by defining an abstract base class to encapsulate the protocol. This is what code that invokes the protocol
|
||||
will see:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
abstract class TwoPartyTradeProtocol {
|
||||
class SellerInitialArgs(
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller
|
||||
|
||||
class BuyerInitialArgs(
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
||||
return TwoPartyTradeProtocolImpl(smm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Let's unpack what this code does:
|
||||
|
||||
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` class, and a couple of methods, one to
|
||||
run the buyer side of the protocol and one to run the seller side.
|
||||
- Two of the classes are simply wrappers for parameters to the trade; things like what is being sold, what the price
|
||||
of the asset is, how much the buyer is willing to pay and so on. The ``myKeyPair`` field is simply the public key
|
||||
that the seller wishes the buyer to send the cash to. The session ID field is sent from buyer to seller when the
|
||||
trade is being set up and is used to keep messages separated on the network, and stop malicious entities trying to
|
||||
interfere with the message stream.
|
||||
- The other two classes define empty abstract classes called ``Buyer`` and ``Seller``. These inherit from a class
|
||||
called ``ProtocolStateMachine`` and provide two type parameters: the arguments class we just defined for each side
|
||||
and the type of the object that the protocol finally produces (this doesn't have to be identical for each side, even
|
||||
though in this case it is).
|
||||
- Finally it simply defines a static method that creates an instance of an object that inherits from this base class
|
||||
and returns it, with a ``StateMachineManager`` as an instance. The Impl class will be defined below.
|
||||
|
||||
.. note:: Session IDs keep different traffic streams separated, so for security they must be large and random enough
|
||||
to be unguessable. 63 bits is good enough.
|
||||
|
||||
Alright, so using this protocol shouldn't be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on the resulting future to 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.
|
||||
|
||||
The only tricky part is how to get one of these things. We need a ``StateMachineManager``. Where does that come from
|
||||
and why do we need one?
|
||||
|
||||
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
|
||||
with a ``MessagingService`` and iterate the right state machine when the time comes. It provides the
|
||||
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
|
||||
-----------------------
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : TwoPartyTradeProtocol() {
|
||||
companion object {
|
||||
val TRADE_TOPIC = "com.r3cev.protocols.trade"
|
||||
}
|
||||
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.seller", SellerImpl::class.java)
|
||||
}
|
||||
|
||||
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.buyer", BuyerImpl::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
We start with a skeleton on which we will build the protocol. Putting things in a *companion object* in Kotlin is like
|
||||
declaring them as static members in Java. Here, we define a "topic" that will identify trade related messages that
|
||||
arrive at a node (see :doc:`messaging` for details).
|
||||
|
||||
The runSeller and runBuyer methods simply start the state machines, passing in a reference to the classes and the topics
|
||||
each side will use.
|
||||
|
||||
Now let's try implementing the seller side. Firstly, we're going to need a message to send to the buyer describing what
|
||||
we want to trade. Remember: this data comes from whatever system was used to find the trading partner to begin with.
|
||||
It could be as simple as a chat room or as complex as a 24/7 exchange.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// 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 buyerSessionID: Long
|
||||
)
|
||||
|
||||
That's simple enough: our opening protocol message will be serialised before being sent over the wire, and it contains
|
||||
the details that were agreed so we can double check them. It also contains a session ID so we can identify this
|
||||
trade's messages, and a pointer to where the asset that is being sold can be found on the ledger.
|
||||
|
||||
Next we add some code to the ``SellerImpl.call`` method:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(args.assetToSell, args.price, args.myKeyPair.public, sessionID)
|
||||
|
||||
// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
That's pretty straight forward. We generate a session ID to identify what's happening on the seller side, fill out
|
||||
the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments:
|
||||
|
||||
- A type argument, which is the object we're expecting to receive from the other side.
|
||||
- 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.
|
||||
- And finally, the thing to send. It'll be serialised and sent automatically.
|
||||
|
||||
Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we'll do a log
|
||||
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.
|
||||
|
||||
.. note:: There are a few 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 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.
|
||||
|
||||
The third rule to bear in mind is that you can't declare variables or methods in these classes and access
|
||||
them from outside of the class, due to the bytecode rewriting and classloader tricks that are used to make this all
|
||||
work. If you want access to something inside the BuyerImpl or SellerImpl classes, you must define a super-interface
|
||||
or super-class (like ``Buyer``/``Seller``) and put what you want to access there.
|
||||
|
||||
OK, let's keep going:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
partialTX.verifySignatures()
|
||||
val wtx = partialTX.txBits.deserialize<WireTransaction>()
|
||||
|
||||
requireThat {
|
||||
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
|
||||
// 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 not be valid according to the contracts of the input states, so we must resolve
|
||||
// and fully audit the transaction chains to convince ourselves that it is actually valid.
|
||||
// - 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, but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
}
|
||||
|
||||
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits)
|
||||
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
|
||||
// We should run it through our full TransactionGroup of all transactions here.
|
||||
fullySigned.verify()
|
||||
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
|
||||
logger().trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, sessionID, timestamped)
|
||||
|
||||
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService))
|
||||
|
||||
Here, we see some assertions and signature checking to satisfy ourselves that we're not about to sign something
|
||||
incorrect. Once we're happy, we calculate a signature over the transaction to authorise the movement of the asset
|
||||
we are selling, and then we verify things to make sure it's all OK. Finally, we request timestamping of the
|
||||
transaction, and send the now finalised and validated transaction back to the buyer.
|
||||
|
||||
.. 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.
|
||||
|
||||
Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different
|
||||
forms.
|
||||
|
||||
Implementing the buyer
|
||||
----------------------
|
||||
|
||||
OK, let's do the same for the buyer side:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
|
||||
// What is the seller trying to sell us?
|
||||
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
|
||||
logger().trace { "Got trade request for a $assetTypeName" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(tradeRequest.sessionID > 0)
|
||||
if (tradeRequest.price > args.acceptablePrice)
|
||||
throw UnacceptablePriceException(tradeRequest.price)
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// 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().craftSpend(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.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
|
||||
|
||||
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
return Pair(fullySigned, ltx)
|
||||
}
|
||||
}
|
||||
|
||||
This code is fairly straightforward. 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.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``.
|
||||
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'.
|
||||
4. Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send
|
||||
back to us a ``TimestampedWireTransaction``, which once we verify it, should be the final outcome of the trade.
|
||||
|
||||
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
|
||||
this problem doesn't occur. It's also restored for you after a protocol state machine is restored after a node
|
||||
restart.
|
||||
|
2
docs/build/html/genindex.html
vendored
2
docs/build/html/genindex.html
vendored
@ -85,6 +85,8 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
2
docs/build/html/getting-set-up.html
vendored
2
docs/build/html/getting-set-up.html
vendored
@ -90,6 +90,8 @@
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
17
docs/build/html/index.html
vendored
17
docs/build/html/index.html
vendored
@ -85,6 +85,8 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
@ -172,6 +174,21 @@ prove or disprove the following hypothesis:</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#non-asset-oriented-based-smart-contracts">Non-asset-oriented based smart contracts</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="messaging.html#messaging-vs-networking">Messaging vs networking</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="messaging.html#interfaces">Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="messaging.html#in-memory-implementation">In memory implementation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#introduction">Introduction</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#theory">Theory</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#a-two-party-trading-protocol">A two party trading protocol</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#the-state-machine-manager">The state machine manager</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#implementing-the-seller">Implementing the seller</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="protocol-state-machines.html#implementing-the-buyer">Implementing the buyer</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
2
docs/build/html/inthebox.html
vendored
2
docs/build/html/inthebox.html
vendored
@ -86,6 +86,8 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
295
docs/build/html/messaging.html
vendored
Normal file
295
docs/build/html/messaging.html
vendored
Normal file
@ -0,0 +1,295 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Networking and messaging — R3 Prototyping 0.1 documentation</title>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="_static/css/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="top" title="R3 Prototyping 0.1 documentation" href="index.html"/>
|
||||
<link rel="next" title="Protocol state machines" href="protocol-state-machines.html"/>
|
||||
<link rel="prev" title="Tutorial" href="tutorial.html"/>
|
||||
|
||||
|
||||
<script src="_static/js/modernizr.min.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="wy-body-for-nav" role="document">
|
||||
|
||||
<div class="wy-grid-for-nav">
|
||||
|
||||
|
||||
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
||||
<div class="wy-side-scroll">
|
||||
<div class="wy-side-nav-search">
|
||||
|
||||
|
||||
|
||||
<a href="index.html" class="icon icon-home"> R3 Prototyping
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="version">
|
||||
0.1
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div role="search">
|
||||
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
|
||||
<input type="text" name="q" placeholder="Search docs" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
||||
|
||||
|
||||
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="inthebox.html">What’s included?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="">Networking and messaging</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#messaging-vs-networking">Messaging vs networking</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#interfaces">Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#in-memory-implementation">In memory implementation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||
|
||||
|
||||
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
|
||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||
<a href="index.html">R3 Prototyping</a>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<div class="wy-nav-content">
|
||||
<div class="rst-content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div role="navigation" aria-label="breadcrumbs navigation">
|
||||
<ul class="wy-breadcrumbs">
|
||||
<li><a href="index.html">Docs</a> »</li>
|
||||
|
||||
<li>Networking and messaging</li>
|
||||
<li class="wy-breadcrumbs-aside">
|
||||
|
||||
|
||||
<a href="_sources/messaging.txt" rel="nofollow"> View page source</a>
|
||||
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
</div>
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
<div itemprop="articleBody">
|
||||
|
||||
<div class="section" id="networking-and-messaging">
|
||||
<h1>Networking and messaging<a class="headerlink" href="#networking-and-messaging" title="Permalink to this headline">¶</a></h1>
|
||||
<p>Although the platform does not currently provide a network backend, some preliminary interfaces are defined along with
|
||||
an in-memory implementation provided for use by unit tests and other exploratory code. An implementation based on Apache
|
||||
Kafka is also being developed, which should be sufficient for real use cases to be implemented in the short run, even
|
||||
though in the long run a fully peer to peer protocol will be required.</p>
|
||||
<p>This article quickly explains the basic networking interfaces in the code.</p>
|
||||
<div class="section" id="messaging-vs-networking">
|
||||
<h2>Messaging vs networking<a class="headerlink" href="#messaging-vs-networking" title="Permalink to this headline">¶</a></h2>
|
||||
<p>It is important to understand that the code expects any networking module to provide the following services:</p>
|
||||
<ul class="simple">
|
||||
<li>Persistent, reliable and secure delivery of complete messages. The module is expected to retry delivery if initial
|
||||
attempts fail.</li>
|
||||
<li>Ability to send messages both 1:1 and 1:many, where ‘many’ may mean the entire group of network users.</li>
|
||||
</ul>
|
||||
<p>The details of how this is achieved are not exposed to the rest of the code.</p>
|
||||
</div>
|
||||
<div class="section" id="interfaces">
|
||||
<h2>Interfaces<a class="headerlink" href="#interfaces" title="Permalink to this headline">¶</a></h2>
|
||||
<p>The most important interface is called <code class="docutils literal"><span class="pre">MessagingService</span></code> and is defined in the <code class="docutils literal"><span class="pre">core/messaging/Messaging.kt</span></code> file.
|
||||
It declares an interface with the following operations:</p>
|
||||
<ul class="simple">
|
||||
<li><code class="docutils literal"><span class="pre">addMessageHandler(topic:</span> <span class="pre">String,</span> <span class="pre">executor:</span> <span class="pre">Executor,</span> <span class="pre">callback:</span> <span class="pre">(Message,</span> <span class="pre">MessageHandlerRegistration)</span> <span class="pre">-></span> <span class="pre">Unit)</span></code></li>
|
||||
<li><code class="docutils literal"><span class="pre">createMessage(topic:</span> <span class="pre">String,</span> <span class="pre">data:</span> <span class="pre">ByteArray):</span> <span class="pre">Message</span></code></li>
|
||||
<li><code class="docutils literal"><span class="pre">send(message:</span> <span class="pre">Message,</span> <span class="pre">targetRecipients:</span> <span class="pre">MessageRecipients)</span></code></li>
|
||||
<li><code class="docutils literal"><span class="pre">stop()</span></code></li>
|
||||
</ul>
|
||||
<p>along with a few misc others that are not important enough to discuss here.</p>
|
||||
<p>A <em>topic</em> is simply a string that identifies the kind of message that is being sent. When a message is received, the
|
||||
topic is compared exactly to the list of registered message handlers and if it matches, the callback is invoked.
|
||||
Adding a handler returns a <code class="docutils literal"><span class="pre">MessageHandlerRegistration</span></code> object that can be used to remove the handler, and that
|
||||
registration object is also passed to each invocation to simplify the case where a handler wishes to remove itself.</p>
|
||||
<p>Some helper functions are also provided that simplify the process of sending a message by using Kryo serialisation, and
|
||||
registering one-shot handlers that remove themselves once they finished running, but those don’t need to be implemented
|
||||
by network module authors themselves.</p>
|
||||
<p>Destinations are represented using opaque classes (i.e. their contents are defined by the implementation). The
|
||||
<code class="docutils literal"><span class="pre">MessageRecipients</span></code> interface represents any possible set of recipients: it’s used when a piece of code doesn’t
|
||||
care who is going to get a message, just that someone does. The <code class="docutils literal"><span class="pre">SingleMessageRecipient</span></code> interface inherits from
|
||||
<code class="docutils literal"><span class="pre">MessageRecipients</span></code> and represents a handle to some specific individual receiver on the network. Whether they are
|
||||
identified by IP address, public key, message router ID or some other kind of address is not exposed at this level.
|
||||
<code class="docutils literal"><span class="pre">MessageRecipientGroup</span></code> is not used anywhere at the moment but represents multiple simultaneous recipients. And
|
||||
finally <code class="docutils literal"><span class="pre">AllPossibleRecipients</span></code> is used for network wide broadcast. It’s also unused right now, outside of unit tests.</p>
|
||||
</div>
|
||||
<div class="section" id="in-memory-implementation">
|
||||
<h2>In memory implementation<a class="headerlink" href="#in-memory-implementation" title="Permalink to this headline">¶</a></h2>
|
||||
<p>To ease unit testing of business logic, a simple in-memory messaging service is provided. To access this you can inherit
|
||||
your test case class from the <code class="docutils literal"><span class="pre">TestWithInMemoryNetwork</span></code> class. This provides a few utility methods to help test
|
||||
code that involves message passing.</p>
|
||||
<p>You can run a mock network session in one of two modes:</p>
|
||||
<ul class="simple">
|
||||
<li>Manually “pumped”</li>
|
||||
<li>Automatically pumped with background threads</li>
|
||||
</ul>
|
||||
<p>“Pumping” is the act of telling a mock network node to pop a message off its queue and process it. Typically you want
|
||||
unit tests to be fast, repeatable and you want to be able to insert your own changes into the middle of any given
|
||||
message sequence. This is what the manual mode is for. In this mode, all logic runs on the same thread (the thread
|
||||
running the unit tests). You can create and use a node like this:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>val (aliceAddr, aliceNode) = makeNode(inBackground = false)
|
||||
val (bobAddr, bobNode) = makeNode(false)
|
||||
|
||||
aliceNode.send("test.topic", aliceAddr, "foo")
|
||||
bobNode.pump(blocking = false)
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">Currently only Kotlin examples are available for networking and protocol state machines. Java examples may
|
||||
follow later. Naming arguments in Kotlin like above is optional but sometimes useful to make code examples clearer.</p>
|
||||
</div>
|
||||
<p>The above code won’t actually do anything because no message handler is registered for “test.topic” so the message will
|
||||
go into a holding area. If/when we add a handler that can accept test.topic, the message will be delivered then.</p>
|
||||
<p>Sometimes you don’t want to have to call the pump method over and over again. You can use the <code class="docutils literal"><span class="pre">runNetwork</span> <span class="pre">{</span> <span class="pre">..</span> <span class="pre">}</span></code>
|
||||
construct to fix this: any code inside the block will be run, and then all nodes you created will be pumped over and
|
||||
over until all of them have reported that they have no work left to do. This means any ping-pongs of messages will
|
||||
be run until everything settles.</p>
|
||||
<p>You can see more examples of how to use this in the file <code class="docutils literal"><span class="pre">InMemoryMessagingTests.kt</span></code>.</p>
|
||||
<p>If you specify <code class="docutils literal"><span class="pre">inBackground</span> <span class="pre">=</span> <span class="pre">true</span></code> to <code class="docutils literal"><span class="pre">makeNode</span></code> then each node will create its own background thread which will
|
||||
sit around waiting for messages to be delivered. Handlers will then be invoked on that background thread. This is a
|
||||
more difficult style of programming that can be used to increase the realism of the unit tests by ensuring multiple
|
||||
nodes run in parallel, just as they would on a real network spread over multiple machines.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
|
||||
|
||||
<a href="protocol-state-machines.html" class="btn btn-neutral float-right" title="Protocol state machines" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
|
||||
|
||||
|
||||
<a href="tutorial.html" class="btn btn-neutral" title="Tutorial" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
<div role="contentinfo">
|
||||
<p>
|
||||
© Copyright 2015, R3 CEV.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
Built with <a href="http://sphinx-doc.org/">Sphinx</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
|
||||
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT:'./',
|
||||
VERSION:'0.1',
|
||||
COLLAPSE_INDEX:false,
|
||||
FILE_SUFFIX:'.html',
|
||||
HAS_SOURCE: true
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/underscore.js"></script>
|
||||
<script type="text/javascript" src="_static/doctools.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript" src="_static/js/theme.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(function () {
|
||||
SphinxRtdTheme.StickyNav.enable();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
597
docs/build/html/protocol-state-machines.html
vendored
Normal file
597
docs/build/html/protocol-state-machines.html
vendored
Normal file
@ -0,0 +1,597 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Protocol state machines — R3 Prototyping 0.1 documentation</title>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="_static/css/custom.css" type="text/css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="top" title="R3 Prototyping 0.1 documentation" href="index.html"/>
|
||||
<link rel="next" title="Using the visualiser" href="visualiser.html"/>
|
||||
<link rel="prev" title="Networking and messaging" href="messaging.html"/>
|
||||
|
||||
|
||||
<script src="_static/js/modernizr.min.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="wy-body-for-nav" role="document">
|
||||
|
||||
<div class="wy-grid-for-nav">
|
||||
|
||||
|
||||
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
||||
<div class="wy-side-scroll">
|
||||
<div class="wy-side-nav-search">
|
||||
|
||||
|
||||
|
||||
<a href="index.html" class="icon icon-home"> R3 Prototyping
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="version">
|
||||
0.1
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div role="search">
|
||||
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
|
||||
<input type="text" name="q" placeholder="Search docs" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
||||
|
||||
|
||||
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="inthebox.html">What’s included?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1 current"><a class="current reference internal" href="">Protocol state machines</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#introduction">Introduction</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#theory">Theory</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#a-two-party-trading-protocol">A two party trading protocol</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#the-state-machine-manager">The state machine manager</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#implementing-the-seller">Implementing the seller</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="#implementing-the-buyer">Implementing the buyer</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||
|
||||
|
||||
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
|
||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||
<a href="index.html">R3 Prototyping</a>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<div class="wy-nav-content">
|
||||
<div class="rst-content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div role="navigation" aria-label="breadcrumbs navigation">
|
||||
<ul class="wy-breadcrumbs">
|
||||
<li><a href="index.html">Docs</a> »</li>
|
||||
|
||||
<li>Protocol state machines</li>
|
||||
<li class="wy-breadcrumbs-aside">
|
||||
|
||||
|
||||
<a href="_sources/protocol-state-machines.txt" rel="nofollow"> View page source</a>
|
||||
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
</div>
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
<div itemprop="articleBody">
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script><div class="section" id="protocol-state-machines">
|
||||
<h1>Protocol state machines<a class="headerlink" href="#protocol-state-machines" title="Permalink to this headline">¶</a></h1>
|
||||
<p>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.</p>
|
||||
<div class="section" id="introduction">
|
||||
<h2>Introduction<a class="headerlink" href="#introduction" title="Permalink to this headline">¶</a></h2>
|
||||
<p>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.</p>
|
||||
<p>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
|
||||
<em>delivery versus payment</em> 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.</p>
|
||||
<p>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:</p>
|
||||
<ul class="simple">
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>Error handling.</li>
|
||||
<li>Message routing.</li>
|
||||
<li>Serialisation.</li>
|
||||
<li>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.</li>
|
||||
<li>Unit testing of the finished protocol.</li>
|
||||
</ul>
|
||||
<p>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.</p>
|
||||
<p>To put these problems in perspective the <em>payment channel protocol</em> in the bitcoinj library, which allows bitcoins to
|
||||
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.</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="section" id="theory">
|
||||
<h2>Theory<a class="headerlink" href="#theory" title="Permalink to this headline">¶</a></h2>
|
||||
<p>A <em>continuation</em> is a suspended stack frame stored in a regular object that can be passed around, serialised,
|
||||
unserialised and resumed from where it was suspended. 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 a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don’t have to know how this works to benefit
|
||||
from it, however.</p>
|
||||
<p>We use continuations for the following reasons:</p>
|
||||
<ul class="simple">
|
||||
<li>It allows us to write code that is free of callbacks, that looks like ordinary sequential code.</li>
|
||||
<li>A suspended continuation takes far less memory than a suspended thread. It can be as low as a few hundred bytes.
|
||||
In contrast a suspended Java stack can easily be 1mb in size.</li>
|
||||
<li>It frees the developer from thinking (much) about persistence and serialisation.</li>
|
||||
</ul>
|
||||
<p>A <em>state machine</em> is a piece of code that moves through various <em>states</em>. 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.</p>
|
||||
</div>
|
||||
<div class="section" id="a-two-party-trading-protocol">
|
||||
<h2>A two party trading protocol<a class="headerlink" href="#a-two-party-trading-protocol" title="Permalink to this headline">¶</a></h2>
|
||||
<p>We would like to implement the “hello world” of shared transaction building protocols: a seller wishes to sell some
|
||||
<em>asset</em> (e.g. some commercial paper) in return for <em>cash</em>. 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.</p>
|
||||
<p>Our protocol has two parties (B and S for buyer and seller) and will proceed as follows:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>S sends a <code class="docutils literal"><span class="pre">StateAndRef</span></code> pointing to the state they want to sell to B, along with info about the price they require
|
||||
B to pay.</li>
|
||||
<li>B sends to S a <code class="docutils literal"><span class="pre">SignedWireTransaction</span></code> that includes the state as input, B’s cash as input, the state with the new
|
||||
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.</li>
|
||||
<li>S signs it and hands the now finalised <code class="docutils literal"><span class="pre">SignedWireTransaction</span></code> back to B.</li>
|
||||
</ol>
|
||||
<p>You can find the implementation of this protocol in the file <code class="docutils literal"><span class="pre">contracts/protocols/TwoPartyTradeProtocol.kt</span></code>.</p>
|
||||
<p>Assuming no malicious termination, they both end the protocol being in posession of a valid, signed transaction that
|
||||
represents an atomic asset swap.</p>
|
||||
<p>Note that it’s the <em>seller</em> who initiates contact with the buyer, not vice-versa as you might imagine.</p>
|
||||
<p>We start by defining an abstract base class to encapsulate the protocol. This is what code that invokes the protocol
|
||||
will see:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>abstract class TwoPartyTradeProtocol {
|
||||
class SellerInitialArgs(
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller
|
||||
|
||||
class BuyerInitialArgs(
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
||||
return TwoPartyTradeProtocolImpl(smm)
|
||||
}
|
||||
}
|
||||
}
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Let’s unpack what this code does:</p>
|
||||
<ul class="simple">
|
||||
<li>It defines a several classes nested inside the main <code class="docutils literal"><span class="pre">TwoPartyTradeProtocol</span></code> class, and a couple of methods, one to
|
||||
run the buyer side of the protocol and one to run the seller side.</li>
|
||||
<li>Two of the classes are simply wrappers for parameters to the trade; things like what is being sold, what the price
|
||||
of the asset is, how much the buyer is willing to pay and so on. The <code class="docutils literal"><span class="pre">myKeyPair</span></code> field is simply the public key
|
||||
that the seller wishes the buyer to send the cash to. The session ID field is sent from buyer to seller when the
|
||||
trade is being set up and is used to keep messages separated on the network, and stop malicious entities trying to
|
||||
interfere with the message stream.</li>
|
||||
<li>The other two classes define empty abstract classes called <code class="docutils literal"><span class="pre">Buyer</span></code> and <code class="docutils literal"><span class="pre">Seller</span></code>. These inherit from a class
|
||||
called <code class="docutils literal"><span class="pre">ProtocolStateMachine</span></code> and provide two type parameters: the arguments class we just defined for each side
|
||||
and the type of the object that the protocol finally produces (this doesn’t have to be identical for each side, even
|
||||
though in this case it is).</li>
|
||||
<li>Finally it simply defines a static method that creates an instance of an object that inherits from this base class
|
||||
and returns it, with a <code class="docutils literal"><span class="pre">StateMachineManager</span></code> as an instance. The Impl class will be defined below.</li>
|
||||
</ul>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">Session IDs keep different traffic streams separated, so for security they must be large and random enough</p>
|
||||
</div>
|
||||
<p>to be unguessable. 63 bits is good enough.</p>
|
||||
<p>Alright, so using this protocol shouldn’t be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call <code class="docutils literal"><span class="pre">.get()</span></code> on the resulting future to 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.</p>
|
||||
<p>The only tricky part is how to get one of these things. We need a <code class="docutils literal"><span class="pre">StateMachineManager</span></code>. Where does that come from
|
||||
and why do we need one?</p>
|
||||
</div>
|
||||
<div class="section" id="the-state-machine-manager">
|
||||
<h2>The state machine manager<a class="headerlink" href="#the-state-machine-manager" title="Permalink to this headline">¶</a></h2>
|
||||
<p>The SMM is a class responsible for taking care of all running protocols in a node. It knows how to register handlers
|
||||
with a <code class="docutils literal"><span class="pre">MessagingService</span></code> and iterate the right state machine when the time comes. It provides the
|
||||
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.</p>
|
||||
<p>To get a <code class="docutils literal"><span class="pre">StateMachineManager</span></code>, you currently have to build one by passing in a <code class="docutils literal"><span class="pre">ServiceHub</span></code> 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.</p>
|
||||
</div>
|
||||
<div class="section" id="implementing-the-seller">
|
||||
<h2>Implementing the seller<a class="headerlink" href="#implementing-the-seller" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : TwoPartyTradeProtocol() {
|
||||
companion object {
|
||||
val TRADE_TOPIC = "com.r3cev.protocols.trade"
|
||||
}
|
||||
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.seller", SellerImpl::class.java)
|
||||
}
|
||||
|
||||
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.buyer", BuyerImpl::class.java)
|
||||
}
|
||||
}
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>We start with a skeleton on which we will build the protocol. Putting things in a <em>companion object</em> in Kotlin is like
|
||||
declaring them as static members in Java. Here, we define a “topic” that will identify trade related messages that
|
||||
arrive at a node (see <a class="reference internal" href="messaging.html"><em>Networking and messaging</em></a> for details).</p>
|
||||
<p>The runSeller and runBuyer methods simply start the state machines, passing in a reference to the classes and the topics
|
||||
each side will use.</p>
|
||||
<p>Now let’s try implementing the seller side. Firstly, we’re going to need a message to send to the buyer describing what
|
||||
we want to trade. Remember: this data comes from whatever system was used to find the trading partner to begin with.
|
||||
It could be as simple as a chat room or as complex as a 24/7 exchange.</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="c1">// This object is serialised to the network and is the first protocol message the seller sends to the buyer.</span>
|
||||
<span class="k">class</span> <span class="nc">SellerTradeInfo</span><span class="p">(</span>
|
||||
<span class="k">val</span> <span class="py">assetForSale</span><span class="p">:</span> <span class="n">StateAndRef</span><span class="p"><</span><span class="n">OwnableState</span><span class="p">>,</span>
|
||||
<span class="k">val</span> <span class="py">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">sellerOwnerKey</span><span class="p">:</span> <span class="n">PublicKey</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">buyerSessionID</span><span class="p">:</span> <span class="n">Long</span>
|
||||
<span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>That’s simple enough: our opening protocol message will be serialised before being sent over the wire, and it contains
|
||||
the details that were agreed so we can double check them. It also contains a session ID so we can identify this
|
||||
trade’s messages, and a pointer to where the asset that is being sold can be found on the ledger.</p>
|
||||
<p>Next we add some code to the <code class="docutils literal"><span class="pre">SellerImpl.call</span></code> method:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="k">val</span> <span class="py">sessionID</span> <span class="p">=</span> <span class="n">random63BitValue</span><span class="p">()</span>
|
||||
|
||||
<span class="c1">// Make the first message we'll send to kick off the protocol.</span>
|
||||
<span class="k">val</span> <span class="py">hello</span> <span class="p">=</span> <span class="n">SellerTradeInfo</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">assetToSell</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">price</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">myKeyPair</span><span class="p">.</span><span class="k">public</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">)</span>
|
||||
|
||||
<span class="c1">// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).</span>
|
||||
<span class="k">val</span> <span class="py">partialTX</span> <span class="p">=</span> <span class="n">sendAndReceive</span><span class="p"><</span><span class="n">SignedWireTransaction</span><span class="p">>(</span><span class="n">TRADE_TOPIC</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">buyerSessionID</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="p">().</span><span class="n">trace</span> <span class="p">{</span> <span class="s">"Received partially signed transaction"</span> <span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>That’s pretty straight forward. We generate a session ID to identify what’s happening on the seller side, fill out
|
||||
the initial protocol message, and then call <code class="docutils literal"><span class="pre">sendAndReceive</span></code>. This function takes a few arguments:</p>
|
||||
<ul class="simple">
|
||||
<li>A type argument, which is the object we’re expecting to receive from the other side.</li>
|
||||
<li>The topic string that ensures the message is routed to the right bit of code in the other side’s node.</li>
|
||||
<li>The session IDs that ensure the messages don’t get mixed up with other simultaneous trades.</li>
|
||||
<li>And finally, the thing to send. It’ll be serialised and sent automatically.</li>
|
||||
</ul>
|
||||
<p>Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we’ll do a log
|
||||
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.</p>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p>There are a few 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 garbage collector. So try to avoid keeping enormous data structures alive unless you really have to.</p>
|
||||
<p>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.</p>
|
||||
<p class="last">The third rule to bear in mind is that you can’t declare variables or methods in these classes and access
|
||||
them from outside of the class, due to the bytecode rewriting and classloader tricks that are used to make this all
|
||||
work. If you want access to something inside the BuyerImpl or SellerImpl classes, you must define a super-interface
|
||||
or super-class (like <code class="docutils literal"><span class="pre">Buyer</span></code>/<code class="docutils literal"><span class="pre">Seller</span></code>) and put what you want to access there.</p>
|
||||
</div>
|
||||
<p>OK, let’s keep going:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="n">partialTX</span><span class="p">.</span><span class="n">verifySignatures</span><span class="p">()</span>
|
||||
<span class="k">val</span> <span class="py">wtx</span> <span class="p">=</span> <span class="n">partialTX</span><span class="p">.</span><span class="n">txBits</span><span class="p">.</span><span class="n">deserialize</span><span class="p"><</span><span class="n">WireTransaction</span><span class="p">>()</span>
|
||||
|
||||
<span class="n">requireThat</span> <span class="p">{</span>
|
||||
<span class="s">"transaction sends us the right amount of cash"</span> <span class="k">by</span> <span class="p">(</span><span class="n">wtx</span><span class="p">.</span><span class="n">outputStates</span><span class="p">.</span><span class="n">sumCashBy</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">myKeyPair</span><span class="p">.</span><span class="k">public</span><span class="p">)</span> <span class="p">==</span> <span class="n">args</span><span class="p">.</span><span class="n">price</span><span class="p">)</span>
|
||||
<span class="c1">// There are all sorts of funny games a malicious secondary might play here, we should fix them:</span>
|
||||
<span class="c1">//</span>
|
||||
<span class="c1">// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if</span>
|
||||
<span class="c1">// we're reusing keys! So don't reuse keys!</span>
|
||||
<span class="c1">// - This tx may not be valid according to the contracts of the input states, so we must resolve</span>
|
||||
<span class="c1">// and fully audit the transaction chains to convince ourselves that it is actually valid.</span>
|
||||
<span class="c1">// - This tx may include output states that impose odd conditions on the movement of the cash,</span>
|
||||
<span class="c1">// once we implement state pairing.</span>
|
||||
<span class="c1">//</span>
|
||||
<span class="c1">// but the goal of this code is not to be fully secure, but rather, just to find good ways to</span>
|
||||
<span class="c1">// express protocol state machines on top of the messaging layer.</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="k">val</span> <span class="py">ourSignature</span> <span class="p">=</span> <span class="n">args</span><span class="p">.</span><span class="n">myKeyPair</span><span class="p">.</span><span class="n">signWithECDSA</span><span class="p">(</span><span class="n">partialTX</span><span class="p">.</span><span class="n">txBits</span><span class="p">.</span><span class="n">bits</span><span class="p">)</span>
|
||||
<span class="k">val</span> <span class="py">fullySigned</span><span class="p">:</span> <span class="n">SignedWireTransaction</span> <span class="p">=</span> <span class="n">partialTX</span><span class="p">.</span><span class="n">copy</span><span class="p">(</span><span class="n">sigs</span> <span class="p">=</span> <span class="n">partialTX</span><span class="p">.</span><span class="n">sigs</span> <span class="p">+</span> <span class="n">ourSignature</span><span class="p">)</span>
|
||||
<span class="c1">// We should run it through our full TransactionGroup of all transactions here.</span>
|
||||
<span class="n">fullySigned</span><span class="p">.</span><span class="n">verify</span><span class="p">()</span>
|
||||
<span class="k">val</span> <span class="py">timestamped</span><span class="p">:</span> <span class="n">TimestampedWireTransaction</span> <span class="p">=</span> <span class="n">fullySigned</span><span class="p">.</span><span class="n">toTimestampedTransaction</span><span class="p">(</span><span class="n">serviceHub</span><span class="p">.</span><span class="n">timestampingService</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="p">().</span><span class="n">trace</span> <span class="p">{</span> <span class="s">"Built finished transaction, sending back to secondary!"</span> <span class="p">}</span>
|
||||
|
||||
<span class="n">send</span><span class="p">(</span><span class="n">TRADE_TOPIC</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">,</span> <span class="n">timestamped</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">Pair</span><span class="p">(</span><span class="n">timestamped</span><span class="p">,</span> <span class="n">timestamped</span><span class="p">.</span><span class="n">verifyToLedgerTransaction</span><span class="p">(</span><span class="n">serviceHub</span><span class="p">.</span><span class="n">timestampingService</span><span class="p">,</span> <span class="n">serviceHub</span><span class="p">.</span><span class="n">identityService</span><span class="p">))</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Here, we see some assertions and signature checking to satisfy ourselves that we’re not about to sign something
|
||||
incorrect. Once we’re happy, we calculate a signature over the transaction to authorise the movement of the asset
|
||||
we are selling, and then we verify things to make sure it’s all OK. Finally, we request timestamping of the
|
||||
transaction, and send the now finalised and validated transaction back to the buyer.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="first admonition-title">Warning</p>
|
||||
<p class="last">This code is <strong>not secure</strong>. 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.</p>
|
||||
</div>
|
||||
<p>Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different
|
||||
forms.</p>
|
||||
</div>
|
||||
<div class="section" id="implementing-the-buyer">
|
||||
<h2>Implementing the buyer<a class="headerlink" href="#implementing-the-buyer" title="Permalink to this headline">¶</a></h2>
|
||||
<p>OK, let’s do the same for the buyer side:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
|
||||
// What is the seller trying to sell us?
|
||||
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
|
||||
logger().trace { "Got trade request for a $assetTypeName" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(tradeRequest.sessionID > 0)
|
||||
if (tradeRequest.price > args.acceptablePrice)
|
||||
throw UnacceptablePriceException(tradeRequest.price)
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// 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().craftSpend(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.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
|
||||
|
||||
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
return Pair(fullySigned, ltx)
|
||||
}
|
||||
}
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>This code is fairly straightforward. Here are some things to pay attention to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>We do some sanity checking on the received message to ensure we’re being offered what we expected to be offered.</li>
|
||||
<li>We create a cash spend in the normal way, by using <code class="docutils literal"><span class="pre">Cash().craftSpend</span></code>.</li>
|
||||
<li>We access the <em>service hub</em> 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’.</li>
|
||||
<li>Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send
|
||||
back to us a <code class="docutils literal"><span class="pre">TimestampedWireTransaction</span></code>, which once we verify it, should be the final outcome of the trade.</li>
|
||||
</ol>
|
||||
<p>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.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="first admonition-title">Warning</p>
|
||||
<p class="last">When accessing things via the <code class="docutils literal"><span class="pre">serviceHub</span></code> 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 <code class="docutils literal"><span class="pre">serviceHub</span></code> field is defined by the <code class="docutils literal"><span class="pre">ProtocolStateMachine</span></code> superclass and is marked transient so
|
||||
this problem doesn’t occur. It’s also restored for you after a protocol state machine is restored after a node
|
||||
restart.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
|
||||
|
||||
<a href="visualiser.html" class="btn btn-neutral float-right" title="Using the visualiser" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
|
||||
|
||||
|
||||
<a href="messaging.html" class="btn btn-neutral" title="Networking and messaging" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
<div role="contentinfo">
|
||||
<p>
|
||||
© Copyright 2015, R3 CEV.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
Built with <a href="http://sphinx-doc.org/">Sphinx</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
|
||||
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT:'./',
|
||||
VERSION:'0.1',
|
||||
COLLAPSE_INDEX:false,
|
||||
FILE_SUFFIX:'.html',
|
||||
HAS_SOURCE: true
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/underscore.js"></script>
|
||||
<script type="text/javascript" src="_static/doctools.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript" src="_static/js/theme.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(function () {
|
||||
SphinxRtdTheme.StickyNav.enable();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
2
docs/build/html/search.html
vendored
2
docs/build/html/search.html
vendored
@ -84,6 +84,8 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
|
||||
</ul>
|
||||
|
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
File diff suppressed because one or more lines are too long
@ -26,6 +26,8 @@ Read on to learn:
|
||||
overview
|
||||
getting-set-up
|
||||
tutorial
|
||||
messaging
|
||||
protocol-state-machines
|
||||
visualiser
|
||||
roadmap
|
||||
|
||||
|
97
docs/source/messaging.rst
Normal file
97
docs/source/messaging.rst
Normal file
@ -0,0 +1,97 @@
|
||||
Networking and messaging
|
||||
========================
|
||||
|
||||
Although the platform does not currently provide a network backend, some preliminary interfaces are defined along with
|
||||
an in-memory implementation provided for use by unit tests and other exploratory code. An implementation based on Apache
|
||||
Kafka is also being developed, which should be sufficient for real use cases to be implemented in the short run, even
|
||||
though in the long run a fully peer to peer protocol will be required.
|
||||
|
||||
This article quickly explains the basic networking interfaces in the code.
|
||||
|
||||
Messaging vs networking
|
||||
-----------------------
|
||||
|
||||
It is important to understand that the code expects any networking module to provide the following services:
|
||||
|
||||
- Persistent, reliable and secure delivery of complete messages. The module is expected to retry delivery if initial
|
||||
attempts fail.
|
||||
- Ability to send messages both 1:1 and 1:many, where 'many' may mean the entire group of network users.
|
||||
|
||||
The details of how this is achieved are not exposed to the rest of the code.
|
||||
|
||||
Interfaces
|
||||
----------
|
||||
|
||||
The most important interface is called ``MessagingService`` and is defined in the ``core/messaging/Messaging.kt`` file.
|
||||
It declares an interface with the following operations:
|
||||
|
||||
- ``addMessageHandler(topic: String, executor: Executor, callback: (Message, MessageHandlerRegistration) -> Unit)``
|
||||
- ``createMessage(topic: String, data: ByteArray): Message``
|
||||
- ``send(message: Message, targetRecipients: MessageRecipients)``
|
||||
- ``stop()``
|
||||
|
||||
along with a few misc others that are not important enough to discuss here.
|
||||
|
||||
A *topic* is simply a string that identifies the kind of message that is being sent. When a message is received, the
|
||||
topic is compared exactly to the list of registered message handlers and if it matches, the callback is invoked.
|
||||
Adding a handler returns a ``MessageHandlerRegistration`` object that can be used to remove the handler, and that
|
||||
registration object is also passed to each invocation to simplify the case where a handler wishes to remove itself.
|
||||
|
||||
Some helper functions are also provided that simplify the process of sending a message by using Kryo serialisation, and
|
||||
registering one-shot handlers that remove themselves once they finished running, but those don't need to be implemented
|
||||
by network module authors themselves.
|
||||
|
||||
Destinations are represented using opaque classes (i.e. their contents are defined by the implementation). The
|
||||
``MessageRecipients`` interface represents any possible set of recipients: it's used when a piece of code doesn't
|
||||
care who is going to get a message, just that someone does. The ``SingleMessageRecipient`` interface inherits from
|
||||
``MessageRecipients`` and represents a handle to some specific individual receiver on the network. Whether they are
|
||||
identified by IP address, public key, message router ID or some other kind of address is not exposed at this level.
|
||||
``MessageRecipientGroup`` is not used anywhere at the moment but represents multiple simultaneous recipients. And
|
||||
finally ``AllPossibleRecipients`` is used for network wide broadcast. It's also unused right now, outside of unit tests.
|
||||
|
||||
In memory implementation
|
||||
------------------------
|
||||
|
||||
To ease unit testing of business logic, a simple in-memory messaging service is provided. To access this you can inherit
|
||||
your test case class from the ``TestWithInMemoryNetwork`` class. This provides a few utility methods to help test
|
||||
code that involves message passing.
|
||||
|
||||
You can run a mock network session in one of two modes:
|
||||
|
||||
- Manually "pumped"
|
||||
- Automatically pumped with background threads
|
||||
|
||||
"Pumping" is the act of telling a mock network node to pop a message off its queue and process it. Typically you want
|
||||
unit tests to be fast, repeatable and you want to be able to insert your own changes into the middle of any given
|
||||
message sequence. This is what the manual mode is for. In this mode, all logic runs on the same thread (the thread
|
||||
running the unit tests). You can create and use a node like this:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val (aliceAddr, aliceNode) = makeNode(inBackground = false)
|
||||
val (bobAddr, bobNode) = makeNode(false)
|
||||
|
||||
aliceNode.send("test.topic", aliceAddr, "foo")
|
||||
bobNode.pump(blocking = false)
|
||||
|
||||
.. note:: Currently only Kotlin examples are available for networking and protocol state machines. Java examples may
|
||||
follow later. Naming arguments in Kotlin like above is optional but sometimes useful to make code examples clearer.
|
||||
|
||||
The above code won't actually do anything because no message handler is registered for "test.topic" so the message will
|
||||
go into a holding area. If/when we add a handler that can accept test.topic, the message will be delivered then.
|
||||
|
||||
Sometimes you don't want to have to call the pump method over and over again. You can use the ``runNetwork { .. }``
|
||||
construct to fix this: any code inside the block will be run, and then all nodes you created will be pumped over and
|
||||
over until all of them have reported that they have no work left to do. This means any ping-pongs of messages will
|
||||
be run until everything settles.
|
||||
|
||||
You can see more examples of how to use this in the file ``InMemoryMessagingTests.kt``.
|
||||
|
||||
If you specify ``inBackground = true`` to ``makeNode`` then each node will create its own background thread which will
|
||||
sit around waiting for messages to be delivered. Handlers will then be invoked on that background thread. This is a
|
||||
more difficult style of programming that can be used to increase the realism of the unit tests by ensuring multiple
|
||||
nodes run in parallel, just as they would on a real network spread over multiple machines.
|
||||
|
||||
|
416
docs/source/protocol-state-machines.rst
Normal file
416
docs/source/protocol-state-machines.rst
Normal file
@ -0,0 +1,416 @@
|
||||
.. 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.
|
||||
|
||||
To put these problems in perspective the *payment channel protocol* in the bitcoinj library, which allows bitcoins to
|
||||
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,
|
||||
unserialised and resumed from where it was suspended. 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 a library called
|
||||
JavaFlow which works through behind-the-scenes bytecode rewriting. You don't have to know how this works to benefit
|
||||
from it, however.
|
||||
|
||||
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.
|
||||
In contrast a suspended Java stack can easily be 1mb in size.
|
||||
* 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.
|
||||
2. B sends to S a ``SignedWireTransaction`` that includes the state as input, B's cash as input, the state with the new
|
||||
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.
|
||||
3. S signs it and hands the now finalised ``SignedWireTransaction`` back to B.
|
||||
|
||||
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.
|
||||
|
||||
We start by defining an abstract base class to encapsulate the protocol. This is what code that invokes the protocol
|
||||
will see:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
abstract class TwoPartyTradeProtocol {
|
||||
class SellerInitialArgs(
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller
|
||||
|
||||
class BuyerInitialArgs(
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
||||
return TwoPartyTradeProtocolImpl(smm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Let's unpack what this code does:
|
||||
|
||||
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` class, and a couple of methods, one to
|
||||
run the buyer side of the protocol and one to run the seller side.
|
||||
- Two of the classes are simply wrappers for parameters to the trade; things like what is being sold, what the price
|
||||
of the asset is, how much the buyer is willing to pay and so on. The ``myKeyPair`` field is simply the public key
|
||||
that the seller wishes the buyer to send the cash to. The session ID field is sent from buyer to seller when the
|
||||
trade is being set up and is used to keep messages separated on the network, and stop malicious entities trying to
|
||||
interfere with the message stream.
|
||||
- The other two classes define empty abstract classes called ``Buyer`` and ``Seller``. These inherit from a class
|
||||
called ``ProtocolStateMachine`` and provide two type parameters: the arguments class we just defined for each side
|
||||
and the type of the object that the protocol finally produces (this doesn't have to be identical for each side, even
|
||||
though in this case it is).
|
||||
- Finally it simply defines a static method that creates an instance of an object that inherits from this base class
|
||||
and returns it, with a ``StateMachineManager`` as an instance. The Impl class will be defined below.
|
||||
|
||||
.. note:: Session IDs keep different traffic streams separated, so for security they must be large and random enough
|
||||
to be unguessable. 63 bits is good enough.
|
||||
|
||||
Alright, so using this protocol shouldn't be too hard: in the simplest case we can just pass in the details of the trade
|
||||
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on the resulting future to 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.
|
||||
|
||||
The only tricky part is how to get one of these things. We need a ``StateMachineManager``. Where does that come from
|
||||
and why do we need one?
|
||||
|
||||
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
|
||||
with a ``MessagingService`` and iterate the right state machine when the time comes. It provides the
|
||||
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
|
||||
-----------------------
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : TwoPartyTradeProtocol() {
|
||||
companion object {
|
||||
val TRADE_TOPIC = "com.r3cev.protocols.trade"
|
||||
}
|
||||
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.seller", SellerImpl::class.java)
|
||||
}
|
||||
|
||||
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.buyer", BuyerImpl::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
We start with a skeleton on which we will build the protocol. Putting things in a *companion object* in Kotlin is like
|
||||
declaring them as static members in Java. Here, we define a "topic" that will identify trade related messages that
|
||||
arrive at a node (see :doc:`messaging` for details).
|
||||
|
||||
The runSeller and runBuyer methods simply start the state machines, passing in a reference to the classes and the topics
|
||||
each side will use.
|
||||
|
||||
Now let's try implementing the seller side. Firstly, we're going to need a message to send to the buyer describing what
|
||||
we want to trade. Remember: this data comes from whatever system was used to find the trading partner to begin with.
|
||||
It could be as simple as a chat room or as complex as a 24/7 exchange.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// 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 buyerSessionID: Long
|
||||
)
|
||||
|
||||
That's simple enough: our opening protocol message will be serialised before being sent over the wire, and it contains
|
||||
the details that were agreed so we can double check them. It also contains a session ID so we can identify this
|
||||
trade's messages, and a pointer to where the asset that is being sold can be found on the ledger.
|
||||
|
||||
Next we add some code to the ``SellerImpl.call`` method:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(args.assetToSell, args.price, args.myKeyPair.public, sessionID)
|
||||
|
||||
// Zero is a special session ID that is being listened to by the buyer (i.e. before a session is started).
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
That's pretty straight forward. We generate a session ID to identify what's happening on the seller side, fill out
|
||||
the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments:
|
||||
|
||||
- A type argument, which is the object we're expecting to receive from the other side.
|
||||
- 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.
|
||||
- And finally, the thing to send. It'll be serialised and sent automatically.
|
||||
|
||||
Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we'll do a log
|
||||
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.
|
||||
|
||||
.. note:: There are a few 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 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.
|
||||
|
||||
The third rule to bear in mind is that you can't declare variables or methods in these classes and access
|
||||
them from outside of the class, due to the bytecode rewriting and classloader tricks that are used to make this all
|
||||
work. If you want access to something inside the BuyerImpl or SellerImpl classes, you must define a super-interface
|
||||
or super-class (like ``Buyer``/``Seller``) and put what you want to access there.
|
||||
|
||||
OK, let's keep going:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
partialTX.verifySignatures()
|
||||
val wtx = partialTX.txBits.deserialize<WireTransaction>()
|
||||
|
||||
requireThat {
|
||||
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
|
||||
// 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 not be valid according to the contracts of the input states, so we must resolve
|
||||
// and fully audit the transaction chains to convince ourselves that it is actually valid.
|
||||
// - 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, but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
}
|
||||
|
||||
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits)
|
||||
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
|
||||
// We should run it through our full TransactionGroup of all transactions here.
|
||||
fullySigned.verify()
|
||||
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
|
||||
logger().trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, sessionID, timestamped)
|
||||
|
||||
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService))
|
||||
|
||||
Here, we see some assertions and signature checking to satisfy ourselves that we're not about to sign something
|
||||
incorrect. Once we're happy, we calculate a signature over the transaction to authorise the movement of the asset
|
||||
we are selling, and then we verify things to make sure it's all OK. Finally, we request timestamping of the
|
||||
transaction, and send the now finalised and validated transaction back to the buyer.
|
||||
|
||||
.. 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.
|
||||
|
||||
Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different
|
||||
forms.
|
||||
|
||||
Implementing the buyer
|
||||
----------------------
|
||||
|
||||
OK, let's do the same for the buyer side:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
|
||||
// What is the seller trying to sell us?
|
||||
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
|
||||
logger().trace { "Got trade request for a $assetTypeName" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(tradeRequest.sessionID > 0)
|
||||
if (tradeRequest.price > args.acceptablePrice)
|
||||
throw UnacceptablePriceException(tradeRequest.price)
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// 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().craftSpend(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.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
|
||||
|
||||
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
return Pair(fullySigned, ltx)
|
||||
}
|
||||
}
|
||||
|
||||
This code is fairly straightforward. 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.
|
||||
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``.
|
||||
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'.
|
||||
4. Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send
|
||||
back to us a ``TimestampedWireTransaction``, which once we verify it, should be the final outcome of the trade.
|
||||
|
||||
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
|
||||
this problem doesn't occur. It's also restored for you after a protocol state machine is restored after a node
|
||||
restart.
|
||||
|
@ -56,10 +56,12 @@ class Cash : Contract {
|
||||
val amount: Amount,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: PublicKey
|
||||
) : ContractState {
|
||||
override val owner: PublicKey
|
||||
) : OwnableState {
|
||||
override val programRef = CASH_PROGRAM_ID
|
||||
override fun toString() = "Cash($amount at $deposit owned by $owner)"
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
// Just for grouping
|
||||
@ -165,7 +167,7 @@ class Cash : Contract {
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun craftSpend(tx: PartialTransaction, amount: Amount, to: PublicKey,
|
||||
wallet: List<StateAndRef<Cash.State>>, onlyFromParties: Set<Party>? = null) {
|
||||
cashStates: List<StateAndRef<Cash.State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
|
||||
// Discussion
|
||||
//
|
||||
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
|
||||
@ -188,7 +190,7 @@ class Cash : Contract {
|
||||
|
||||
val currency = amount.currency
|
||||
val acceptableCoins = run {
|
||||
val ofCurrency = wallet.filter { it.state.amount.currency == currency }
|
||||
val ofCurrency = cashStates.filter { it.state.amount.currency == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.deposit.party in onlyFromParties }
|
||||
else
|
||||
@ -229,7 +231,9 @@ class Cash : Contract {
|
||||
for (state in gathered) tx.addInputState(state.ref)
|
||||
for (state in outputs) tx.addOutputState(state)
|
||||
// What if we already have a move command with the right keys? Filter it out here or in platform code?
|
||||
tx.addArg(WireCommand(Commands.Move(), keysUsed.toList()))
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addArg(WireCommand(Commands.Move(), keysList))
|
||||
return keysList
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,13 +38,14 @@ class CommercialPaper : Contract {
|
||||
|
||||
data class State(
|
||||
val issuance: PartyReference,
|
||||
val owner: PublicKey,
|
||||
override val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
) : ContractState {
|
||||
) : OwnableState {
|
||||
override val programRef = CP_PROGRAM_ID
|
||||
|
||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
interface Commands : Command {
|
||||
|
207
src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt
Normal file
207
src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt
Normal file
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package contracts.protocols
|
||||
|
||||
import contracts.Cash
|
||||
import contracts.sumCashBy
|
||||
import core.*
|
||||
import core.messaging.*
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.trace
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* This asset trading protocol has two parties (B and S for buyer and seller) and the following steps:
|
||||
*
|
||||
* 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
|
||||
* B to pay. For example this has probably been agreed on an exchange.
|
||||
* 2. B sends to S a [SignedWireTransaction] that includes the state as input, B's cash as input, the state with the new
|
||||
* 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.
|
||||
* 3. S signs it and hands the now finalised SignedWireTransaction back to B.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* To get an implementation of this class, use the static [TwoPartyTradeProtocol.create] method. Then use either
|
||||
* the [runBuyer] or [runSeller] methods, depending on which side of the trade your node is taking. These methods
|
||||
* return a future which will complete once the trade is over and a fully signed transaction is available: you can
|
||||
* either block your thread waiting for the protocol to complete by using [ListenableFuture.get] or more usefully,
|
||||
* register a callback that will be invoked when the time comes.
|
||||
*
|
||||
* To see an example of how to use this class, look at the unit tests.
|
||||
*/
|
||||
abstract class TwoPartyTradeProtocol {
|
||||
class SellerInitialArgs(
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller
|
||||
|
||||
class BuyerInitialArgs(
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
||||
return TwoPartyTradeProtocolImpl(smm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The implementation of the [TwoPartyTradeProtocol] base class. */
|
||||
private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : TwoPartyTradeProtocol() {
|
||||
companion object {
|
||||
val TRADE_TOPIC = "com.r3cev.protocols.trade"
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// The seller's side of the protocol. IMPORTANT: This class is loaded in a separate classloader and auto-mangled
|
||||
// by JavaFlow. Therefore, we cannot cast the object to Seller and poke it directly because the class we'd be
|
||||
// trying to poke at is different to the one we saw at compile time, so we'd get ClassCastExceptions. All
|
||||
// interaction with this class must be through either interfaces, the supertype, or objects passed to and from
|
||||
// the continuation by the state machine framework. Please refer to the documentation website (docs/build/html) to
|
||||
// learn more about the protocol state machine framework.
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(args.assetToSell, args.price, args.myKeyPair.public, sessionID)
|
||||
|
||||
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
partialTX.verifySignatures()
|
||||
val wtx = partialTX.txBits.deserialize<WireTransaction>()
|
||||
|
||||
requireThat {
|
||||
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
|
||||
// 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 not be valid according to the contracts of the input states, so we must resolve
|
||||
// and fully audit the transaction chains to convince ourselves that it is actually valid.
|
||||
// - 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, but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
}
|
||||
|
||||
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits)
|
||||
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
|
||||
// We should run it through our full TransactionGroup of all transactions here.
|
||||
fullySigned.verify()
|
||||
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
|
||||
logger().trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, args.buyerSessionID, timestamped)
|
||||
|
||||
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService))
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
|
||||
// What is the seller trying to sell us?
|
||||
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
|
||||
logger().trace { "Got trade request for a $assetTypeName" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(tradeRequest.sessionID > 0)
|
||||
if (tradeRequest.price > args.acceptablePrice)
|
||||
throw UnacceptablePriceException(tradeRequest.price)
|
||||
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
|
||||
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
|
||||
|
||||
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
|
||||
// validate them to audit the other side and ensure it actually owns the state we are being offered!
|
||||
// For now, just assume validity!
|
||||
|
||||
// Generate the shared transaction that both sides will sign, using the data we have.
|
||||
val ptx = PartialTransaction()
|
||||
// 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().craftSpend(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.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
|
||||
|
||||
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
|
||||
|
||||
logger().trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
return Pair(fullySigned, ltx)
|
||||
}
|
||||
}
|
||||
|
||||
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.seller", SellerImpl::class.java)
|
||||
}
|
||||
|
||||
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.buyer", BuyerImpl::class.java)
|
||||
}
|
||||
}
|
105
src/main/kotlin/core/Services.kt
Normal file
105
src/main/kotlin/core/Services.kt
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core
|
||||
|
||||
import core.messaging.MessagingService
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This file defines various 'services' which are not currently fleshed out. A service is a module that provides
|
||||
* immutable snapshots of data that may be changing in response to user or network events.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wallet (name may be temporary) wraps a set of states that are useful for us to keep track of, for instance,
|
||||
* because we own them. This class represents an immutable, stable state of a wallet: it is guaranteed not to
|
||||
* change out from underneath you, even though the canonical currently-best-known wallet may change as we learn
|
||||
* about new transactions from our peers and generate new transactiont that consume states ourselves.
|
||||
*/
|
||||
data class Wallet(val states: List<StateAndRef<OwnableState>>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : OwnableState> statesOfType() = states.filter { it.state is T } as List<StateAndRef<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [WalletService] is responsible for securely and safely persisting the current state of a wallet to storage. The
|
||||
* wallet service vends immutable snapshots of the current wallet for working with: if you build a transaction based
|
||||
* on a wallet that isn't current, be aware that it may end up being invalid if the states that were used have been
|
||||
* consumed by someone else first!
|
||||
*/
|
||||
interface WalletService {
|
||||
/**
|
||||
* Returns a read-only snapshot of the wallet at the time the call is made. Note that if you consume states or
|
||||
* keys in this wallet, you must inform the wallet service so it can update its internal state.
|
||||
*/
|
||||
val currentWallet: Wallet
|
||||
}
|
||||
|
||||
/**
|
||||
* The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example,
|
||||
* call out to a hardware security module that enforces various auditing and frequency-of-use requirements.
|
||||
*
|
||||
* The current interface is obviously not usable for those use cases: this is just where we'd put a real signing
|
||||
* interface if/when one is developed.
|
||||
*/
|
||||
interface KeyManagementService {
|
||||
val keys: Map<PublicKey, PrivateKey>
|
||||
|
||||
fun toPrivate(publicKey: PublicKey) = keys[publicKey] ?: throw IllegalStateException("No private key known for requested public key")
|
||||
|
||||
/** Generates a new random key and adds it to the exposed map. */
|
||||
fun freshKey(): KeyPair
|
||||
}
|
||||
|
||||
/**
|
||||
* An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports
|
||||
* lookup of a party given its key. This is obviously very incomplete and does not reflect everything a real identity
|
||||
* service would provide.
|
||||
*/
|
||||
interface IdentityService {
|
||||
fun partyFromKey(key: PublicKey): Party?
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple interface (for testing) to an abstract timestamping service, in the style of RFC 3161. Note that this is not
|
||||
* 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the
|
||||
* current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp
|
||||
* triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is
|
||||
* important in the absence of blocks. Here we model the timestamp as an opaque byte array.
|
||||
*/
|
||||
interface TimestamperService {
|
||||
fun timestamp(hash: SecureHash): ByteArray
|
||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
||||
}
|
||||
|
||||
/**
|
||||
* A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like
|
||||
* transactions, serialised protocol state machines and so on. Again, this isn't intended to imply lack of SQL or
|
||||
* anything like that, this interface is only big enough to support the prototyping work.
|
||||
*/
|
||||
interface StorageService {
|
||||
fun <K,V> getMap(tableName: String): MutableMap<K, V>
|
||||
}
|
||||
|
||||
/**
|
||||
* A service hub simply vends references to the other services a node has. Some of those services may be missing or
|
||||
* mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of
|
||||
* functionality and you don't want to hard-code which types in the interface.
|
||||
*/
|
||||
interface ServiceHub {
|
||||
val walletService: WalletService
|
||||
val keyManagementService: KeyManagementService
|
||||
val identityService: IdentityService
|
||||
val timestampingService: TimestamperService
|
||||
val storageService: StorageService
|
||||
val networkService: MessagingService // TODO: Rename class to be consistent.
|
||||
}
|
@ -58,9 +58,9 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
val commands: List<WireCommand>) {
|
||||
fun serializeForSignature(): ByteArray = serialize()
|
||||
|
||||
fun toLedgerTransaction(timestamp: Instant?, partyKeyMap: Map<PublicKey, Party>, originalHash: SecureHash): LedgerTransaction {
|
||||
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
|
||||
val authenticatedArgs = commands.map {
|
||||
val institutions = it.pubkeys.mapNotNull { pk -> partyKeyMap[pk] }
|
||||
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
|
||||
AuthenticatedObject(it.pubkeys, institutions, it.command)
|
||||
}
|
||||
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp, originalHash)
|
||||
@ -133,18 +133,6 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
||||
fun commands(): List<WireCommand> = ArrayList(commands)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple interface (for testing) to an abstract timestamping service, in the style of RFC 3161. Note that this is not
|
||||
* 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the
|
||||
* current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp
|
||||
* triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is
|
||||
* important in the absence of blocks. Here we model the timestamp as an opaque byte array.
|
||||
*/
|
||||
interface TimestamperService {
|
||||
fun timestamp(hash: SecureHash): ByteArray
|
||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
||||
}
|
||||
|
||||
data class SignedWireTransaction(val txBits: OpaqueBytes, val sigs: List<DigitalSignature.WithKey>) {
|
||||
init {
|
||||
check(sigs.isNotEmpty())
|
||||
@ -203,11 +191,11 @@ data class TimestampedWireTransaction(
|
||||
) {
|
||||
val transactionID: SecureHash = serialize().sha256()
|
||||
|
||||
fun verifyToLedgerTransaction(timestamper: TimestamperService, partyKeyMap: Map<PublicKey, Party>): LedgerTransaction {
|
||||
fun verifyToLedgerTransaction(timestamper: TimestamperService, identityService: IdentityService): LedgerTransaction {
|
||||
val stx: SignedWireTransaction = signedWireTX.deserialize()
|
||||
val wtx: WireTransaction = stx.verify()
|
||||
val instant: Instant? = if (timestamp != null) timestamper.verifyTimestamp(signedWireTX.sha256(), timestamp.bits) else null
|
||||
return wtx.toLedgerTransaction(instant, partyKeyMap, transactionID)
|
||||
return wtx.toLedgerTransaction(instant, identityService, transactionID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,14 @@
|
||||
package core
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import org.slf4j.Logger
|
||||
import java.security.SecureRandom
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
||||
open class OpaqueBytes(val bits: ByteArray) {
|
||||
@ -38,3 +44,24 @@ val Int.days: Duration get() = Duration.ofDays(this.toLong())
|
||||
val Int.hours: Duration get() = Duration.ofHours(this.toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
||||
|
||||
/**
|
||||
* Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to
|
||||
* avoid potential bugs where the value is used in a context where negative numbers are not expected.
|
||||
*/
|
||||
fun random63BitValue(): Long = Math.abs(SecureRandom.getInstanceStrong().nextLong())
|
||||
|
||||
fun <T> ListenableFuture<T>.whenComplete(executor: Executor? = null, body: () -> Unit) {
|
||||
addListener(Runnable { body() }, executor ?: MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
|
||||
fun <T> SettableFuture<T>.setFrom(logger: Logger? = null, block: () -> T): SettableFuture<T> {
|
||||
try {
|
||||
set(block())
|
||||
} catch (e: Exception) {
|
||||
logger?.error("Caught exception", e)
|
||||
setException(e)
|
||||
}
|
||||
return this
|
||||
}
|
@ -12,8 +12,6 @@ import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import core.sha256
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
@ -22,6 +20,7 @@ import javax.annotation.concurrent.GuardedBy
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.concurrent.currentThread
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.fail
|
||||
|
||||
/**
|
||||
* An in-memory network allows you to manufacture [Node]s for a set of participants. Each
|
||||
@ -32,12 +31,13 @@ import kotlin.concurrent.thread
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class InMemoryNetwork {
|
||||
companion object {
|
||||
private val L = loggerFor<InMemoryNetwork>()
|
||||
}
|
||||
|
||||
@GuardedBy("this") private var counter = 0 // -1 means stopped.
|
||||
private val networkMap: MutableMap<InMemoryNodeHandle, Node> = Collections.synchronizedMap(HashMap())
|
||||
private var counter = 0 // -1 means stopped.
|
||||
private val networkMap = HashMap<Handle, Node>()
|
||||
// All messages are kept here until the messages are pumped off the queue by a caller to the node class.
|
||||
// Queues are created on-demand when a message is sent to an address: the receiving node doesn't have to have
|
||||
// been created yet. If the node identified by the given handle has gone away/been shut down then messages
|
||||
// stack up here waiting for it to come back. The intent of this is to simulate a reliable messaging network.
|
||||
private val messageQueues = HashMap<Handle, LinkedBlockingQueue<Message>>()
|
||||
|
||||
/**
|
||||
* Creates a node and returns the new object that identifies its location on the network to senders, and the
|
||||
@ -49,91 +49,128 @@ public class InMemoryNetwork {
|
||||
* executor.
|
||||
*/
|
||||
@Synchronized
|
||||
fun createNode(manuallyPumped: Boolean): Pair<SingleMessageRecipient, MessagingSystemBuilder<Node>> {
|
||||
fun createNode(manuallyPumped: Boolean): Pair<Handle, MessagingServiceBuilder<Node>> {
|
||||
check(counter >= 0) { "In memory network stopped: please recreate. "}
|
||||
|
||||
val id = InMemoryNodeHandle(counter)
|
||||
val builder = createNodeWithID(manuallyPumped, counter) as Builder
|
||||
counter++
|
||||
return Pair(id, Builder(manuallyPumped, id))
|
||||
val id = builder.id
|
||||
return Pair(id, builder)
|
||||
}
|
||||
|
||||
val entireNetwork: AllPossibleRecipients = object : AllPossibleRecipients {}
|
||||
/** Creates a node at the given address: useful if you want to recreate a node to simulate a restart */
|
||||
fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder<Node> {
|
||||
return Builder(manuallyPumped, Handle(id))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun netSend(message: Message, recipients: MessageRecipients) {
|
||||
when (recipients) {
|
||||
is Handle -> getQueueForHandle(recipients).add(message)
|
||||
|
||||
is AllPossibleRecipients -> {
|
||||
// This means all possible recipients _that the network knows about at the time_, not literally everyone
|
||||
// who joins into the indefinite future.
|
||||
for (handle in networkMap.keys)
|
||||
getQueueForHandle(handle).add(message)
|
||||
}
|
||||
else -> fail("Unknown type of recipient handle")
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun netNodeHasShutdown(handle: Handle) {
|
||||
networkMap.remove(handle)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getQueueForHandle(recipients: Handle) = messageQueues.getOrPut(recipients) { LinkedBlockingQueue() }
|
||||
|
||||
val everyoneOnline: AllPossibleRecipients = object : AllPossibleRecipients {}
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
for (node in networkMap.values) {
|
||||
// toArrayList here just copies the collection, which we need because node.stop() will delete itself from
|
||||
// the network map by calling netNodeHasShutdown. So we would get a CoModException if we didn't copy first.
|
||||
for (node in networkMap.values.toArrayList())
|
||||
node.stop()
|
||||
}
|
||||
|
||||
counter = -1
|
||||
networkMap.clear()
|
||||
messageQueues.clear()
|
||||
}
|
||||
|
||||
private inner class Builder(val manuallyPumped: Boolean, val id: InMemoryNodeHandle) : MessagingSystemBuilder<Node> {
|
||||
inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder<Node> {
|
||||
override fun start(): ListenableFuture<Node> {
|
||||
val node = Node(manuallyPumped)
|
||||
networkMap[id] = node
|
||||
return Futures.immediateFuture(node)
|
||||
synchronized(this@InMemoryNetwork) {
|
||||
val node = Node(manuallyPumped, id)
|
||||
networkMap[id] = node
|
||||
return Futures.immediateFuture(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InMemoryNodeHandle(val id: Int) : SingleMessageRecipient {
|
||||
class Handle(val id: Int) : SingleMessageRecipient {
|
||||
override fun toString() = "In memory node $id"
|
||||
override fun equals(other: Any?) = other is InMemoryNodeHandle && other.id == id
|
||||
override fun equals(other: Any?) = other is Handle && other.id == id
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Node] provides a [MessagingSystem] that isn't backed by any kind of network or disk storage
|
||||
* An [Node] provides a [MessagingService] that isn't backed by any kind of network or disk storage
|
||||
* system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience
|
||||
* when all entities on 'the network' are being simulated in-process.
|
||||
*
|
||||
* An instance can be obtained by creating a builder and then using the start method.
|
||||
*/
|
||||
inner class Node(private val manuallyPumped: Boolean): MessagingSystem {
|
||||
inner class Handler(val executor: Executor?, val topic: String, val callback: (Message) -> Unit) : MessageHandlerRegistration
|
||||
inner class Node(private val manuallyPumped: Boolean, private val handle: Handle): MessagingService {
|
||||
inner class Handler(val executor: Executor?, val topic: String, val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
|
||||
@GuardedBy("this")
|
||||
protected val handlers: MutableList<Handler> = ArrayList()
|
||||
@GuardedBy("this")
|
||||
protected var running = true
|
||||
protected val q = LinkedBlockingQueue<Message>()
|
||||
@GuardedBy("this")
|
||||
protected val pendingRedelivery = LinkedList<Message>()
|
||||
|
||||
protected val backgroundThread = if (manuallyPumped) null else thread(isDaemon = true, name = "In-memory message dispatcher ") {
|
||||
while (!currentThread.isInterrupted) pumpInternal(true)
|
||||
while (!currentThread.isInterrupted) {
|
||||
try {
|
||||
pumpInternal(true)
|
||||
} catch(e: InterruptedException) {
|
||||
if (synchronized(this) { running })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun addMessageHandler(executor: Executor?, topic: String, callback: (Message) -> Unit): MessageHandlerRegistration {
|
||||
check(running)
|
||||
return Handler(executor, topic, callback).apply { handlers.add(this) }
|
||||
override fun addMessageHandler(topic: String, executor: Executor?, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
|
||||
checkRunning()
|
||||
val handler = Handler(executor, topic, callback).apply { handlers.add(this) }
|
||||
if (pendingRedelivery.isNotEmpty()) {
|
||||
val items = ArrayList(pendingRedelivery)
|
||||
pendingRedelivery.clear()
|
||||
items.forEach { netSend(it, handle) }
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeMessageHandler(registration: MessageHandlerRegistration) {
|
||||
check(running)
|
||||
checkRunning()
|
||||
check(handlers.remove(registration as Handler))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun send(message: Message, target: MessageRecipients) {
|
||||
check(running)
|
||||
L.trace { "Sending $message to '$target'" }
|
||||
when (target) {
|
||||
is InMemoryNodeHandle -> {
|
||||
val node = networkMap[target] ?: throw IllegalArgumentException("Unknown message recipient: $target")
|
||||
node.q.put(message)
|
||||
}
|
||||
entireNetwork -> {
|
||||
for (node in networkMap.values) {
|
||||
node.q.put(message)
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unhandled type of target: $target")
|
||||
}
|
||||
checkRunning()
|
||||
netSend(message, target)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun stop() {
|
||||
backgroundThread?.interrupt()
|
||||
running = false
|
||||
backgroundThread?.interrupt()
|
||||
netNodeHasShutdown(handle)
|
||||
}
|
||||
|
||||
/** Returns the given (topic, data) pair as a newly created message object.*/
|
||||
@ -151,16 +188,22 @@ public class InMemoryNetwork {
|
||||
|
||||
/**
|
||||
* Delivers a single message from the internal queue. If there are no messages waiting to be delivered and block
|
||||
* is true, waits until one has been provided on a different thread via send. If block is false, the return result
|
||||
* indicates whether a message was delivered or not.
|
||||
* is true, waits until one has been provided on a different thread via send. If block is false, the return
|
||||
* result indicates whether a message was delivered or not.
|
||||
*/
|
||||
fun pump(block: Boolean): Boolean {
|
||||
check(manuallyPumped)
|
||||
synchronized(this) { check(running) }
|
||||
checkRunning()
|
||||
return pumpInternal(block)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun checkRunning() {
|
||||
check(running)
|
||||
}
|
||||
|
||||
private fun pumpInternal(block: Boolean): Boolean {
|
||||
val q = getQueueForHandle(handle)
|
||||
val message = if (block) q.take() else q.poll()
|
||||
|
||||
if (message == null)
|
||||
@ -170,9 +213,21 @@ public class InMemoryNetwork {
|
||||
handlers.filter { if (it.topic.isBlank()) true else message.topic == it.topic }
|
||||
}
|
||||
|
||||
if (deliverTo.isEmpty()) {
|
||||
// Got no handlers for this message yet. Keep the message around and attempt redelivery after a new
|
||||
// handler has been registered. The purpose of this path is to make unit tests that have multi-threading
|
||||
// reliable, as a sender may attempt to send a message to a receiver that hasn't finished setting
|
||||
// up a handler for yet. Most unit tests don't run threaded, but we want to test true parallelism at
|
||||
// least sometimes.
|
||||
synchronized(this) {
|
||||
pendingRedelivery.add(message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for (handler in deliverTo) {
|
||||
// Now deliver via the requested executor, or on this thread if no executor was provided at registration time.
|
||||
(handler.executor ?: MoreExecutors.directExecutor()).execute { handler.callback(message) }
|
||||
(handler.executor ?: MoreExecutors.directExecutor()).execute { handler.callback(message, handler) }
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -9,13 +9,13 @@
|
||||
package core.messaging
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.time.Duration
|
||||
import core.serialization.serialize
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.Executor
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A [MessagingSystem] sits at the boundary between a message routing / networking layer and the core platform code.
|
||||
* A [MessagingService] sits at the boundary between a message routing / networking layer and the core platform code.
|
||||
*
|
||||
* A messaging system must provide the ability to send 1:many messages, potentially to an abstract "group", the
|
||||
* membership of which is defined elsewhere. Messages are atomic and the system guarantees that a sent message
|
||||
@ -25,26 +25,25 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* is *reliable* and as such messages may be stored to disk once queued.
|
||||
*/
|
||||
@ThreadSafe
|
||||
interface MessagingSystem {
|
||||
interface MessagingService {
|
||||
/**
|
||||
* The provided function will be invoked for each received message whose topic matches the given string, on the given
|
||||
* executor. The topic can be the empty string to match all messages.
|
||||
*
|
||||
* If no executor is received then the callback will run on threads provided by the messaging system, and the
|
||||
* If no executor is received then the callback will run on threads provided by the messaging service, and the
|
||||
* callback is expected to be thread safe as a result.
|
||||
*
|
||||
* The returned object is an opaque handle that may be used to un-register handlers later with [addMessageHandler].
|
||||
*
|
||||
* If the callback throws an exception then the message is discarded and will not be retried, unless the exception
|
||||
* is a subclass of [RetryMessageLaterException], in which case the message will be queued and attempted later.
|
||||
* The returned object is an opaque handle that may be used to un-register handlers later with [removeMessageHandler].
|
||||
* The handle is passed to the callback as well, to avoid race conditions whereby the callback wants to unregister
|
||||
* itself and yet addMessageHandler hasn't returned the handle yet.
|
||||
*/
|
||||
fun addMessageHandler(executor: Executor? = null, topic: String = "", callback: (Message) -> Unit): MessageHandlerRegistration
|
||||
fun addMessageHandler(topic: String = "", executor: Executor? = null, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration
|
||||
|
||||
/**
|
||||
* Removes a handler given the object returned from [addMessageHandler]. The callback will no longer be invoked once
|
||||
* this method has returned, although executions that are currently in flight will not be interrupted.
|
||||
*
|
||||
* @throws IllegalArgumentException if the given registration isn't valid for this messaging system.
|
||||
* @throws IllegalArgumentException if the given registration isn't valid for this messaging service.
|
||||
* @throws IllegalStateException if the given registration was already de-registered.
|
||||
*/
|
||||
fun removeMessageHandler(registration: MessageHandlerRegistration)
|
||||
@ -69,25 +68,34 @@ interface MessagingSystem {
|
||||
}
|
||||
|
||||
/**
|
||||
* This class lets you start up a [MessagingSystem]. Its purpose is to stop you from getting access to the methods
|
||||
* on the messaging system interface until you have successfully started up the system. One of these objects should
|
||||
* be the only way to obtain a reference to a [MessagingSystem]. Startup may be a slow process: some implementations
|
||||
* Registers a handler for the given topic that runs the given callback with the message and then removes itself. This
|
||||
* is useful for one-shot handlers that aren't supposed to stick around permanently. Note that this callback doesn't
|
||||
* take the registration object, unlike the callback to [MessagingService.addMessageHandler].
|
||||
*/
|
||||
fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) {
|
||||
addMessageHandler(topic, executor) { msg, reg ->
|
||||
callback(msg)
|
||||
removeMessageHandler(reg)
|
||||
}
|
||||
}
|
||||
|
||||
fun MessagingService.send(topic: String, to: MessageRecipients, obj: Any) = send(createMessage(topic, obj.serialize()), to)
|
||||
|
||||
/**
|
||||
* This class lets you start up a [MessagingService]. Its purpose is to stop you from getting access to the methods
|
||||
* on the messaging service interface until you have successfully started up the system. One of these objects should
|
||||
* be the only way to obtain a reference to a [MessagingService]. Startup may be a slow process: some implementations
|
||||
* may let you cast the returned future to an object that lets you get status info.
|
||||
*
|
||||
* A specific implementation of the controller class will have extra features that let you customise it before starting
|
||||
* it up.
|
||||
*/
|
||||
interface MessagingSystemBuilder<T : MessagingSystem> {
|
||||
fun start(): ListenableFuture<T>
|
||||
interface MessagingServiceBuilder<out T : MessagingService> {
|
||||
fun start(): ListenableFuture<out T>
|
||||
}
|
||||
|
||||
interface MessageHandlerRegistration
|
||||
|
||||
class RetryMessageLaterException : Exception() {
|
||||
/** If set, the message will be re-queued and retried after the requested interval. */
|
||||
var delayPeriod: Duration? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A message is defined, at this level, to be a (topic, timestamp, byte arrays) triple, where the topic is a string in
|
||||
* Java-style reverse dns form, with "platform." being a prefix reserved by the platform for its own use. Vendor
|
||||
|
293
src/main/kotlin/core/messaging/StateMachines.kt
Normal file
293
src/main/kotlin/core/messaging/StateMachines.kt
Normal file
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.messaging
|
||||
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.SecureHash
|
||||
import core.ServiceHub
|
||||
import core.serialization.THREAD_LOCAL_KRYO
|
||||
import core.serialization.createKryo
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.trace
|
||||
import core.whenComplete
|
||||
import org.apache.commons.javaflow.Continuation
|
||||
import org.apache.commons.javaflow.ContinuationClassLoader
|
||||
import org.objenesis.instantiator.ObjectInstantiator
|
||||
import org.objenesis.strategy.InstantiatorStrategy
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A StateMachineManager is responsible for coordination and persistence of multiple [ProtocolStateMachine] objects.
|
||||
* Each such object represents an instantiation of a (two-party) protocol that has reached a particular point.
|
||||
*
|
||||
* An implementation of this class will persist state machines to long term storage so they can survive process restarts
|
||||
* and, if run with a single-threaded executor, will ensure no two state machines run concurrently with each other
|
||||
* (bad for performance, good for programmer mental health!).
|
||||
*
|
||||
* A "state machine" is a class with a single call method. The call method and any others it invokes are rewritten by
|
||||
* a bytecode rewriting engine called JavaFlow, to ensure the code can be suspended and resumed at any point.
|
||||
*
|
||||
* TODO: The framework should propagate exceptions and handle error handling automatically.
|
||||
* TODO: This needs extension to the >2 party case.
|
||||
* TODO: Consider the issue of continuation identity more deeply: is it a safe assumption that a serialised
|
||||
* continuation is always unique?
|
||||
*/
|
||||
@ThreadSafe
|
||||
class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) {
|
||||
// This map is backed by a database and will be used to store serialised state machines to disk, so we can resurrect
|
||||
// them across node restarts.
|
||||
private val checkpointsMap = serviceHub.storageService.getMap<SecureHash, ByteArray>("state machines")
|
||||
// A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines
|
||||
// property.
|
||||
private val _stateMachines = Collections.synchronizedList(ArrayList<ProtocolStateMachine<*,*>>())
|
||||
|
||||
/** Returns a snapshot of the currently registered state machines. */
|
||||
val stateMachines: List<ProtocolStateMachine<*,*>> get() {
|
||||
synchronized(_stateMachines) {
|
||||
return ArrayList(_stateMachines)
|
||||
}
|
||||
}
|
||||
|
||||
// This class will be serialised, so everything it points to transitively must also be serialisable (with Kryo).
|
||||
private class Checkpoint(
|
||||
val continuation: Continuation,
|
||||
val otherSide: MessageRecipients,
|
||||
val loggerName: String,
|
||||
val awaitingTopic: String,
|
||||
val awaitingObjectOfType: String // java class name
|
||||
)
|
||||
|
||||
init {
|
||||
restoreCheckpoints()
|
||||
}
|
||||
|
||||
/** Reads the database map and resurrects any serialised state machines. */
|
||||
private fun restoreCheckpoints() {
|
||||
for (bytes in checkpointsMap.values) {
|
||||
val kryo = createKryo()
|
||||
|
||||
// Set up Kryo to use the JavaFlow classloader when deserialising, so the magical continuation bytecode
|
||||
// rewriting is performed correctly.
|
||||
var _psm: ProtocolStateMachine<*, *>? = null
|
||||
kryo.instantiatorStrategy = object : InstantiatorStrategy {
|
||||
val forwardingTo = kryo.instantiatorStrategy
|
||||
|
||||
override fun <T> newInstantiatorOf(type: Class<T>): ObjectInstantiator<T> {
|
||||
// If this is some object that isn't a state machine, use the default behaviour.
|
||||
if (!ProtocolStateMachine::class.java.isAssignableFrom(type))
|
||||
return forwardingTo.newInstantiatorOf(type)
|
||||
|
||||
// Otherwise, return an 'object instantiator' (i.e. factory) that uses the JavaFlow classloader.
|
||||
@Suppress("UNCHECKED_CAST", "CAST_NEVER_SUCCEEDS")
|
||||
return ObjectInstantiator<T> {
|
||||
val p = loadContinuationClass(type as Class<out ProtocolStateMachine<*, *>>).first
|
||||
// Pass the new object a pointer to the service hub where it can find objects that don't
|
||||
// survive restarts.
|
||||
p.serviceHub = serviceHub
|
||||
_psm = p
|
||||
p as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val checkpoint = bytes.deserialize<Checkpoint>(kryo)
|
||||
val continuation = checkpoint.continuation
|
||||
|
||||
// We know _psm can't be null here, because we always serialise a ProtocolStateMachine subclass, so the
|
||||
// code above that does "_psm = p" will always run. But the Kotlin compiler can't know that so we have to
|
||||
// forcibly cast away the nullness with the !! operator.
|
||||
val psm = _psm!!
|
||||
registerStateMachine(psm)
|
||||
|
||||
val logger = LoggerFactory.getLogger(checkpoint.loggerName)
|
||||
val awaitingObjectOfType = Class.forName(checkpoint.awaitingObjectOfType)
|
||||
|
||||
// And now re-wire the deserialised continuation back up to the network service.
|
||||
setupNextMessageHandler(logger, serviceHub.networkService, continuation, checkpoint.otherSide,
|
||||
awaitingObjectOfType, checkpoint.awaitingTopic, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off a brand new state machine of the given class. It will send messages to the network node identified by
|
||||
* the [otherSide] parameter, log with the named logger, and the [initialArgs] object will be passed to the call
|
||||
* method of the [ProtocolStateMachine] object that is created. The state machine will be persisted when it suspends
|
||||
* and will be removed once it completes.
|
||||
*/
|
||||
fun <T : ProtocolStateMachine<I, *>, I> add(otherSide: MessageRecipients, initialArgs: I, loggerName: String,
|
||||
continuationClass: Class<out T>): T {
|
||||
val logger = LoggerFactory.getLogger(loggerName)
|
||||
val (sm, continuation) = loadContinuationClass(continuationClass)
|
||||
sm.serviceHub = serviceHub
|
||||
registerStateMachine(sm)
|
||||
runInThread.execute {
|
||||
// The current state of the continuation is held in the closure attached to the messaging system whenever
|
||||
// the continuation suspends and tells us it expects a response.
|
||||
iterateStateMachine(continuation, serviceHub.networkService, otherSide, initialArgs, logger, null)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return sm as T
|
||||
}
|
||||
|
||||
private fun registerStateMachine(psm: ProtocolStateMachine<*, *>) {
|
||||
_stateMachines.add(psm)
|
||||
psm.resultFuture.whenComplete(runInThread) {
|
||||
_stateMachines.remove(psm)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun loadContinuationClass(continuationClass: Class<out ProtocolStateMachine<*, *>>): Pair<ProtocolStateMachine<*, *>, Continuation> {
|
||||
val url = continuationClass.protectionDomain.codeSource.location
|
||||
val cl = ContinuationClassLoader(arrayOf(url), this.javaClass.classLoader)
|
||||
val obj = cl.forceLoadClass(continuationClass.name).newInstance() as ProtocolStateMachine<*, *>
|
||||
return Pair(obj, Continuation.startSuspendedWith(obj))
|
||||
}
|
||||
|
||||
private fun persistCheckpoint(prev: ByteArray?, new: ByteArray) {
|
||||
// It's OK for this to be unsynchronised, as the prev/new byte arrays are specific to a continuation instance,
|
||||
// and the underlying map provided by the database layer is expected to be thread safe.
|
||||
if (prev != null)
|
||||
checkpointsMap.remove(SecureHash.sha256(prev))
|
||||
checkpointsMap[SecureHash.sha256(new)] = new
|
||||
}
|
||||
|
||||
private fun iterateStateMachine(c: Continuation, net: MessagingService, otherSide: MessageRecipients,
|
||||
continuationInput: Any?, logger: Logger,
|
||||
prevPersistedBytes: ByteArray?): Continuation {
|
||||
// This will resume execution of the run() function inside the continuation at the place it left off.
|
||||
val oldLogger = CONTINUATION_LOGGER.get()
|
||||
val nextState: Continuation? = try {
|
||||
CONTINUATION_LOGGER.set(logger)
|
||||
Continuation.continueWith(c, continuationInput)
|
||||
} catch (t: Throwable) {
|
||||
logger.error("Caught error whilst invoking protocol state machine", t)
|
||||
throw t
|
||||
} finally {
|
||||
CONTINUATION_LOGGER.set(oldLogger)
|
||||
}
|
||||
|
||||
// If continuation returns null, it's finished and the result future has been set.
|
||||
if (nextState == null)
|
||||
return c
|
||||
|
||||
val req = nextState.value() as? ContinuationResult ?: return c
|
||||
|
||||
// Else, it wants us to do something: send, receive, or send-and-receive.
|
||||
if (req is ContinuationResult.ExpectingResponse<*>) {
|
||||
// Prepare a listener on the network that runs in the background thread when we received a message.
|
||||
val topic = "${req.topic}.${req.sessionIDForReceive}"
|
||||
setupNextMessageHandler(logger, net, nextState, otherSide, req.responseType, topic, prevPersistedBytes)
|
||||
}
|
||||
// If an object to send was provided (not null), send it now.
|
||||
req.obj?.let {
|
||||
val topic = "${req.topic}.${req.sessionIDForSend}"
|
||||
logger.trace { "-> $topic : message of type ${it.javaClass.name}" }
|
||||
net.send(net.createMessage(topic, it.serialize()), otherSide)
|
||||
}
|
||||
if (req is ContinuationResult.NotExpectingResponse) {
|
||||
// We sent a message, but don't expect a response, so re-enter the continuation to let it keep going.
|
||||
return iterateStateMachine(nextState, net, otherSide, null, logger, prevPersistedBytes)
|
||||
} else {
|
||||
return nextState
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNextMessageHandler(logger: Logger, net: MessagingService, nextState: Continuation,
|
||||
otherSide: MessageRecipients, responseType: Class<*>,
|
||||
topic: String, prevPersistedBytes: ByteArray?) {
|
||||
val checkpoint = Checkpoint(nextState, otherSide, logger.name, topic, responseType.name)
|
||||
val curPersistedBytes = checkpoint.serialize()
|
||||
persistCheckpoint(prevPersistedBytes, curPersistedBytes)
|
||||
net.runOnNextMessage(topic, runInThread) { netMsg ->
|
||||
val obj: Any = THREAD_LOCAL_KRYO.get().readObject(Input(netMsg.data), responseType)
|
||||
logger.trace { "<- $topic : message of type ${obj.javaClass.name}" }
|
||||
iterateStateMachine(nextState, net, otherSide, obj, logger, curPersistedBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val CONTINUATION_LOGGER = ThreadLocal<Logger>()
|
||||
|
||||
/**
|
||||
* The base class that should be used by any object that wishes to act as a protocol state machine. Sub-classes should
|
||||
* override the [call] method and return whatever the final result of the protocol is. Inside the call method,
|
||||
* the rules of normal object oriented programming are a little different:
|
||||
*
|
||||
* - You can call send/receive/sendAndReceive in order to suspend the state machine and request network interaction.
|
||||
* This does not block a thread and when a state machine is suspended like this, it will be serialised and written
|
||||
* to stable storage. That means all objects on the stack and referenced from fields must be serialisable as well
|
||||
* (with Kryo, so they don't have to implement the Java Serializable interface). The state machine may be resumed
|
||||
* at some arbitrary later point.
|
||||
* - Because of this, if you need access to data that might change over time, you should request it just-in-time
|
||||
* via the [serviceHub] property which is provided. Don't try and keep data you got from a service across calls to
|
||||
* send/receive/sendAndReceive because the world might change in arbitrary ways out from underneath you, for instance,
|
||||
* if the node is restarted or reconfigured!
|
||||
* - Don't pass initial data in using a constructor. This object will be instantiated using reflection so you cannot
|
||||
* define your own constructor. Instead define a separate class that holds your initial arguments, and take it as
|
||||
* the argument to [call].
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
abstract class ProtocolStateMachine<T, R> : Runnable {
|
||||
protected fun logger(): Logger = CONTINUATION_LOGGER.get()
|
||||
|
||||
// These fields shouldn't be serialised.
|
||||
@Transient private var _resultFuture: SettableFuture<R> = SettableFuture.create<R>()
|
||||
/** This future will complete when the call method returns. */
|
||||
val resultFuture: ListenableFuture<R> get() = _resultFuture
|
||||
|
||||
/** This field is initialised by the framework to point to various infrastructure submodules. */
|
||||
@Transient lateinit var serviceHub: ServiceHub
|
||||
|
||||
abstract fun call(args: T): R
|
||||
|
||||
override fun run() {
|
||||
// TODO: Catch any exceptions here and put them in the future.
|
||||
val r = call(Continuation.getContext() as T)
|
||||
if (r != null)
|
||||
_resultFuture.set(r)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST")
|
||||
inline fun <S : Any> ProtocolStateMachine<*, *>.send(topic: String, sessionID: Long, obj: S) =
|
||||
Continuation.suspend(ContinuationResult.NotExpectingResponse(topic, sessionID, obj))
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified R : Any> ProtocolStateMachine<*, *>.sendAndReceive(
|
||||
topic: String, sessionIDForSend: Long, sessionIDForReceive: Long, obj: Any): R {
|
||||
return Continuation.suspend(ContinuationResult.ExpectingResponse(topic, sessionIDForSend, sessionIDForReceive,
|
||||
obj, R::class.java)) as R
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified R : Any> ProtocolStateMachine<*, *>.receive(
|
||||
topic: String, sessionIDForReceive: Long): R {
|
||||
return Continuation.suspend(ContinuationResult.ExpectingResponse(topic, -1, sessionIDForReceive, null, R::class.java)) as R
|
||||
}
|
||||
|
||||
open class ContinuationResult(val topic: String, val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {
|
||||
class ExpectingResponse<R : Any>(
|
||||
topic: String,
|
||||
sessionIDForSend: Long,
|
||||
sessionIDForReceive: Long,
|
||||
obj: Any?,
|
||||
val responseType: Class<R>
|
||||
) : ContinuationResult(topic, sessionIDForSend, sessionIDForReceive, obj)
|
||||
|
||||
class NotExpectingResponse(topic: String, sessionIDForSend: Long, obj: Any?) : ContinuationResult(topic, sessionIDForSend, -1, obj)
|
||||
}
|
@ -12,8 +12,10 @@ import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import core.OpaqueBytes
|
||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Serialization utilities, using the Kryo framework with a custom serialiser for immutable data classes and a dead
|
||||
@ -63,5 +65,7 @@ fun createKryo(): Kryo {
|
||||
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
|
||||
// no-arg constructor available.
|
||||
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
||||
|
||||
register(Arrays.asList( "" ).javaClass, ArraysAsListSerializer());
|
||||
}
|
||||
}
|
@ -65,10 +65,10 @@ class BriefLogFormatter : Formatter() {
|
||||
handlers[0].formatter = BriefLogFormatter()
|
||||
}
|
||||
|
||||
fun initVerbose() {
|
||||
fun initVerbose(packageSpec: String = "") {
|
||||
init()
|
||||
loggerRef.level = Level.ALL
|
||||
loggerRef.handlers[0].level = Level.ALL
|
||||
Logger.getLogger(packageSpec).level = Level.ALL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.utilities.continuations
|
||||
|
||||
import org.apache.commons.javaflow.Continuation
|
||||
import org.apache.commons.javaflow.ContinuationClassLoader
|
||||
|
||||
/**
|
||||
* A "continuation" is an object that represents a suspended execution of a function. They allow you to write code
|
||||
* that suspends itself half way through, bundles up everything that was on the stack into a (potentially serialisable)
|
||||
* object, and then be resumed from the exact same spot later. Continuations are not natively supported by the JVM
|
||||
* but we can use the Apache JavaFlow library which implements them using bytecode rewriting.
|
||||
*
|
||||
* The primary benefit of using continuations is that state machine/protocol code that would otherwise be very
|
||||
* convoluted and hard to read becomes very clear and straightforward.
|
||||
*
|
||||
* TODO: Document classloader interactions and gotchas here.
|
||||
*/
|
||||
inline fun <reified T : Runnable> loadContinuationClass(classLoader: ClassLoader): Continuation {
|
||||
val klass = T::class.java
|
||||
val url = klass.protectionDomain.codeSource.location
|
||||
val cl = ContinuationClassLoader(arrayOf(url), classLoader)
|
||||
val obj = cl.forceLoadClass(klass.name).newInstance() as Runnable
|
||||
return Continuation.startSuspendedWith(obj)
|
||||
}
|
@ -107,7 +107,7 @@ class CommercialPaperTests {
|
||||
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
@ -124,7 +124,7 @@ class CommercialPaperTests {
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -138,7 +138,7 @@ class CommercialPaperTests {
|
||||
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
return ptx.toSignedTransaction().verify().toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
return ptx.toSignedTransaction().verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||
|
@ -105,7 +105,7 @@ class CrowdFundTests {
|
||||
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
// let's give Alice some funds that she can invest
|
||||
@ -123,7 +123,7 @@ class CrowdFundTests {
|
||||
ptx.signWith(ALICE_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -137,7 +137,7 @@ class CrowdFundTests {
|
||||
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
return stx.verify().toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
return stx.verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
|
||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||
|
@ -10,53 +10,54 @@
|
||||
|
||||
package core.messaging
|
||||
|
||||
import core.serialization.deserialize
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InMemoryMessagingTests {
|
||||
val nodes: MutableMap<SingleMessageRecipient, InMemoryNetwork.Node> = HashMap()
|
||||
open class TestWithInMemoryNetwork {
|
||||
val nodes: MutableMap<InMemoryNetwork.Handle, InMemoryNetwork.Node> = HashMap()
|
||||
lateinit var network: InMemoryNetwork
|
||||
|
||||
init {
|
||||
// BriefLogFormatter.initVerbose()
|
||||
}
|
||||
|
||||
fun makeNode(): Pair<SingleMessageRecipient, InMemoryNetwork.Node> {
|
||||
fun makeNode(inBackground: Boolean = false): Pair<InMemoryNetwork.Handle, InMemoryNetwork.Node> {
|
||||
// The manuallyPumped = true bit means that we must call the pump method on the system in order to
|
||||
val (address, builder) = network.createNode(manuallyPumped = true)
|
||||
val (address, builder) = network.createNode(!inBackground)
|
||||
val node = builder.start().get()
|
||||
nodes[address] = node
|
||||
return Pair(address, node)
|
||||
}
|
||||
|
||||
fun pumpAll() {
|
||||
nodes.values.forEach { it.pump(false) }
|
||||
}
|
||||
|
||||
// Utilities to help define messaging rounds.
|
||||
fun roundWithPumpings(times: Int, body: () -> Unit) {
|
||||
body()
|
||||
repeat(times) { pumpAll() }
|
||||
}
|
||||
|
||||
fun round(body: () -> Unit) = roundWithPumpings(1, body)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
fun setupNetwork() {
|
||||
network = InMemoryNetwork()
|
||||
nodes.clear()
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
fun stopNetwork() {
|
||||
network.stop()
|
||||
}
|
||||
|
||||
fun pumpAll(blocking: Boolean) = nodes.values.map { it.pump(blocking) }
|
||||
|
||||
// Keep calling "pump" in rounds until every node in the network reports that it had nothing to do
|
||||
fun <T> runNetwork(body: () -> T): T {
|
||||
val result = body()
|
||||
while (pumpAll(false).any { it }) {}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryMessagingTests : TestWithInMemoryNetwork() {
|
||||
init {
|
||||
// BriefLogFormatter.initVerbose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun topicStringValidation() {
|
||||
TopicStringValidator.check("this.is.ok")
|
||||
@ -82,19 +83,19 @@ class InMemoryMessagingTests {
|
||||
var finalDelivery: Message? = null
|
||||
|
||||
with(node2) {
|
||||
addMessageHandler {
|
||||
send(it, addr3)
|
||||
addMessageHandler { msg, registration ->
|
||||
send(msg, addr3)
|
||||
}
|
||||
}
|
||||
|
||||
with(node3) {
|
||||
addMessageHandler {
|
||||
finalDelivery = it
|
||||
addMessageHandler { msg, registration ->
|
||||
finalDelivery = msg
|
||||
}
|
||||
}
|
||||
|
||||
// Node 1 sends a message and it should end up in finalDelivery, after we pump each node.
|
||||
roundWithPumpings(2) {
|
||||
runNetwork {
|
||||
node1.send(node1.createMessage("test.topic", bits), addr2)
|
||||
}
|
||||
|
||||
@ -110,10 +111,46 @@ class InMemoryMessagingTests {
|
||||
val bits = "test-content".toByteArray()
|
||||
|
||||
var counter = 0
|
||||
listOf(node1, node2, node3).forEach { it.addMessageHandler { counter++ } }
|
||||
round {
|
||||
node1.send(node2.createMessage("test.topic", bits), network.entireNetwork)
|
||||
listOf(node1, node2, node3).forEach { it.addMessageHandler { msg, registration -> counter++ } }
|
||||
runNetwork {
|
||||
node1.send(node2.createMessage("test.topic", bits), network.everyoneOnline)
|
||||
}
|
||||
assertEquals(3, counter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downAndUp() {
|
||||
// Test (re)delivery of messages to nodes that aren't created yet, or were stopped and then restarted.
|
||||
// The purpose of this functionality is to simulate a reliable messaging system that keeps trying until
|
||||
// messages are delivered.
|
||||
val (addr1, node1) = makeNode()
|
||||
var (addr2, node2) = makeNode()
|
||||
|
||||
node1.send("test.topic", addr2, "hello!")
|
||||
node2.pump(false) // No handler registered, so the message goes into a holding area.
|
||||
var runCount = 0
|
||||
node2.addMessageHandler("test.topic") { msg, registration ->
|
||||
if (msg.data.deserialize<String>() == "hello!")
|
||||
runCount++
|
||||
}
|
||||
node2.pump(false) // Try again now the handler is registered
|
||||
assertEquals(1, runCount)
|
||||
|
||||
// Shut node2 down for a while. Node 1 keeps sending it messages though.
|
||||
node2.stop()
|
||||
|
||||
node1.send("test.topic", addr2, "are you there?")
|
||||
node1.send("test.topic", addr2, "wake up!")
|
||||
|
||||
// Now re-create node2 with the same address as last time, and re-register a message handler.
|
||||
// Check that the messages that were sent whilst it was gone are still there, waiting for it.
|
||||
node2 = network.createNodeWithID(true, addr2.id).start().get()
|
||||
node2.addMessageHandler("test.topic") { a, b -> runCount++ }
|
||||
assertTrue(node2.pump(false))
|
||||
assertEquals(2, runCount)
|
||||
assertTrue(node2.pump(false))
|
||||
assertEquals(3, runCount)
|
||||
assertFalse(node2.pump(false))
|
||||
assertEquals(3, runCount)
|
||||
}
|
||||
}
|
191
src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt
Normal file
191
src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt
Normal file
@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.messaging
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import contracts.Cash
|
||||
import contracts.CommercialPaper
|
||||
import contracts.protocols.TwoPartyTradeProtocol
|
||||
import core.*
|
||||
import core.testutils.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* In this example, Alice wishes to sell her commercial paper to Bob in return for $1,000,000 and they wish to do
|
||||
* it on the ledger atomically. Therefore they must work together to build a transaction.
|
||||
*
|
||||
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
|
||||
*/
|
||||
class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
@Before
|
||||
fun initLogging() {
|
||||
Logger.getLogger("").handlers[0].level = Level.ALL
|
||||
Logger.getLogger("").handlers[0].formatter = object : Formatter() {
|
||||
override fun format(record: LogRecord) = "${record.threadID} ${record.loggerName}: ${record.message}\n"
|
||||
}
|
||||
Logger.getLogger("com.r3cev.protocols.trade").level = Level.ALL
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopLogging() {
|
||||
Logger.getLogger("com.r3cev.protocols.trade").level = Level.INFO
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cashForCP() {
|
||||
val backgroundThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
transactionGroupFor<ContractState> {
|
||||
// Bob (Buyer) has some cash, Alice (Seller) has some commercial paper she wants to sell to Bob.
|
||||
roots {
|
||||
transaction(CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), ALICE, 1200.DOLLARS, TEST_TX_TIME + 7.days) label "alice's paper")
|
||||
transaction(800.DOLLARS.CASH `owned by` BOB label "bob cash1")
|
||||
transaction(300.DOLLARS.CASH `owned by` BOB label "bob cash2")
|
||||
}
|
||||
|
||||
val bobsWallet = listOf<StateAndRef<Cash.State>>(lookup("bob cash1"), lookup("bob cash2"))
|
||||
|
||||
val (alicesAddress, alicesNode) = makeNode(inBackground = true)
|
||||
val (bobsAddress, bobsNode) = makeNode(inBackground = true)
|
||||
|
||||
val alicesServices = MockServices(wallet = null, keyManagement = null, net = alicesNode)
|
||||
val bobsServices = MockServices(
|
||||
wallet = MockWalletService(bobsWallet),
|
||||
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
|
||||
net = bobsNode
|
||||
)
|
||||
|
||||
val tpSeller = TwoPartyTradeProtocol.create(StateMachineManager(alicesServices, backgroundThread))
|
||||
val tpBuyer = TwoPartyTradeProtocol.create(StateMachineManager(bobsServices, backgroundThread))
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
val aliceResult = tpSeller.runSeller(
|
||||
bobsAddress,
|
||||
TwoPartyTradeProtocol.SellerInitialArgs(
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
)
|
||||
val bobResult = tpBuyer.runBuyer(
|
||||
alicesAddress,
|
||||
TwoPartyTradeProtocol.BuyerInitialArgs(
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(aliceResult.resultFuture.get(), bobResult.resultFuture.get())
|
||||
|
||||
txns.add(aliceResult.resultFuture.get().second)
|
||||
verify()
|
||||
}
|
||||
backgroundThread.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeAndRestore() {
|
||||
transactionGroupFor<ContractState> {
|
||||
// Buyer Bob has some cash, Seller Alice has some commercial paper she wants to sell to Bob.
|
||||
roots {
|
||||
transaction(CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), ALICE, 1200.DOLLARS, TEST_TX_TIME + 7.days) label "alice's paper")
|
||||
transaction(800.DOLLARS.CASH `owned by` BOB label "bob cash1")
|
||||
transaction(300.DOLLARS.CASH `owned by` BOB label "bob cash2")
|
||||
}
|
||||
|
||||
val bobsWallet = listOf<StateAndRef<Cash.State>>(lookup("bob cash1"), lookup("bob cash2"))
|
||||
|
||||
val (alicesAddress, alicesNode) = makeNode(inBackground = false)
|
||||
var (bobsAddress, bobsNode) = makeNode(inBackground = false)
|
||||
|
||||
val bobsStorage = MockStorageService()
|
||||
|
||||
val alicesServices = MockServices(wallet = null, keyManagement = null, net = alicesNode)
|
||||
var bobsServices = MockServices(
|
||||
wallet = MockWalletService(bobsWallet),
|
||||
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
|
||||
net = bobsNode,
|
||||
storage = bobsStorage
|
||||
)
|
||||
|
||||
val tpSeller = TwoPartyTradeProtocol.create(StateMachineManager(alicesServices, MoreExecutors.directExecutor()))
|
||||
val smmBuyer = StateMachineManager(bobsServices, MoreExecutors.directExecutor())
|
||||
val tpBuyer = TwoPartyTradeProtocol.create(smmBuyer)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
tpSeller.runSeller(
|
||||
bobsAddress,
|
||||
TwoPartyTradeProtocol.SellerInitialArgs(
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
)
|
||||
tpBuyer.runBuyer(
|
||||
alicesAddress,
|
||||
TwoPartyTradeProtocol.BuyerInitialArgs(
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
)
|
||||
|
||||
// Everything is on this thread so we can now step through the protocol one step at a time.
|
||||
// Seller Alice already sent a message to Buyer Bob. Pump once:
|
||||
bobsNode.pump(false)
|
||||
|
||||
// OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
|
||||
// Save the state machine to "disk" (i.e. a variable, here)
|
||||
assertEquals(1, bobsStorage.getMap<Any, Any>("state machines").size)
|
||||
|
||||
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
|
||||
bobsNode.stop()
|
||||
|
||||
// Alice doesn't know that and sends Bob the now finalised transaction. Alice sends a message to a node
|
||||
// that has gone offline.
|
||||
alicesNode.pump(false)
|
||||
|
||||
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
|
||||
// that Bob was waiting on before the reboot occurred.
|
||||
bobsNode = network.createNodeWithID(true, bobsAddress.id).start().get()
|
||||
val smm = StateMachineManager(
|
||||
MockServices(wallet = null, keyManagement = null, net = bobsNode, storage = bobsStorage),
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
|
||||
// Find the future representing the result of this state machine again.
|
||||
assertEquals(1, smm.stateMachines.size)
|
||||
var bobFuture = smm.stateMachines.filterIsInstance<TwoPartyTradeProtocol.Buyer>().first().resultFuture
|
||||
|
||||
// Let Bob process his mailbox.
|
||||
assertTrue(bobsNode.pump(false))
|
||||
|
||||
// Bob is now finished and has the same transaction as Alice.
|
||||
val tx = bobFuture.get()
|
||||
txns.add(tx.second)
|
||||
verify()
|
||||
|
||||
assertTrue(smm.stateMachines.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
@ -88,7 +88,7 @@ class TransactionSerializationTests {
|
||||
fun timestamp() {
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
|
||||
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, TEST_KEYS_TO_CORP_MAP)
|
||||
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
||||
assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inStateRefs)
|
||||
assertEquals(tx.outputStates(), ltx.outStates)
|
||||
@ -97,7 +97,7 @@ class TransactionSerializationTests {
|
||||
val ltx2: LedgerTransaction = tx.
|
||||
toSignedTransaction().
|
||||
toTimestampedTransaction(DUMMY_TIMESTAMPER).
|
||||
verifyToLedgerTransaction(DUMMY_TIMESTAMPER, TEST_KEYS_TO_CORP_MAP)
|
||||
verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
||||
assertEquals(TEST_TX_TIME, ltx2.time)
|
||||
}
|
||||
}
|
@ -13,15 +13,19 @@ package core.testutils
|
||||
import com.google.common.io.BaseEncoding
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.messaging.MessagingService
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.fail
|
||||
@ -89,6 +93,53 @@ class DummyTimestamper(private val time: Instant = TEST_TX_TIME) : TimestamperSe
|
||||
|
||||
val DUMMY_TIMESTAMPER = DummyTimestamper()
|
||||
|
||||
object MockIdentityService : IdentityService {
|
||||
override fun partyFromKey(key: PublicKey): Party? = TEST_KEYS_TO_CORP_MAP[key]
|
||||
}
|
||||
|
||||
class MockKeyManagementService(
|
||||
override val keys: Map<PublicKey, PrivateKey>,
|
||||
val nextKeys: MutableList<KeyPair> = arrayListOf(KeyPairGenerator.getInstance("EC").genKeyPair())
|
||||
) : KeyManagementService {
|
||||
override fun freshKey() = nextKeys.removeAt(nextKeys.lastIndex)
|
||||
}
|
||||
|
||||
class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService {
|
||||
override val currentWallet = Wallet(states)
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
class MockStorageService : StorageService {
|
||||
private val mapOfMaps = HashMap<String, MutableMap<Any, Any>>()
|
||||
|
||||
@Synchronized
|
||||
override fun <K, V> getMap(tableName: String): MutableMap<K, V> {
|
||||
return mapOfMaps.getOrPut(tableName) { Collections.synchronizedMap(HashMap<Any, Any>()) } as MutableMap<K, V>
|
||||
}
|
||||
}
|
||||
|
||||
class MockServices(
|
||||
val wallet: WalletService?,
|
||||
val keyManagement: KeyManagementService?,
|
||||
val net: MessagingService?,
|
||||
val identity: IdentityService? = MockIdentityService,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val timestamping: TimestamperService? = DUMMY_TIMESTAMPER
|
||||
) : ServiceHub {
|
||||
override val walletService: WalletService
|
||||
get() = wallet ?: throw UnsupportedOperationException()
|
||||
override val keyManagementService: KeyManagementService
|
||||
get() = keyManagement ?: throw UnsupportedOperationException()
|
||||
override val identityService: IdentityService
|
||||
get() = identity ?: throw UnsupportedOperationException()
|
||||
override val timestampingService: TimestamperService
|
||||
get() = timestamping ?: throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService
|
||||
get() = net ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||
@ -220,7 +271,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
private val inStates = ArrayList<ContractStateRef>()
|
||||
|
||||
fun input(label: String) {
|
||||
inStates.add(labelToRefs[label] ?: throw IllegalArgumentException("Unknown label \"$label\""))
|
||||
inStates.add(label.outputRef)
|
||||
}
|
||||
|
||||
|
||||
@ -230,11 +281,14 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
*/
|
||||
fun toLedgerTransaction(time: Instant): LedgerTransaction {
|
||||
val wireCmds = commands.map { WireCommand(it.value, it.signers) }
|
||||
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
}
|
||||
|
||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||
val String.outputRef: ContractStateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"")
|
||||
|
||||
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||
|
||||
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
||||
@ -263,11 +317,12 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
||||
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = ContractStateRef(ltx.hash, index)
|
||||
outputsToLabels[state.state] = label
|
||||
labelToOutputs[label] = state.state as T
|
||||
}
|
||||
rootTxns.add(ltx)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user