mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
commit
b3bfe9d532
9
.gitignore
vendored
9
.gitignore
vendored
@ -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
23
.idea/runConfigurations/All_tests.xml
generated
Normal 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>
|
70
build.gradle
70
build.gradle
@ -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"
|
||||
}
|
||||
|
350
docs/build/html/_sources/protocol-state-machines.txt
vendored
350
docs/build/html/_sources/protocol-state-machines.txt
vendored
@ -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
|
||||
|
1
docs/build/html/index.html
vendored
1
docs/build/html/index.html
vendored
@ -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>
|
||||
|
353
docs/build/html/protocol-state-machines.html
vendored
353
docs/build/html/protocol-state-machines.html
vendored
@ -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’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’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.</p>
|
||||
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.</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’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<OwnableState>,
|
||||
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">"platform.trade"</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"><</span><span class="n">OwnableState</span><span class="p">>,</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"><</span><span class="n">Pair</span><span class="p"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">>></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">"$TRADE_TOPIC.seller"</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<out OwnableState>,
|
||||
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"><</span><span class="k">out</span> <span class="n">OwnableState</span><span class="p">>,</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"><</span><span class="n">Pair</span><span class="p"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">>></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">"$TRADE_TOPIC.buyer"</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"><</span><span class="n">OwnableState</span><span class="p">>,</span>
|
||||
<span class="k">val</span> <span class="py">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">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"><</span><span class="n">Pair</span><span class="p"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">>>()</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"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">></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<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
<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"><</span><span class="n">OwnableState</span><span class="p">>,</span>
|
||||
<span class="k">val</span> <span class="py">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">sellerOwnerKey</span><span class="p">:</span> <span class="n">PublicKey</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">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">"The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="c1">// The buyer'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"><</span><span class="k">out</span> <span class="n">OwnableState</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> <span class="p">:</span> <span class="n">ProtocolStateMachine</span><span class="p"><</span><span class="n">Pair</span><span class="p"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">>>()</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"><</span><span class="n">WireTransaction</span><span class="p">,</span> <span class="n">LedgerTransaction</span><span class="p">></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’s unpack what this code does:</p>
|
||||
<ul class="simple">
|
||||
<li>It defines a several classes nested inside the main <code class="docutils literal"><span class="pre">TwoPartyTradeProtocol</span></code> class, and a couple of methods, one to
|
||||
run the buyer side of the protocol and one to run the seller side.</li>
|
||||
<li>Two of the classes are simply wrappers for parameters to the trade; things like what is being sold, what the price
|
||||
of the asset is, how much the buyer is willing to pay and so on. The <code class="docutils literal"><span class="pre">myKeyPair</span></code> field is simply the public key
|
||||
that the seller wishes the buyer to send the cash to. The session ID field is sent from buyer to seller when the
|
||||
trade is being set up and is used to keep messages separated on the network, and stop malicious entities trying to
|
||||
interfere with the message stream.</li>
|
||||
<li>The other two classes define empty abstract classes called <code class="docutils literal"><span class="pre">Buyer</span></code> and <code class="docutils literal"><span class="pre">Seller</span></code>. These inherit from a class
|
||||
called <code class="docutils literal"><span class="pre">ProtocolStateMachine</span></code> and provide two type parameters: the arguments class we just defined for each side
|
||||
and the type of the object that the protocol finally produces (this doesn’t have to be identical for each side, even
|
||||
though in this case it is).</li>
|
||||
<li>Finally it simply defines a static method that creates an instance of an object that inherits from this base class
|
||||
and returns it, with a <code class="docutils literal"><span class="pre">StateMachineManager</span></code> as an instance. The Impl class will be defined below.</li>
|
||||
<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 “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.</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<OwnableState></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<out</span> <span class="pre">OwnableState></span></code> - 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.</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’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’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">@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">@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’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’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’s done.</p>
|
||||
</div>
|
||||
<div class="section" id="implementing-the-seller">
|
||||
<h2>Implementing the seller<a class="headerlink" href="#implementing-the-seller" title="Permalink to this headline">¶</a></h2>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : TwoPartyTradeProtocol() {
|
||||
companion object {
|
||||
val TRADE_TOPIC = "com.r3cev.protocols.trade"
|
||||
}
|
||||
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun runSeller(otherSide: SingleMessageRecipient, args: SellerInitialArgs): Seller {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.seller", SellerImpl::class.java)
|
||||
}
|
||||
|
||||
override fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer {
|
||||
return smm.add(otherSide, args, "$TRADE_TOPIC.buyer", BuyerImpl::class.java)
|
||||
}
|
||||
}
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>We start with a skeleton on which we will build the protocol. Putting things in a <em>companion object</em> in Kotlin is like
|
||||
declaring them as static members in Java. Here, we define a “topic” that will identify trade related messages that
|
||||
arrive at a node (see <a class="reference internal" href="messaging.html"><em>Networking and messaging</em></a> for details).</p>
|
||||
<p>The runSeller and runBuyer methods simply start the state machines, passing in a reference to the classes and the topics
|
||||
each side will use.</p>
|
||||
<p>Now let’s try implementing the seller side. Firstly, we’re going to need a message to send to the buyer describing what
|
||||
we want to trade. Remember: this data comes from whatever system was used to find the trading partner to begin with.
|
||||
It could be as simple as a chat room or as complex as a 24/7 exchange.</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="c1">// This object is serialised to the network and is the first protocol message</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"><</span><span class="n">OwnableState</span><span class="p">>,</span>
|
||||
<span class="k">val</span> <span class="py">price</span><span class="p">:</span> <span class="n">Amount</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">sellerOwnerKey</span><span class="p">:</span> <span class="n">PublicKey</span><span class="p">,</span>
|
||||
<span class="k">val</span> <span class="py">buyerSessionID</span><span class="p">:</span> <span class="n">Long</span>
|
||||
<span class="p">)</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>That’s simple enough: our opening protocol message will be serialised before being sent over the wire, and it contains
|
||||
the details that were agreed so we can double check them. It also contains a session ID so we can identify this
|
||||
trade’s messages, and a pointer to where the asset that is being sold can be found on the ledger.</p>
|
||||
<p>Next we add some code to the <code class="docutils literal"><span class="pre">SellerImpl.call</span></code> method:</p>
|
||||
<p>Let’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'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"><</span><span class="n">SignedWireTransaction</span><span class="p">>(</span><span class="n">TRADE_TOPIC</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">buyerSessionID</span><span class="p">,</span>
|
||||
<span class="n">sessionID</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
|
||||
<span class="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">"Received partially signed transaction"</span> <span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
@ -372,27 +381,23 @@ trade’s messages, and a pointer to where the asset that is being sold can
|
||||
<p>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 <code class="docutils literal"><span class="pre">sendAndReceive</span></code>. This function takes a few arguments:</p>
|
||||
<ul class="simple">
|
||||
<li>A type argument, which is the object we’re expecting to receive from the other side.</li>
|
||||
<li>The topic string that ensures the message is routed to the right bit of code in the other side’s node.</li>
|
||||
<li>The session IDs that ensure the messages don’t get mixed up with other simultaneous trades.</li>
|
||||
<li>And finally, the thing to send. It’ll be serialised and sent automatically.</li>
|
||||
<li>The thing to send. It’ll be serialised and sent automatically.</li>
|
||||
<li>Finally a type argument, which is the kind of object we’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’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’t require objects be marked as serialisable, but even so,
|
||||
doing things like creating threads from inside these calls would be a bad idea. They should only contain business
|
||||
logic.</p>
|
||||
<p class="last">The third rule to bear in mind is that you can’t declare variables or methods in these classes and access
|
||||
them from outside of the class, due to the bytecode rewriting and classloader tricks that are used to make this all
|
||||
work. If you want access to something inside the BuyerImpl or SellerImpl classes, you must define a super-interface
|
||||
or super-class (like <code class="docutils literal"><span class="pre">Buyer</span></code>/<code class="docutils literal"><span class="pre">Seller</span></code>) and put what you want to access there.</p>
|
||||
</div>
|
||||
<p>OK, let’s keep going:</p>
|
||||
<div class="codeset container">
|
||||
@ -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">"Built finished transaction, sending back to secondary!"</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’re not about to sign something
|
||||
incorrect. Once we’re happy, we calculate a signature over the transaction to authorise the movement of the asset
|
||||
we are selling, and then we verify things to make sure it’s all OK. Finally, we request timestamping of the
|
||||
transaction, and send the now finalised and validated transaction back to the buyer.</p>
|
||||
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.</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’s do the same for the buyer side:</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre>@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)
|
||||
}
|
||||
</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’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’t
|
||||
|
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
File diff suppressed because one or more lines are too long
@ -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
BIN
lib/quasar.jar
Normal file
Binary file not shown.
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
27
src/main/kotlin/core/messaging/NetworkMap.kt
Normal file
27
src/main/kotlin/core/messaging/NetworkMap.kt
Normal 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>
|
||||
}
|
@ -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)
|
||||
}
|
125
src/main/kotlin/core/node/TimestamperNodeService.kt
Normal file
125
src/main/kotlin/core/node/TimestamperNodeService.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
91
src/test/kotlin/core/MockServices.kt
Normal file
91
src/test/kotlin/core/MockServices.kt
Normal 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()
|
||||
}
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
131
src/test/kotlin/core/node/TimestamperNodeServiceTest.kt
Normal file
131
src/test/kotlin/core/node/TimestamperNodeServiceTest.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user