Major: Separate out the dep resolution protocol into a couple of sub protocols and use on both sides of the trade.

* Dependency resolution/checking is now working on both sides of the two party trading protocol.
* The commercial paper contract was changed to check timestamping authority identities by name instead of key.
* The trader demo has been rewritten to use the protocol framework, which simplifies the code.
This commit is contained in:
Mike Hearn 2016-02-17 15:40:13 +01:00
parent eb47d8af4d
commit cd28733360
12 changed files with 602 additions and 352 deletions

View File

@ -79,7 +79,11 @@ class CommercialPaper : Contract {
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be
// hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about
// who or what is a trusted authority.
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("The dummy timestamper", "Bank of Zurich")
for (group in groups) {
when (command.value) {

View File

@ -130,3 +130,19 @@ fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority:
return timestampCmds.singleOrNull()?.value as? TimestampCommand
}
/**
* Returns a timestamp that was signed by any of the the named authorities, or returns null if missing.
* Note that matching here is done by (verified, legal) name, not by public key. Any signature by any
* party with a name that matches (case insensitively) any of the given names will yield a match.
*/
fun List<AuthenticatedObject<CommandData>>.getTimestampByName(vararg names: String): TimestampCommand? {
val timestampCmd = filter { it.value is TimestampCommand }.singleOrNull() ?: return null
val tsaNames = timestampCmd.signingParties.map { it.name.toLowerCase() }
val acceptableNames = names.map { it.toLowerCase() }
val acceptableNameFound = tsaNames.intersect(acceptableNames).isNotEmpty()
if (acceptableNameFound)
return timestampCmd.value as TimestampCommand
else
return null
}

View File

@ -161,7 +161,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
/**
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
* To get the right signature from the timestamping service, use the [timestamp] method after building is
* finished.
* finished, or run use the [TimestampingProtocol] yourself.
*
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
@ -208,8 +208,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
*/
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)
sig.verifyWithECDSA(toWireTransaction().serialize())
currentSigs.add(sig)
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package contracts.protocols
import co.paralleluniverse.fibers.Suspendable
import core.SignedTransaction
import core.crypto.SecureHash
import core.messaging.ProtocolLogic
import core.messaging.SingleMessageRecipient
import core.messaging.UntrustworthyData
import core.node.DataVendingService
import core.random63BitValue
import java.util.*
/**
* Given a set of tx hashes (IDs), either loads them from local disk or asks the remote peer to provide them.
* A malicious response in which the data provided by the remote peer does not hash to the requested hash results in
* [DownloadedVsRequestedDataMismatch] being thrown. If the remote peer doesn't have an entry, it results in a
* HashNotFound. Note that returned transactions are not inserted into the database, because it's up to the caller
* to actually verify the transactions are valid.
*/
class FetchTransactionsProtocol(private val requests: Set<SecureHash>,
private val otherSide: SingleMessageRecipient) : ProtocolLogic<FetchTransactionsProtocol.Result>() {
data class Result(val fromDisk: List<SignedTransaction>, val downloaded: List<SignedTransaction>)
@Suspendable
override fun call(): Result {
val sid = random63BitValue()
// Load the transactions we have from disk and figure out which we're missing.
val (fromDisk, toFetch) = loadWhatWeHave()
return if (toFetch.isEmpty()) {
Result(fromDisk, emptyList())
} else {
logger.trace("Requesting ${toFetch.size} dependency(s) for verification")
val fetchReq = DataVendingService.Request(toFetch, serviceHub.networkService.myAddress, sid)
val maybeTxns = sendAndReceive<ArrayList<SignedTransaction?>>("platform.fetch.tx", otherSide, 0, sid, fetchReq)
// Check for a buggy/malicious peer answering with something that we didn't ask for.
// Note that we strip the UntrustworthyData marker here, but of course the returned transactions may be
// invalid in other ways! Perhaps we should keep it.
Result(fromDisk, validateTXFetchResponse(maybeTxns, toFetch))
}
}
private fun loadWhatWeHave(): Pair<List<SignedTransaction>, List<SecureHash>> {
val fromDisk = ArrayList<SignedTransaction>()
val toFetch = ArrayList<SecureHash>()
for (txid in requests) {
val stx = serviceHub.storageService.validatedTransactions[txid]
if (stx == null)
toFetch += txid
else
fromDisk += stx
}
return Pair(fromDisk, toFetch)
}
private fun validateTXFetchResponse(maybeTxns: UntrustworthyData<ArrayList<SignedTransaction?>>,
requests: List<SecureHash>): List<SignedTransaction> =
maybeTxns.validate { response ->
if (response.size != requests.size)
throw BadAnswer()
for ((index, resp) in response.withIndex()) {
if (resp == null) throw HashNotFound(requests[index])
}
val answers = response.requireNoNulls()
// Check transactions actually hash to what we requested, if this fails the remote node
// is a malicious protocol violator or buggy.
for ((index, stx) in answers.withIndex())
if (stx.id != requests[index])
throw DownloadedVsRequestedDataMismatch(requests[index], stx.id)
answers
}
open class BadAnswer : Exception()
class HashNotFound(val requested: SecureHash) : BadAnswer()
class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : BadAnswer()
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package contracts.protocols
import co.paralleluniverse.fibers.Suspendable
import core.LedgerTransaction
import core.SignedTransaction
import core.TransactionGroup
import core.WireTransaction
import core.crypto.SecureHash
import core.messaging.ProtocolLogic
import core.messaging.SingleMessageRecipient
import java.util.*
/**
* This protocol fetches each transaction identified by the given hashes from either disk or network, along with all
* their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then
* all the transactions have been successfully verified and inserted into the local database.
*
* A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that
* transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted
* into the database as long as a [SignedTransaction] was provided. If only the [WireTransaction] form was provided
* then this isn't enough to put into the local database, so only the dependencies are inserted. This way to use the
* protocol is helpful when resolving and verifying a finished but partially signed transaction.
*/
class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
private val otherSide: SingleMessageRecipient) : ProtocolLogic<Unit>() {
companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
}
// Transactions to verify after the dependencies.
private var stx: SignedTransaction? = null
private var wtx: WireTransaction? = null
constructor(stx: SignedTransaction, otherSide: SingleMessageRecipient) : this(stx.tx, otherSide) {
this.stx = stx
}
constructor(wtx: WireTransaction, otherSide: SingleMessageRecipient) : this(dependencyIDs(wtx), otherSide) {
this.wtx = wtx
}
@Suspendable
override fun call(): Unit {
val toVerify = HashSet<LedgerTransaction>()
val alreadyVerified = HashSet<LedgerTransaction>()
val downloadedSignedTxns = ArrayList<SignedTransaction>()
// This fills out toVerify, alreadyVerified (roots) and downloadedSignedTxns.
fetchDependenciesAndCheckSignatures(txHashes, toVerify, alreadyVerified, downloadedSignedTxns)
if (stx != null) {
// Check the signatures on the stx first.
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService)
} else if (wtx != null) {
wtx!!.toLedgerTransaction(serviceHub.identityService)
}
// Run all the contracts and throw an exception if any of them reject.
TransactionGroup(toVerify, alreadyVerified).verify(serviceHub.storageService.contractPrograms)
// Now write all the transactions we just validated back to the database for next time, including
// signatures so we can serve up these transactions to other peers when we, in turn, send one that
// depends on them onto another peer.
//
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
// dependencies aren't, or an unvalidated and possibly broken tx is there.
serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id })
}
@Suspendable
private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set<SecureHash>,
toVerify: HashSet<LedgerTransaction>,
alreadyVerified: HashSet<LedgerTransaction>,
downloadedSignedTxns: ArrayList<SignedTransaction>) {
// Maintain a work queue of all hashes to load/download, initialised with our starting set.
// Then either fetch them from the database or request them from the other side. Look up the
// signatures against our identity database, filtering the transactions into 'already checked'
// and 'need to check' sets.
//
// TODO: This approach has two problems. Analyze and resolve them:
//
// (1) This protocol leaks private data. If you download a transaction and then do NOT request a
// dependency, it means you already have it, which in turn means you must have been involved with it before
// somehow, either in the tx itself or in any following spend of it. If there were no following spends, then
// your peer knows for sure that you were involved ... this is bad! The only obvious ways to fix this are
// something like onion routing of requests, secure hardware, or both.
//
// (2) If the identity service changes the assumed identity of one of the public keys, it's possible
// that the "tx in db is valid" invariant is violated if one of the contracts checks the identity! Should
// the db contain the identities that were resolved when the transaction was first checked, or should we
// accept this kind of change is possible?
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck)
var limitCounter = 0
while (nextRequests.isNotEmpty()) {
val (fromDisk, downloads) = subProtocol(FetchTransactionsProtocol(nextRequests, otherSide))
nextRequests.clear()
// Resolve any legal identities from known public keys in the signatures.
val downloadedTxns = downloads.map { it.verifyToLedgerTransaction(serviceHub.identityService) }
// Do the same for transactions loaded from disk (i.e. we checked them previously).
val loadedTxns = fromDisk.map { it.verifyToLedgerTransaction(serviceHub.identityService) }
toVerify.addAll(downloadedTxns)
alreadyVerified.addAll(loadedTxns)
downloadedSignedTxns.addAll(downloads)
// And now add all the input states to the work queue for database or remote resolution.
nextRequests.addAll(downloadedTxns.flatMap { it.inputs }.map { it.txhash })
// And loop around ...
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a guess.
limitCounter += nextRequests.size
if (limitCounter > 5000)
throw TwoPartyTradeProtocol.ExcessivelyLargeTransactionGraphException()
}
}
}

