Merged in quasar (pull request #12)

Upgrade protocol framework
This commit is contained in:
Mike Hearn 2016-01-13 13:56:12 +01:00
commit b3bfe9d532
28 changed files with 1390 additions and 978 deletions

9
.gitignore vendored
View File

@ -12,7 +12,14 @@ TODO
*.iml
## Directory-based project format:
.idea/
.idea/*.xml
.idea/.name
.idea/copyright
.idea/inspectionProfiles
.idea/libraries
.idea/shelf
# if you remove the above rule, at least ignore the following:
# User-specific stuff:

23
.idea/runConfigurations/All_tests.xml generated Normal file
View File

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All tests" type="JUnit" factoryName="JUnit">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<module name="r3prototyping" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="-ea -Dco.paralleluniverse.fibers.verifyInstrumentation -javaagent:lib/quasar.jar" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
</option>
<envs />
<patterns />
<method />
</configuration>
</component>

View File

@ -3,7 +3,7 @@ version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.dokka'
//apply plugin: 'org.jetbrains.dokka'
allprojects {
sourceCompatibility = 1.8
@ -12,6 +12,9 @@ allprojects {
buildscript {
ext.kotlin_version = '1.0.0-beta-4584'
ext.quasar_version = '0.7.4-SNAPSHOT'
ext.asm_version = '0.5.3'
repositories {
mavenCentral()
jcenter()
@ -22,7 +25,9 @@ buildscript {
}
}
repositories {
// mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
@ -30,38 +35,61 @@ repositories {
jcenter()
}
configurations {
quasar
}
// This block makes Gradle print an error and refuse to continue if we get multiple versions of the same library
// included simultaneously.
configurations.all() {
resolutionStrategy {
failOnVersionConflict()
}
}
dependencies {
testCompile 'junit:junit:4.11'
testCompile 'junit:junit:4.12'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
compile "com.google.guava:guava:18.0"
compile "com.esotericsoftware:kryo:3.0.3"
compile "com.google.guava:guava:19.0"
compile("com.esotericsoftware:kryo:3.0.3") {
force = true
}
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"
// Quasar: for the bytecode rewriting for state machines.
compile("co.paralleluniverse:quasar-core:${quasar_version}:jdk8") {
// Quasar currently depends on an old version of Kryo, but it works fine with the newer version, so exclude it
// here so the newer version is picked up.
exclude group: "com.esotericsoftware.kryo", module: "kryo"
}
quasar("co.paralleluniverse:quasar-core:${quasar_version}:jdk8@jar") {
exclude group: "com.esotericsoftware.kryo", module: "kryo"
}
// For visualisation
compile "org.graphstream:gs-core:1.3"
compile "org.graphstream:gs-ui:1.3"
compile "com.intellij:forms_rt:7.0.3"
compile("com.intellij:forms_rt:7.0.3") {
exclude group: "asm"
}
}
// These lines tell Gradle to add a couple of JVM command line arguments to unit test and program runs, which set up
// the Quasar bytecode rewriting system so fibers can be suspended. The verifyInstrumentation line makes things run
// slower but you get a much better error message if you forget to annotate a method with @Suspendable that needs it.
//
// In Java 9 (hopefully) the requirement to annotate methods as @Suspendable will go away.
tasks.withType(Test) {
jvmArgs "-javaagent:${configurations.quasar.singleFile}"
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
}
tasks.withType(JavaExec) {
jvmArgs "-javaagent:${configurations.quasar.singleFile}"
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
}

View File

@ -43,7 +43,7 @@ Actor frameworks can solve some of the above but they are often tightly bound to
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
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
@ -56,10 +56,10 @@ 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 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.
unserialised and resumed from where it was suspended. This concept is sometimes referred to as "fibers". This may
sound abstract but don't worry, the examples below will make it clearer. The JVM does not natively support
continuations, so we implement them using a library called Quasar which works through behind-the-scenes
bytecode rewriting. You don't have to know how this works to benefit from it, however.
We use continuations for the following reasons:
@ -98,73 +98,135 @@ 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:
We start by defining a wrapper that namespaces the protocol code, two functions to start either the buy or sell side
of the protocol, and two classes that will contain the protocol definition. We also pick what data will be used by
each side.
.. container:: codeset
.. sourcecode:: kotlin
abstract class TwoPartyTradeProtocol {
class SellerInitialArgs(
val assetToSell: StateAndRef<OwnableState>,
object TwoPartyTradeProtocol {
val TRADE_TOPIC = "platform.trade"
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
smm.add("$TRADE_TOPIC.seller", seller)
return seller.resultFuture
}
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
sessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
smm.add("$TRADE_TOPIC.buyer", buyer)
return buyer.resultFuture
}
class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
TODO()
}
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
private class SellerTradeInfo(
val assetForSale: 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 sellerOwnerKey: PublicKey,
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>>()
private class UnacceptablePriceException(val givenPrice: Amount) : Exception()
private class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
}
companion object {
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
return TwoPartyTradeProtocolImpl(smm)
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
TODO()
}
}
}
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.
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one
to run the buyer side of the protocol and one to run the seller side.
- It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved
by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation.
- The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run,
use them to construct a ``Buyer`` or ``Seller`` object respectively, and then add the new instances to the
``StateMachineManager``. The purpose of this class is described below. The ``smm.add`` method takes a logger name as
the first parameter, this is just a standard JDK logging identifier string, and the instance to add.
Going through the data needed to become a seller, we have:
- ``timestampingAuthority: LegallyIdentifiableNode`` - a reference to a node on the P2P network that acts as a trusted
timestamper. The use of timestamping is described in :doc:`data-model`.
- ``otherSide: SingleMessageRecipient`` - the network address of the node with which you are trading.
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
- ``price: Amount`` - the agreed on price that the asset is being sold for.
- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction.
- ``buyerSessionID: Long`` - a unique number that identifies this trade to the buyer. It is expected that the buyer
knows that the trade is going to take place and has sent you such a number already. (This field may go away in a future
iteration of the framework)
.. 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.
And for the buyer:
- ``acceptablePrice: Amount`` - the price that was agreed upon out of band. If the seller specifies a price less than
or equal to this, then the trade will go ahead.
- ``typeToBuy: Class<out OwnableState>`` - the type of state that is being purchased. This is used to check that the
sell side of the protocol isn't trying to sell us the wrong thing, whether by accident or on purpose.
- ``sessionID: Long`` - the session ID that was handed to the seller in order to start the protocol.
The run methods return a ``ListenableFuture`` that will complete when the protocol has finished.
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 ``.resultFuture.get()`` on resulting object to
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on resulting object 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?
Finally, we define a couple of exceptions, and a class that will be used as a protocol message called ``SellerTradeInfo``.
Suspendable methods
-------------------
The ``call`` method of the buyer/seller classes is marked with the ``@Suspendable`` annotation. What does this mean?
As mentioned above, our protocol framework will at points suspend the code and serialise it to disk. For this to work,
any methods on the call stack must have been pre-marked as ``@Suspendable`` so the bytecode rewriter knows to modify
the underlying code to support this new feature. A protocol is suspended when calling either ``receive``, ``send`` or
``sendAndReceive`` which we will learn more about below. For now, just be aware that when one of these methods is
invoked, all methods on the stack must have been marked. If you forget, then in the unit test environment you will
get a useful error message telling you which methods you didn't mark. The fix is simple enough: just add the annotation
and try again.
.. note:: A future version of Java is likely to remove this pre-marking requirement completely.
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
with a ``MessagingService`` and iterate the right state machine when messages arrive. 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.
@ -175,66 +237,8 @@ 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:
Let's implement the ``Seller.call`` method. This will be invoked by the platform when the protocol is started by the
``StateMachineManager``.
.. container:: codeset
@ -243,25 +247,24 @@ Next we add some code to the ``SellerImpl.call`` method:
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 hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID,
sessionID, hello)
val partialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
logger().trace { "Received partially signed transaction" }
That's pretty straightforward. 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.
- The thing to send. It'll be serialised and sent automatically.
- Finally a type argument, which is the kind of object we're expecting to receive from the other side.
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.
.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by
the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to.
@ -270,11 +273,6 @@ cash put into the transaction and their signature on it authorising the movement
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
@ -294,14 +292,10 @@ OK, let's keep going:
// 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!" }
@ -313,7 +307,8 @@ OK, let's keep going:
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.
transaction, in case the contracts governing the asset we're selling require it, 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
@ -332,74 +327,73 @@ OK, let's do the same for the buyer side:
.. 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)
@Suspendable
override fun call(): Pair<TimestampedWireTransaction, LedgerTransaction> {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { "Got trade request for a $assetTypeName" }
// 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)
// Check the start message for acceptability.
check(tradeRequest.sessionID > 0)
if (tradeRequest.price > acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(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!
// 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 = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().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))
// Generate the shared transaction that both sides will sign, using the data we have.
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().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)
// 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(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx,
TimestampedWireTransaction::class.java)
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:
This code is longer but still 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``. See the contracts tutorial if this isn't

View File

@ -201,6 +201,7 @@ prove or disprove the following hypothesis:</p>
<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#suspendable-methods">Suspendable methods</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>

View File

@ -95,6 +95,7 @@
<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="#suspendable-methods">Suspendable methods</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>
@ -183,7 +184,7 @@ when actually they need to receive/send another.</li>
<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&#8217;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
<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
@ -194,10 +195,10 @@ construction of them that automatically handles many of the concerns outlined ab
<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&#8217;t worry, the examples below
will make it clearer. The JVM does not natively support continuations, so we implement them using a library called
JavaFlow which works through behind-the-scenes bytecode rewriting. You don&#8217;t have to know how this works to benefit
from it, however.</p>
unserialised and resumed from where it was suspended. This concept is sometimes referred to as &#8220;fibers&#8221;. This may
sound abstract but don&#8217;t worry, the examples below will make it clearer. The JVM does not natively support
continuations, so we implement them using a library called Quasar which works through behind-the-scenes
bytecode rewriting. You don&#8217;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>
@ -230,71 +231,132 @@ it lacks a signature from S authorising movement of the asset.</li>
<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&#8217;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>
<p>We start by defining a wrapper that namespaces the protocol code, two functions to start either the buy or sell side
of the protocol, and two classes that will contain the protocol definition. We also pick what data will be used by
each side.</p>
<div class="codeset container">
<div class="highlight-kotlin"><div class="highlight"><pre>abstract class TwoPartyTradeProtocol {
class SellerInitialArgs(
val assetToSell: StateAndRef&lt;OwnableState&gt;,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long
)
<div class="highlight-kotlin"><div class="highlight"><pre><span class="k">object</span> <span class="nc">TwoPartyTradeProtocol</span> <span class="p">{</span>
<span class="k">val</span> <span class="py">TRADE_TOPIC</span> <span class="p">=</span> <span class="s">&quot;platform.trade&quot;</span>
abstract fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller
<span class="k">fun</span> <span class="nf">runSeller</span><span class="p">(</span><span class="n">smm</span><span class="p">:</span> <span class="n">StateMachineManager</span><span class="p">,</span> <span class="n">timestampingAuthority</span><span class="p">:</span> <span class="n">LegallyIdentifiableNode</span><span class="p">,</span>
<span class="n">otherSide</span><span class="p">:</span> <span class="n">SingleMessageRecipient</span><span class="p">,</span> <span class="n">assetToSell</span><span class="p">:</span> <span class="n">StateAndRef</span><span class="p">&lt;</span><span class="n">OwnableState</span><span class="p">&gt;,</span> <span class="n">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
<span class="n">myKeyPair</span><span class="p">:</span> <span class="n">KeyPair</span><span class="p">,</span> <span class="n">buyerSessionID</span><span class="p">:</span> <span class="n">Long</span><span class="p">):</span> <span class="n">ListenableFuture</span><span class="p">&lt;</span><span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
<span class="k">val</span> <span class="py">seller</span> <span class="p">=</span> <span class="n">Seller</span><span class="p">(</span><span class="n">otherSide</span><span class="p">,</span> <span class="n">timestampingAuthority</span><span class="p">,</span> <span class="n">assetToSell</span><span class="p">,</span> <span class="n">price</span><span class="p">,</span> <span class="n">myKeyPair</span><span class="p">,</span> <span class="n">buyerSessionID</span><span class="p">)</span>
<span class="n">smm</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="s">&quot;$TRADE_TOPIC.seller&quot;</span><span class="p">,</span> <span class="n">seller</span><span class="p">)</span>
<span class="k">return</span> <span class="n">seller</span><span class="p">.</span><span class="n">resultFuture</span>
<span class="p">}</span>
class BuyerInitialArgs(
val acceptablePrice: Amount,
val typeToBuy: Class&lt;out OwnableState&gt;,
val sessionID: Long
)
<span class="k">fun</span> <span class="nf">runBuyer</span><span class="p">(</span><span class="n">smm</span><span class="p">:</span> <span class="n">StateMachineManager</span><span class="p">,</span> <span class="n">timestampingAuthority</span><span class="p">:</span> <span class="n">LegallyIdentifiableNode</span><span class="p">,</span>
<span class="n">otherSide</span><span class="p">:</span> <span class="n">SingleMessageRecipient</span><span class="p">,</span> <span class="n">acceptablePrice</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span> <span class="n">typeToBuy</span><span class="p">:</span> <span class="n">Class</span><span class="p">&lt;</span><span class="k">out</span> <span class="n">OwnableState</span><span class="p">&gt;,</span>
<span class="n">sessionID</span><span class="p">:</span> <span class="n">Long</span><span class="p">):</span> <span class="n">ListenableFuture</span><span class="p">&lt;</span><span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;&gt;</span> <span class="p">{</span>
<span class="k">val</span> <span class="py">buyer</span> <span class="p">=</span> <span class="n">Buyer</span><span class="p">(</span><span class="n">otherSide</span><span class="p">,</span> <span class="n">timestampingAuthority</span><span class="p">.</span><span class="n">identity</span><span class="p">,</span> <span class="n">acceptablePrice</span><span class="p">,</span> <span class="n">typeToBuy</span><span class="p">,</span> <span class="n">sessionID</span><span class="p">)</span>
<span class="n">smm</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="s">&quot;$TRADE_TOPIC.buyer&quot;</span><span class="p">,</span> <span class="n">buyer</span><span class="p">)</span>
<span class="k">return</span> <span class="n">buyer</span><span class="p">.</span><span class="n">resultFuture</span>
<span class="p">}</span>
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
<span class="k">class</span> <span class="nc">Seller</span><span class="p">(</span><span class="k">val</span> <span class="py">otherSide</span><span class="p">:</span> <span class="n">SingleMessageRecipient</span><span class="p">,</span>
<span class="k">val</span> <span class="py">timestampingAuthority</span><span class="p">:</span> <span class="n">LegallyIdentifiableNode</span><span class="p">,</span>
<span class="k">val</span> <span class="py">assetToSell</span><span class="p">:</span> <span class="n">StateAndRef</span><span class="p">&lt;</span><span class="n">OwnableState</span><span class="p">&gt;,</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">myKeyPair</span><span class="p">:</span> <span class="n">KeyPair</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> <span class="p">:</span> <span class="n">ProtocolStateMachine</span><span class="p">&lt;</span><span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;&gt;()</span> <span class="p">{</span>
<span class="n">@Suspendable</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">call</span><span class="p">():</span> <span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;</span> <span class="p">{</span>
<span class="n">TODO</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
abstract class Buyer : ProtocolStateMachine&lt;BuyerInitialArgs, Pair&lt;TimestampedWireTransaction, LedgerTransaction&gt;&gt;()
abstract class Seller : ProtocolStateMachine&lt;SellerInitialArgs, Pair&lt;TimestampedWireTransaction, LedgerTransaction&gt;&gt;()
<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">private</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">&lt;</span><span class="n">OwnableState</span><span class="p">&gt;,</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">sessionID</span><span class="p">:</span> <span class="n">Long</span>
<span class="p">)</span>
companion object {
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
return TwoPartyTradeProtocolImpl(smm)
}
}
}
<span class="k">private</span> <span class="k">class</span> <span class="nc">UnacceptablePriceException</span><span class="p">(</span><span class="k">val</span> <span class="py">givenPrice</span><span class="p">:</span> <span class="n">Amount</span><span class="p">)</span> <span class="p">:</span> <span class="n">Exception</span><span class="p">()</span>
<span class="k">private</span> <span class="k">class</span> <span class="nc">AssetMismatchException</span><span class="p">(</span><span class="k">val</span> <span class="py">expectedTypeName</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">typeName</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">:</span> <span class="n">Exception</span><span class="p">()</span> <span class="p">{</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">=</span> <span class="s">&quot;The submitted asset didn&#39;t match the expected type: $expectedTypeName vs $typeName&quot;</span>
<span class="p">}</span>
<span class="c1">// The buyer&#39;s side of the protocol. See note above Seller to learn about the caveats here.</span>
<span class="k">class</span> <span class="nc">Buyer</span><span class="p">(</span><span class="k">val</span> <span class="py">otherSide</span><span class="p">:</span> <span class="n">SingleMessageRecipient</span><span class="p">,</span>
<span class="k">val</span> <span class="py">timestampingAuthority</span><span class="p">:</span> <span class="n">Party</span><span class="p">,</span>
<span class="k">val</span> <span class="py">acceptablePrice</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
<span class="k">val</span> <span class="py">typeToBuy</span><span class="p">:</span> <span class="n">Class</span><span class="p">&lt;</span><span class="k">out</span> <span class="n">OwnableState</span><span class="p">&gt;,</span>
<span class="k">val</span> <span class="py">sessionID</span><span class="p">:</span> <span class="n">Long</span><span class="p">)</span> <span class="p">:</span> <span class="n">ProtocolStateMachine</span><span class="p">&lt;</span><span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;&gt;()</span> <span class="p">{</span>
<span class="n">@Suspendable</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">call</span><span class="p">():</span> <span class="n">Pair</span><span class="p">&lt;</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">&gt;</span> <span class="p">{</span>
<span class="n">TODO</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
</div>
</div>
<p>Let&#8217;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&#8217;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>
<li>It defines a several classes nested inside the main <code class="docutils literal"><span class="pre">TwoPartyTradeProtocol</span></code> singleton, and a couple of methods, one
to run the buyer side of the protocol and one to run the seller side.</li>
<li>It defines the &#8220;trade topic&#8221;, which is just a string that namespaces this protocol. The prefix &#8220;platform.&#8221; is reserved
by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation.</li>
<li>The <code class="docutils literal"><span class="pre">runBuyer</span></code> and <code class="docutils literal"><span class="pre">runSeller</span></code> methods take a number of parameters that specialise the protocol for this run,
use them to construct a <code class="docutils literal"><span class="pre">Buyer</span></code> or <code class="docutils literal"><span class="pre">Seller</span></code> object respectively, and then add the new instances to the
<code class="docutils literal"><span class="pre">StateMachineManager</span></code>. The purpose of this class is described below. The <code class="docutils literal"><span class="pre">smm.add</span></code> method takes a logger name as
the first parameter, this is just a standard JDK logging identifier string, and the instance to add.</li>
</ul>
<p>Going through the data needed to become a seller, we have:</p>
<ul class="simple">
<li><code class="docutils literal"><span class="pre">timestampingAuthority:</span> <span class="pre">LegallyIdentifiableNode</span></code> - a reference to a node on the P2P network that acts as a trusted
timestamper. The use of timestamping is described in <a class="reference internal" href="data-model.html"><em>Data model</em></a>.</li>
<li><code class="docutils literal"><span class="pre">otherSide:</span> <span class="pre">SingleMessageRecipient</span></code> - the network address of the node with which you are trading.</li>
<li><code class="docutils literal"><span class="pre">assetToSell:</span> <span class="pre">StateAndRef&lt;OwnableState&gt;</span></code> - a pointer to the ledger entry that represents the thing being sold.</li>
<li><code class="docutils literal"><span class="pre">price:</span> <span class="pre">Amount</span></code> - the agreed on price that the asset is being sold for.</li>
<li><code class="docutils literal"><span class="pre">myKeyPair:</span> <span class="pre">KeyPair</span></code> - the key pair that controls the asset being sold. It will be used to sign the transaction.</li>
<li><code class="docutils literal"><span class="pre">buyerSessionID:</span> <span class="pre">Long</span></code> - a unique number that identifies this trade to the buyer. It is expected that the buyer
knows that the trade is going to take place and has sent you such a number already. (This field may go away in a future
iteration of the framework)</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
to be unguessable. 63 bits is good enough.</p>
</div>
<p>And for the buyer:</p>
<ul class="simple">
<li><code class="docutils literal"><span class="pre">acceptablePrice:</span> <span class="pre">Amount</span></code> - the price that was agreed upon out of band. If the seller specifies a price less than
or equal to this, then the trade will go ahead.</li>
<li><code class="docutils literal"><span class="pre">typeToBuy:</span> <span class="pre">Class&lt;out</span> <span class="pre">OwnableState&gt;</span></code> - the type of state that is being purchased. This is used to check that the
sell side of the protocol isn&#8217;t trying to sell us the wrong thing, whether by accident or on purpose.</li>
<li><code class="docutils literal"><span class="pre">sessionID:</span> <span class="pre">Long</span></code> - the session ID that was handed to the seller in order to start the protocol.</li>
</ul>
<p>The run methods return a <code class="docutils literal"><span class="pre">ListenableFuture</span></code> that will complete when the protocol has finished.</p>
<p>Alright, so using this protocol shouldn&#8217;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">.resultFuture.get()</span></code> on resulting object to
to either runBuyer or runSeller, depending on who we are, and then call <code class="docutils literal"><span class="pre">.get()</span></code> on resulting object 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&#8217;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>
<p>Finally, we define a couple of exceptions, and a class that will be used as a protocol message called <code class="docutils literal"><span class="pre">SellerTradeInfo</span></code>.</p>
</div>
<div class="section" id="suspendable-methods">
<h2>Suspendable methods<a class="headerlink" href="#suspendable-methods" title="Permalink to this headline"></a></h2>
<p>The <code class="docutils literal"><span class="pre">call</span></code> method of the buyer/seller classes is marked with the <code class="docutils literal"><span class="pre">&#64;Suspendable</span></code> annotation. What does this mean?</p>
<p>As mentioned above, our protocol framework will at points suspend the code and serialise it to disk. For this to work,
any methods on the call stack must have been pre-marked as <code class="docutils literal"><span class="pre">&#64;Suspendable</span></code> so the bytecode rewriter knows to modify
the underlying code to support this new feature. A protocol is suspended when calling either <code class="docutils literal"><span class="pre">receive</span></code>, <code class="docutils literal"><span class="pre">send</span></code> or
<code class="docutils literal"><span class="pre">sendAndReceive</span></code> which we will learn more about below. For now, just be aware that when one of these methods is
invoked, all methods on the stack must have been marked. If you forget, then in the unit test environment you will
get a useful error message telling you which methods you didn&#8217;t mark. The fix is simple enough: just add the annotation
and try again.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">A future version of Java is likely to remove this pre-marking requirement completely.</p>
</div>
</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
with a <code class="docutils literal"><span class="pre">MessagingService</span></code> and iterate the right state machine when messages arrive. 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&#8217;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
@ -303,68 +365,15 @@ unit tests to see how it&#8217;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 = &quot;com.r3cev.protocols.trade&quot;
}
class SellerImpl : Seller() {
override fun call(args: SellerInitialArgs): Pair&lt;TimestampedWireTransaction, LedgerTransaction&gt; {
TODO()
}
}
class BuyerImpl : Buyer() {
override fun call(args: BuyerInitialArgs): Pair&lt;TimestampedWireTransaction, LedgerTransaction&gt; {
TODO()
}
}
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
return smm.add(otherSide, args, &quot;$TRADE_TOPIC.seller&quot;, SellerImpl::class.java)
}
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
return smm.add(otherSide, args, &quot;$TRADE_TOPIC.buyer&quot;, 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 &#8220;topic&#8221; 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&#8217;s try implementing the seller side. Firstly, we&#8217;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</span>
<span class="c1">// 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">&lt;</span><span class="n">OwnableState</span><span class="p">&gt;,</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&#8217;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&#8217;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>
<p>Let&#8217;s implement the <code class="docutils literal"><span class="pre">Seller.call</span></code> method. This will be invoked by the platform when the protocol is started by the
<code class="docutils literal"><span class="pre">StateMachineManager</span></code>.</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&#39;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="k">val</span> <span class="py">hello</span> <span class="p">=</span> <span class="n">SellerTradeInfo</span><span class="p">(</span><span class="n">assetToSell</span><span class="p">,</span> <span class="n">price</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="k">val</span> <span class="py">partialTX</span> <span class="p">=</span> <span class="n">sendAndReceive</span><span class="p">&lt;</span><span class="n">SignedWireTransaction</span><span class="p">&gt;(</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="k">val</span> <span class="py">partialTX</span> <span class="p">=</span> <span class="n">sendAndReceive</span><span class="p">(</span><span class="n">TRADE_TOPIC</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">SignedWireTransaction</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</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">&quot;Received partially signed transaction&quot;</span> <span class="p">}</span>
</pre></div>
</div>
@ -372,27 +381,23 @@ trade&#8217;s messages, and a pointer to where the asset that is being sold can
<p>That&#8217;s pretty straightforward. We generate a session ID to identify what&#8217;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&#8217;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&#8217;s node.</li>
<li>The session IDs that ensure the messages don&#8217;t get mixed up with other simultaneous trades.</li>
<li>And finally, the thing to send. It&#8217;ll be serialised and sent automatically.</li>
<li>The thing to send. It&#8217;ll be serialised and sent automatically.</li>
<li>Finally a type argument, which is the kind of object we&#8217;re expecting to receive from the other side.</li>
</ul>
<p>Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we&#8217;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.
<p>There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by
the 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
<p class="last">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&#8217;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&#8217;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&#8217;s keep going:</p>
<div class="codeset container">
@ -409,14 +414,10 @@ or super-class (like <code class="docutils literal"><span class="pre">Buyer</spa
<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">&quot;Built finished transaction, sending back to secondary!&quot;</span> <span class="p">}</span>
@ -430,7 +431,8 @@ or super-class (like <code class="docutils literal"><span class="pre">Buyer</spa
<p>Here, we see some assertions and signature checking to satisfy ourselves that we&#8217;re not about to sign something
incorrect. Once we&#8217;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&#8217;s all OK. Finally, we request timestamping of the
transaction, and send the now finalised and validated transaction back to the buyer.</p>
transaction, in case the contracts governing the asset we&#8217;re selling require it, 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
@ -445,76 +447,75 @@ forms.</p>
<h2>Implementing the buyer<a class="headerlink" href="#implementing-the-buyer" title="Permalink to this headline"></a></h2>
<p>OK, let&#8217;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&lt;TimestampedWireTransaction, LedgerTransaction&gt; {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive&lt;SellerTradeInfo&gt;(TRADE_TOPIC, args.sessionID)
<div class="highlight-kotlin"><div class="highlight"><pre>@Suspendable
override fun call(): Pair&lt;TimestampedWireTransaction, LedgerTransaction&gt; {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { &quot;Got trade request for a $assetTypeName&quot; }
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { &quot;Got trade request for a $assetTypeName&quot; }
// Check the start message for acceptability.
check(tradeRequest.sessionID &gt; 0)
if (tradeRequest.price &gt; args.acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
// Check the start message for acceptability.
check(tradeRequest.sessionID &gt; 0)
if (tradeRequest.price &gt; acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(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!
// 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 = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType&lt;Cash.State&gt;()
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))
// Generate the shared transaction that both sides will sign, using the data we have.
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType&lt;Cash.State&gt;()
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 { &quot;Sending partially signed transaction to seller&quot; }
// 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&lt;TimestampedWireTransaction&gt;(TRADE_TOPIC,
tradeRequest.sessionID, args.sessionID, stx)
logger().trace { &quot;Got fully signed transaction, verifying ... &quot;}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
serviceHub.identityService)
logger().trace { &quot;Fully signed transaction was valid. Trade complete! :-)&quot; }
return Pair(fullySigned, ltx)
// 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 { &quot;Sending partially signed transaction to seller&quot; }
// 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(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx,
TimestampedWireTransaction::class.java)
logger().trace { &quot;Got fully signed transaction, verifying ... &quot;}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
serviceHub.identityService)
logger().trace { &quot;Fully signed transaction was valid. Trade complete! :-)&quot; }
return Pair(fullySigned, ltx)
}
</pre></div>
</div>
</div>
<p>This code is fairly straightforward. Here are some things to pay attention to:</p>
<p>This code is longer but still 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&#8217;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>. See the contracts tutorial if this isn&#8217;t

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@ Actor frameworks can solve some of the above but they are often tightly bound to
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
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
@ -56,10 +56,10 @@ 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 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.
unserialised and resumed from where it was suspended. This concept is sometimes referred to as "fibers". This may
sound abstract but don't worry, the examples below will make it clearer. The JVM does not natively support
continuations, so we implement them using a library called Quasar which works through behind-the-scenes
bytecode rewriting. You don't have to know how this works to benefit from it, however.
We use continuations for the following reasons:
@ -98,73 +98,135 @@ 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:
We start by defining a wrapper that namespaces the protocol code, two functions to start either the buy or sell side
of the protocol, and two classes that will contain the protocol definition. We also pick what data will be used by
each side.
.. container:: codeset
.. sourcecode:: kotlin
abstract class TwoPartyTradeProtocol {
class SellerInitialArgs(
val assetToSell: StateAndRef<OwnableState>,
object TwoPartyTradeProtocol {
val TRADE_TOPIC = "platform.trade"
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
smm.add("$TRADE_TOPIC.seller", seller)
return seller.resultFuture
}
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
sessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
smm.add("$TRADE_TOPIC.buyer", buyer)
return buyer.resultFuture
}
class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
TODO()
}
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
private class SellerTradeInfo(
val assetForSale: 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 sellerOwnerKey: PublicKey,
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>>()
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"
}
companion object {
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
return TwoPartyTradeProtocolImpl(smm)
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
TODO()
}
}
}
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.
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one
to run the buyer side of the protocol and one to run the seller side.
- It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved
by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation.
- The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run,
use them to construct a ``Buyer`` or ``Seller`` object respectively, and then add the new instances to the
``StateMachineManager``. The purpose of this class is described below. The ``smm.add`` method takes a logger name as
the first parameter, this is just a standard JDK logging identifier string, and the instance to add.
Going through the data needed to become a seller, we have:
- ``timestampingAuthority: LegallyIdentifiableNode`` - a reference to a node on the P2P network that acts as a trusted
timestamper. The use of timestamping is described in :doc:`data-model`.
- ``otherSide: SingleMessageRecipient`` - the network address of the node with which you are trading.
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
- ``price: Amount`` - the agreed on price that the asset is being sold for.
- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction.
- ``buyerSessionID: Long`` - a unique number that identifies this trade to the buyer. It is expected that the buyer
knows that the trade is going to take place and has sent you such a number already. (This field may go away in a future
iteration of the framework)
.. 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.
And for the buyer:
- ``acceptablePrice: Amount`` - the price that was agreed upon out of band. If the seller specifies a price less than
or equal to this, then the trade will go ahead.
- ``typeToBuy: Class<out OwnableState>`` - the type of state that is being purchased. This is used to check that the
sell side of the protocol isn't trying to sell us the wrong thing, whether by accident or on purpose.
- ``sessionID: Long`` - the session ID that was handed to the seller in order to start the protocol.
The run methods return a ``ListenableFuture`` that will complete when the protocol has finished.
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 ``.resultFuture.get()`` on resulting object to
to either runBuyer or runSeller, depending on who we are, and then call ``.get()`` on resulting object 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?
Finally, we define a couple of exceptions, and a class that will be used as a protocol message called ``SellerTradeInfo``.
Suspendable methods
-------------------
The ``call`` method of the buyer/seller classes is marked with the ``@Suspendable`` annotation. What does this mean?
As mentioned above, our protocol framework will at points suspend the code and serialise it to disk. For this to work,
any methods on the call stack must have been pre-marked as ``@Suspendable`` so the bytecode rewriter knows to modify
the underlying code to support this new feature. A protocol is suspended when calling either ``receive``, ``send`` or
``sendAndReceive`` which we will learn more about below. For now, just be aware that when one of these methods is
invoked, all methods on the stack must have been marked. If you forget, then in the unit test environment you will
get a useful error message telling you which methods you didn't mark. The fix is simple enough: just add the annotation
and try again.
.. note:: A future version of Java is likely to remove this pre-marking requirement completely.
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
with a ``MessagingService`` and iterate the right state machine when messages arrive. 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.
@ -175,66 +237,8 @@ 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:
Let's implement the ``Seller.call`` method. This will be invoked by the platform when the protocol is started by the
``StateMachineManager``.
.. container:: codeset
@ -243,25 +247,24 @@ Next we add some code to the ``SellerImpl.call`` method:
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 hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID,
sessionID, hello)
val partialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
logger().trace { "Received partially signed transaction" }
That's pretty straightforward. 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.
- The thing to send. It'll be serialised and sent automatically.
- Finally a type argument, which is the kind of object we're expecting to receive from the other side.
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.
.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation.
The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by
the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to.
@ -270,11 +273,6 @@ cash put into the transaction and their signature on it authorising the movement
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
@ -294,14 +292,10 @@ OK, let's keep going:
// 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!" }
@ -313,7 +307,8 @@ OK, let's keep going:
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.
transaction, in case the contracts governing the asset we're selling require it, 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
@ -332,74 +327,73 @@ OK, let's do the same for the buyer side:
.. 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)
@Suspendable
override fun call(): Pair<TimestampedWireTransaction, LedgerTransaction> {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { "Got trade request for a $assetTypeName" }
// 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)
// Check the start message for acceptability.
check(tradeRequest.sessionID > 0)
if (tradeRequest.price > acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(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!
// 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 = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().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))
// Generate the shared transaction that both sides will sign, using the data we have.
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().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)
// 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(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx,
TimestampedWireTransaction::class.java)
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:
This code is longer but still 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``. See the contracts tutorial if this isn't

BIN
lib/quasar.jar Normal file

Binary file not shown.

View File

@ -8,17 +8,25 @@
package contracts.protocols
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import contracts.Cash
import contracts.sumCashBy
import core.*
import core.messaging.*
import core.messaging.LegallyIdentifiableNode
import core.messaging.ProtocolStateMachine
import core.messaging.SingleMessageRecipient
import core.messaging.StateMachineManager
import core.node.TimestamperClient
import core.serialization.deserialize
import core.utilities.trace
import java.security.KeyPair
import java.security.PublicKey
import java.time.Instant
/**
* This asset trading protocol has two parties (B and S for buyer and seller) and the following steps:
* This asset trading protocol implements a "delivery vs payment" type swap. It 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.
@ -40,69 +48,46 @@ import java.security.PublicKey
*
* 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
)
object TwoPartyTradeProtocol {
val TRADE_TOPIC = "platform.trade"
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<WireTransaction, LedgerTransaction>>()
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<WireTransaction, 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"
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
smm.add("$TRADE_TOPIC.seller", seller)
return seller.resultFuture
}
// 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
)
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
sessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
smm.add("$TRADE_TOPIC.buyer", buyer)
return buyer.resultFuture
}
// 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<WireTransaction, LedgerTransaction> {
class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, 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 hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val partialTX = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, args.buyerSessionID, sessionID, hello)
logger().trace { "Received partially signed transaction" }
val partialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
logger.trace { "Received partially signed transaction" }
partialTX.verifySignatures()
val wtx: WireTransaction = partialTX.txBits.deserialize()
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(myKeyPair.public) == 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
@ -116,39 +101,57 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// express protocol state machines on top of the messaging layer.
}
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits)
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
// We should run it through our full TransactionGroup of all transactions here.
fullySigned.verify()
logger().trace { "Built finished transaction, sending back to secondary!" }
// Sign with our key and get the timestamping authorities key as well.
// These two steps could be done in parallel, in theory.
val ourSignature = myKeyPair.signWithECDSA(partialTX.txBits)
val tsaSig = TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
val fullySigned = partialTX.withAdditionalSignature(tsaSig).withAdditionalSignature(ourSignature)
send(TRADE_TOPIC, args.buyerSessionID, fullySigned)
// We should run it through our full TransactionGroup of all transactions here.
logger.trace { "Built finished transaction, sending back to secondary!" }
send(TRADE_TOPIC, otherSide, buyerSessionID, fullySigned)
return Pair(wtx, fullySigned.verifyToLedgerTransaction(serviceHub.identityService))
}
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
private class SellerTradeInfo(
val assetForSale: StateAndRef<OwnableState>,
val price: Amount,
val sellerOwnerKey: PublicKey,
val sessionID: Long
)
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<WireTransaction, LedgerTransaction> {
class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
val tradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { "Got trade request for a $assetTypeName" }
logger.trace { "Got trade request for a $assetTypeName" }
// Check the start message for acceptability.
check(tradeRequest.sessionID > 0)
if (tradeRequest.price > args.acceptablePrice)
if (tradeRequest.price > acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!args.typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(args.typeToBuy.name, assetTypeName)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(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!
@ -168,6 +171,10 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
ptx.addOutputState(state)
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one.
ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds)
// Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) {
val priv = serviceHub.keyManagementService.toPrivate(k)
@ -179,27 +186,21 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
logger().trace { "Sending partially signed transaction to seller" }
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<SignedWireTransaction>(TRADE_TOPIC, tradeRequest.sessionID, args.sessionID, stx)
logger().trace { "Got fully signed transaction, verifying ... "}
val fullySigned = sendAndReceive(TRADE_TOPIC, otherSide, tradeRequest.sessionID,
sessionID, stx, SignedWireTransaction::class.java)
logger.trace { "Got fully signed transaction, verifying ... "}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned.tx, 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)
}
}

View File

@ -57,6 +57,7 @@ open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueByte
/** A digital signature that identifies who the public key is owned by. */
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int = 0) : DigitalSignature(bits, covering) {
fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this)
fun verifyWithECDSA(content: OpaqueBytes) = by.verifyWithECDSA(content.bits, this)
}
class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)

