Minor: make TwoPartyTradeProtocolTests use the new MockNode infrastructure

This commit is contained in:
Mike Hearn 2016-03-01 15:25:16 +01:00
parent a69a663354
commit 7f5eb5bf2f
4 changed files with 193 additions and 207 deletions

View File

@ -83,7 +83,7 @@ class CommercialPaper : Contract {
// 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")
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Bank of Zurich")
for (group in groups) {
when (command.value) {

View File

@ -25,5 +25,5 @@ interface TimestamperService {
// The timestamper itself is implemented in the unit test part of the code (in TestUtils.kt).
object DummyTimestampingAuthority {
val key = generateKeyPair()
val identity = Party("The dummy timestamper", key.public)
val identity = Party("Mock Company 0", key.public)
}

View File

@ -16,6 +16,6 @@ import java.security.PublicKey
* Scaffolding: a dummy identity service that just expects to have identities loaded off disk or found elsewhere.
*/
class FixedIdentityService(private val identities: List<Party>) : IdentityService {
private val keyToParties = identities.associateBy { it.owningKey }
private val keyToParties: Map<PublicKey, Party> get() = identities.associateBy { it.owningKey }
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
}

View File

@ -8,18 +8,25 @@
package core.messaging
import com.google.common.util.concurrent.MoreExecutors
import contracts.Cash
import contracts.CommercialPaper
import contracts.protocols.TwoPartyTradeProtocol
import core.*
import core.crypto.SecureHash
import core.node.MockNetwork
import core.node.NodeAttachmentStorage
import core.node.NodeWalletService
import core.testutils.*
import core.utilities.BriefLogFormatter
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@ -33,9 +40,17 @@ 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 net: MockNetwork
@Before
fun before() {
BriefLogFormatter.initVerbose("platform.trade", "core.TransactionGroup", "recordingmap")
net = MockNetwork(false)
BriefLogFormatter.loggingOn("platform.trade", "core.TransactionGroup", "recordingmap")
}
@After
fun after() {
BriefLogFormatter.loggingOff("platform.trade", "core.TransactionGroup", "recordingmap")
}
@Test
@ -43,96 +58,76 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// 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()
net = MockNetwork(true)
transactionGroupFor<ContractState> {
val (bobsWallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
val (aliceNode, bobNode) = net.createTwoNodes()
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
val alicesFakePaper = fillUpForSeller(false, aliceNode.legallyIdentifableAddress.identity).second
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(BOB_KEY),
net = bobsNode,
storage = MockStorageService()
)
loadFakeTxnsIntoStorage(bobsFakeCash, bobsServices.storageService)
loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue()
val aliceResult = TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, backgroundThread),
timestamper,
bobsAddress,
aliceNode.smm,
aliceNode.legallyIdentifableAddress,
bobNode.net.myAddress,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
val bobResult = TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, backgroundThread),
timestamper,
alicesAddress,
bobNode.smm,
aliceNode.legallyIdentifableAddress,
aliceNode.net.myAddress,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
// TODO: Verify that the result was inserted into the transaction database.
// assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id])
assertEquals(aliceResult.get(), bobResult.get())
txns.add(aliceResult.get().tx)
verify()
aliceNode.stop()
bobNode.stop()
}
backgroundThread.shutdown()
}
@Test
fun `shut down and restore`() {
fun shutdownAndRestore() {
transactionGroupFor<ContractState> {
val (wallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
var (aliceNode, bobNode) = net.createTwoNodes()
val aliceAddr = aliceNode.net.myAddress
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
val timestamperAddr = aliceNode.legallyIdentifableAddress
val (alicesAddress, alicesNode) = makeNode()
var (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true)
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity).second
val bobsStorage = MockStorageService()
val alicesServices = MockServices(wallet = null, keyManagement = null, net = alicesNode)
var bobsServices = MockServices(
wallet = MockWalletService(wallet.states),
keyManagement = MockKeyManagementService(BOB_KEY),
net = bobsNode,
storage = bobsStorage
)
loadFakeTxnsIntoStorage(bobsFakeCash, bobsStorage)
loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
val smmBuyer = StateMachineManager(bobsServices, MoreExecutors.directExecutor())
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
// Horrible Gradle/Kryo/Quasar FUBAR workaround: just skip these tests when run under Gradle for now.
if (!smmBuyer.checkpointing)
// TODO: Fix this once Quasar issue 153 is resolved.
if (!bobNode.smm.checkpointing)
return
val buyerSessionID = random63BitValue()
TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, MoreExecutors.directExecutor()),
timestamper.first,
bobsAddress,
val aliceFuture = TwoPartyTradeProtocol.runSeller(
aliceNode.smm,
timestamperAddr,
bobAddr,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
TwoPartyTradeProtocol.runBuyer(
smmBuyer,
timestamper.first,
alicesAddress,
bobNode.smm,
timestamperAddr,
aliceAddr,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
@ -140,95 +135,118 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// Everything is on this thread so we can now step through the protocol one step at a time.
// Seller Alice already sent a message to Buyer Bob. Pump once:
bobsNode.pump(false)
fun pumpAlice() = (aliceNode.net as InMemoryMessagingNetwork.InMemoryMessaging).pump(false)
fun pumpBob() = (bobNode.net as InMemoryMessagingNetwork.InMemoryMessaging).pump(false)
pumpBob()
// Bob sends a couple of queries for the dependencies back to Alice. Alice reponds.
alicesNode.pump(false)
bobsNode.pump(false)
alicesNode.pump(false)
bobsNode.pump(false)
pumpAlice()
pumpBob()
pumpAlice()
pumpBob()
// OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
// Save the state machine to "disk" (i.e. a variable, here)
assertEquals(1, bobsStorage.getMap<Any, Any>("state machines").size)
val savedCheckpoints = HashMap(bobNode.storage.getMap<Any, Any>("state machines"))
assertEquals(1, savedCheckpoints.size)
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
bobsNode.stop()
bobNode.stop()
// 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))
assertTrue(pumpAlice())
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
// that Bob was waiting on before the reboot occurred.
bobsNode = network.createNodeWithID(true, bobsAddress.id).start().get()
val smm = StateMachineManager(
MockServices(wallet = null, keyManagement = null, net = bobsNode, storage = bobsStorage),
MoreExecutors.directExecutor()
)
bobNode = net.createNode(timestamperAddr, bobAddr.id) { path, nodeConfiguration, net, timestamper ->
object : MockNetwork.MockNode(path, nodeConfiguration, net, timestamper, bobAddr.id) {
override fun initialiseStorageService(dir: Path): StorageService {
val ss = super.initialiseStorageService(dir)
val smMap = ss.getMap<Any, Any>("state machines")
smMap.putAll(savedCheckpoints)
return ss
}
}
}
// Find the future representing the result of this state machine again.
var bobFuture = smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).single().second
var bobFuture = bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).single().second
// And off we go again.
runNetwork()
net.runNetwork()
// Bob is now finished and has the same transaction as Alice.
val stx = bobFuture.get()
txns.add(stx.tx)
verify()
assertEquals(bobFuture.get(), aliceFuture.get())
assertTrue(smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).isEmpty())
assertTrue(bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).isEmpty())
}
}
// Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order
// of gets and puts.
private fun makeNodeWithTracking(name: String): MockNetwork.MockNode {
// Create a node in the mock network ...
return net.createNode(null) { path, config, net, tsNode ->
object : MockNetwork.MockNode(path, config, net, tsNode) {
// That constructs the storage service object in a customised way ...
override fun constructStorageService(attachments: NodeAttachmentStorage, identity: Party, keypair: KeyPair): StorageServiceImpl {
// By tweaking the standard StorageServiceImpl class ...
return object : StorageServiceImpl(attachments, identity, keypair) {
// To use RecordingMaps instead of ordinary HashMaps.
@Suppress("UNCHECKED_CAST")
override fun <K, V> getMap(tableName: String): MutableMap<K, V> {
synchronized(tables) {
return tables.getOrPut(tableName) {
val map = Collections.synchronizedMap(HashMap<Any, Any>())
RecordingMap(map, LoggerFactory.getLogger("recordingmap.$name"))
} as MutableMap<K, V>
}
}
}
}
}
}
}
@Test
fun `check dependencies of the sale asset are resolved`() {
fun checkDependenciesOfSaleAssetAreResolved() {
transactionGroupFor<ContractState> {
val (bobsWallet, bobsFakeCash) = fillUpForBuyer(false)
val alicesFakePaper = fillUpForSeller(false).second
val aliceNode = makeNodeWithTracking("alice")
val timestamperAddr = aliceNode.legallyIdentifableAddress
val bobNode = makeNodeWithTracking("bob")
val (alicesAddress, alicesNode) = makeNode()
val (bobsAddress, bobsNode) = makeNode()
val timestamper = network.setupTimestampingNode(true).first
val alicesServices = MockServices(
net = alicesNode,
storage = MockStorageService(mapOf("validated-transactions" to "alice"))
)
val bobsServices = MockServices(
wallet = MockWalletService(bobsWallet.states),
keyManagement = MockKeyManagementService(BOB_KEY),
net = bobsNode,
storage = MockStorageService(mapOf("validated-transactions" to "bob"))
)
val bobsSignedTxns = loadFakeTxnsIntoStorage(bobsFakeCash, bobsServices.storageService)
val alicesSignedTxns = loadFakeTxnsIntoStorage(alicesFakePaper, alicesServices.storageService)
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity).second
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue()
TwoPartyTradeProtocol.runSeller(
StateMachineManager(alicesServices, RunOnCallerThread),
timestamper,
bobsAddress,
aliceNode.smm,
timestamperAddr,
bobNode.net.myAddress,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
TwoPartyTradeProtocol.runBuyer(
StateMachineManager(bobsServices, RunOnCallerThread),
timestamper,
alicesAddress,
bobNode.smm,
timestamperAddr,
aliceNode.net.myAddress,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
runNetwork()
net.runNetwork()
run {
val records = (bobsServices.storageService.validatedTransactions as RecordingMap).records
val records = (bobNode.storage.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 ..
@ -246,7 +264,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// And from Alice's perspective ...
run {
val records = (alicesServices.storageService.validatedTransactions as RecordingMap).records
val records = (aliceNode.storage.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
@ -276,115 +294,83 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
@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(BOB_KEY),
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()
}
assertTrue(e.cause is TransactionVerificationException)
assertTrue(e.cause!!.cause!!.message!!.contains("at least one cash input"))
runWithError(true, false, "at least one cash input")
}
}
@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(BOB_KEY),
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"))
runWithError(false, true, "must be timestamped")
}
}
private fun TransactionGroupDSL<ContractState>.loadFakeTxnsIntoStorage(wtxToSign: List<WireTransaction>,
ss: StorageService): Map<SecureHash, SignedTransaction> {
val txStorage = ss.validatedTransactions
val map = signAll(wtxToSign).associateBy { it.id }
if (txStorage is RecordingMap) {
txStorage.putAllUnrecorded(map)
} else
txStorage.putAll(map)
return map
private fun TransactionGroupDSL<ContractState>.runWithError(bobError: Boolean, aliceError: Boolean,
expectedMessageSubstring: String) {
var (aliceNode, bobNode) = net.createTwoNodes()
val aliceAddr = aliceNode.net.myAddress
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
val timestamperAddr = aliceNode.legallyIdentifableAddress
val bobKey = bobNode.keyManagement.freshKey()
val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second
val alicesFakePaper = fillUpForSeller(aliceError, timestamperAddr.identity).second
insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobKey)
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue()
val aliceResult = TwoPartyTradeProtocol.runSeller(
aliceNode.smm,
timestamperAddr,
bobAddr,
lookup("alice's paper"),
1000.DOLLARS,
ALICE_KEY,
buyerSessionID
)
val bobResult = TwoPartyTradeProtocol.runBuyer(
bobNode.smm,
timestamperAddr,
aliceAddr,
1000.DOLLARS,
CommercialPaper.State::class.java,
buyerSessionID
)
net.runNetwork()
val e = assertFailsWith<ExecutionException> {
if (bobError)
aliceResult.get()
else
bobResult.get()
}
assertTrue(e.cause is TransactionVerificationException)
assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring))
}
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean): Pair<Wallet, List<WireTransaction>> {
private fun TransactionGroupDSL<ContractState>.insertFakeTransactions(wtxToSign: List<WireTransaction>,
services: ServiceHub,
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
val txStorage = services.storageService.validatedTransactions
val signed = signAll(wtxToSign, *extraKeys).associateBy { it.id }
if (txStorage is RecordingMap) {
txStorage.putAllUnrecorded(signed)
} else
txStorage.putAll(signed)
try {
services.walletService.notifyAll(signed.map { it.value.tx })
} catch(e: Throwable) {
// TODO: Remove this hack once all the tests are converted to use MockNode.
}
return signed
}
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean, bobKey: PublicKey = BOB): 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.
@ -400,13 +386,13 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
// Bob gets some cash onto the ledger from BoE
val bc1 = transaction {
input("elbonian money 1")
output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` BOB }
output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` bobKey }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
}
val bc2 = transaction {
input("elbonian money 2")
output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` BOB }
output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` bobKey }
output { 700.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } // Change output.
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
}
@ -415,14 +401,14 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
return Pair(wallet, listOf(eb1, bc1, bc2))
}
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean): Pair<Wallet, List<WireTransaction>> {
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, timestamper: Party): 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() }
if (!withError)
timestamp(TEST_TX_TIME)
arg(timestamper.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
}
val wallet = Wallet(listOf<StateAndRef<Cash.State>>(lookup("alice's paper")))