View File

@ -14,16 +14,16 @@ import contracts.Cash
import contracts.sumCashBy
import core.*
import core.crypto.DigitalSignature
import core.crypto.SecureHash
import core.crypto.signWithECDSA
import core.messaging.*
import core.node.DataVendingService
import core.messaging.LegallyIdentifiableNode
import core.messaging.ProtocolLogic
import core.messaging.SingleMessageRecipient
import core.messaging.StateMachineManager
import core.node.TimestampingProtocol
import core.utilities.trace
import java.security.KeyPair
import java.security.PublicKey
import java.time.Instant
import java.util.*
/**
* This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
@ -116,135 +116,31 @@ object TwoPartyTradeProtocol {
checkDependencies(it)
// TODO: Verify that the transaction is contract-valid even though it lacks sufficient signatures.
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputs.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
// we're reusing keys! So don't reuse keys!
// - This tx may not be valid according to the contracts of the input states, so we must resolve
// and fully audit the transaction chains to convince ourselves that it is actually valid.
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure, but rather, just to find good ways to
// express protocol state machines on top of the messaging layer.
}
return it
}
}
@Suspendable
private fun checkDependencies(txToCheck: SignedTransaction) {
val toVerify = HashSet<LedgerTransaction>()
val alreadyVerified = HashSet<LedgerTransaction>()
val downloadedSignedTxns = ArrayList<SignedTransaction>()
fetchDependenciesAndCheckSignatures(txToCheck.tx.inputs, toVerify, alreadyVerified, downloadedSignedTxns)
TransactionGroup(toVerify, alreadyVerified).verify(serviceHub.storageService.contractPrograms)
// Now write all the transactions we just validated back to the database for next time, including
// signatures so we can serve up these transactions to other peers when we, in turn, send one that
// depends on them onto another peer.
//
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
// dependencies aren't, or an unvalidated and possibly broken tx is there.
serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id })
}
@Suspendable
private fun fetchDependenciesAndCheckSignatures(depsToCheck: List<StateRef>,
toVerify: HashSet<LedgerTransaction>,
alreadyVerified: HashSet<LedgerTransaction>,
downloadedSignedTxns: ArrayList<SignedTransaction>) {
// A1. Create a work queue of transaction hashes waiting for resolution. Create a TransactionGroup.
//
// B1. Pop a hash. Look it up in the database. If it's not there, put the hash into a list for sending to
// the other peer. If it is there, load it and put its outputs into the TransactionGroup as unverified
// roots, because it's already been validated before.
// B2. If the queue is not empty, GOTO B1
// B3. If the request list is empty, GOTO D1
//
// C1. Send the request for hashes to the peer and wait for the response. Clear the request list.
// C2. For each transaction returned, verify that it does indeed hash to the requested transaction.
// C3. Add each transaction to the TransactionGroup.
// C4. Add each input state in each transaction to the work queue.
//
// D1. Verify the transaction group.
// D2. Write all the transactions in the group to the database.
// END
//
// Note: This protocol leaks private data. If you download a transaction and then do NOT request a
// dependency, it means you already have it, which in turn means you must have been involved with it before
// somehow, either in the tx itself or in any following spend of it. If there were no following spends, then
// your peer knows for sure that you were involved ... this is bad! The only obvious ways to fix this are
// something like onion routing of requests, secure hardware, or both.
//
// TODO: This needs to be factored out into a general subprotocol and subprotocol handling improved.
val workQ = ArrayList<StateRef>()
workQ.addAll(depsToCheck)
val nextRequests = ArrayList<SecureHash>()
val db = serviceHub.storageService.validatedTransactions
var limitCounter = 0
while (true) {
for (ref in workQ) {
val stx: SignedTransaction? = db[ref.txhash]
if (stx == null) {
// Transaction wasn't found in our local database, so we have to ask for it.
nextRequests.add(ref.txhash)
} else {
alreadyVerified.add(stx.verifyToLedgerTransaction(serviceHub.identityService))
}
}
workQ.clear()
if (nextRequests.isEmpty())
break
val sid = random63BitValue()
val fetchReq = DataVendingService.Request(nextRequests, serviceHub.networkService.myAddress, sid)
logger.info("Requesting ${nextRequests.size} dependency(s) for verification")
val maybeTxns: UntrustworthyData<ArrayList<SignedTransaction?>> =
sendAndReceive("platform.fetch.tx", otherSide, 0, sid, fetchReq)
// Check for a buggy/malicious peer answering with something that we didn't ask for, and then
// verify the signatures on the transactions and look up the identities to get LedgerTransactions.
// Note that this doesn't run contracts: just checks the signatures match the data.
val stxns: List<SignedTransaction> = validateTXFetchResponse(maybeTxns, nextRequests)
nextRequests.clear()
val ltxns = stxns.map { it.verifyToLedgerTransaction(serviceHub.identityService) }
// Add to the TransactionGroup, pending verification.
toVerify.addAll(ltxns)
downloadedSignedTxns.addAll(stxns)
// And now add all the input states to the work queue for database or remote resolution.
workQ.addAll(ltxns.flatMap { it.inputs })
// And loop around ...
limitCounter += workQ.size
if (limitCounter > 5000)
throw ExcessivelyLargeTransactionGraphException()
}
}
private fun validateTXFetchResponse(maybeTxns: UntrustworthyData<ArrayList<SignedTransaction?>>,
nextRequests: ArrayList<SecureHash>): List<SignedTransaction> {
return maybeTxns.validate { response ->
require(response.size == nextRequests.size)
val answers = response.requireNoNulls()
// Check transactions actually hash to what we requested, if this fails the remote node
// is a malicious protocol violator or buggy.
for ((index, stx) in answers.withIndex())
require(stx.id == nextRequests[index])
answers
}
private fun checkDependencies(stx: SignedTransaction) {
// Download and check all the transactions that this transaction depends on, but do not check this
// transaction itself.
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
}
private fun signWithOurKey(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
@ -287,25 +183,25 @@ object TwoPartyTradeProtocol {
// Wait for a trade request to come in on our pre-provided session ID.
val maybeTradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, sessionID)
val tradeRequest = maybeTradeRequest.validate {
maybeTradeRequest.validate {
// What is the seller trying to sell us?
val assetTypeName = it.assetForSale.state.javaClass.name
logger.trace { "Got trade request for a $assetTypeName" }
val asset = it.assetForSale.state
val assetTypeName = asset.javaClass.name
logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }
// Check the start message for acceptability.
check(it.sessionID > 0)
if (it.price > acceptablePrice)
throw UnacceptablePriceException(it.price)
if (!typeToBuy.isInstance(it.assetForSale.state))
if (!typeToBuy.isInstance(asset))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
return@validate it
}
// Check the transaction that contains the state which is being resolved.
// We only have a hash here, so if we don't know it already, we have to ask for it.
subProtocol(ResolveTransactionsProtocol(setOf(it.assetForSale.ref.txhash), otherSide))
// 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!
return tradeRequest
return it
}
}
@Suspendable

View File

@ -60,6 +60,6 @@ class DataVendingService(private val net: MessagingService, private val storage:
}
private fun handleContractRequest(msg: Message) {
TODO("PLT-12: Basic module/sandbox system for contracts")
TODO("PLT-12: Basic module/sandbox system for contracts: $msg")
}
}

View File

@ -36,9 +36,9 @@ class TimestampingMessages {
*/
@ThreadSafe
class TimestamperNodeService(private val net: MessagingService,
private val identity: Party,
private val signingKey: KeyPair,
private val clock: Clock = Clock.systemDefaultZone(),
val identity: Party,
val signingKey: KeyPair,
val clock: Clock = Clock.systemDefaultZone(),
val tolerance: Duration = 30.seconds) {
companion object {
val TIMESTAMPING_PROTOCOL_TOPIC = "platform.timestamping.request"
@ -92,28 +92,27 @@ class TimestamperNodeService(private val net: MessagingService,
}
}
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 maybeSignature = 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
maybeSignature.validate { sig ->
sig.verifyWithECDSA(wtxBytes)
return sig
}
}
}
/**
* The TimestampingProtocol class is the client code that talks to a [TimestamperNodeService] on some remote node. It is a
* [ProtocolLogic], meaning it can either be a sub-protocol of some other protocol, or be driven independently.
*
* If you are not yourself authoring a protocol and want to timestamp something, the [TimestampingProtocol.Client] class
* implements the [TimestamperService] interface, meaning it can be passed to [TransactionBuilder.timestamp] to timestamp
* the built transaction. Please be aware that this will block, meaning it should not be used on a thread that is handling
* a network message: use it only from spare application threads that don't have to respond to anything.
*/
class TimestampingProtocol(private val node: LegallyIdentifiableNode,
private val wtxBytes: SerializedBytes<WireTransaction>) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
class Client(private val stateMachineManager: StateMachineManager, private val node: LegallyIdentifiableNode) : TimestamperService {
override val identity: Party = node.identity
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
return stateMachineManager.add("platform.timestamping", TimestampingProtocol(node, wtxBytes)).get()
}
}
@Suspendable
override fun call(): DigitalSignature.LegallyIdentifiable {
val sessionID = random63BitValue()

View File

@ -8,15 +8,15 @@
package core.node
import co.paralleluniverse.fibers.Suspendable
import com.google.common.net.HostAndPort
import contracts.CommercialPaper
import contracts.protocols.TwoPartyTradeProtocol
import core.*
import core.crypto.generateKeyPair
import core.messaging.LegallyIdentifiableNode
import core.messaging.ProtocolLogic
import core.messaging.SingleMessageRecipient
import core.messaging.runOnNextMessage
import core.messaging.send
import core.serialization.deserialize
import core.utilities.BriefLogFormatter
import core.utilities.Emoji
@ -74,6 +74,12 @@ fun main(args: Array<String>) {
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
val listening = options.has(serviceFakeTradesArg)
if (listening && config.myLegalName != "Bank of Zurich") {
println("The buyer node must have a legal name of 'Bank of Zurich'. Please edit the config file.")
exitProcess(1)
}
// The timestamping node runs in the same process as the buyer protocol is run.
val timestamperId = if (options.has(timestamperIdentityFile)) {
val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(Node.DEFAULT_PORT)
val path = Paths.get(options.valueOf(timestamperIdentityFile))
@ -83,109 +89,132 @@ fun main(args: Array<String>) {
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId) }
// Now do some fake nonsense just to give us some activity.
(node.services.walletService as E2ETestWalletService).fillWithSomeTestCash(1500.DOLLARS)
val timestampingAuthority = node.services.networkMapService.timestampingNodes.first()
if (listening) {
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band
// via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
// next stage in our building site, we will just auto-generate fake trades to give our nodes something to do.
//
// Note that currently, the two-party trade protocol doesn't actually resolve dependencies of transactions!
// Thus, we can make up whatever junk we like and trade non-existent cash/assets: the other side won't notice.
// Obviously, fixing that is the next step.
//
// As the seller initiates the DVP/two-party trade protocol, here, we will be the buyer.
node.net.addMessageHandler("test.junktrade") { msg, handlerRegistration ->
val replyTo = msg.data.deserialize<SingleMessageRecipient>(includeClassName = true)
val buyerSessionID = random63BitValue()
println("Got a new junk trade request, sending back session ID and starting buy protocol")
val future = TwoPartyTradeProtocol.runBuyer(node.smm, timestampingAuthority, replyTo, 1000.DOLLARS,
CommercialPaper.State::class.java, buyerSessionID)
future success {
println()
println("Purchase complete - we are a happy customer! Final transaction is:")
println()
println(Emoji.renderIfSupported(it.tx))
println()
println("Waiting for another seller to connect. Or press Ctrl-C to shut me down.")
} failure {
println()
println("Something went wrong whilst trading!")
println()
}
node.net.send("test.junktrade.initiate", replyTo, buyerSessionID)
}
println()
println("Waiting for a seller to connect to us (run the other node) ...")
println()
node.smm.add("demo.buyer", TraderDemoProtocolBuyer()).get() // This thread will halt forever here.
} else {
// Grab a session ID for the fake trade from the other side, then kick off the seller and sell them some junk.
if (!options.has(fakeTradeWithArg)) {
println("Need the --fake-trade-with command line argument")
exitProcess(1)
}
val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT)
val otherSide = ArtemisMessagingService.makeRecipient(peerAddr)
node.net.runOnNextMessage("test.junktrade.initiate") { msg ->
val sessionID = msg.data.deserialize<Long>()
println("Got session ID back, now starting the sell protocol")
val cpOwnerKey = node.keyManagement.freshKey()
val commercialPaper = makeFakeCommercialPaper(node.storage, cpOwnerKey.public)
val future = TwoPartyTradeProtocol.runSeller(node.smm, timestampingAuthority,
otherSide, commercialPaper, 1000.DOLLARS, cpOwnerKey, sessionID)
future success {
println()
println("Sale completed - we have a happy customer!")
println()
println("Final transaction is")
println()
println(Emoji.renderIfSupported(it.tx))
println()
node.stop()
} failure {
println()
println("Something went wrong whilst trading!")
println()
}
}
println()
println("Sending a message to the listening/buying node ...")
println()
node.net.send("test.junktrade", otherSide, node.net.myAddress, includeClassName = true)
node.smm.add("demo.seller", TraderDemoProtocolSeller(myNetAddr, otherSide)).get()
node.stop()
}
}
fun makeFakeCommercialPaper(storageService: StorageService, ownedBy: PublicKey): StateAndRef<CommercialPaper.State> {
// Make a fake company that's issued its own paper.
val keyPair = generateKeyPair()
val party = Party("MegaCorp, Inc", keyPair.public)
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
val issuance = run {
val tx = CommercialPaper().generateIssue(party.ref(1,2,3), 1100.DOLLARS, Instant.now() + 10.days)
tx.signWith(keyPair)
tx.toSignedTransaction(true)
class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
@Suspendable
override fun call() {
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
// and over again and the double spends will never be detected! Fixing that is the next step.
(serviceHub.walletService as E2ETestWalletService).fillWithSomeTestCash(1500.DOLLARS)
while (true) {
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band
// via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
// next stage in our building site, we will just auto-generate fake trades to give our nodes something to do.
//
// As the seller initiates the DVP/two-party trade protocol, here, we will be the buyer.
try {
println()
println("Waiting for a seller to connect to us!")
val hostname = receive<HostAndPort>("test.junktrade", 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
val newPartnerAddr = ArtemisMessagingService.makeRecipient(hostname)
val sessionID = random63BitValue()
println()
println("Got a new junk trade request from $newPartnerAddr, sending back a fresh session ID and starting buy protocol")
println()
send("test.junktrade", newPartnerAddr, 0, sessionID)
val tsa = serviceHub.networkMapService.timestampingNodes[0]
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, tsa.identity, 1000.DOLLARS,
CommercialPaper.State::class.java, sessionID)
val tradeTX: SignedTransaction = subProtocol(buyer)
println()
println("Purchase complete - we are a happy customer! Final transaction is:")
println()
println(Emoji.renderIfSupported(tradeTX.tx))
println()
println("Waiting for another seller to connect. Or press Ctrl-C to shut me down.")
} catch(e: Exception) {
println()
println("Something went wrong whilst trading!")
println()
e.printStackTrace()
}
}
}
}
class TraderDemoProtocolSeller(val myAddress: HostAndPort,
val otherSide: SingleMessageRecipient) : ProtocolLogic<Unit>() {
@Suspendable
override fun call() {
println()
println("Announcing ourselves to the buyer node!")
println()
val sessionID = sendAndReceive<Long>("test.junktrade", otherSide, 0, 0, myAddress).validate { it }
println()
println("Got session ID back, issuing and timestamping some commercial paper")
val tsa = serviceHub.networkMapService.timestampingNodes[0]
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val commercialPaper = makeFakeCommercialPaper(cpOwnerKey.public, tsa)
println()
println("Timestamped my commercial paper issuance, starting the trade protocol.")
val seller = TwoPartyTradeProtocol.Seller(otherSide, tsa, commercialPaper, 1000.DOLLARS, cpOwnerKey, sessionID)
val tradeTX: SignedTransaction = subProtocol(seller)
println()
println("Sale completed - we have a happy customer!")
println()
println("Final transaction is")
println()
println(Emoji.renderIfSupported(tradeTX.tx))
println()
}
val move = run {
val tx = TransactionBuilder()
CommercialPaper().generateMove(tx, issuance.tx.outRef(0), ownedBy)
tx.signWith(keyPair)
tx.toSignedTransaction(true)
@Suspendable
fun makeFakeCommercialPaper(ownedBy: PublicKey, tsa: LegallyIdentifiableNode): StateAndRef<CommercialPaper.State> {
// Make a fake company that's issued its own paper.
val keyPair = generateKeyPair()
val party = Party("MegaCorp, Inc", keyPair.public)
val issuance = run {
val tx = CommercialPaper().generateIssue(party.ref(1,2,3), 1100.DOLLARS, Instant.now() + 10.days)
tx.setTime(Instant.now(), tsa.identity, 30.seconds)
val tsaSig = subProtocol(TimestampingProtocol(tsa, tx.toWireTransaction().serialized))
tx.checkAndAddSignature(tsaSig)
tx.signWith(keyPair)
tx.toSignedTransaction(true)
}
val move = run {
val tx = TransactionBuilder()
CommercialPaper().generateMove(tx, issuance.tx.outRef(0), ownedBy)
tx.signWith(keyPair)
tx.toSignedTransaction(true)
}
with(serviceHub.storageService) {
validatedTransactions[issuance.id] = issuance
validatedTransactions[move.id] = move
}
return move.tx.outRef(0)
}
storageService.validatedTransactions[issuance.id] = issuance
storageService.validatedTransactions[move.id] = move
return move.tx.outRef(0)
}
private fun loadConfigFile(configFile: Path): NodeConfiguration {
@ -219,22 +248,15 @@ private fun loadConfigFile(configFile: Path): NodeConfiguration {
private fun createDefaultConfigFile(configFile: Path?, defaultLegalName: String) {
Files.write(configFile,
"""
# Node configuration: adjust below as needed, then delete this comment.
# Node configuration: give the buyer node the name 'Bank of Zurich' (no quotes)
# The seller node can be named whatever you like.
myLegalName = $defaultLegalName
""".trimIndent().toByteArray())
}
private fun printHelp() {
println("""
To run the listening node, alias "alpha" to "localhost" in your
/etc/hosts file and then try a command line like this:
--dir=alpha --service-fake-trades --network-address=alpha
To run the node that initiates a trade, alias "beta" to "localhost"
in your /etc/hosts file and then try a command line like this:
--dir=beta --fake-trade-with=alpha --network-address=beta:31338 --timestamper-identity-file=alpha/identity-public --timestamper-address=alpha
Please refer to the documentation in docs/build/index.html to learn how to run the demo.
""".trimIndent())
}

View File

@ -13,18 +13,19 @@ import contracts.Cash
import contracts.CommercialPaper
import contracts.protocols.TwoPartyTradeProtocol
import core.*
import core.crypto.SecureHash
import core.testutils.*
import core.utilities.BriefLogFormatter
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
// TODO: Refactor this test some more to cut down on messy setup duplication.
/**
* In this example, Alice wishes to sell her commercial paper to Bob in return for $1,000,000 and they wish to do
* it on the ledger atomically. Therefore they must work together to build a transaction.
@ -32,23 +33,20 @@ import kotlin.test.assertTrue
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
*/
class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
lateinit var backgroundThread: ExecutorService
@Before
fun before() {
backgroundThread = Executors.newSingleThreadExecutor()
BriefLogFormatter.initVerbose("platform.trade")
}
@After
fun after() {
backgroundThread.shutdown()
BriefLogFormatter.initVerbose("platform.trade", "core.TransactionGroup", "recordingmap")
}
@Test
fun `trade cash for commercial paper`() {
// We run this in parallel threads to help catch any race conditions that may exist. The other tests
// we run in the unit test thread exclusively to speed things up, ensure deterministic results and
// allow interruption half way through.
val backgroundThread = Executors.newSingleThreadExecutor()
transactionGroupFor<ContractState> {
val bobsWallet = fillUp(false).first
val (bobsWallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
val (alicesAddress, alicesNode) = makeNode(inBackground = true)
val (bobsAddress, bobsNode) = makeNode(inBackground = true)
@ -61,7 +59,8 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
net = bobsNode,
storage = MockStorageService()
)
loadFakeTxnsIntoStorage(bobsServices.storageService)
loadFakeTxnsIntoStorage(bobsFakeCash, bobsServices.storageService)
loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
val buyerSessionID = random63BitValue()
@ -88,15 +87,17 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
txns.add(aliceResult.get().tx)
verify()
}
backgroundThread.shutdown()
}
@Test
fun `shut down and restore`() {
transactionGroupFor<ContractState> {
val wallet = fillUp(false).first
val (wallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
val (alicesAddress, alicesNode) = makeNode(inBackground = false)
var (bobsAddress, bobsNode) = makeNode(inBackground = false)
val (alicesAddress, alicesNode) = makeNode()
var (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true)
val bobsStorage = MockStorageService()
@ -108,7 +109,8 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
net = bobsNode,
storage = bobsStorage
)
loadFakeTxnsIntoStorage(bobsStorage)
loadFakeTxnsIntoStorage(bobsFakeCash, bobsStorage)
loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
val smmBuyer = StateMachineManager(bobsServices, MoreExecutors.directExecutor())
@ -153,10 +155,8 @@ 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 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))
// Alice doesn't know that and carries on: she wants to know about the cash transactions he's trying to use.
// She will wait around until Bob comes back.
assertTrue(alicesNode.pump(false))
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
@ -170,8 +170,8 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// Find the future representing the result of this state machine again.
var bobFuture = smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).single().second
// Let Bob process his mailbox.
assertTrue(bobsNode.pump(false))
// And off we go again.
runNetwork()
// Bob is now finished and has the same transaction as Alice.
val stx = bobFuture.get()
@ -185,78 +185,30 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
@Test
fun `check dependencies of the sale asset are resolved`() {
transactionGroupFor<ContractState> {
val (bobsWallet, fakeTxns) = fillUp(false)
val (bobsWallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
val (alicesAddress, alicesNode) = makeNode(inBackground = true)
val (bobsAddress, bobsNode) = makeNode(inBackground = true)
val timestamper = network.setupTimestampingNode(false).first
val (alicesAddress, alicesNode) = makeNode()
val (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true).first
val alicesServices = MockServices(net = alicesNode)
val alicesServices = MockServices(
net = alicesNode,
storage = MockStorageService(mapOf("validated-transactions" to "alice"))
)
val bobsServices = MockServices(
wallet = MockWalletService(bobsWallet.states),
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
net = bobsNode,
storage = MockStorageService(isRecording = true)
storage = MockStorageService(mapOf("validated-transactions" to "bob"))
)
loadFakeTxnsIntoStorage(bobsServices.storageService)
val bobsSignedTxns = loadFakeTxnsIntoStorage(bobsFakeCash, bobsServices.storageService)
val alicesSignedTxns = loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
val buyerSessionID = random63BitValue()
val aliceResult = TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, backgroundThread),
timestamper,
bobsAddress,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
val bobResult = TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, backgroundThread),
timestamper,
alicesAddress,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
// This line forces the protocol to run to completion.
assertEquals(aliceResult.get(), bobResult.get())
val records = (bobsServices.storageService.validatedTransactions as RecordingMap).records
val expected = listOf(
RecordingMap.Get(fakeTxns[1].id),
RecordingMap.Get(fakeTxns[2].id),
RecordingMap.Get(fakeTxns[3].id),
RecordingMap.Get(fakeTxns[0].id),
RecordingMap.Get(fakeTxns[0].id)
)
assertEquals(expected, records)
}
}
@Test
fun `dependency with error`() {
transactionGroupFor<ContractState> {
val bobsWallet = fillUp(withError = true).first
val (alicesAddress, alicesNode) = makeNode(inBackground = true)
val (bobsAddress, bobsNode) = makeNode(inBackground = true)
val timestamper = network.setupTimestampingNode(false).first
val alicesServices = MockServices(net = alicesNode)
val bobsServices = MockServices(
wallet = MockWalletService(bobsWallet.states),
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
net = bobsNode,
storage = MockStorageService(isRecording = true)
)
loadFakeTxnsIntoStorage(bobsServices.storageService)
val buyerSessionID = random63BitValue()
val aliceResult = TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, backgroundThread),
TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, RunOnCallerThread),
timestamper,
bobsAddress,
lookup("alice's paper"),
@ -265,7 +217,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
buyerSessionID
)
TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, backgroundThread),
StateMachineManager(bobsServices, RunOnCallerThread),
timestamper,
alicesAddress,
1000.DOLLARS,
@ -273,6 +225,91 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
buyerSessionID
)
runNetwork()
run {
val records = (bobsServices.storageService.validatedTransactions as RecordingMap).records
// Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice.
val expected = listOf(
// Buyer Bob is told about Alice's commercial paper, but doesn't know it ..
RecordingMap.Get(alicesFakePaper[0].id),
// He asks and gets the tx, validates it, sees it's a self issue with no dependencies, stores.
RecordingMap.Put(alicesFakePaper[0].id, alicesSignedTxns.values.first()),
// Alice gets Bob's proposed transaction and doesn't know his two cash states. She asks, Bob answers.
RecordingMap.Get(bobsFakeCash[1].id),
RecordingMap.Get(bobsFakeCash[2].id),
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know. She asks, Bob answers.
RecordingMap.Get(bobsFakeCash[0].id)
)
assertEquals(expected, records)
}
// And from Alice's perspective ...
run {
val records = (alicesServices.storageService.validatedTransactions as RecordingMap).records
val expected = listOf(
// Seller Alice sends her seller info to Bob, who wants to check the asset for sale.
// He requests, Alice looks up in her DB to send the tx to Bob
RecordingMap.Get(alicesFakePaper[0].id),
// Seller Alice gets a proposed tx which depends on Bob's two cash txns and her own tx.
RecordingMap.Get(bobsFakeCash[1].id),
RecordingMap.Get(bobsFakeCash[2].id),
RecordingMap.Get(alicesFakePaper[0].id),
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know.
RecordingMap.Get(bobsFakeCash[0].id),
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
// Bob's transactions are valid, so she commits to the database
RecordingMap.Put(bobsFakeCash[1].id, bobsSignedTxns[bobsFakeCash[1].id]),
RecordingMap.Put(bobsFakeCash[2].id, bobsSignedTxns[bobsFakeCash[2].id]),
RecordingMap.Put(bobsFakeCash[0].id, bobsSignedTxns[bobsFakeCash[0].id])
)
assertEquals(expected, records)
}
}
}
@Test
fun `dependency with error on buyer side`() {
transactionGroupFor<ContractState> {
val (bobsWallet, fakeBobCash) = fillUpForBuyer(withError = true)
val fakeAlicePaper = fillUpForSeller(false).second
val (alicesAddress, alicesNode) = makeNode()
val (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true).first
val alicesServices = MockServices(net = alicesNode)
val bobsServices = MockServices(
wallet = MockWalletService(bobsWallet.states),
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
net = bobsNode,
storage = MockStorageService(mapOf("validated-transactions" to "bob"))
)
loadFakeTxnsIntoStorage(fakeBobCash, bobsServices.storageService)
loadFakeTxnsIntoStorage(fakeAlicePaper, alicesServices.storageService)
val buyerSessionID = random63BitValue()
val aliceResult = TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, RunOnCallerThread),
timestamper,
bobsAddress,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, RunOnCallerThread),
timestamper,
alicesAddress,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
runNetwork()
val e = assertFailsWith<ExecutionException> {
aliceResult.get()
}
@ -281,16 +318,68 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
}
}
private fun TransactionGroupDSL<ContractState>.loadFakeTxnsIntoStorage(ss: StorageService) {
@Test
fun `dependency with error on seller side`() {
transactionGroupFor<ContractState> {
val (bobsWallet, fakeBobCash) = fillUpForBuyer(withError = false)
val fakeAlicePaper = fillUpForSeller(withError = true).second
val (alicesAddress, alicesNode) = makeNode()
val (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true).first
val alicesServices = MockServices(net = alicesNode)
val bobsServices = MockServices(
wallet = MockWalletService(bobsWallet.states),
keyManagement = MockKeyManagementService(mapOf(BOB to BOB_KEY.private)),
net = bobsNode,
storage = MockStorageService(mapOf("validated-transactions" to "bob"))
)
loadFakeTxnsIntoStorage(fakeBobCash, bobsServices.storageService)
loadFakeTxnsIntoStorage(fakeAlicePaper, alicesServices.storageService)
val buyerSessionID = random63BitValue()
TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, RunOnCallerThread),
timestamper,
bobsAddress,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
val bobResult = TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, RunOnCallerThread),
timestamper,
alicesAddress,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
runNetwork()
val e = assertFailsWith<ExecutionException> {
bobResult.get()
}
assertTrue(e.cause is TransactionVerificationException)
assertTrue(e.cause!!.cause!!.message!!.contains("must be timestamped"))
}
}
private fun TransactionGroupDSL<ContractState>.loadFakeTxnsIntoStorage(wtxToSign: List<WireTransaction>,
ss: StorageService): Map<SecureHash, SignedTransaction> {
val txStorage = ss.validatedTransactions
val map = signAll().associateBy { it.id }
val map = signAll(wtxToSign).associateBy { it.id }
if (txStorage is RecordingMap) {
txStorage.putAllUnrecorded(map)
} else
txStorage.putAll(map)
return map
}
private fun TransactionGroupDSL<ContractState>.fillUp(withError: Boolean): Pair<Wallet, List<WireTransaction>> {
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean): Pair<Wallet, List<WireTransaction>> {
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
// wants to sell to Bob.
@ -317,15 +406,21 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
}
val wallet = Wallet(listOf<StateAndRef<Cash.State>>(lookup("bob cash 1"), lookup("bob cash 2")))
return Pair(wallet, listOf(eb1, bc1, bc2))
}
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean): Pair<Wallet, List<WireTransaction>> {
val ap = transaction {
output("alice's paper") {
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), ALICE, 1200.DOLLARS, TEST_TX_TIME + 7.days)
}
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
if (!withError)
timestamp(TEST_TX_TIME)
}
val wallet = Wallet(listOf<StateAndRef<Cash.State>>(lookup("bob cash 1"), lookup("bob cash 2")))
return Pair(wallet, listOf(eb1, bc1, bc2, ap))
val wallet = Wallet(listOf<StateAndRef<Cash.State>>(lookup("alice's paper")))
return Pair(wallet, listOf(ap))
}
}

