Re-factoring of CashBalances code (moved to VaultService)

Re-factoring of OnLedgerAsset generateSpend code (moved to VaultService)

Fixed broken tests caused by missing Transaction Context (when moving from InMemory to Db implementation of vault service in MockNetwork)

Adapted all Contract tests that perform Cash Spending to use the Vault Service.
Note: pending resolution of 2 failing tests (IRSSimulation, CP issue, move & redeem)

Fixed items raised by MH in CRD-CR-58 code review.

Decommissioned InMemoryVaultService service (all dependent Tests updated to use NodeVaultService)

Merge remote-tracking branch 'remotes/origin/master' into colljos-vault-code-clean-up-refactor

Fixed conflict.

Fixed failing Commercial Paper test.

FungibleAsset reverted back to original filename.
This commit is contained in:
Jose Coll 2016-10-17 11:16:53 +01:00
parent 63b1e356ad
commit bd21326bc3
27 changed files with 494 additions and 419 deletions

View File

@ -7,6 +7,7 @@ import com.r3corda.core.contracts.Timestamp;
import com.r3corda.core.contracts.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*;
import com.r3corda.core.node.services.*;
import com.r3corda.core.transactions.*;
import kotlin.*;
import org.jetbrains.annotations.*;
@ -304,8 +305,8 @@ public class JavaCommercialPaper implements Contract {
return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
}
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> vault) throws InsufficientBalanceException {
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), vault, null);
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
tx.addInputState(paper);
tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner()));
}

View File

@ -1,8 +1,8 @@
package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.FungibleAsset
import com.r3corda.core.contracts.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.core.contracts.*
@ -14,6 +14,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toBase58String
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.random63BitValue
import com.r3corda.core.schemas.MappedSchema
import com.r3corda.core.schemas.PersistentState
@ -218,10 +219,10 @@ class CommercialPaper : Contract {
* @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer.
*/
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: List<StateAndRef<Cash.State>>) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
// Add the cash movement using the states in our vault.
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
Cash().generateSpend(tx, amount, paper.state.data.owner, vault)
vault.generateSpend(tx, amount, paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner)
}

View File

@ -1,7 +1,7 @@
package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullPublicKey
@ -9,6 +9,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
@ -125,10 +126,9 @@ class CommercialPaperLegacy : Contract {
}
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: Vault) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
// Add the cash movement using the states in our vault.
Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(),
paper.state.data.owner, vault.statesOfType<Cash.State>())
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner))
}

View File

@ -196,18 +196,6 @@ fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Is
return filterIsInstance<Cash.State>().map { it.amount }.sumOrZero(currency)
}
/**
* Returns a map 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 present in map), not 0.
*/
val Vault.cashBalances: Map<Currency, Amount<Currency>> get() = states.
// Select the states we own which are cash, ignore the rest, take the amounts.
mapNotNull { (it.state.data as? Cash.State)?.amount }.
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
groupBy { it.token.product }.
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() }
fun Cash.State.ownedBy(owner: PublicKey) = copy(owner = owner)
fun Cash.State.issuedBy(party: Party) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party))))
fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit)))

View File

@ -47,25 +47,6 @@ abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Co
generateExitCommand = { amount -> generateExitCommand(amount) }
)
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the vault is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null): List<PublicKey>
= conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { generateMoveCommand() })
abstract fun generateExitCommand(amount: Amount<Issued<T>>): FungibleAsset.Commands.Exit<T>
abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue
abstract fun generateMoveCommand(): FungibleAsset.Commands.Move

View File

@ -1,9 +1,9 @@
package com.r3corda.contracts.clause
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumFungibleOrNull
import com.r3corda.contracts.asset.sumFungibleOrZero
import com.r3corda.core.contracts.FungibleAsset
import com.r3corda.core.contracts.InsufficientBalanceException
import com.r3corda.core.contracts.sumFungibleOrNull
import com.r3corda.core.contracts.sumFungibleOrZero
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party
@ -85,90 +85,6 @@ abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T :
return amountIssued.token.issuer.party.owningKey
}
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the vault is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null,
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "vault", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val currency = amount.token
var acceptableCoins = run {
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
tx.notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions spending inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
deriveState(coins.first().state, totalAmount, to)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
deriveState(gathered.last().state, change, changeKey)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(generateMoveCommand(), keysList)
return keysList
}
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,

View File

