First pass trivial wallet: basic tracking of relevant states.

This commit is contained in:
Mike Hearn 2016-02-29 22:02:21 +01:00
parent 02e9473201
commit 1330f33aba
7 changed files with 195 additions and 57 deletions

View File

@ -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))
}
/**

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}
}

View 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.
}
}