View File

@ -61,7 +61,6 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() {
class TestPSM(val server: LegallyIdentifiableNode, val now: Instant) : ProtocolLogic<Boolean>() {
@Suspendable
override fun call(): Boolean {
val client = TimestamperClient(psm, server)
val ptx = TransactionBuilder().apply {
addInputState(StateRef(SecureHash.randomSHA256(), 0))
addOutputState(100.DOLLARS.CASH)
@ -69,7 +68,7 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() {
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())
val sig = subProtocol(TimestampingProtocol(server, wtx.serialized))
ptx.checkAndAddSignature(sig)
return true
}

View File

@ -27,6 +27,8 @@ object TestUtils {
val keypair = generateKeyPair()
val keypair2 = generateKeyPair()
}
// A dummy time at which we will be pretending test transactions are created.
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
// A few dummy values for testing.
val MEGA_CORP_KEY = TestUtils.keypair
@ -46,12 +48,10 @@ val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, Dum
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf(
MEGA_CORP_PUBKEY to MEGA_CORP,
MINI_CORP_PUBKEY to MINI_CORP
MINI_CORP_PUBKEY to MINI_CORP,
DUMMY_TIMESTAMPER.identity.owningKey to DUMMY_TIMESTAMPER.identity
)
// A dummy time at which we will be pretending test transactions are created.
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
// a sandbox. For unit tests we just have a hard-coded list.
val TEST_PROGRAM_MAP: Map<SecureHash, Class<out Contract>> = mapOf(