View File

@ -8,6 +8,7 @@
package core
import co.paralleluniverse.fibers.Suspendable
import core.messaging.MessagingService
import core.serialization.SerializedBytes
import java.security.KeyPair
@ -78,6 +79,7 @@ interface IdentityService {
* themselves.
*/
interface TimestamperService {
@Suspendable
fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable
/** The name+pubkey that this timestamper will sign with. */
@ -99,6 +101,13 @@ object DummyTimestampingAuthority {
*/
interface StorageService {
fun <K,V> getMap(tableName: String): MutableMap<K, V>
/**
* Returns the legal identity that this node is configured with. Assumed to be initialised when the node is
* first installed.
*/
val myLegalIdentity: Party
val myLegalIdentityKey: KeyPair
}
/**
@ -110,7 +119,6 @@ interface ServiceHub {
val walletService: WalletService
val keyManagementService: KeyManagementService
val identityService: IdentityService
val timestampingService: TimestamperService
val storageService: StorageService
val networkService: MessagingService
}

View File

@ -8,6 +8,8 @@
package core
import co.paralleluniverse.fibers.Suspendable
import core.node.TimestampingError
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.serialization.serialize
@ -106,15 +108,11 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
verify()
return tx.toLedgerTransaction(identityService, id)
}
/** Returns the same transaction but with an additional (unchecked) signature */
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
}
/**
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the transaction
* is too far in the past or future relative to the local clock and thus the timestamper would reject it.
*/
class NotOnTimeException : Exception()
/** A mutable transaction that's in the process of being built, before all signatures are present. */
class TransactionBuilder(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
private val outputStates: MutableList<ContractState> = arrayListOf(),
@ -163,6 +161,20 @@ class TransactionBuilder(private val inputStates: MutableList<ContractStateRef>
currentSigs.add(key.signWithECDSA(data.bits))
}
/**
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then
* adds it.
*
* @throws SignatureException if the signature didn't match the transaction contents
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
require(commands.count { it.pubkeys.contains(sig.by) } > 0) { "Signature key doesn't match any command" }
val data = toWireTransaction().serialize()
sig.verifyWithECDSA(data.bits)
currentSigs.add(sig)
}
/**
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
@ -173,15 +185,14 @@ class TransactionBuilder(private val inputStates: MutableList<ContractStateRef>
*
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
*/
@Suspendable
fun timestamp(timestamper: TimestamperService, clock: Clock = Clock.systemUTC()) {
// TODO: Once we switch to a more advanced bytecode rewriting framework, we can call into a real implementation.
check(timestamper.javaClass.simpleName == "DummyTimestamper")
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
// Obviously this is just a hard-coded dummy value for now.
val maxExpectedLatency = 5.seconds
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
throw NotOnTimeException()
throw TimestampingError.NotOnTimeException()
// The timestamper may also throw NotOnTimeException if our clocks are desynchronised or if we are right on the
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative

View File

@ -44,4 +44,6 @@ fun <T> SettableFuture<T>.setFrom(logger: Logger? = null, block: () -> T): Setta
}
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
val RunOnCallerThread = MoreExecutors.directExecutor()

View File

@ -11,7 +11,11 @@ package core.messaging
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import core.Party
import core.node.TimestamperNodeService
import core.sha256
import core.utilities.loggerFor
import java.security.KeyPairGenerator
import java.time.Instant
import java.util.*
import java.util.concurrent.Executor
@ -31,13 +35,15 @@ import kotlin.concurrent.thread
@ThreadSafe
public class InMemoryNetwork {
private var counter = 0 // -1 means stopped.
private val networkMap = HashMap<Handle, Node>()
private val handleNodeMap = 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>>()
val nodes: List<Node> @Synchronized get() = handleNodeMap.values.toList()
/**
* Creates a node and returns the new object that identifies its location on the network to senders, and the
* [Node] that the recipient/in-memory node uses to receive messages and send messages itself.
@ -69,7 +75,7 @@ public class InMemoryNetwork {
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)
for (handle in handleNodeMap.keys)
getQueueForHandle(handle).add(message)
}
else -> throw IllegalArgumentException("Unknown type of recipient handle")
@ -78,7 +84,7 @@ public class InMemoryNetwork {
@Synchronized
private fun netNodeHasShutdown(handle: Handle) {
networkMap.remove(handle)
handleNodeMap.remove(handle)
}
@Synchronized
@ -90,11 +96,11 @@ public class InMemoryNetwork {
fun stop() {
// 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())
for (node in handleNodeMap.values.toArrayList())
node.stop()
counter = -1
networkMap.clear()
handleNodeMap.clear()
messageQueues.clear()
}
@ -102,7 +108,7 @@ public class InMemoryNetwork {
override fun start(): ListenableFuture<Node> {
synchronized(this@InMemoryNetwork) {
val node = Node(manuallyPumped, id)
networkMap[id] = node
handleNodeMap[id] = node
return Futures.immediateFuture(node)
}
}
@ -114,6 +120,20 @@ public class InMemoryNetwork {
override fun hashCode() = id.hashCode()
}
private var timestampingAdvert: LegallyIdentifiableNode? = null
@Synchronized
fun setupTimestampingNode(manuallyPumped: Boolean): Pair<LegallyIdentifiableNode, Node> {
check(timestampingAdvert == null)
val (handle, builder) = createNode(manuallyPumped)
val node = builder.start().get()
val key = KeyPairGenerator.getInstance("EC").genKeyPair()
val identity = Party("Unit test timestamping authority", key.public)
TimestamperNodeService(node, identity, key)
timestampingAdvert = LegallyIdentifiableNode(handle, identity)
return Pair(timestampingAdvert!!, node)
}
/**
* 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
@ -132,6 +152,10 @@ public class InMemoryNetwork {
override val myAddress: SingleMessageRecipient = handle
override val networkMap: NetworkMap get() = object : NetworkMap {
override val timestampingNodes = if (timestampingAdvert != null) listOf(timestampingAdvert!!) else emptyList()
}
protected val backgroundThread = if (manuallyPumped) null else thread(isDaemon = true, name = "In-memory message dispatcher ") {
while (!currentThread.isInterrupted) {
try {
@ -228,7 +252,13 @@ public class InMemoryNetwork {
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) }
(handler.executor ?: MoreExecutors.directExecutor()).execute {
try {
handler.callback(message, handler)
} catch(e: Exception) {
loggerFor<InMemoryNetwork>().error("Caught exception in handler for $this/${handler.topic}", e)
}
}
}
return true

View File

@ -66,8 +66,11 @@ interface MessagingService {
*/
fun createMessage(topic: String, data: ByteArray): Message
/** Returns an address that refers to this node */
/** Returns an address that refers to this node. */
val myAddress: SingleMessageRecipient
/** Allows you to look up services and nodes that are available on the network. */
val networkMap: NetworkMap
}
/**

View File

@ -0,0 +1,27 @@
/*
* 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 core.Party
/** Info about a network node that has is operated by some sort of verified identity. */
data class LegallyIdentifiableNode(val address: SingleMessageRecipient, val identity: Party)
/**
* A NetworkMap allows you to look up various types of services provided by nodes on the network, and find node
* addresses given legal identities (NB: not all nodes may have legal identities).
*
* A real implementation would probably do RPCs to a lookup service which might in turn be backed by a ZooKeeper
* cluster or equivalent.
*
* For now, this class is truly minimal.
*/
interface NetworkMap {
val timestampingNodes: List<LegallyIdentifiableNode>
}

View File

@ -8,8 +8,14 @@
package core.messaging
import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.FiberExecutorScheduler
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.SettableFuture
import core.SecureHash
import core.ServiceHub
@ -17,15 +23,13 @@ import core.serialization.THREAD_LOCAL_KRYO
import core.serialization.createKryo
import core.serialization.deserialize
import core.serialization.serialize
import core.sha256
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.io.ByteArrayOutputStream
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.Executor
import javax.annotation.concurrent.ThreadSafe
@ -41,9 +45,10 @@ import javax.annotation.concurrent.ThreadSafe
* 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: Session IDs should be set up and propagated automatically, on demand.
* TODO: Consider the issue of continuation identity more deeply: is it a safe assumption that a serialised
* continuation is always unique?
* TODO: Think about how to bring the system to a clean stop so it can be upgraded without any serialised stacks on disk
*/
@ThreadSafe
class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) {
@ -52,22 +57,28 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
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<*,*>>())
private val _stateMachines = Collections.synchronizedList(ArrayList<ProtocolStateMachine<*>>())
/** Returns a snapshot of the currently registered state machines. */
val stateMachines: List<ProtocolStateMachine<*,*>> get() {
val stateMachines: List<ProtocolStateMachine<*>> get() {
synchronized(_stateMachines) {
return ArrayList(_stateMachines)
}
}
// Used to work around a small limitation in Quasar.
private val QUASAR_UNBLOCKER = run {
val field = Fiber::class.java.getDeclaredField("SERIALIZER_BLOCKER")
field.isAccessible = true
field.get(null)
}
// 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
val serialisedFiber: ByteArray,
val loggerName: String,
val awaitingTopic: String,
val awaitingObjectOfType: String // java class name
)
init {
@ -77,155 +88,125 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
/** 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 checkpoint = bytes.deserialize<Checkpoint>()
val checkpointKey = SecureHash.sha256(bytes)
// Grab the Kryo engine configured by Quasar for its own stuff, and then do our own configuration on top
// so we can deserialised the nested stream that holds the fiber.
val psm = deserializeFiber(checkpoint.serialisedFiber)
_stateMachines.add(psm)
val logger = LoggerFactory.getLogger(checkpoint.loggerName)
val awaitingObjectOfType = Class.forName(checkpoint.awaitingObjectOfType)
val topic = checkpoint.awaitingTopic
// And now re-wire the deserialised continuation back up to the network service.
setupNextMessageHandler(logger, serviceHub.networkService, continuation, checkpoint.otherSide,
awaitingObjectOfType, checkpoint.awaitingTopic, bytes)
serviceHub.networkService.runOnNextMessage(topic, runInThread) { netMsg ->
val obj: Any = THREAD_LOCAL_KRYO.get().readObject(Input(netMsg.data), awaitingObjectOfType)
logger.trace { "<- $topic : message of type ${obj.javaClass.name}" }
iterateStateMachine(psm, serviceHub.networkService, logger, obj, checkpointKey) {
Fiber.unparkDeserialized(it, SameThreadFiberScheduler)
}
}
}
}
private fun deserializeFiber(bits: ByteArray): ProtocolStateMachine<*> {
val deserializer = Fiber.getFiberSerializer() as KryoSerializer
val kryo = createKryo(deserializer.kryo)
val psm = kryo.readClassAndObject(Input(bits)) as ProtocolStateMachine<*>
return psm
}
/**
* 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.
* Kicks off a brand new state machine of the given class. It will log with the named logger, and the
* [initialArgs] object will be passed to the call method of the [ProtocolStateMachine] object.
* The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is
* restarted with checkpointed state machines in the storage service.
*/
fun <T : ProtocolStateMachine<I, *>, I> add(otherSide: MessageRecipients, initialArgs: I, loggerName: String,
continuationClass: Class<out T>): T {
fun <T : ProtocolStateMachine<*>> add(loggerName: String, fiber: 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)
iterateStateMachine(fiber, serviceHub.networkService, logger, null, null) {
it.start()
}
@Suppress("UNCHECKED_CAST")
return sm as T
return fiber
}
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) {
private fun persistCheckpoint(prevCheckpointKey: SecureHash?, new: ByteArray): SecureHash {
// 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
if (prevCheckpointKey != null)
checkpointsMap.remove(prevCheckpointKey)
val key = SecureHash.sha256(new)
checkpointsMap[key] = new
return key
}
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)
private fun iterateStateMachine(psm: ProtocolStateMachine<*>, net: MessagingService, logger: Logger,
obj: Any?, prevCheckpointKey: SecureHash?, resumeFunc: (ProtocolStateMachine<*>) -> Unit) {
val onSuspend = fun(request: FiberRequest, serFiber: ByteArray) {
// We have a request to do something: send, receive, or send-and-receive.
if (request is FiberRequest.ExpectingResponse<*>) {
// Prepare a listener on the network that runs in the background thread when we received a message.
checkpointAndSetupMessageHandler(logger, net, psm, request.responseType,
"${request.topic}.${request.sessionIDForReceive}", prevCheckpointKey, serFiber)
}
// If an object to send was provided (not null), send it now.
request.obj?.let {
val topic = "${request.topic}.${request.sessionIDForSend}"
logger.trace { "-> ${request.destination}/$topic : message of type ${it.javaClass.name}" }
net.send(net.createMessage(topic, it.serialize().bits), request.destination!!)
}
if (request is FiberRequest.NotExpectingResponse) {
// We sent a message, but don't expect a response, so re-enter the continuation to let it keep going.
iterateStateMachine(psm, net, logger, null, prevCheckpointKey) {
Fiber.unpark(it, QUASAR_UNBLOCKER)
}
}
}
psm.prepareForResumeWith(serviceHub, obj, logger, onSuspend)
try {
// Now either start or carry on with the protocol from where it left off (or at the start).
resumeFunc(psm)
// We're back! Check if the fiber is finished and if so, clean up.
if (psm.isTerminated) {
_stateMachines.remove(psm)
checkpointsMap.remove(prevCheckpointKey)
}
} 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().bits), 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)
private fun checkpointAndSetupMessageHandler(logger: Logger, net: MessagingService, psm: ProtocolStateMachine<*>,
responseType: Class<*>, topic: String, prevCheckpointKey: SecureHash?,
serialisedFiber: ByteArray) {
val checkpoint = Checkpoint(serialisedFiber, logger.name, topic, responseType.name)
val curPersistedBytes = checkpoint.serialize().bits
persistCheckpoint(prevPersistedBytes, curPersistedBytes)
persistCheckpoint(prevCheckpointKey, curPersistedBytes)
val newCheckpointKey = curPersistedBytes.sha256()
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)
iterateStateMachine(psm, net, logger, obj, newCheckpointKey) {
Fiber.unpark(it, QUASAR_UNBLOCKER)
}
}
}
}
val CONTINUATION_LOGGER = ThreadLocal<Logger>()
object SameThreadFiberScheduler : FiberExecutorScheduler("Same thread scheduler", MoreExecutors.directExecutor())
/**
* 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:
* The base class that should be used by any object that wishes to act as a protocol state machine. A PSM is
* a kind of "fiber", and a fiber in turn is a bit like a thread, but a thread that can be suspended to the heap,
* serialised to disk, and resumed on demand.
*
* 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
@ -236,58 +217,99 @@ val CONTINUATION_LOGGER = ThreadLocal<Logger>()
* 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].
*
* Note that the result of the [call] method can be obtained in a couple of different ways. One is to call the get
* method, as the PSM is a [Future]. But that will block the calling thread until the result is ready, which may not
* be what you want (unless you know it's finished already). So you can also use the [resultFuture] property, which is
* a [ListenableFuture] and will let you register a callback.
*
* Once created, a PSM should be passed to a [StateMachineManager] which will start it and manage its execution.
*/
@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. */
abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberScheduler), Callable<R> {
// These fields shouldn't be serialised, so they are marked @Transient.
@Transient private var suspendFunc: ((result: FiberRequest, serFiber: ByteArray) -> Unit)? = null
@Transient private var resumeWithObject: Any? = null
@Transient lateinit var serviceHub: ServiceHub
@Transient protected lateinit var logger: Logger
@Transient private var _resultFuture: SettableFuture<R>? = SettableFuture.create<R>()
abstract fun call(args: T): R
/** This future will complete when the call method returns. */
val resultFuture: ListenableFuture<R> get() {
return _resultFuture ?: run {
val f = SettableFuture.create<R>()
_resultFuture = f
return f
}
}
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)
fun prepareForResumeWith(serviceHub: ServiceHub, withObject: Any?, logger: Logger,
suspendFunc: (FiberRequest, ByteArray) -> Unit) {
this.suspendFunc = suspendFunc
this.logger = logger
this.resumeWithObject = withObject
this.serviceHub = serviceHub
}
// This line may look useless, but it's needed to convince the Quasar bytecode rewriter to do the right thing.
@Suspendable override abstract fun call(): R
@Suspendable @Suppress("UNCHECKED_CAST")
override fun run(): R {
val result = call()
if (result != null)
(resultFuture as SettableFuture<R>).set(result)
return result
}
@Suspendable @Suppress("UNCHECKED_CAST")
private fun <T : Any> suspendAndExpectReceive(with: FiberRequest): T {
Fiber.parkAndSerialize { fiber, serializer ->
// We don't use the passed-in serializer here, because we need to use our own augmented Kryo.
val deserializer = Fiber.getFiberSerializer() as KryoSerializer
val kryo = createKryo(deserializer.kryo)
val stream = ByteArrayOutputStream()
Output(stream).use {
kryo.writeClassAndObject(it, this)
}
suspendFunc!!(with, stream.toByteArray())
}
val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something")
resumeWithObject = null
return tmp as T
}
@Suspendable @Suppress("UNCHECKED_CAST")
fun <T : Any> sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long,
obj: Any, recvType: Class<T>): T {
val result = FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType)
return suspendAndExpectReceive(result)
}
@Suspendable
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): T {
val result = FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType)
return suspendAndExpectReceive(result)
}
@Suspendable
fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) {
val result = FiberRequest.NotExpectingResponse(topic, destination, sessionID, obj)
Fiber.parkAndSerialize { fiber, writer -> suspendFunc!!(result, writer.write(fiber)) }
}
}
@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?) {
// TODO: Clean this up
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {
class ExpectingResponse<R : Any>(
topic: String,
destination: MessageRecipients?,
sessionIDForSend: Long,
sessionIDForReceive: Long,
obj: Any?,
val responseType: Class<R>
) : ContinuationResult(topic, sessionIDForSend, sessionIDForReceive, obj)
) : FiberRequest(topic, destination, sessionIDForSend, sessionIDForReceive, obj)
class NotExpectingResponse(topic: String, sessionIDForSend: Long, obj: Any?) : ContinuationResult(topic, sessionIDForSend, -1, obj)
class NotExpectingResponse(topic: String, destination: MessageRecipients, sessionIDForSend: Long, obj: Any?)
: FiberRequest(topic, destination, sessionIDForSend, -1, obj)
}

