From 7f5eb5bf2f3811698781202c3720972cbffe04b1 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 1 Mar 2016 15:25:16 +0100 Subject: [PATCH] Minor: make TwoPartyTradeProtocolTests use the new MockNode infrastructure --- contracts/src/main/kotlin/CommercialPaper.kt | 2 +- .../main/kotlin/core/TimestamperService.kt | 2 +- .../kotlin/core/node/FixedIdentityService.kt | 2 +- .../messaging/TwoPartyTradeProtocolTests.kt | 394 +++++++++--------- 4 files changed, 193 insertions(+), 207 deletions(-) diff --git a/contracts/src/main/kotlin/CommercialPaper.kt b/contracts/src/main/kotlin/CommercialPaper.kt index 0f7c1870b1..225fb197a5 100644 --- a/contracts/src/main/kotlin/CommercialPaper.kt +++ b/contracts/src/main/kotlin/CommercialPaper.kt @@ -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) { diff --git a/core/src/main/kotlin/core/TimestamperService.kt b/core/src/main/kotlin/core/TimestamperService.kt index 4d7b92fb6e..bcd835fb40 100644 --- a/core/src/main/kotlin/core/TimestamperService.kt +++ b/core/src/main/kotlin/core/TimestamperService.kt @@ -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) } diff --git a/src/main/kotlin/core/node/FixedIdentityService.kt b/src/main/kotlin/core/node/FixedIdentityService.kt index ebad4368d0..00d3bf0bb5 100644 --- a/src/main/kotlin/core/node/FixedIdentityService.kt +++ b/src/main/kotlin/core/node/FixedIdentityService.kt @@ -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) : IdentityService { - private val keyToParties = identities.associateBy { it.owningKey } + private val keyToParties: Map get() = identities.associateBy { it.owningKey } override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] } \ No newline at end of file diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt index 3c4b92b115..707c83f9d6 100644 --- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt +++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt @@ -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 { - 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 { - 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("state machines").size) + val savedCheckpoints = HashMap(bobNode.storage.getMap("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("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 getMap(tableName: String): MutableMap { + synchronized(tables) { + return tables.getOrPut(tableName) { + val map = Collections.synchronizedMap(HashMap()) + RecordingMap(map, LoggerFactory.getLogger("recordingmap.$name")) + } as MutableMap + } + } + } + } + } } } @Test - fun `check dependencies of the sale asset are resolved`() { + fun checkDependenciesOfSaleAssetAreResolved() { transactionGroupFor { - 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 { - 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 { - 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 { - 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 { - 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.loadFakeTxnsIntoStorage(wtxToSign: List, - ss: StorageService): Map { - 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.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 { + if (bobError) + aliceResult.get() + else + bobResult.get() + } + assertTrue(e.cause is TransactionVerificationException) + assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring)) } - private fun TransactionGroupDSL.fillUpForBuyer(withError: Boolean): Pair> { + private fun TransactionGroupDSL.insertFakeTransactions(wtxToSign: List, + services: ServiceHub, + vararg extraKeys: KeyPair): Map { + 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.fillUpForBuyer(withError: Boolean, bobKey: PublicKey = BOB): Pair> { // 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.fillUpForSeller(withError: Boolean): Pair> { + private fun TransactionGroupDSL.fillUpForSeller(withError: Boolean, timestamper: Party): Pair> { 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>(lookup("alice's paper")))