@ -1,6 +1,6 @@
package com.r3corda.contracts.clause
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.core.contracts.FungibleAsset
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.Clause

View File

@ -6,6 +6,7 @@ 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.Issued
import com.r3corda.core.contracts.PartyAndReference
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party
@ -14,6 +15,7 @@ import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.utilities.DUMMY_NOTARY
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
@ -34,7 +36,9 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
atMostThisManyStates: Int = 10,
rng: Random = Random(),
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
ownedBy: PublicKey? = null): Vault {
ownedBy: PublicKey? = null,
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER,
issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault {
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
val myKey: PublicKey = ownedBy ?: myInfo.legalIdentity.owningKey
@ -43,8 +47,8 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
val cash = Cash()
val transactions: List<SignedTransaction> = amounts.map { pennies ->
val issuance = TransactionType.General.Builder(null)
cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, outputNotary)
issuance.signWith(DUMMY_CASH_ISSUER_KEY)
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), myKey, outputNotary)
issuance.signWith(issuerKey)
return@map issuance.toSignedTransaction(true)
}

View File

@ -239,26 +239,27 @@ object TwoPartyTradeProtocol {
private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
val ptx = TransactionType.General.Builder(notary)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
val vault = serviceHub.vaultService.currentVault
val cashStates = vault.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey)
// Add inputs/outputs/a command for the movement of the asset.
ptx.addInputState(tradeRequest.assetForSale)
tx.addInputState(tradeRequest.assetForSale)
// Just pick some new public key for now. This won't be linked with our identity in any way, which is what
// we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
// reveal who the owner actually is. The key management service is expected to derive a unique key from some
// initial seed in order to provide privacy protection.
val freshKey = serviceHub.keyManagementService.freshKey()
val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public)
ptx.addOutputState(state, tradeRequest.assetForSale.state.notary)
ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner)
tx.addOutputState(state, tradeRequest.assetForSale.state.notary)
tx.addCommand(command, tradeRequest.assetForSale.state.data.owner)
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one.
val currentTime = serviceHub.clock.instant()
ptx.setTime(currentTime, 30.seconds)
return Pair(ptx, cashSigningPubKeys)
tx.setTime(currentTime, 30.seconds)
return Pair(tx, cashSigningPubKeys)
}
}
}

View File

