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.KeyPair
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
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
|
* 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.
|
* keys in this wallet, you must inform the wallet service so it can update its internal state.
|
||||||
*/
|
*/
|
||||||
val currentWallet: Wallet
|
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)
|
storage = makeStorageService(dir)
|
||||||
net = makeMessagingService()
|
net = makeMessagingService()
|
||||||
smm = StateMachineManager(services, serverThread)
|
smm = StateMachineManager(services, serverThread)
|
||||||
wallet = E2ETestWalletService(services)
|
wallet = NodeWalletService(services)
|
||||||
keyManagement = E2ETestKeyManagementService()
|
keyManagement = E2ETestKeyManagementService()
|
||||||
|
|
||||||
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are
|
// 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 contracts.Cash
|
||||||
import core.*
|
import core.*
|
||||||
|
import core.utilities.loggerFor
|
||||||
|
import core.utilities.trace
|
||||||
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
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.
|
* states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@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
|
// 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
|
// inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference
|
||||||
// to wallet somewhere.
|
// to wallet somewhere.
|
||||||
@ -30,6 +35,71 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService {
|
|||||||
|
|
||||||
override val currentWallet: Wallet get() = mutex.locked { wallet }
|
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
|
* 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.
|
* 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
|
* 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
|
* 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 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 amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||||
|
|
||||||
val myIdentity = services.storageService.myLegalIdentity
|
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.
|
// TODO: Centralise the process of transaction acceptance and filtering into the wallet, then move this out.
|
||||||
services.storageService.validatedTransactions.putAll(transactions.associateBy { it.id })
|
services.storageService.validatedTransactions.putAll(transactions.associateBy { it.id })
|
||||||
|
|
||||||
val statesAndRefs = transactions.map {
|
return notifyAll(transactions.map { it.tx })
|
||||||
StateAndRef(it.tx.outputs[0] as OwnableState, StateRef(it.id, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
mutex.locked {
|
|
||||||
wallet = wallet.copy(wallet.states + statesAndRefs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray {
|
private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray {
|
@ -123,7 +123,7 @@ class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
|||||||
override fun call() {
|
override fun call() {
|
||||||
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
|
// 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.
|
// 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) {
|
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
|
// 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 {
|
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)
|
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