Testing: change fillWithTestCash to issue cash under the DUMMY_CASH_ISSUER identity and adjust code that uses it. Introduce some code that'll prove useful later in WalletWithCashTest.

This change reduces the  testing confusion that can occur when cash is issued by one of the parties in a transaction rather than e.g. a neutral third party like a central bank.
This commit is contained in:
Mike Hearn
2016-07-29 13:10:58 +02:00
parent dd53acfb64
commit ba05b90b8f
5 changed files with 43 additions and 67 deletions

View File

@ -2,6 +2,8 @@
package com.r3corda.contracts.testing package com.r3corda.contracts.testing
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued import com.r3corda.core.contracts.Issued
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.contracts.SignedTransaction
@ -11,16 +13,14 @@ import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
import java.security.PublicKey
import java.util.* import java.util.*
/** /**
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them * Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
* to the wallet. This is intended for unit tests. * to the wallet. This is intended for unit tests. The cash is issued by [DUMMY_CASH_ISSUER] and owned by the legal
* * identity key from the storage service.
* The cash is self issued with the current nodes identity, as fetched from the storage service. Thus it
* would not be trusted by any sensible market participant and is effectively an IOU. If it had been issued by
* the central bank, well ... that'd be a different story altogether.
* *
* The service hub needs to provide at least a key management service and a storage service. * The service hub needs to provide at least a key management service and a storage service.
* *
@ -31,23 +31,18 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
atLeastThisManyStates: Int = 3, atLeastThisManyStates: Int = 3,
atMostThisManyStates: Int = 10, atMostThisManyStates: Int = 10,
rng: Random = Random(), rng: Random = Random(),
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 0 }))): Wallet { ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
ownedBy: PublicKey? = null): Wallet {
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
val myIdentity = storageService.myLegalIdentity val myKey: PublicKey = ownedBy ?: storageService.myLegalIdentityKey.public
val myKey = storageService.myLegalIdentityKey
// We will allocate one state to one transaction, for simplicities sake. // We will allocate one state to one transaction, for simplicities sake.
val cash = Cash() val cash = Cash()
val transactions: List<SignedTransaction> = amounts.map { pennies -> val transactions: List<SignedTransaction> = amounts.map { pennies ->
// This line is what makes the cash self issued. We just use zero as our deposit reference: we don't need
// this field as there's no other database or source of truth we need to sync with.
val depositRef = myIdentity.ref(ref)
val issuance = TransactionType.General.Builder() val issuance = TransactionType.General.Builder()
val freshKey = keyManagementService.freshKey() cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, notary)
cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary) issuance.signWith(DUMMY_CASH_ISSUER_KEY)
issuance.signWith(myKey)
return@map issuance.toSignedTransaction(true) return@map issuance.toSignedTransaction(true)
} }
@ -77,8 +72,8 @@ private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, m
// Handle inexact rounding. // Handle inexact rounding.
amounts[i] = howMuch.quantity - filledSoFar amounts[i] = howMuch.quantity - filledSoFar
} }
check(amounts[i] >= 0) { amounts[i] } check(amounts[i] >= 0) { "${amounts[i]} : $filledSoFar : $howMuch" }
} }
check(amounts.sum() == howMuch.quantity) check(amounts.sum() == howMuch.quantity)
return amounts return amounts
} }

View File

@ -2,10 +2,7 @@ package com.r3corda.node.messaging
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.contracts.CommercialPaper import com.r3corda.contracts.CommercialPaper
import com.r3corda.contracts.asset.CASH import com.r3corda.contracts.asset.*
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.`issued by`
import com.r3corda.contracts.asset.`owned by`
import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
@ -93,9 +90,8 @@ class TwoPartyTradeProtocolTests {
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY) val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
bobNode.services.fillWithSomeTestCash(2000.DOLLARS) bobNode.services.fillWithSomeTestCash(2000.DOLLARS)
val issuer = bobNode.services.storageService.myLegalIdentity.ref(0)
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
1200.DOLLARS `issued by` issuer, notaryNode.info.identity, null).second 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, notaryNode.info.identity, null).second
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey, notaryNode.storage.myLegalIdentityKey) insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey, notaryNode.storage.myLegalIdentityKey)
@ -134,7 +130,6 @@ class TwoPartyTradeProtocolTests {
@Test @Test
fun `shutdown and restore`() { fun `shutdown and restore`() {
ledger { ledger {
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY) val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
@ -142,13 +137,12 @@ class TwoPartyTradeProtocolTests {
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
val networkMapAddr = notaryNode.info val networkMapAddr = notaryNode.info
val issuer = bobNode.services.storageService.myLegalIdentity.ref(0)
net.runNetwork() // Clear network map registration messages net.runNetwork() // Clear network map registration messages
bobNode.services.fillWithSomeTestCash(2000.DOLLARS) bobNode.services.fillWithSomeTestCash(2000.DOLLARS)
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
1200.DOLLARS `issued by` issuer, notaryNode.info.identity, null).second 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, notaryNode.info.identity, null).second
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey) insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue() val buyerSessionID = random63BitValue()

View File

@ -1,14 +1,14 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.cashBalances import com.r3corda.contracts.asset.cashBalances
import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.WalletService
import com.r3corda.core.node.services.testing.MockKeyManagementService
import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.node.services.testing.UnitTestServices
import com.r3corda.core.testing.* import com.r3corda.core.testing.*
import com.r3corda.core.utilities.BriefLogFormatter import com.r3corda.core.utilities.BriefLogFormatter
import com.r3corda.node.services.wallet.NodeWalletService import com.r3corda.node.services.wallet.NodeWalletService
@ -23,11 +23,22 @@ import kotlin.test.assertNull
// TODO: Move this to the cash contract tests once mock services are further split up. // TODO: Move this to the cash contract tests once mock services are further split up.
class WalletWithCashTest { class WalletWithCashTest {
val kms = MockKeyManagementService(ALICE_KEY) lateinit var services: UnitTestServices
val wallet: WalletService get() = services.walletService
@Before @Before
fun setUp() { fun setUp() {
BriefLogFormatter.loggingOn(NodeWalletService::class) BriefLogFormatter.loggingOn(NodeWalletService::class)
services = object : UnitTestServices() {
override val walletService: WalletService = NodeWalletService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
walletService.notify(stx.tx)
}
}
}
} }
@After @After
@ -35,37 +46,24 @@ class WalletWithCashTest {
BriefLogFormatter.loggingOff(NodeWalletService::class) BriefLogFormatter.loggingOff(NodeWalletService::class)
} }
fun make(): Pair<NodeWalletService, ServiceHub> {
val services = MockServices(keyManagement = kms)
return Pair(services.walletService as NodeWalletService, services)
}
@Test @Test
fun splits() { fun splits() {
val (wallet, services) = make()
val ref = OpaqueBytes(ByteArray(1, {0}))
kms.nextKeys += Array(3) { ALICE_KEY }
// Fix the PRNG so that we get the same splits every time. // Fix the PRNG so that we get the same splits every time.
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L), ref) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
val w = wallet.currentWallet val w = wallet.currentWallet
assertEquals(3, w.states.size) assertEquals(3, w.states.size)
val state = w.states[0].state.data as Cash.State val state = w.states[0].state.data as Cash.State
val myIdentity = services.storageService.myLegalIdentity assertEquals(29.01.DOLLARS `issued by` DUMMY_CASH_ISSUER, state.amount)
val myPartyRef = myIdentity.ref(ref) assertEquals(services.key.public, state.owner)
assertEquals(29.01.DOLLARS `issued by` myPartyRef, state.amount)
assertEquals(ALICE_PUBKEY, state.owner)
assertEquals(35.38.DOLLARS `issued by` myPartyRef, (w.states[2].state.data as Cash.State).amount) assertEquals(35.38.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.states[2].state.data as Cash.State).amount)
assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state.data as Cash.State).amount) assertEquals(35.61.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.states[1].state.data as Cash.State).amount)
} }
@Test @Test
fun basics() { fun basics() {
val (wallet, services) = make()
// A tx that sends us money. // A tx that sends us money.
val freshKey = services.keyManagementService.freshKey() val freshKey = services.keyManagementService.freshKey()
val usefulTX = TransactionType.General.Builder().apply { val usefulTX = TransactionType.General.Builder().apply {
@ -102,10 +100,7 @@ class WalletWithCashTest {
@Test @Test
fun branchingLinearStatesFails() { fun branchingLinearStatesFails() {
val (wallet, services) = make()
val freshKey = services.keyManagementService.freshKey() val freshKey = services.keyManagementService.freshKey()
val thread = SecureHash.sha256("thread") val thread = SecureHash.sha256("thread")
// Issue a linear state // Issue a linear state
@ -131,8 +126,6 @@ class WalletWithCashTest {
@Test @Test
fun sequencingLinearStatesWorks() { fun sequencingLinearStatesWorks() {
val (wallet, services) = make()
val freshKey = services.keyManagementService.freshKey() val freshKey = services.keyManagementService.freshKey()
val thread = SecureHash.sha256("thread") val thread = SecureHash.sha256("thread")

View File

@ -6,9 +6,6 @@ import com.r3corda.core.testing.utilities.TestTimestamp
import com.r3corda.core.testing.utilities.assertExitOrKill import com.r3corda.core.testing.utilities.assertExitOrKill
import com.r3corda.core.testing.utilities.spawn import com.r3corda.core.testing.utilities.spawn
import org.junit.Test import org.junit.Test
import java.nio.file.Paths
import java.text.SimpleDateFormat
import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
class TraderDemoTest { class TraderDemoTest {

View File

@ -136,7 +136,6 @@ fun runTraderDemo(args: Array<String>): Int {
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network // One of the two servers needs to run the network map and notary services. In such a trivial two-node network
// the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's // the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's
// the side that sticks around waiting for the seller. // the side that sticks around waiting for the seller.
var cashIssuer: Party? = null
val networkMapId = if (role == Role.BUYER) { val networkMapId = if (role == Role.BUYER) {
advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type) advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type)
null null
@ -148,7 +147,6 @@ fun runTraderDemo(args: Array<String>): Int {
val path = Paths.get(baseDirectory, Role.BUYER.name.toLowerCase(), "identity-public") val path = Paths.get(baseDirectory, Role.BUYER.name.toLowerCase(), "identity-public")
val party = Files.readAllBytes(path).deserialize<Party>() val party = Files.readAllBytes(path).deserialize<Party>()
advertisedServices = emptySet() advertisedServices = emptySet()
cashIssuer = party
NodeInfo(ArtemisMessagingService.makeRecipient(theirNetAddr), party, setOf(NetworkMapService.Type)) NodeInfo(ArtemisMessagingService.makeRecipient(theirNetAddr), party, setOf(NetworkMapService.Type))
} }
@ -157,18 +155,15 @@ fun runTraderDemo(args: Array<String>): Int {
Node(directory, myNetAddr, apiNetAddr, config, networkMapId, advertisedServices).setup().start() Node(directory, myNetAddr, apiNetAddr, config, networkMapId, advertisedServices).setup().start()
} }
// TODO: Replace with a separate trusted cash issuer
if (cashIssuer == null) {
cashIssuer = node.services.storageService.myLegalIdentity
}
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role // What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
// will contact the buyer and actually make something happen. // will contact the buyer and actually make something happen.
val amount = 1000.DOLLARS val amount = 1000.DOLLARS
if (role == Role.BUYER) { if (role == Role.BUYER) {
runBuyer(node, amount) runBuyer(node, amount)
} else { } else {
runSeller(node, amount, cashIssuer) node.networkMapRegistrationFuture.get()
val party = node.netMapCache.getNodeByLegalName("Bank A")?.identity ?: throw IllegalStateException("Cannot find other node?!")
runSeller(node, amount, party)
} }
return 0 return 0
@ -213,7 +208,9 @@ private fun runBuyer(node: Node, amount: Amount<Currency>) {
// Self issue some cash. // Self issue some cash.
// //
// TODO: At some point this demo should be extended to have a central bank node. // TODO: At some point this demo should be extended to have a central bank node.
node.services.fillWithSomeTestCash(3000.DOLLARS, node.info.identity) node.services.fillWithSomeTestCash(3000.DOLLARS,
notary = node.info.identity, // In this demo, the buyer and notary are the same.
ownedBy = node.services.keyManagementService.freshKey().public)
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band // 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 // via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
@ -250,7 +247,7 @@ private class TraderDemoProtocolBuyer(val otherSide: Party,
progressTracker.currentStep = STARTING_BUY progressTracker.currentStep = STARTING_BUY
send(otherSide, 0, sessionID) send(otherSide, 0, sessionID)
val notary = serviceHub.networkMapCache.notaryNodes[0] val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val buyer = TwoPartyTradeProtocol.Buyer( val buyer = TwoPartyTradeProtocol.Buyer(
otherSide, otherSide,
notary.identity, notary.identity,
@ -323,7 +320,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party,
progressTracker.currentStep = SELF_ISSUING progressTracker.currentStep = SELF_ISSUING
val notary = serviceHub.networkMapCache.notaryNodes[0] val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val cpOwnerKey = serviceHub.keyManagementService.freshKey() val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary) val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary)