mirror of
https://github.com/corda/corda.git
synced 2025-02-03 01:31:24 +00:00
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:
parent
eb47d8af4d
commit
cd28733360
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user