@ -33,7 +33,7 @@ public class CashTestsJava {
tx.tweak(tw -> {
tw.output(outState);
// No command arguments
return tw.failsWith("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command");
return tw.failsWith("required com.r3corda.core.contracts.FungibleAsset.Commands.Move command");
});
tx.tweak(tw -> {
tw.output(outState);

View File

@ -7,6 +7,8 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days
import com.r3corda.core.node.recordTransactions
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.seconds
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.transactions.SignedTransaction
@ -14,8 +16,12 @@ import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.TEST_TX_TIME
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.*
import com.r3corda.testing.node.makeTestDataSourceProperties
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@ -197,13 +203,61 @@ class CommercialPaperTestsGeneric {
return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
}
/**
* Unit test requires two separate Database instances to represent each of the two
* transaction participants (enforces uniqueness of vault content in lieu of partipant identity)
*/
private lateinit var bigCorpServices: MockServices
private lateinit var bigCorpVault: Vault
private lateinit var bigCorpVaultService: VaultService
private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault
private lateinit var moveTX: SignedTransaction
@Test
fun `issue move and then redeem`() {
val aliceServices = MockServices()
val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS)
val bigCorpServices = MockServices()
val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS)
val dataSourceAndDatabaseAlice = configureDatabase(makeTestDataSourceProperties())
val databaseAlice = dataSourceAndDatabaseAlice.second
databaseTransaction(databaseAlice) {
aliceServices = object : MockServices() {
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
aliceVaultService = aliceServices.vaultService
}
val dataSourceAndDatabaseBigCorp = configureDatabase(makeTestDataSourceProperties())
val databaseBigCorp = dataSourceAndDatabaseBigCorp.second
databaseTransaction(databaseBigCorp) {
bigCorpServices = object : MockServices() {
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
bigCorpVaultService = bigCorpServices.vaultService
}
// Propagate the cash transactions to each side.
aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
@ -213,48 +267,52 @@ class CommercialPaperTestsGeneric {
val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
val issuance = bigCorpServices.myInfo.legalIdentity.ref(1)
val issueTX: SignedTransaction =
CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
setTime(TEST_TX_TIME, 30.seconds)
signWith(bigCorpServices.key)
signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction()
CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
setTime(TEST_TX_TIME, 30.seconds)
signWith(bigCorpServices.key)
signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction()
// Alice pays $9000 to BigCorp to own some of their debt.
val moveTX: SignedTransaction = run {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesVault.statesOfType<Cash.State>())
CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
ptx.signWith(bigCorpServices.key)
ptx.signWith(aliceServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
ptx.toSignedTransaction()
databaseTransaction(databaseAlice) {
// Alice pays $9000 to BigCorp to own some of their debt.
moveTX = run {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
aliceVaultService.generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public)
CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
ptx.signWith(bigCorpServices.key)
ptx.signWith(aliceServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
ptx.toSignedTransaction()
}
}
fun makeRedeemTX(time: Instant): SignedTransaction {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
ptx.setTime(time, 30.seconds)
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVault.statesOfType<Cash.State>())
ptx.signWith(aliceServices.key)
ptx.signWith(bigCorpServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
return ptx.toSignedTransaction()
databaseTransaction(databaseBigCorp) {
fun makeRedeemTX(time: Instant): SignedTransaction {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
ptx.setTime(time, 30.seconds)
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService)
ptx.signWith(aliceServices.key)
ptx.signWith(bigCorpServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
return ptx.toSignedTransaction()
}
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
// Verify the txns are valid and insert into both sides.
listOf(issueTX, moveTX).forEach {
it.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(it)
bigCorpServices.recordTransactions(it)
}
val e = assertFailsWith(TransactionVerificationException::class) {
tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
}
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
validRedemption.toLedgerTransaction(aliceServices).verify()
}
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
// Verify the txns are valid and insert into both sides.
listOf(issueTX, moveTX).forEach {
it.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(it)
bigCorpServices.recordTransactions(it)
}
val e = assertFailsWith(TransactionVerificationException::class) {
tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
}
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
validRedemption.toLedgerTransaction(aliceServices).verify()
}
}

View File

@ -1,15 +1,32 @@
package com.r3corda.contracts.asset
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.DUMMY_PUBKEY_2
import com.r3corda.core.utilities.LogHelper
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.*
import com.r3corda.testing.node.MockKeyManagementService
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.node.makeTestDataSourceProperties
import org.jetbrains.exposed.sql.Database
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.Closeable
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
import kotlin.test.*
@ -29,6 +46,51 @@ class CashTests {
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
)
lateinit var services: MockServices
val vault: VaultService get() = services.vaultService
lateinit var dataSource: Closeable
lateinit var database: Database
lateinit var vaultService: Vault
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
dataSource = dataSourceAndDatabase.first
database = dataSourceAndDatabase.second
databaseTransaction(database) {
services = object : MockServices() {
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1)
vaultService = services.vaultService.currentVault
}
}
@After
fun tearDown() {
LogHelper.reset(NodeVaultService::class)
dataSource.close()
}
@Test
fun trivial() {
transaction {
@ -42,7 +104,7 @@ class CashTests {
tweak {
output { outState }
// No command arguments
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
}
tweak {
output { outState }
@ -312,7 +374,7 @@ class CashTests {
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
@ -402,7 +464,9 @@ class CashTests {
//
// Spend tx generation
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
val OUR_KEY: KeyPair by lazy { generateKeyPair() }
val OUR_PUBKEY_1: PublicKey get() = OUR_KEY.public
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
fun makeCash(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1) =
@ -428,8 +492,10 @@ class CashTests {
}
fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction {
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(tx, amount, dest, WALLET)
var tx = TransactionType.General.Builder(DUMMY_NOTARY)
databaseTransaction(database) {
vault.generateSpend(tx, amount, dest)
}
return tx.toWireTransaction()
}
@ -485,58 +551,92 @@ class CashTests {
@Test
fun generateSimpleDirectSpend() {
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
databaseTransaction(database) {
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSimpleSpendWithParties() {
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
assertEquals(WALLET[2].ref, tx.inputStates()[0])
databaseTransaction(database) {
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP))
assertEquals(vaultService.states.elementAt(2).ref, tx.inputStates()[0])
}
}
@Test
fun generateSimpleSpendWithChange() {
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(WALLET[0].state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
databaseTransaction(database) {
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(vaultState.state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendWithTwoInputs() {
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[1].ref, wtx.inputs[1])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
databaseTransaction(database) {
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
val vaultState1 = vaultService.states.elementAt(1)
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendMixedDeposits() {
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
assertEquals(3, wtx.inputs.size)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[1].ref, wtx.inputs[1])
assertEquals(WALLET[2].ref, wtx.inputs[2])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(WALLET[2].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
databaseTransaction(database) {
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
assertEquals(3, wtx.inputs.size)
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
val vaultState1 = vaultService.states.elementAt(1)
val vaultState2 = vaultService.states.elementAt(2) as StateAndRef<Cash.State>
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState2.ref, wtx.inputs[2])
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendInsufficientBalance() {
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
}
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
assertFailsWith(InsufficientBalanceException::class) {
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
databaseTransaction(database) {
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
}
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
assertFailsWith(InsufficientBalanceException::class) {
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
}
}
}

View File

@ -61,7 +61,7 @@ class ObligationTests {
tweak {
output { outState }
// No command arguments
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
}
tweak {
output { outState }
@ -655,7 +655,7 @@ class ObligationTests {
tweak {
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef)) }
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
tweak {
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }

View File

@ -1,6 +1,5 @@
package com.r3corda.contracts.asset
package com.r3corda.core.contracts
import com.r3corda.core.contracts.*
import java.security.PublicKey
class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() {

View File

@ -467,3 +467,5 @@ interface Attachment : NamedByHash {
throw FileNotFoundException()
}
}

View File

@ -4,11 +4,13 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction
import rx.Observable
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
import java.util.*
/**
* Session ID to use for services listening for the first message in a session (before a
@ -100,6 +102,20 @@ interface VaultService {
*/
val updates: Observable<Vault.Update>
/**
* Returns a map 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 present in map), not 0.
*/
@Suppress("UNCHECKED_CAST")
val cashBalances: Map<Currency, Amount<Currency>>
get() = currentVault.states.
// Select the states we own which are cash, ignore the rest, take the amounts.
mapNotNull { (it.state.data as? FungibleAsset<Currency>)?.amount }.
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
groupBy { it.token.product }.
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() }
/**
* Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the
* first subscriber is registered so as to avoid racing with early updates.
@ -147,6 +163,15 @@ interface VaultService {
}
return future
}
/**
* Fungible Asset operations
**/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<Currency>,
to: PublicKey,
onlyFromParties: Set<Party>? = null): Pair<TransactionBuilder, List<PublicKey>>
}
inline fun <reified T : LinearState> VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java)

View File

@ -1,133 +0,0 @@
package com.r3corda.core.testing
import com.r3corda.core.ThreadBox
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.*
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace
import rx.Observable
import rx.subjects.PublishSubject
import java.security.PublicKey
import java.util.*
import javax.annotation.concurrent.ThreadSafe
/**
* This class implements a simple, in memory vault that tracks states that are owned by us, and also has a convenience
* method to auto-generate some self-issued cash states that can be used for test trading. A real vault would persist
* states relevant to us into a database and once such a vault is implemented, this scaffolding can be removed.
*/
@ThreadSafe
open class InMemoryVaultService(protected val services: ServiceHub) : SingletonSerializeAsToken(), VaultService {
open protected val log = loggerFor<InMemoryVaultService>()
// 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 vault somewhere.
protected class InnerState {
var vault = Vault(emptyList<StateAndRef<ContractState>>())
val _updatesPublisher = PublishSubject.create<Vault.Update>()
}
protected val mutex = ThreadBox(InnerState())
override val currentVault: Vault get() = mutex.locked { vault }
override val updates: Observable<Vault.Update>
get() = mutex.content._updatesPublisher
override fun track(): Pair<Vault, Observable<Vault.Update>> {
return mutex.locked {
Pair(vault, updates.bufferUntilSubscribed())
}
}
/**
* Returns a snapshot of the heads of LinearStates.
*/
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
get() = currentVault.let { vault ->
vault.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
}
override fun notifyAll(txns: Iterable<WireTransaction>): Vault {
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 vault.
// - 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 vault 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)
var netDelta = Vault.NoUpdate
val changedVault = mutex.locked {
// Starting from the current vault, keep applying the transaction updates, calculating a new vault each
// time, until we get to the result (this is perhaps a bit inefficient, but it's functional and easily
// unit tested).
val vaultAndNetDelta = txns.fold(Pair(currentVault, Vault.NoUpdate)) { vaultAndDelta, tx ->
val (vault, delta) = vaultAndDelta.first.update(tx, ourKeys)
val combinedDelta = delta + vaultAndDelta.second
Pair(vault, combinedDelta)
}
vault = vaultAndNetDelta.first
netDelta = vaultAndNetDelta.second
return@locked vault
}
if (netDelta != Vault.NoUpdate) {
mutex.locked {
_updatesPublisher.onNext(netDelta)
}
}
return changedVault
}
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>): Boolean {
return if (state is OwnableState) {
state.owner in ourKeys
} else if (state is LinearState) {
// It's potentially of interest to the vault
state.isRelevant(ourKeys)
} else {
false
}
}
private fun Vault.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Pair<Vault, Vault.Update> {
val ourNewStates = tx.outputs.
filter { isRelevant(it.data, ourKeys) }.
map { tx.outRef<ContractState>(it.data) }
// 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 vault, ignoring" }
return Pair(this, Vault.NoUpdate)
}
val change = Vault.Update(consumed, HashSet(ourNewStates))
// And calculate the new vault.
val newStates = states.filter { it.ref !in consumed } + ourNewStates
log.trace {
"Applied tx ${tx.id.prefixChars()} to the vault: consumed ${consumed.size} states and added ${newStates.size}"
}
return Pair(Vault(newStates), change)
}
}

View File

@ -1,7 +1,7 @@
package com.r3corda.node.internal
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.InsufficientBalanceException
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort
@ -79,21 +79,14 @@ class ServerRPCOps(
val builder: TransactionBuilder = TransactionType.General.Builder(null)
// TODO: Have some way of restricting this to states the caller controls
try {
val vaultCashStates = services.vaultService.currentVault.statesOfType<Cash.State>()
// TODO: Move cash state filtering by issuer down to the contract itself
val cashStatesOfRightCurrency = vaultCashStates.filter { it.state.data.amount.token == req.amount.token }
val keysForSigning = Cash().generateSpend(
tx = builder,
amount = req.amount.withoutIssuer(),
to = req.recipient.owningKey,
assetsStates = cashStatesOfRightCurrency,
onlyFromParties = setOf(req.amount.token.issuer.party)
)
val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey)
keysForSigning.forEach {
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
builder.signWith(KeyPair(it, key))
}
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false)
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted(
smm.add(protocol).id,

View File

@ -1,8 +1,7 @@
package com.r3corda.node.services.vault
import com.codahale.metrics.Gauge
import com.r3corda.contracts.asset.cashBalances
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.node.services.api.ServiceHubInternal
import java.util.*
@ -13,7 +12,7 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
init {
// TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded
serviceHubInternal.vaultService.updates.subscribe { update ->
exportCashBalancesViaMetrics(serviceHubInternal.vaultService.currentVault)
exportCashBalancesViaMetrics(serviceHubInternal.vaultService)
}
}
@ -24,7 +23,7 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
private val balanceMetrics = HashMap<Currency, BalanceMetric>()
private fun exportCashBalancesViaMetrics(vault: Vault) {
private fun exportCashBalancesViaMetrics(vault: VaultService) {
// This is just for demo purposes. We probably shouldn't expose balances via JMX in a real node as that might
// be commercially sensitive info that the sysadmins aren't even meant to know.
//

View File

@ -1,13 +1,16 @@
package com.r3corda.node.services.vault
import com.google.common.collect.Sets
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.ThreadBox
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace
@ -20,6 +23,7 @@ import org.jetbrains.exposed.sql.statements.InsertStatement
import rx.Observable
import rx.subjects.PublishSubject
import java.security.PublicKey
import java.util.*
/**
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
@ -106,6 +110,121 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
return currentVault
}
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
override fun generateSpend(tx: TransactionBuilder,
amount: Amount<Currency>,
to: PublicKey,
onlyFromParties: Set<Party>?): Pair<TransactionBuilder, List<PublicKey>> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "vault", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val assetsStates = currentVault.statesOfType<Cash.State>()
val currency = amount.token
var acceptableCoins = run {
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
tx.notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions spending inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
deriveState(coins.first().state, totalAmount, to)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
deriveState(gathered.last().state, change, changeKey)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(Cash().generateMoveCommand(), keysList)
// update Vault
// notify(tx.toWireTransaction())
// Vault update must be completed AFTER transaction is recorded to ledger storage!!!
// (this is accomplished within the recordTransaction function)
return Pair(tx, keysList)
}
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: PublicKey)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
/**
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun gatherCoins(acceptableCoins: Collection<StateAndRef<Cash.State>>,
amount: Amount<Currency>): Pair<ArrayList<StateAndRef<Cash.State>>, Amount<Currency>> {
val gathered = arrayListOf<StateAndRef<Cash.State>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
return Pair(gathered, gatheredAmount)
}
private fun makeUpdate(tx: WireTransaction, netDelta: Vault.Update, ourKeys: Set<PublicKey>): Vault.Update {
val ourNewStates = tx.outputs.
filter { isRelevant(it.data, ourKeys) }.
@ -140,4 +259,5 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
false
}
}
}

View File

@ -7,11 +7,11 @@ import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.node.internal.ServerRPCOps
import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents
import com.r3corda.testing.node.MockNetwork
@ -54,7 +54,9 @@ class ServerRPCTest {
val ref = OpaqueBytes(ByteArray(1) {1})
// Check the monitoring service wallet is empty
assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext())
databaseTransaction(aliceNode.database) {
assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext())
}
// Tell the monitoring service node to issue some cash
val recipient = aliceNode.info.legalIdentity

View File

@ -7,7 +7,6 @@ import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.*
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.testing.InMemoryVaultService
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.serialization.NodeClock
import com.r3corda.node.services.api.MessagingServiceInternal
@ -17,6 +16,7 @@ import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.node.services.persistence.DataVending
import com.r3corda.node.services.schema.NodeSchemaService
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.testing.MOCK_IDENTITY_SERVICE
import com.r3corda.testing.node.MockNetworkMapCache
import com.r3corda.testing.node.MockStorageService
@ -37,7 +37,7 @@ open class MockServiceHubInternal(
val protocolFactory: ProtocolLogicRefFactory? = ProtocolLogicRefFactory(),
val schemas: SchemaService? = NodeSchemaService()
) : ServiceHubInternal() {
override val vaultService: VaultService = customVault ?: InMemoryVaultService(this)
override val vaultService: VaultService = customVault ?: NodeVaultService(this)
override val keyManagementService: KeyManagementService
get() = keyManagement ?: throw UnsupportedOperationException()
override val identityService: IdentityService

View File

@ -6,23 +6,23 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.days
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.recordTransactions
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.ProtocolLogicRef
import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.node.services.events.NodeSchedulerService
import com.r3corda.node.services.persistence.DBCheckpointStorage
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.node.utilities.AffinityExecutor
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.ALICE_KEY
import com.r3corda.testing.node.InMemoryMessagingNetwork
import com.r3corda.testing.node.MockKeyManagementService
import com.r3corda.testing.node.TestClock
import com.r3corda.testing.node.makeTestDataSourceProperties
import com.r3corda.testing.node.*
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.exposed.sql.Database
import org.junit.After
@ -52,7 +52,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
val factory = ProtocolLogicRefFactory(mapOf(Pair(TestProtocolLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name))))
lateinit var services: MockServiceHubInternal
lateinit var services: MockServiceHubInternal
lateinit var scheduler: NodeSchedulerService
lateinit var smmExecutor: AffinityExecutor.ServiceAffinityExecutor
lateinit var dataSource: Closeable
@ -80,7 +81,20 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
dataSource = dataSourceAndDatabase.first
database = dataSourceAndDatabase.second
// Switched from InMemoryVault usage to NodeVault
databaseTransaction(database) {
val services1 = object : MockServices() {
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
vaultService.notify(stx.tx)
}
}
}
val kms = MockKeyManagementService(ALICE_KEY)
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database)
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
@ -265,19 +279,19 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
private fun scheduleTX(instant: Instant, increment: Int = 1): ScheduledStateRef? {
var scheduledRef: ScheduledStateRef? = null
apply {
val freshKey = services.keyManagementService.freshKey()
val state = TestState(factory.create(TestProtocolLogic::class.java, increment), instant)
val usefulTX = TransactionType.General.Builder(null).apply {
addOutputState(state, DUMMY_NOTARY)
addCommand(Command(), freshKey.public)
signWith(freshKey)
}.toSignedTransaction()
val txHash = usefulTX.id
databaseTransaction(database) {
apply {
val freshKey = services.keyManagementService.freshKey()
val state = TestState(factory.create(TestProtocolLogic::class.java, increment), instant)
val usefulTX = TransactionType.General.Builder(null).apply {
addOutputState(state, DUMMY_NOTARY)
addCommand(Command(), freshKey.public)
signWith(freshKey)
}.toSignedTransaction()
val txHash = usefulTX.id
services.recordTransactions(usefulTX)
scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant)
databaseTransaction(database) {
services.recordTransactions(usefulTX)
scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant)
scheduler.scheduleStateActivity(scheduledRef!!)
}
}

View File

@ -2,7 +2,6 @@ package com.r3corda.node.services
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.cashBalances
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.node.recordTransactions
@ -89,15 +88,19 @@ class VaultWithCashTest {
Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
signWith(MEGA_CORP_KEY)
}.toSignedTransaction()
val myOutput = usefulTX.toLedgerTransaction(services).outRef<Cash.State>(0)
assertNull(vault.cashBalances[USD])
services.recordTransactions(usefulTX)
// A tx that spends our money.
val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput))
vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY)
signWith(freshKey)
signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction()
assertEquals(100.DOLLARS, vault.cashBalances[USD])
// A tx that doesn't send us anything.
val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY)
@ -105,14 +108,11 @@ class VaultWithCashTest {
signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction()
assertNull(vault.currentVault.cashBalances[USD])
services.recordTransactions(usefulTX)
assertEquals(100.DOLLARS, vault.currentVault.cashBalances[USD])
services.recordTransactions(irrelevantTX)
assertEquals(100.DOLLARS, vault.currentVault.cashBalances[USD])
assertEquals(100.DOLLARS, vault.cashBalances[USD])
services.recordTransactions(spendTX)
assertEquals(20.DOLLARS, vault.currentVault.cashBalances[USD])
assertEquals(20.DOLLARS, vault.cashBalances[USD])
// TODO: Flesh out these tests as needed.
}

View File

@ -11,6 +11,7 @@ import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.node.services.persistence.DataVending.Service.NotifyTransactionHandler
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.protocols.BroadcastTransactionProtocol.NotifyTxRequest
import com.r3corda.testing.MEGA_CORP
import com.r3corda.testing.node.MockNetwork
@ -45,15 +46,17 @@ class DataVendingServiceTests {
val registerKey = registerNode.services.legalIdentityKey
ptx.signWith(registerKey)
val tx = ptx.toSignedTransaction()
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
databaseTransaction(vaultServiceNode.database) {
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
registerNode.sendNotifyTx(tx, vaultServiceNode)
registerNode.sendNotifyTx(tx, vaultServiceNode)
// Check the transaction is in the receiving node
val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull()
val expected = tx.tx.outRef<Cash.State>(0)
// Check the transaction is in the receiving node
val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull()
val expected = tx.tx.outRef<Cash.State>(0)
assertEquals(expected, actual)
assertEquals(expected, actual)
}
}
/**
@ -74,12 +77,14 @@ class DataVendingServiceTests {
val registerKey = registerNode.services.legalIdentityKey
ptx.signWith(registerKey)
val tx = ptx.toSignedTransaction(false)
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
databaseTransaction(vaultServiceNode.database) {
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
registerNode.sendNotifyTx(tx, vaultServiceNode)
registerNode.sendNotifyTx(tx, vaultServiceNode)
// Check the transaction is not in the receiving node
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
// Check the transaction is not in the receiving node
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
}
}
private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) {

View File

@ -5,7 +5,6 @@ import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.contracts.CommercialPaper
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.cashBalances
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
@ -258,7 +257,7 @@ private class TraderDemoProtocolBuyer(val otherSide: Party,
}
private fun logBalance() {
val balances = serviceHub.vaultService.currentVault.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" }
val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" }
logger.info("Remaining balance: ${balances.joinToString()}")
}

View File

@ -10,7 +10,6 @@ import com.r3corda.core.node.services.KeyManagementService
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.random63BitValue
import com.r3corda.core.testing.InMemoryVaultService
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.internal.AbstractNode
@ -23,6 +22,7 @@ import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.InMemoryUniquenessProvider
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.services.transactions.ValidatingNotaryService
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.AffinityExecutor
import com.r3corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import org.slf4j.Logger
@ -125,7 +125,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
override fun makeIdentityService() = MockIdentityService(mockNet.identities)
override fun makeVaultService(): VaultService = InMemoryVaultService(services)
override fun makeVaultService(): VaultService = NodeVaultService(services)
override fun makeKeyManagementService(): KeyManagementService = E2ETestKeyManagementService(partyKeys)