mirror of
https://github.com/corda/corda.git
synced 2025-03-21 11:35:57 +00:00
First pass trivial wallet: basic tracking of relevant states.
This commit is contained in:
parent
02e9473201
commit
1330f33aba
@ -16,6 +16,7 @@ import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This file defines various 'services' which are not currently fleshed out. A service is a module that provides
|
||||
@ -45,6 +46,25 @@ interface WalletService {
|
||||
* keys in this wallet, you must inform the wallet service so it can update its internal state.
|
||||
*/
|
||||
val currentWallet: Wallet
|
||||
|
||||
/**
|
||||
* Returns a snapshot of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null, not 0.
|
||||
*/
|
||||
val cashBalances: Map<Currency, Amount>
|
||||
|
||||
/**
|
||||
* Possibly update the wallet by marking as spent states that these transactions consume, and adding any relevant
|
||||
* new states that they create. You should only insert transactions that have been successfully verified here!
|
||||
*
|
||||
* Returns the new wallet that resulted from applying the transactions (note: it may quickly become out of date).
|
||||
*
|
||||
* TODO: Consider if there's a good way to enforce the must-be-verified requirement in the type system.
|
||||
*/
|
||||
fun notifyAll(txns: Iterable<WireTransaction>): Wallet
|
||||
|
||||
/** Same as notifyAll but with a single transaction. */
|
||||
fun notify(tx: WireTransaction): Wallet = notifyAll(listOf(tx))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,7 +86,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
storage = makeStorageService(dir)
|
||||
net = makeMessagingService()
|
||||
smm = StateMachineManager(services, serverThread)
|
||||
wallet = E2ETestWalletService(services)
|
||||
wallet = NodeWalletService(services)
|
||||
keyManagement = E2ETestKeyManagementService()
|
||||
|
||||
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are
|
||||
|
@ -10,6 +10,9 @@ package core.node
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
@ -19,7 +22,9 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class E2ETestWalletService(private val services: ServiceHub) : WalletService {
|
||||
class NodeWalletService(private val services: ServiceHub) : WalletService {
|
||||
private val log = loggerFor<NodeWalletService>()
|
||||
|
||||
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
||||
// inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference
|
||||
// to wallet somewhere.
|
||||
@ -30,6 +35,71 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService {
|
||||
|
||||
override val currentWallet: Wallet get() = mutex.locked { wallet }
|
||||
|
||||
/**
|
||||
* Returns a snapshot of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null, not 0.
|
||||
*/
|
||||
override val cashBalances: Map<Currency, Amount>
|
||||
get() = mutex.locked { wallet }.let { wallet ->
|
||||
wallet.states.
|
||||
// Select the states we own which are cash, ignore the rest, take the amounts.
|
||||
mapNotNull { (it.state as? Cash.State)?.amount }.
|
||||
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.currency }.
|
||||
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
|
||||
mapValues { it.value.sumOrThrow() }
|
||||
}
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
||||
val ourKeys = services.keyManagementService.keys.keys
|
||||
|
||||
// Note how terribly incomplete this all is!
|
||||
//
|
||||
// - We don't notify anyone of anything, there are no event listeners.
|
||||
// - We don't handle or even notice invalidations due to double spends of things in our wallet.
|
||||
// - We have no concept of confidence (for txns where there is no definite finality).
|
||||
// - No notification that keys are used, for the case where we observe a spend of our own states.
|
||||
// - No ability to create complex spends.
|
||||
// - No logging or tracking of how the wallet got into this state.
|
||||
// - No persistence.
|
||||
// - Does tx relevancy calculation and key management need to be interlocked? Probably yes.
|
||||
//
|
||||
// ... and many other things .... (Wallet.java in bitcoinj is several thousand lines long)
|
||||
|
||||
mutex.locked {
|
||||
// Starting from the current wallet, keep applying the transaction updates, calculating a new Wallet each
|
||||
// time, until we get to the result (this is perhaps a bit inefficient, but it's functional and easily
|
||||
// unit tested).
|
||||
wallet = txns.fold(currentWallet) { current, tx -> current.update(tx, ourKeys) }
|
||||
return wallet
|
||||
}
|
||||
}
|
||||
|
||||
private fun Wallet.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Wallet {
|
||||
val ourNewStates = tx.outputs.
|
||||
filterIsInstance<OwnableState>().
|
||||
filter { it.owner in ourKeys }.
|
||||
map { tx.outRef<OwnableState>(it) }
|
||||
|
||||
// Now calculate the states that are being spent by this transaction.
|
||||
val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs)
|
||||
|
||||
// Is transaction irrelevant?
|
||||
if (consumed.isEmpty() && ourNewStates.isEmpty()) {
|
||||
log.trace { "tx ${tx.id} was irrelevant to this wallet, ignoring" }
|
||||
return this
|
||||
}
|
||||
|
||||
// And calculate the new wallet.
|
||||
val newStates = states.filter { it.ref !in consumed } + ourNewStates
|
||||
|
||||
log.trace {
|
||||
"Applied tx ${tx.id.prefixChars()} to the wallet: consumed ${consumed.size} states and added ${newStates.size}"
|
||||
}
|
||||
|
||||
return Wallet(newStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -37,8 +107,11 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService {
|
||||
* 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.
|
||||
*
|
||||
* TODO: Move this out of NodeWalletService
|
||||
*/
|
||||
fun fillWithSomeTestCash(howMuch: Amount, atLeastThisManyStates: Int = 3, atMostThisManyStates: Int = 10, rng: Random = Random()) {
|
||||
fun fillWithSomeTestCash(howMuch: Amount, atLeastThisManyStates: Int = 3, atMostThisManyStates: Int = 10,
|
||||
rng: Random = Random()): Wallet {
|
||||
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||
|
||||
val myIdentity = services.storageService.myLegalIdentity
|
||||
@ -62,13 +135,7 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService {
|
||||
// TODO: Centralise the process of transaction acceptance and filtering into the wallet, then move this out.
|
||||
services.storageService.validatedTransactions.putAll(transactions.associateBy { it.id })
|
||||
|
||||
val statesAndRefs = transactions.map {
|
||||
StateAndRef(it.tx.outputs[0] as OwnableState, StateRef(it.id, 0))
|
||||
}
|
||||
|
||||
mutex.locked {
|
||||
wallet = wallet.copy(wallet.states + statesAndRefs)
|
||||
}
|
||||
return notifyAll(transactions.map { it.tx })
|
||||
}
|
||||
|
||||
private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray {
|
@ -123,7 +123,7 @@ class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
||||
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)
|
||||
(serviceHub.walletService as NodeWalletService).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
|
||||
|
@ -75,6 +75,13 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementServi
|
||||
}
|
||||
|
||||
class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService {
|
||||
override val cashBalances: Map<Currency, Amount>
|
||||
get() = TODO("Use NodeWalletService instead")
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
||||
TODO("Use NodeWalletService instead")
|
||||
}
|
||||
|
||||
override val currentWallet = Wallet(states)
|
||||
}
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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 core.node
|
||||
|
||||
import contracts.Cash
|
||||
import core.DOLLARS
|
||||
import core.MockKeyManagementService
|
||||
import core.MockServices
|
||||
import core.ServiceHub
|
||||
import core.testutils.ALICE
|
||||
import core.testutils.ALICE_KEY
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class E2ETestWalletServiceTest {
|
||||
val kms = MockKeyManagementService()
|
||||
val services: ServiceHub = MockServices(
|
||||
keyManagement = kms
|
||||
)
|
||||
|
||||
@Test fun splits() {
|
||||
val wallet = E2ETestWalletService(services)
|
||||
kms.nextKeys += Array(3) { ALICE_KEY }
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
wallet.fillWithSomeTestCash(100.DOLLARS, 3, 3, Random(0L))
|
||||
|
||||
val w = wallet.currentWallet
|
||||
assertEquals(3, w.states.size)
|
||||
|
||||
val state = w.states[0].state as Cash.State
|
||||
assertEquals(services.storageService.myLegalIdentity, state.deposit.party)
|
||||
assertEquals(services.storageService.myLegalIdentityKey.public, state.deposit.party.owningKey)
|
||||
assertEquals(29.01.DOLLARS, state.amount)
|
||||
assertEquals(ALICE, state.owner)
|
||||
|
||||
assertEquals(33.34.DOLLARS, (w.states[2].state as Cash.State).amount)
|
||||
assertEquals(35.61.DOLLARS, (w.states[1].state as Cash.State).amount)
|
||||
}
|
||||
}
|
90
src/test/kotlin/core/node/NodeWalletServiceTest.kt
Normal file
90
src/test/kotlin/core/node/NodeWalletServiceTest.kt
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 core.node
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class NodeWalletServiceTest {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
val services: ServiceHub = MockServices(keyManagement = kms)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
BriefLogFormatter.loggingOn(NodeWalletService::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
BriefLogFormatter.loggingOff(NodeWalletService::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun splits() {
|
||||
val wallet = NodeWalletService(services)
|
||||
kms.nextKeys += Array(3) { ALICE_KEY }
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
wallet.fillWithSomeTestCash(100.DOLLARS, 3, 3, Random(0L))
|
||||
|
||||
val w = wallet.currentWallet
|
||||
assertEquals(3, w.states.size)
|
||||
|
||||
val state = w.states[0].state as Cash.State
|
||||
assertEquals(services.storageService.myLegalIdentity, state.deposit.party)
|
||||
assertEquals(services.storageService.myLegalIdentityKey.public, state.deposit.party.owningKey)
|
||||
assertEquals(29.01.DOLLARS, state.amount)
|
||||
assertEquals(ALICE, state.owner)
|
||||
|
||||
assertEquals(33.34.DOLLARS, (w.states[2].state as Cash.State).amount)
|
||||
assertEquals(35.61.DOLLARS, (w.states[1].state as Cash.State).amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basics() {
|
||||
val wallet = NodeWalletService(services)
|
||||
|
||||
// A tx that sends us money.
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val usefulTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService).outRef<Cash.State>(0)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder().apply {
|
||||
Cash().generateSpend(this, 80.DOLLARS, BOB, listOf(myOutput))
|
||||
signWith(freshKey)
|
||||
}.toSignedTransaction()
|
||||
|
||||
// A tx that doesn't send us anything.
|
||||
val irrelevantTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), BOB_KEY.public)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
assertNull(wallet.cashBalances[USD])
|
||||
wallet.notify(usefulTX.tx)
|
||||
assertEquals(100.DOLLARS, wallet.cashBalances[USD])
|
||||
wallet.notify(irrelevantTX.tx)
|
||||
assertEquals(100.DOLLARS, wallet.cashBalances[USD])
|
||||
wallet.notify(spendTX.tx)
|
||||
assertEquals(20.DOLLARS, wallet.cashBalances[USD])
|
||||
|
||||
// TODO: Flesh out these tests as needed.
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user