View File

@ -0,0 +1,125 @@
/*
* 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.node
import co.paralleluniverse.common.util.VisibleForTesting
import co.paralleluniverse.fibers.Suspendable
import core.*
import core.messaging.LegallyIdentifiableNode
import core.messaging.MessageRecipients
import core.messaging.MessagingService
import core.messaging.ProtocolStateMachine
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.serialization.serialize
import org.slf4j.LoggerFactory
import java.security.KeyPair
import java.time.Clock
import java.time.Duration
import javax.annotation.concurrent.ThreadSafe
class TimestampingMessages {
// TODO: Improve the messaging api to have a notion of sender+replyTo topic (optional?)
data class Request(val tx: SerializedBytes<WireTransaction>, val replyTo: MessageRecipients, val replyToTopic: String)
}
sealed class TimestampingError : Exception() {
class RequiresExactlyOneCommand : TimestampingError()
/**
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the
* transaction is too far in the past or future relative to the local clock and thus the timestamper would reject
* it.
*/
class NotOnTimeException : TimestampingError()
/** Thrown if the command in the transaction doesn't list this timestamping authorities public key as a signer */
class NotForMe : TimestampingError()
}
/**
* This class implements the server side of the timestamping protocol, using the local clock. A future version might
* add features like checking against other NTP servers to make sure the clock hasn't drifted by too much.
*
* See the doc site to learn more about timestamping authorities (nodes) and the role they play in the data model.
*/
@ThreadSafe
class TimestamperNodeService(private val net: MessagingService,
private val identity: Party,
private val signingKey: KeyPair,
private val clock: Clock = Clock.systemDefaultZone(),
val tolerance: Duration = 30.seconds) {
companion object {
val TIMESTAMPING_PROTOCOL_TOPIC = "platform.timestamping.request"
private val logger = LoggerFactory.getLogger(TimestamperNodeService::class.java)
}
init {
require(identity.owningKey == signingKey.public)
net.addMessageHandler(TIMESTAMPING_PROTOCOL_TOPIC + ".0", null) { message, r ->
try {
val req = message.data.deserialize<TimestampingMessages.Request>()
val signature = processRequest(req)
val msg = net.createMessage(req.replyToTopic, signature.serialize().bits)
net.send(msg, req.replyTo)
} catch(e: TimestampingError) {
logger.warn("Failure during timestamping request due to bad request: ${e.javaClass.name}")
} catch(e: Exception) {
logger.error("Exception during timestamping", e)
}
}
}
@VisibleForTesting
fun processRequest(req: TimestampingMessages.Request): DigitalSignature.LegallyIdentifiable {
// We don't bother verifying signatures anything about the transaction here: we simply don't need to see anything
// except the relevant command, and a future privacy upgrade should ensure we only get a torn-off command
// rather than the full transaction.
val tx = req.tx.deserialize()
val cmd = tx.commands.filter { it.data is TimestampCommand }.singleOrNull()
if (cmd == null)
throw TimestampingError.RequiresExactlyOneCommand()
if (!cmd.pubkeys.contains(identity.owningKey))
throw TimestampingError.NotForMe()
val tsCommand = cmd.data as TimestampCommand
val before = tsCommand.before
val after = tsCommand.after
val now = clock.instant()
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
// constructor already checks that.
if (before != null && before until now > tolerance)
throw TimestampingError.NotOnTimeException()
if (after != null && now until after > tolerance)
throw TimestampingError.NotOnTimeException()
return signingKey.signWithECDSA(req.tx.bits, identity)
}
}
@ThreadSafe
class TimestamperClient(private val psm: ProtocolStateMachine<*>, private val node: LegallyIdentifiableNode) : TimestamperService {
override val identity: Party = node.identity
@Suspendable
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
val sessionID = random63BitValue()
val replyTopic = "${TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC}.$sessionID"
val req = TimestampingMessages.Request(wtxBytes, psm.serviceHub.networkService.myAddress, replyTopic)
val signature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0,
sessionID, req, DigitalSignature.LegallyIdentifiable::class.java)
// Check that the timestamping authority gave us back a valid signature and didn't break somehow
signature.verifyWithECDSA(wtxBytes)
return signature
}
}

View File

@ -8,6 +8,8 @@
package core.serialization
import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Serializer
@ -16,13 +18,13 @@ import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.JavaSerializer
import core.SecureHash
import core.SignedWireTransaction
import core.TimestampCommand
import core.sha256
import de.javakaffee.kryoserializers.ArraysAsListSerializer
import org.objenesis.strategy.StdInstantiatorStrategy
import java.io.ByteArrayOutputStream
import java.lang.reflect.InvocationTargetException
import java.security.KeyPairGenerator
import java.time.Instant
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty
@ -153,8 +155,8 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
}
}
fun createKryo(): Kryo {
return Kryo().apply {
fun createKryo(k: Kryo = Kryo()): Kryo {
return k.apply {
// Allow any class to be deserialized (this is insecure but for prototyping we don't care)
isRegistrationRequired = false
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
@ -163,17 +165,30 @@ fun createKryo(): Kryo {
register(Arrays.asList( "" ).javaClass, ArraysAsListSerializer());
val keyPair = KeyPairGenerator.getInstance("EC").genKeyPair()
// Because we like to stick a Kryo object in a ThreadLocal to speed things up a bit, we can end up trying to
// serialise the Kryo object itself when suspending a fiber. That's dumb, useless AND can cause crashes, so
// we avoid it here.
register(Kryo::class.java, object : Serializer<Kryo>() {
override fun write(kryo: Kryo, output: Output, obj: Kryo) {
}
override fun read(kryo: Kryo, input: Input, type: Class<Kryo>): Kryo {
return createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo)
}
})
// Some things where the JRE provides an efficient custom serialisation.
val ser = JavaSerializer()
val keyPair = KeyPairGenerator.getInstance("EC").genKeyPair()
register(keyPair.public.javaClass, ser)
register(keyPair.private.javaClass, ser)
register(Instant::class.java, ser)
// Some classes have to be handled with the ImmutableClassSerializer because they need to have their
// constructors be invoked (typically for lazy members).
val immutables = listOf(
SignedWireTransaction::class,
SerializedBytes::class,
TimestampCommand::class
SerializedBytes::class
)
immutables.forEach {

View File

@ -39,7 +39,7 @@ class BriefLogFormatter : Formatter() {
arguments[1] = className
arguments[2] = logRecord.sourceMethodName
arguments[3] = Date(logRecord.millis)
arguments[4] = logRecord.message
arguments[4] = MessageFormat.format(logRecord.message, *logRecord.parameters)
if (logRecord.thrown != null) {
val result = StringWriter()
logRecord.thrown.printStackTrace(PrintWriter(result))
@ -56,19 +56,22 @@ class BriefLogFormatter : Formatter() {
// OpenJDK made a questionable, backwards incompatible change to the Logger implementation. It internally uses
// weak references now which means simply fetching the logger and changing its configuration won't work. We must
// keep a reference to our custom logger around.
private lateinit var loggerRef: Logger
private val loggerRefs = ArrayList<Logger>()
/** Configures JDK logging to use this class for everything. */
fun init() {
loggerRef = Logger.getLogger("")
val handlers = loggerRef.handlers
val logger = Logger.getLogger("")
val handlers = logger.handlers
handlers[0].formatter = BriefLogFormatter()
loggerRefs.add(logger)
}
fun initVerbose(packageSpec: String = "") {
init()
loggerRef.handlers[0].level = Level.ALL
Logger.getLogger(packageSpec).level = Level.ALL
loggerRefs[0].handlers[0].level = Level.ALL
val logger = Logger.getLogger(packageSpec)
logger.level = Level.ALL
loggerRefs.add(logger)
}
}
}

View File

@ -1,31 +0,0 @@
/*
* 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)
}

View File

@ -9,6 +9,7 @@
package contracts
import core.*
import core.node.TimestampingError
import core.testutils.*
import org.junit.Test
import java.time.Clock
@ -81,7 +82,7 @@ class CommercialPaperTests {
CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
assertFailsWith(NotOnTimeException::class) {
assertFailsWith(TimestampingError.NotOnTimeException::class) {
timestamp(DummyTimestamper(Clock.fixed(TEST_TX_TIME + 5.hours, ZoneOffset.UTC)))
}
}
@ -89,7 +90,7 @@ class CommercialPaperTests {
CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
assertFailsWith(NotOnTimeException::class) {
assertFailsWith(TimestampingError.NotOnTimeException::class) {
val tsaClock = Clock.fixed(TEST_TX_TIME - 5.hours, ZoneOffset.UTC)
timestamp(DummyTimestamper(tsaClock), Clock.fixed(TEST_TX_TIME, ZoneOffset.UTC))
}

View File

@ -0,0 +1,91 @@
/*
* 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 core.node.TimestampingError
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.testutils.TEST_KEYS_TO_CORP_MAP
import core.testutils.TEST_TX_TIME
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.time.Clock
import java.time.Duration
import java.time.ZoneId
import java.util.*
import javax.annotation.concurrent.ThreadSafe
/**
* A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
* the provided clock which defaults to [TEST_TX_TIME], an arbitrary point on the timeline.
*/
class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
val tolerance: Duration = 30.seconds) : TimestamperService {
override val identity = DummyTimestampingAuthority.identity
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
val wtx = wtxBytes.deserialize()
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
if (timestamp.before!! until clock.instant() > tolerance)
throw TimestampingError.NotOnTimeException()
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
}
}
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 {
override val myLegalIdentityKey: KeyPair = KeyPairGenerator.getInstance("EC").genKeyPair()
override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public)
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? = null,
val keyManagement: KeyManagementService? = null,
val net: MessagingService? = null,
val identity: IdentityService? = MockIdentityService,
val storage: StorageService? = MockStorageService()
) : 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 networkService: MessagingService
get() = net ?: throw UnsupportedOperationException()
override val storageService: StorageService
get() = storage ?: throw UnsupportedOperationException()
}

View File

@ -43,7 +43,7 @@ open class TestWithInMemoryNetwork {
network.stop()
}
fun pumpAll(blocking: Boolean) = nodes.values.map { it.pump(blocking) }
fun pumpAll(blocking: Boolean) = network.nodes.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 {

View File

@ -62,40 +62,38 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
val (alicesAddress, alicesNode) = makeNode(inBackground = true)
val (bobsAddress, bobsNode) = makeNode(inBackground = true)
val timestamper = network.setupTimestampingNode(false).first
val alicesServices = MockServices(wallet = null, keyManagement = null, net = alicesNode)
val alicesServices = MockServices(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(
val aliceResult = TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, backgroundThread),
timestamper,
bobsAddress,
TwoPartyTradeProtocol.SellerInitialArgs(
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
val bobResult = tpBuyer.runBuyer(
val bobResult = TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, backgroundThread),
timestamper,
alicesAddress,
TwoPartyTradeProtocol.BuyerInitialArgs(
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
assertEquals(aliceResult.resultFuture.get(), bobResult.resultFuture.get())
assertEquals(aliceResult.get(), bobResult.get())
txns.add(aliceResult.resultFuture.get().second)
txns.add(aliceResult.get().second)
verify()
}
backgroundThread.shutdown()
@ -115,6 +113,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
val (alicesAddress, alicesNode) = makeNode(inBackground = false)
var (bobsAddress, bobsNode) = makeNode(inBackground = false)
val timestamper = network.setupTimestampingNode(true)
val bobsStorage = MockStorageService()
@ -126,28 +125,26 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
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(
TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, MoreExecutors.directExecutor()),
timestamper.first,
bobsAddress,
TwoPartyTradeProtocol.SellerInitialArgs(
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
tpBuyer.runBuyer(
TwoPartyTradeProtocol.runBuyer(
smmBuyer,
timestamper.first,
alicesAddress,
TwoPartyTradeProtocol.BuyerInitialArgs(
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
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.
@ -161,9 +158,11 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// .. 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)
// Alice doesn't know that and carries on: first timestamping and then sending Bob the now finalised
// transaction. Alice sends a message to a node that has gone offline.
assertTrue(alicesNode.pump(false))
assertTrue(timestamper.second.pump(false))
assertTrue(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.

View File

@ -0,0 +1,131 @@
/*
* 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.node
import co.paralleluniverse.fibers.Suspendable
import core.*
import core.messaging.*
import core.serialization.serialize
import core.testutils.ALICE
import core.testutils.ALICE_KEY
import core.testutils.CASH
import core.utilities.BriefLogFormatter
import org.junit.Before
import org.junit.Test
import java.security.PublicKey
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class TimestamperNodeServiceTest : TestWithInMemoryNetwork() {
lateinit var myNode: Pair<InMemoryNetwork.Handle, InMemoryNetwork.Node>
lateinit var serviceNode: Pair<InMemoryNetwork.Handle, InMemoryNetwork.Node>
lateinit var service: TimestamperNodeService
val ptx = TransactionBuilder().apply {
addInputState(ContractStateRef(SecureHash.randomSHA256(), 0))
addOutputState(100.DOLLARS.CASH)
}
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
lateinit var mockServices: ServiceHub
lateinit var serverKey: PublicKey
init {
BriefLogFormatter.initVerbose("dlg.timestamping.request")
}
@Before
fun setup() {
myNode = makeNode()
serviceNode = makeNode()
mockServices = MockServices(net = serviceNode.second, storage = MockStorageService())
serverKey = network.setupTimestampingNode(true).first.identity.owningKey
// And a separate one to be tested directly, to make the unit tests a bit faster.
service = TimestamperNodeService(serviceNode.second, Party("Unit test suite", ALICE), ALICE_KEY)
}
class TestPSM(val server: LegallyIdentifiableNode, val now: Instant) : ProtocolStateMachine<Boolean>() {
@Suspendable
override fun call(): Boolean {
val client = TimestamperClient(this, server)
val ptx = TransactionBuilder().apply {
addInputState(ContractStateRef(SecureHash.randomSHA256(), 0))
addOutputState(100.DOLLARS.CASH)
}
ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), server.identity.owningKey)
val wtx = ptx.toWireTransaction()
// This line will invoke sendAndReceive to interact with the network.
val sig = client.timestamp(wtx.serialize())
ptx.checkAndAddSignature(sig)
return true
}
}
@Test
fun successWithNetwork() {
val psm = runNetwork {
val smm = StateMachineManager(MockServices(net = myNode.second), RunOnCallerThread)
val logName = TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC
val psm = TestPSM(myNode.second.networkMap.timestampingNodes[0], clock.instant())
smm.add(logName, psm)
psm
}
assertTrue(psm.isDone)
}
@Test
fun wrongCommands() {
// Zero commands is not OK.
assertFailsWith(TimestampingError.RequiresExactlyOneCommand::class) {
val wtx = ptx.toWireTransaction()
service.processRequest(TimestampingMessages.Request(wtx.serialize(), myNode.first, "ignored"))
}
// More than one command is not OK.
assertFailsWith(TimestampingError.RequiresExactlyOneCommand::class) {
ptx.addCommand(TimestampCommand(clock.instant(), 30.seconds), ALICE)
ptx.addCommand(TimestampCommand(clock.instant(), 40.seconds), ALICE)
val wtx = ptx.toWireTransaction()
service.processRequest(TimestampingMessages.Request(wtx.serialize(), myNode.first, "ignored"))
}
}
@Test
fun tooEarly() {
assertFailsWith(TimestampingError.NotOnTimeException::class) {
val now = clock.instant()
ptx.addCommand(TimestampCommand(now - 60.seconds, now - 40.seconds), ALICE)
val wtx = ptx.toWireTransaction()
service.processRequest(TimestampingMessages.Request(wtx.serialize(), myNode.first, "ignored"))
}
}
@Test
fun tooLate() {
assertFailsWith(TimestampingError.NotOnTimeException::class) {
val now = clock.instant()
ptx.addCommand(TimestampCommand(now - 60.seconds, now - 40.seconds), ALICE)
val wtx = ptx.toWireTransaction()
service.processRequest(TimestampingMessages.Request(wtx.serialize(), myNode.first, "ignored"))
}
}
@Test
fun success() {
val now = clock.instant()
ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), ALICE)
val wtx = ptx.toWireTransaction()
val sig = service.processRequest(TimestampingMessages.Request(wtx.serialize(), myNode.first, "ignored"))
ptx.checkAndAddSignature(sig)
ptx.toSignedTransaction(false).verifySignatures()
}
}

View File

@ -12,20 +12,11 @@ package core.testutils
import contracts.*
import core.*
import core.messaging.MessagingService
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.visualiser.GraphVisualiser
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.util.*
import javax.annotation.concurrent.ThreadSafe
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.fail
@ -66,72 +57,6 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
DUMMY_PROGRAM_ID to DummyContract
)
/**
* A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
* the provided clock which defaults to [TEST_TX_TIME], an arbitrary point on the timeline.
*/
class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
val tolerance: Duration = 30.seconds) : TimestamperService {
override val identity = DummyTimestampingAuthority.identity
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
val wtx = wtxBytes.deserialize()
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
throw NotOnTimeException()
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
}
}
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.