From bd21326bc3b64c7472219d2bbbbac8c91bcdc936 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 17 Oct 2016 11:16:53 +0100 Subject: [PATCH 01/31] 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. --- .../contracts/JavaCommercialPaper.java | 5 +- .../com/r3corda/contracts/CommercialPaper.kt | 9 +- .../contracts/CommercialPaperLegacy.kt | 8 +- .../com/r3corda/contracts/asset/Cash.kt | 12 -- .../r3corda/contracts/asset/OnLedgerAsset.kt | 19 -- .../clause/AbstractConserveAmount.kt | 92 +--------- .../contracts/clause/NoZeroSizedOutputs.kt | 2 +- .../r3corda/contracts/testing/VaultFiller.kt | 10 +- .../protocols/TwoPartyTradeProtocol.kt | 19 +- .../contracts/asset/CashTestsJava.java | 2 +- .../r3corda/contracts/CommercialPaperTests.kt | 144 ++++++++++----- .../com/r3corda/contracts/asset/CashTests.kt | 172 ++++++++++++++---- .../contracts/asset/ObligationTests.kt | 4 +- .../r3corda/core/contracts}/FungibleAsset.kt | 3 +- .../com/r3corda/core/contracts/Structures.kt | 4 +- .../r3corda/core/node/services/Services.kt | 25 +++ .../core/testing/InMemoryVaultService.kt | 133 -------------- .../com/r3corda/node/internal/ServerRPCOps.kt | 17 +- .../vault/CashBalanceAsMetricsObserver.kt | 7 +- .../node/services/vault/NodeVaultService.kt | 120 ++++++++++++ .../kotlin/com/r3corda/node/ServerRPCTest.kt | 6 +- .../node/services/MockServiceHubInternal.kt | 4 +- .../node/services/NodeSchedulerServiceTest.kt | 48 +++-- .../node/services/VaultWithCashTest.kt | 16 +- .../persistence/DataVendingServiceTests.kt | 25 ++- .../kotlin/com/r3corda/demos/TraderDemo.kt | 3 +- .../com/r3corda/testing/node/MockNode.kt | 4 +- 27 files changed, 494 insertions(+), 419 deletions(-) rename {contracts/src/main/kotlin/com/r3corda/contracts/asset => core/src/main/kotlin/com/r3corda/core/contracts}/FungibleAsset.kt (97%) delete mode 100644 core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index ca850ecf14..d344bd2b9f 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -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 paper, List> 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 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())); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 48f0105259..f157849159 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -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, vault: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, 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) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt index a4cc7e2e15..cc97eb7637 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt @@ -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, vault: Vault) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, 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()) + vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner)) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index d4fb1db7e9..423509073a 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -196,18 +196,6 @@ fun Iterable.sumCashOrZero(currency: Issued): Amount().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> 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> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.token.product }. - // Collapse to Map 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))) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt index 273df7bbec..9c97b1cc60 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -47,25 +47,6 @@ abstract class OnLedgerAsset> : 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, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null): List - = conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties, - deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, - generateMoveCommand = { generateMoveCommand() }) - abstract fun generateExitCommand(amount: Amount>): FungibleAsset.Commands.Exit abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue abstract fun generateMoveCommand(): FungibleAsset.Commands.Move diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index 5095f9cbf0..3313ff4215 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -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, 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, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, - generateMoveCommand: () -> CommandData): List { - // 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, outputs: List, diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt index f214587c89..366d7b9d30 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt @@ -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 diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt index 21e2e2ee4c..3d28dd4e72 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt @@ -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, 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, val cash = Cash() val transactions: List = 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) } diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 5756af476e..8ce6d421b8 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -239,26 +239,27 @@ object TwoPartyTradeProtocol { private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { 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() - 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) } } } \ No newline at end of file diff --git a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java index 28bb658bf0..651b5d306f 100644 --- a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java +++ b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java @@ -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); diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 8c4e3ec3c6..0e004b9f2a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -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) { + 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) { + 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()) - 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()) - 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() } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index e2fbcbaacf..7a9ca1c085 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -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) { + 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, corp: Party, depositRef: Byte = 1) = @@ -428,8 +492,10 @@ class CashTests { } fun makeSpend(amount: Amount, 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 + 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 + 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 + 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 + val vaultState1 = vaultService.states.elementAt(1) + val vaultState2 = vaultService.states.elementAt(2) as StateAndRef + 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) + } } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index 0eff89a578..e265a5bbb8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -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() } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt similarity index 97% rename from contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt rename to core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt index f1bed905f6..5f6be7908d 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt @@ -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() { diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index baa3c2e539..61ad26b0eb 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -466,4 +466,6 @@ interface Attachment : NamedByHash { } throw FileNotFoundException() } -} \ No newline at end of file +} + + diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index f1939180dd..322517c859 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -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 + /** + * 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> + get() = currentVault.states. + // Select the states we own which are cash, ignore the rest, take the amounts. + mapNotNull { (it.state.data as? FungibleAsset)?.amount }. + // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } + groupBy { it.token.product }. + // Collapse to Map 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, + to: PublicKey, + onlyFromParties: Set? = null): Pair> } inline fun VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt deleted file mode 100644 index f17c540f0d..0000000000 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ /dev/null @@ -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() - - // 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>()) - val _updatesPublisher = PublishSubject.create() - } - - protected val mutex = ThreadBox(InnerState()) - - override val currentVault: Vault get() = mutex.locked { vault } - - override val updates: Observable - get() = mutex.content._updatesPublisher - - override fun track(): Pair> { - return mutex.locked { - Pair(vault, updates.bufferUntilSubscribed()) - } - } - - /** - * Returns a snapshot of the heads of LinearStates. - */ - override val linearHeads: Map> - get() = currentVault.let { vault -> - vault.states.filterStatesOfType().associateBy { it.state.data.linearId }.mapValues { it.value } - } - - override fun notifyAll(txns: Iterable): 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): 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): Pair { - val ourNewStates = tx.outputs. - filter { isRelevant(it.data, ourKeys) }. - map { tx.outRef(it.data) } - - // Now calculate the states that are being spent by this transaction. - val consumed: Set = 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) - } -} diff --git a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt index 0b00a6fc6c..38b1ec0a7f 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -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() - // 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, diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt index 62db734f9f..01bcaad092 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt @@ -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() - 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. // diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt index 995826cac6..2e50747e49 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt @@ -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, + to: PublicKey, + onlyFromParties: Set?): Pair> { + // 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() + + 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, amount: Amount>, 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>, + amount: Amount): Pair>, Amount> { + val gathered = arrayListOf>() + 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): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }. @@ -140,4 +259,5 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT false } } + } diff --git a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt index fc6acf55b7..d0a40228f6 100644 --- a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt @@ -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 diff --git a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt index 24dcf112a7..7195123744 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt @@ -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 diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt index cf7194eb1c..9c6182ae7c 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -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) { + 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!!) } } diff --git a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt index 957ab779fb..73e5b8e7b5 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt @@ -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(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. } diff --git a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt index 5e03089070..e497be06de 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt @@ -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(0) + // Check the transaction is in the receiving node + val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull() + val expected = tx.tx.outRef(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) { diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index c5d63a2014..6fdee2b41c 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -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()}") } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt index 7cace026c4..9f5dd04315 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt @@ -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) From b3dc24d8b39e4d5df7c4205d59d047571eae27b0 Mon Sep 17 00:00:00 2001 From: RogerWillis Date: Fri, 28 Oct 2016 13:33:36 +0100 Subject: [PATCH 02/31] Fixed typo in 'client/build.gradle' --- client/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/build.gradle b/client/build.gradle index e304e4721d..ab4814856e 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -40,7 +40,7 @@ sourceSets { publishing { publications { - clients(MavenPublication) { + client(MavenPublication) { from components.java artifactId 'client' From c3ac4efa70c6e9084925310fd14a745ebc0a1b99 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Mon, 24 Oct 2016 15:05:18 +0100 Subject: [PATCH 03/31] Removed trader demo. --- .../kotlin/com/r3corda/demos/TraderDemo.kt | 381 ------------------ 1 file changed, 381 deletions(-) delete mode 100644 src/main/kotlin/com/r3corda/demos/TraderDemo.kt diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt deleted file mode 100644 index 6fdee2b41c..0000000000 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ /dev/null @@ -1,381 +0,0 @@ -package com.r3corda.demos - -import co.paralleluniverse.fibers.Suspendable -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.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.days -import com.r3corda.core.logElapsedTime -import com.r3corda.core.node.NodeInfo -import com.r3corda.core.node.services.ServiceInfo -import com.r3corda.core.protocols.ProtocolLogic -import com.r3corda.core.seconds -import com.r3corda.core.success -import com.r3corda.core.transactions.SignedTransaction -import com.r3corda.core.utilities.Emoji -import com.r3corda.core.utilities.LogHelper -import com.r3corda.core.utilities.ProgressTracker -import com.r3corda.node.internal.Node -import com.r3corda.node.services.config.ConfigHelper -import com.r3corda.node.services.config.FullNodeConfiguration -import com.r3corda.node.services.messaging.NodeMessagingClient -import com.r3corda.node.services.network.NetworkMapService -import com.r3corda.node.services.persistence.NodeAttachmentService -import com.r3corda.node.services.transactions.ValidatingNotaryService -import com.r3corda.node.utilities.databaseTransaction -import com.r3corda.protocols.NotaryProtocol -import com.r3corda.protocols.TwoPartyTradeProtocol -import joptsimple.OptionParser -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.nio.file.Path -import java.nio.file.Paths -import java.security.PublicKey -import java.time.Instant -import java.util.* -import kotlin.concurrent.thread -import kotlin.system.exitProcess -import kotlin.test.assertEquals - -// TRADING DEMO -// -// Please see docs/build/html/running-the-demos.html -// -// This program is a simple driver for exercising the two party trading protocol. Until Corda has a unified node server -// programs like this are required to wire up the pieces and run a demo scenario end to end. -// -// If you are creating a new scenario, you can use this program as a template for creating your own driver. Make sure to -// copy/paste the right parts of the build.gradle file to make sure it gets a script to run it deposited in -// build/install/r3prototyping/bin -// -// In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for the CP. The seller learns -// that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits -// and the buyer goes back to waiting. The buyer will sell as much CP as he can! -// -// The different roles in the scenario this program can adopt are: - -enum class Role { - BUYER, - SELLER -} - -// And this is the directory under the current working directory where each node will create its own server directory, -// which holds things like checkpoints, keys, databases, message logs etc. -val DEFAULT_BASE_DIRECTORY = "./build/trader-demo" - -private val log: Logger = LoggerFactory.getLogger("TraderDemo") - -fun main(args: Array) { - val parser = OptionParser() - - val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required() - val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost") - val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost") - val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost") - val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY) - val h2PortArg = parser.accepts("h2-port").withRequiredArg().ofType(Int::class.java).defaultsTo(-1) - val options = try { - parser.parse(*args) - } catch (e: Exception) { - log.error(e.message) - printHelp(parser) - exitProcess(1) - } - - val role = options.valueOf(roleArg)!! - - val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort( - when (role) { - Role.BUYER -> 31337 - Role.SELLER -> 31340 - } - ) - val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort( - when (role) { - Role.BUYER -> 31340 - Role.SELLER -> 31337 - } - ) - val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1) - val h2Port = if (options.valueOf(h2PortArg) < 0) { - myNetAddr.port + 2 - } else options.valueOf(h2PortArg) - - val baseDirectory = options.valueOf(baseDirectoryArg)!! - - // Suppress the Artemis MQ noise, and activate the demo logging. - // - // The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging - // for protocols will change in future. - LogHelper.setLevel("+demo.buyer", "+demo.seller", "-org.apache.activemq") - - val directory = Paths.get(baseDirectory, role.name.toLowerCase()) - log.info("Using base demo directory $directory") - - // Override the default config file (which you can find in the file "reference.conf") to give each node a name. - val config = run { - val myLegalName = when (role) { - Role.BUYER -> "Bank A" - Role.SELLER -> "Bank B" - } - val configOverrides = mapOf( - "myLegalName" to myLegalName, - "artemisAddress" to myNetAddr.toString(), - "webAddress" to apiNetAddr.toString(), - "h2port" to h2Port.toString() - ) - FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides)) - } - - // Which services will this instance of the node provide to the network? - val advertisedServices: Set - - // One of the two servers needs to run the network map and notary services. In such a trivial two-node network - // the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's - // the side that sticks around waiting for the seller. - val networkMapId = if (role == Role.BUYER) { - advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) - null - } else { - advertisedServices = emptySet() - NodeMessagingClient.makeNetworkMapAddress(theirNetAddr) - } - - // And now construct then start the node object. It takes a little while. - val node = logElapsedTime("Node startup", log) { - Node(config, networkMapId, advertisedServices).setup().start() - } - - // What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role - // will contact the buyer and actually make something happen. - val amount = 1000.DOLLARS - if (role == Role.BUYER) { - runBuyer(node, amount) - } else { - node.networkMapRegistrationFuture.success { - val party = node.netMapCache.getNodeByLegalName("Bank A")?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!") - runSeller(node, amount, party) - } - } - - node.run() -} - -private fun runSeller(node: Node, amount: Amount, otherSide: Party) { - // The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash. - // - // The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an - // attachment. Make sure we have the transaction prospectus attachment loaded into our store. - // - // This can also be done via an HTTP upload, but here we short-circuit and do it from code. - if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) { - TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use { - val id = node.storage.attachments.importAttachment(it) - assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id) - } - } - - val tradeTX: ListenableFuture - if (node.isPreviousCheckpointsPresent) { - tradeTX = node.smm.findStateMachines(TraderDemoProtocolSeller::class.java).single().second - } else { - val seller = TraderDemoProtocolSeller(otherSide, amount) - tradeTX = node.services.startProtocol(seller) - } - - tradeTX.success { - log.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(it.tx)}") - thread { - node.stop() - } - } -} - -private fun runBuyer(node: Node, amount: Amount) { - // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. - val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let { - it.automaticallyExtractAttachments = true - it.storePath - } - - // Self issue some cash. - // - // TODO: At some point this demo should be extended to have a central bank node. - databaseTransaction(node.database) { - node.services.fillWithSomeTestCash(300000.DOLLARS, - outputNotary = node.info.notaryIdentity, // In this demo, the buyer and notary are on the same node, but need to use right key. - ownedBy = node.info.legalIdentity.owningKey) - } - - // Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band - // via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the - // next stage in our building site, we will just auto-generate fake trades to give our nodes something to do. - // - // As the seller initiates the two-party trade protocol, here, we will be the buyer. - node.services.registerProtocolInitiator(TraderDemoProtocolSeller::class) { otherParty -> - TraderDemoProtocolBuyer(otherParty, attachmentsPath, amount) - } -} - -// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic. - -private class TraderDemoProtocolBuyer(val otherSide: Party, - private val attachmentsPath: Path, - val amount: Amount, - override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : ProtocolLogic() { - - object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") - - @Suspendable - override fun call() { - progressTracker.currentStep = STARTING_BUY - - val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val buyer = TwoPartyTradeProtocol.Buyer( - otherSide, - notary.notaryIdentity, - amount, - CommercialPaper.State::class.java) - - // This invokes the trading protocol and out pops our finished transaction. - val tradeTX: SignedTransaction = subProtocol(buyer, shareParentSessions = true) - // TODO: This should be moved into the protocol itself. - serviceHub.recordTransactions(listOf(tradeTX)) - - log.info("Purchase complete - we are a happy customer! Final transaction is: " + - "\n\n${Emoji.renderIfSupported(tradeTX.tx)}") - - logIssuanceAttachment(tradeTX) - logBalance() - } - - private fun logBalance() { - val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" } - logger.info("Remaining balance: ${balances.joinToString()}") - } - - private fun logIssuanceAttachment(tradeTX: SignedTransaction) { - // Find the original CP issuance. - val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx)) - search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, - followInputsOfType = CommercialPaper.State::class.java) - val cpIssuance = search.call().single() - - cpIssuance.attachments.first().let { - val p = attachmentsPath.toAbsolutePath().resolve("$it.jar") - log.info(""" - -The issuance of the commercial paper came with an attachment. You can find it expanded in this directory: -$p - -${Emoji.renderIfSupported(cpIssuance)}""") - } - } -} - -private class TraderDemoProtocolSeller(val otherSide: Party, - val amount: Amount, - override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic() { - companion object { - val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") - - object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper") - - object TRADING : ProgressTracker.Step("Starting the trade protocol") { - override fun childProgressTracker(): ProgressTracker = TwoPartyTradeProtocol.Seller.tracker() - } - - // We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some - // point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being - // surprised when it appears as a new set of tasks below the current one. - fun tracker() = ProgressTracker(SELF_ISSUING, TRADING) - } - - @Suspendable - override fun call(): SignedTransaction { - progressTracker.currentStep = SELF_ISSUING - - val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey = serviceHub.legalIdentityKey - val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary) - - progressTracker.currentStep = TRADING - - val seller = TwoPartyTradeProtocol.Seller( - otherSide, - notary, - commercialPaper, - amount, - cpOwnerKey, - progressTracker.getChildProgressTracker(TRADING)!!) - val tradeTX: SignedTransaction = subProtocol(seller, shareParentSessions = true) - serviceHub.recordTransactions(listOf(tradeTX)) - - return tradeTX - } - - @Suspendable - fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, notaryNode: NodeInfo): StateAndRef { - // Make a fake company that's issued its own paper. - val keyPair = generateKeyPair() - val party = Party("Bank of London", keyPair.public) - - val issuance: SignedTransaction = run { - val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, - Instant.now() + 10.days, notaryNode.notaryIdentity) - - // TODO: Consider moving these two steps below into generateIssue. - - // Attach the prospectus. - tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) - - // Requesting timestamping, all CP must be timestamped. - tx.setTime(Instant.now(), 30.seconds) - - // Sign it as ourselves. - tx.signWith(keyPair) - - // Get the notary to sign the timestamp - val notarySig = subProtocol(NotaryProtocol.Client(tx.toSignedTransaction(false))) - tx.addSignatureUnchecked(notarySig) - - // Commit it to local storage. - val stx = tx.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(stx)) - - stx - } - - // Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works. - val move: SignedTransaction = run { - val builder = TransactionType.General.Builder(notaryNode.notaryIdentity) - CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) - builder.signWith(keyPair) - val notarySignature = subProtocol(NotaryProtocol.Client(builder.toSignedTransaction(false))) - builder.addSignatureUnchecked(notarySignature) - val tx = builder.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(tx)) - tx - } - - return move.tx.outRef(0) - } - -} - -private fun printHelp(parser: OptionParser) { - println(""" - Usage: trader-demo --role [BUYER|SELLER] [options] - Please refer to the documentation in docs/build/index.html for more info. - - """.trimIndent()) - parser.printHelpOn(System.out) -} - From 39cbab9ce1246bf01572ce691014e98abc4e9061 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 25 Oct 2016 11:25:49 +0100 Subject: [PATCH 04/31] Removed trader demo integration test. Fixed the AttachmentDemo which had unnecessary coupling to the traderdemo. Added new test util func. --- .../r3corda/core/testing/TraderDemoTest.kt | 55 ------------------ .../demos/attachment/AttachmentDemo.kt | 2 +- .../{ => attachment}/bank-of-london-cp.jar | Bin .../com/r3corda/testing/CoreTestUtils.kt | 5 +- 4 files changed, 5 insertions(+), 57 deletions(-) delete mode 100644 src/integration-test/kotlin/com/r3corda/core/testing/TraderDemoTest.kt rename src/main/resources/com/r3corda/demos/{ => attachment}/bank-of-london-cp.jar (100%) diff --git a/src/integration-test/kotlin/com/r3corda/core/testing/TraderDemoTest.kt b/src/integration-test/kotlin/com/r3corda/core/testing/TraderDemoTest.kt deleted file mode 100644 index 233139ca41..0000000000 --- a/src/integration-test/kotlin/com/r3corda/core/testing/TraderDemoTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.r3corda.core.testing - -import com.google.common.net.HostAndPort -import com.r3corda.testing.* -import org.junit.Test -import kotlin.test.assertEquals - -class TraderDemoTest { - @Test fun `runs trader demo`() { - val buyerAddr = freeLocalHostAndPort() - val buyerApiAddr = freeLocalHostAndPort() - val directory = "./build/integration-test/${TestTimestamp.timestamp}/trader-demo" - var nodeProc: Process? = null - try { - nodeProc = runBuyer(directory, buyerAddr, buyerApiAddr) - runSeller(directory, buyerAddr) - } finally { - nodeProc?.destroy() - } - } - - companion object { - private fun runBuyer(baseDirectory: String, buyerAddr: HostAndPort, buyerApiAddr: HostAndPort): Process { - println("Running Buyer") - val args = listOf( - "--role", "BUYER", - "--network-address", buyerAddr.toString(), - "--api-address", buyerApiAddr.toString(), - "--base-directory", baseDirectory, - "--h2-port", "0" - ) - val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoBuyer") - NodeApi.ensureNodeStartsOrKill(proc, buyerApiAddr) - return proc - } - - private fun runSeller(baseDirectory: String, buyerAddr: HostAndPort) { - println("Running Seller") - val sellerAddr = freeLocalHostAndPort() - val sellerApiAddr = freeLocalHostAndPort() - val args = listOf( - "--role", "SELLER", - "--network-address", sellerAddr.toString(), - "--api-address", sellerApiAddr.toString(), - "--other-network-address", buyerAddr.toString(), - "--base-directory", baseDirectory, - "--h2-port", "0" - ) - val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoSeller") - assertExitOrKill(proc) - assertEquals(proc.exitValue(), 0) - } - - } -} diff --git a/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt b/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt index 41ae3768ab..1325d7aa99 100644 --- a/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt @@ -164,7 +164,7 @@ private fun runSender(node: Node, otherSide: Party) { // Make sure we have the file in storage // TODO: We should have our own demo file, not share the trader demo file if (serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH) == null) { - com.r3corda.demos.Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use { + Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use { val id = node.storage.attachments.importAttachment(it) assertEquals(PROSPECTUS_HASH, id) } diff --git a/src/main/resources/com/r3corda/demos/bank-of-london-cp.jar b/src/main/resources/com/r3corda/demos/attachment/bank-of-london-cp.jar similarity index 100% rename from src/main/resources/com/r3corda/demos/bank-of-london-cp.jar rename to src/main/resources/com/r3corda/demos/attachment/bank-of-london-cp.jar diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/com/r3corda/testing/CoreTestUtils.kt index a6e3327f0d..f59f11c806 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/CoreTestUtils.kt @@ -21,6 +21,7 @@ import com.r3corda.node.services.statemachine.StateMachineManager.Change import com.r3corda.node.utilities.AddOrRemove.ADD import com.r3corda.testing.node.MockIdentityService import com.r3corda.testing.node.MockServices +import com.typesafe.config.Config import rx.Subscriber import java.net.ServerSocket import java.security.KeyPair @@ -163,4 +164,6 @@ inline fun > AbstractNode.initiateSingleShotProtoco smm.changes.subscribe(subscriber) return future -} \ No newline at end of file +} + +fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name)) \ No newline at end of file From 7eeea9765371c94199ac8aa3b531cfb4688ba236 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 25 Oct 2016 14:23:18 +0100 Subject: [PATCH 05/31] Added new test utilities for HTTP requests. --- test-utils/build.gradle | 3 ++ .../com/r3corda/testing/http/HttpApi.kt | 17 +++++++++ .../com/r3corda/testing/http/HttpUtils.kt | 37 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt create mode 100644 test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt diff --git a/test-utils/build.gradle b/test-utils/build.gradle index c4eed4c13a..3c063219f7 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -44,6 +44,9 @@ dependencies { // Guava: Google test library (collections test suite) compile "com.google.guava:guava-testlib:19.0" + + // OkHTTP: Simple HTTP library. + compile 'com.squareup.okhttp3:okhttp:3.3.1' } quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes') diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt new file mode 100644 index 0000000000..f7758b86e8 --- /dev/null +++ b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt @@ -0,0 +1,17 @@ +package com.r3corda.testing.http + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.common.net.HostAndPort +import java.net.URL + +class HttpApi(val root: URL) { + fun putJson(path: String, data: Any) = HttpUtils.putJson(URL(root, path), toJson(data)) + fun postJson(path: String, data: Any) = HttpUtils.postJson(URL(root, path), toJson(data)) + + private fun toJson(any: Any) = ObjectMapper().writeValueAsString(any) + + companion object { + fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi + = HttpApi(URL("$protocol://$hostAndPort/$base/")) + } +} \ No newline at end of file diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt new file mode 100644 index 0000000000..f2e91b8e4f --- /dev/null +++ b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt @@ -0,0 +1,37 @@ +package com.r3corda.testing.http + +import com.r3corda.core.utilities.loggerFor +import okhttp3.* +import java.net.URL +import java.util.concurrent.TimeUnit + +/** + * A small set of utilities for making HttpCalls, aimed at demos and tests. + */ +object HttpUtils { + private val logger = loggerFor() + private val client by lazy { + OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS).build() + } + + fun putJson(url: URL, data: String) : Boolean { + val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data) + return makeRequest(Request.Builder().url(url).put(body).build()) + } + + fun postJson(url: URL, data: String) : Boolean { + val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data) + return makeRequest(Request.Builder().url(url).post(body).build()) + } + + private fun makeRequest(request: Request): Boolean { + val response = client.newCall(request).execute() + + if (!response.isSuccessful) { + logger.error("Could not fulfill HTTP request. Status Code: ${response.code()}. Message: ${response.body().string()}") + } + return response.isSuccessful + } +} \ No newline at end of file From fdbd67db5ccf3650ead8e3549919799cfc608233 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 25 Oct 2016 16:08:37 +0100 Subject: [PATCH 06/31] Added some more error logging to Node. --- node/src/main/kotlin/com/r3corda/node/driver/Driver.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt b/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt index 69132b7d48..8bcb39d40b 100644 --- a/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt @@ -276,6 +276,7 @@ open class DriverDSL( val conn = url.openConnection() as HttpURLConnection conn.requestMethod = "GET" if (conn.responseCode != 200) { + log.error("Received response code ${conn.responseCode} from api/info during startup.") return null } // For now the NodeInfo is tunneled in its Kryo format over the Node's Web interface. @@ -285,6 +286,7 @@ open class DriverDSL( om.registerModule(module) return om.readValue(conn.inputStream, NodeInfo::class.java) } catch(e: Exception) { + log.error("Could not query node info", e) return null } } From c42983fcfc5c208c60637824888814a466fd1e73 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 25 Oct 2016 16:39:23 +0100 Subject: [PATCH 07/31] Removed now unused resource file. --- .../demos/attachment/bank-of-london-cp.jar | Bin 71644 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/resources/com/r3corda/demos/attachment/bank-of-london-cp.jar diff --git a/src/main/resources/com/r3corda/demos/attachment/bank-of-london-cp.jar b/src/main/resources/com/r3corda/demos/attachment/bank-of-london-cp.jar deleted file mode 100644 index 95840a556f3338dc16daf1add443dd093ccce48b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71644 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[VYQQ?NY{ZhEmZy&Z89BzA}6(g#*pv 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 z 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 Z!PHFYD?Gw~mSO*m#((oCeCU7p{{Tr+^& Date: Fri, 28 Oct 2016 09:26:48 +0100 Subject: [PATCH 08/31] Improved error reporting. --- node/src/main/kotlin/com/r3corda/node/driver/Driver.kt | 4 ++-- .../src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt b/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt index 8bcb39d40b..04b5d3e2df 100644 --- a/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/com/r3corda/node/driver/Driver.kt @@ -276,7 +276,7 @@ open class DriverDSL( val conn = url.openConnection() as HttpURLConnection conn.requestMethod = "GET" if (conn.responseCode != 200) { - log.error("Received response code ${conn.responseCode} from api/info during startup.") + log.error("Received response code ${conn.responseCode} from $url during startup.") return null } // For now the NodeInfo is tunneled in its Kryo format over the Node's Web interface. @@ -286,7 +286,7 @@ open class DriverDSL( om.registerModule(module) return om.readValue(conn.inputStream, NodeInfo::class.java) } catch(e: Exception) { - log.error("Could not query node info", e) + log.error("Could not query node info at $url due to an exception.", e) return null } } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt index f2e91b8e4f..b183242d2f 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpUtils.kt @@ -30,7 +30,7 @@ object HttpUtils { val response = client.newCall(request).execute() if (!response.isSuccessful) { - logger.error("Could not fulfill HTTP request. Status Code: ${response.code()}. Message: ${response.body().string()}") + logger.error("Could not fulfill HTTP request of type ${request.method()} to ${request.url()}. Status Code: ${response.code()}. Message: ${response.body().string()}") } return response.isSuccessful } From 5898a15579442818af2bf11cebbafcbbc1345e97 Mon Sep 17 00:00:00 2001 From: "rick.parker" Date: Fri, 28 Oct 2016 17:39:10 +0100 Subject: [PATCH 09/31] Upgrade H2 to 1.4 to fix curious file corruption issue encountered by Patrick. --- node/build.gradle | 2 +- node/src/main/resources/reference.conf | 2 +- .../src/main/kotlin/com/r3corda/testing/node/MockServices.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node/build.gradle b/node/build.gradle index 9be446bad3..1a0bf168b4 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -122,7 +122,7 @@ dependencies { testCompile 'com.pholser:junit-quickcheck-core:0.6' // For H2 database support in persistence - compile "com.h2database:h2:1.3.176" + compile "com.h2database:h2:1.4.192" // Exposed: Kotlin SQL library - under evaluation compile "org.jetbrains.exposed:exposed:0.5.0" diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 18e8ee805d..b5b1dbaad7 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -6,7 +6,7 @@ keyStorePassword = "cordacadevpass" trustStorePassword = "trustpass" dataSourceProperties = { dataSourceClassName = org.h2.jdbcx.JdbcDataSource - "dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;MVCC=true;MV_STORE=true;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} + "dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port} "dataSource.user" = sa "dataSource.password" = "" } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockServices.kt index b684dcf3ad..531eb3d6a0 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockServices.kt @@ -164,7 +164,7 @@ class MockStorageService(override val attachments: AttachmentStorage = MockAttac fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;MVCC=TRUE;DB_CLOSE_ON_EXIT=FALSE") + props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;DB_CLOSE_ON_EXIT=FALSE") props.setProperty("dataSource.user", "sa") props.setProperty("dataSource.password", "") return props From f34683fbc42a333ec290ecccf41491619becfb93 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 31 Oct 2016 14:01:07 +0000 Subject: [PATCH 10/31] Fixed bug in generateSpending whereby Issuer Ref was not being checked. --- .../test/kotlin/com/r3corda/contracts/asset/CashTests.kt | 6 +++--- .../main/kotlin/com/r3corda/core/node/services/Services.kt | 2 +- .../main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt | 4 ++-- .../com/r3corda/node/services/vault/NodeVaultService.kt | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index 7a9ca1c085..40d2d9dc2c 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -32,7 +32,7 @@ import java.util.* import kotlin.test.* class CashTests { - val defaultRef = OpaqueBytes(ByteArray(1, {1})) + val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, @@ -264,7 +264,7 @@ class CashTests { // Include the previously issued cash in a new issuance command ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef(0)) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @Test @@ -569,7 +569,7 @@ class CashTests { databaseTransaction(database) { val tx = TransactionType.General.Builder(DUMMY_NOTARY) - vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP)) + vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP.ref(1))) assertEquals(vaultService.states.elementAt(2).ref, tx.inputStates()[0]) } diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 322517c859..46b154e665 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -171,7 +171,7 @@ interface VaultService { fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - onlyFromParties: Set? = null): Pair> + onlyFromIssuers: Set? = null): Pair> } inline fun VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java) diff --git a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt index 38b1ec0a7f..54bbbdae0c 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -79,8 +79,8 @@ class ServerRPCOps( val builder: TransactionBuilder = TransactionType.General.Builder(null) // TODO: Have some way of restricting this to states the caller controls try { - val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey) - + val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey, + setOf(req.amount.token.issuer)) keysForSigning.forEach { val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}") builder.signWith(KeyPair(it, key)) diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt index 2e50747e49..b347ae89ce 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt @@ -120,7 +120,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - onlyFromParties: Set?): Pair> { + onlyFromIssuers: Set?): Pair> { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. @@ -146,8 +146,8 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT 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 } + if (onlyFromIssuers != null) + ofCurrency.filter { it.state.data.deposit in onlyFromIssuers } else ofCurrency } From 3f364620537df0bf61bdb5aa83c9fad4cedff95d Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 31 Oct 2016 15:53:15 +0000 Subject: [PATCH 11/31] core: Bind client socket to getLocalHost explicitly, fixes #6 --- .../kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt index 5e065526f7..1c8afa418d 100644 --- a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt @@ -230,6 +230,10 @@ class X509UtilitiesTest { clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking clientSocket.sslParameters = clientParams clientSocket.useClientMode = true + // We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind + // to whatever resolves to(as that's what the server binds to). In particular on Debian + // resolves to 127.0.1.1 instead of the external address of the interface, so the ssl handshake fails. + clientSocket.bind(InetSocketAddress(InetAddress.getLocalHost(), 0)) val lock = Object() var done = false @@ -281,4 +285,4 @@ class X509UtilitiesTest { serverSocket.close() assertTrue(done) } -} \ No newline at end of file +} From 308d7c1df7e7d334f360b4624513c3366403d746 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 31 Oct 2016 17:15:06 +0000 Subject: [PATCH 12/31] Added PluginServiceHub for use by Corda plugin service extensions. --- .../r3corda/core/node/CordaPluginRegistry.kt | 4 +-- .../com/r3corda/core/node/PluginServiceHub.kt | 28 +++++++++++++++++++ .../com/r3corda/node/internal/AbstractNode.kt | 2 +- .../node/services/NotaryChangeService.kt | 3 +- .../node/services/api/ServiceHubInternal.kt | 21 ++------------ .../persistence/DataVendingService.kt | 3 +- 6 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt diff --git a/core/src/main/kotlin/com/r3corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/com/r3corda/core/node/CordaPluginRegistry.kt index de3f97f022..b3d4876b2f 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/CordaPluginRegistry.kt @@ -30,8 +30,8 @@ abstract class CordaPluginRegistry { /** * List of additional long lived services to be hosted within the node. - * They are expected to have a single parameter constructor that takes a ServiceHubInternal as input. - * The ServiceHubInternal will be fully constructed before the plugin service is created and will + * They are expected to have a single parameter constructor that takes a [PluginServiceHub] as input. + * The [PluginServiceHub] will be fully constructed before the plugin service is created and will * allow access to the protocol factory and protocol initiation entry points there. */ open val servicePlugins: List> = emptyList() diff --git a/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt b/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt new file mode 100644 index 0000000000..7f6d6c2894 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt @@ -0,0 +1,28 @@ +package com.r3corda.core.node + +import com.r3corda.core.crypto.Party +import com.r3corda.core.protocols.ProtocolLogic +import kotlin.reflect.KClass + +/** + * A service hub to be used by the [CordaPluginRegistry] + */ +interface PluginServiceHub : ServiceHub { + /** + * Register the protocol factory we wish to use when a initiating party attempts to communicate with us. The + * registration is done against a marker [KClass] which is sent in the session handshake by the other party. If this + * marker class has been registered then the corresponding factory will be used to create the protocol which will + * communicate with the other side. If there is no mapping then the session attempt is rejected. + * @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class] + * using the

::class
construct. Conventionally this is a [ProtocolLogic] subclass, however any class can + * be used, with the default being the class of the initiating protocol. This enables the registration to be of the + * form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol) + * @param protocolFactory The protocol factory generating the initiated protocol. + */ + fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) + + /** + * Return the protocol factory that has been registered with [markerClass], or null if no factory is found. + */ + fun getProtocolFactory(markerClass: Class<*>): ((Party) -> ProtocolLogic<*>)? +} diff --git a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt index d4b88fe829..1e19492c0c 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/AbstractNode.kt @@ -307,7 +307,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo val pluginServices = pluginRegistries.flatMap { x -> x.servicePlugins } val serviceList = mutableListOf() for (serviceClass in pluginServices) { - val service = serviceClass.getConstructor(ServiceHubInternal::class.java).newInstance(services) + val service = serviceClass.getConstructor(PluginServiceHub::class.java).newInstance(services) serviceList.add(service) tokenizableServices.add(service) if (service is AcceptsFileUpload) { diff --git a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt index b6e89211b8..f444519e91 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/NotaryChangeService.kt @@ -1,6 +1,7 @@ package com.r3corda.node.services import com.r3corda.core.node.CordaPluginRegistry +import com.r3corda.core.node.PluginServiceHub import com.r3corda.core.serialization.SingletonSerializeAsToken import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.protocols.NotaryChangeProtocol @@ -14,7 +15,7 @@ object NotaryChange { * A service that monitors the network for requests for changing the notary of a state, * and immediately runs the [NotaryChangeProtocol] if the auto-accept criteria are met. */ - class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() { + class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { init { services.registerProtocolInitiator(NotaryChangeProtocol.Instigator::class) { NotaryChangeProtocol.Acceptor(it) } } diff --git a/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt index 9e8991264a..224ac57d27 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/api/ServiceHubInternal.kt @@ -3,6 +3,7 @@ package com.r3corda.node.services.api import com.google.common.util.concurrent.ListenableFuture import com.r3corda.core.crypto.Party import com.r3corda.core.messaging.MessagingService +import com.r3corda.core.node.PluginServiceHub import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.TxWritableStorageService import com.r3corda.core.protocols.ProtocolLogic @@ -37,7 +38,7 @@ interface MessagingServiceBuilder { private val log = LoggerFactory.getLogger(ServiceHubInternal::class.java) -abstract class ServiceHubInternal : ServiceHub { +abstract class ServiceHubInternal : PluginServiceHub { abstract val monitoringService: MonitoringService abstract val protocolLogicRefFactory: ProtocolLogicRefFactory abstract val schemaService: SchemaService @@ -71,24 +72,6 @@ abstract class ServiceHubInternal : ServiceHub { */ abstract fun startProtocol(logic: ProtocolLogic): ListenableFuture - /** - * Register the protocol factory we wish to use when a initiating party attempts to communicate with us. The - * registration is done against a marker [KClass] which is sent in the session handshake by the other party. If this - * marker class has been registered then the corresponding factory will be used to create the protocol which will - * communicate with the other side. If there is no mapping then the session attempt is rejected. - * @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class] - * using the
::class
construct. Conventionally this is a [ProtocolLogic] subclass, however any class can - * be used, with the default being the class of the initiating protocol. This enables the registration to be of the - * form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol) - * @param protocolFactory The protocol factory generating the initiated protocol. - */ - abstract fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) - - /** - * Return the protocol factory that has been registered with [markerClass], or null if no factory is found. - */ - abstract fun getProtocolFactory(markerClass: Class<*>): ((Party) -> ProtocolLogic<*>)? - override fun invokeProtocolAsync(logicType: Class>, vararg args: Any?): ListenableFuture { val logicRef = protocolLogicRefFactory.create(logicType, *args) @Suppress("UNCHECKED_CAST") diff --git a/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt b/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt index 6ed9dc5208..149ce3d21f 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt @@ -3,6 +3,7 @@ package com.r3corda.node.services.persistence import co.paralleluniverse.fibers.Suspendable import com.r3corda.core.crypto.Party import com.r3corda.core.node.CordaPluginRegistry +import com.r3corda.core.node.PluginServiceHub import com.r3corda.core.node.recordTransactions import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.serialization.SingletonSerializeAsToken @@ -34,7 +35,7 @@ object DataVending { // TODO: I don't like that this needs ServiceHubInternal, but passing in a state machine breaks MockServices because // the state machine isn't set when this is constructed. [NodeSchedulerService] has the same problem, and both // should be fixed at the same time. - class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() { + class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { companion object { val logger = loggerFor() From c4ce05cc1c30bdd4cb6cfd564d79ae8b749224a9 Mon Sep 17 00:00:00 2001 From: Richard Green Date: Tue, 1 Nov 2016 11:47:53 +0000 Subject: [PATCH 13/31] Changing permission on scripts runnodes to uog+rx --- buildSrc/scripts/runnodes | 0 .../cordformation/src/main/resources/com/r3corda/plugins/runnodes | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 buildSrc/scripts/runnodes mode change 100644 => 100755 gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes diff --git a/buildSrc/scripts/runnodes b/buildSrc/scripts/runnodes old mode 100644 new mode 100755 diff --git a/gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes b/gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes old mode 100644 new mode 100755 From 7d08c0b068d4352a2cd5a21bb763c2bea22b8f7e Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 27 Oct 2016 17:13:11 +0100 Subject: [PATCH 14/31] Removed attachment demo. Added ApiUtils - a library for managing api lifecycles with less boilerplate. Added default values to http api and improved the api utils. Fixed spacing and comments. Removed withName and added a bad request response to handle error cases. Replaced use of 400 error with a 404 and error message as per HTTP spec. --- core/build.gradle | 3 + .../com/r3corda/core/utilities/ApiUtils.kt | 27 +++ .../demos/attachment/AttachmentDemo.kt | 200 ------------------ .../com/r3corda/testing/http/HttpApi.kt | 4 +- 4 files changed, 32 insertions(+), 202 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt delete mode 100644 src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt diff --git a/core/build.gradle b/core/build.gradle index 28b10af031..2d7c7fdfad 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -85,6 +85,9 @@ dependencies { // JPA 2.1 annotations. compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" + + // RS API: Response type and codes for ApiUtils. + compile "javax.ws.rs:javax.ws.rs-api:2.0" } publishing { diff --git a/core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt b/core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt new file mode 100644 index 0000000000..1ca42e2821 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt @@ -0,0 +1,27 @@ +package com.r3corda.core.utilities + +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.parsePublicKeyBase58 +import com.r3corda.core.node.ServiceHub +import javax.ws.rs.core.Response + +/** + * Utility functions to reduce boilerplate when developing HTTP APIs + */ +class ApiUtils(val services: ServiceHub) { + private val defaultNotFound = { msg: String -> Response.status(Response.Status.NOT_FOUND).entity(msg).build() } + + /** + * Get a party and then execute the passed function with the party public key as a parameter. + * Usage: withParty(key) { doSomethingWith(it) } + */ + fun withParty(partyKeyStr: String, notFound: (String) -> Response = defaultNotFound, found: (Party) -> Response): Response { + return try { + val partyKey = parsePublicKeyBase58(partyKeyStr) + val party = services.identityService.partyFromKey(partyKey) + if(party == null) notFound("Unknown party") else found(party) + } catch (e: IllegalArgumentException) { + notFound("Invalid base58 key passed for party key") + } + } +} diff --git a/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt b/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt deleted file mode 100644 index 1325d7aa99..0000000000 --- a/src/main/kotlin/com/r3corda/demos/attachment/AttachmentDemo.kt +++ /dev/null @@ -1,200 +0,0 @@ -package com.r3corda.demos.attachment - -import com.google.common.net.HostAndPort -import com.r3corda.core.contracts.TransactionType -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.failure -import com.r3corda.core.logElapsedTime -import com.r3corda.core.node.services.ServiceInfo -import com.r3corda.core.success -import com.r3corda.core.utilities.Emoji -import com.r3corda.core.utilities.LogHelper -import com.r3corda.node.internal.Node -import com.r3corda.node.services.config.ConfigHelper -import com.r3corda.node.services.config.FullNodeConfiguration -import com.r3corda.node.services.messaging.NodeMessagingClient -import com.r3corda.node.services.network.NetworkMapService -import com.r3corda.node.services.transactions.SimpleNotaryService -import com.r3corda.protocols.FinalityProtocol -import com.r3corda.testing.ALICE_KEY -import joptsimple.OptionParser -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.nio.file.Paths -import kotlin.concurrent.thread -import kotlin.system.exitProcess -import kotlin.test.assertEquals - -// ATTACHMENT DEMO -// -// Please see docs/build/html/running-the-demos.html and docs/build/html/tutorial-attachments.html -// -// This program is a simple demonstration of sending a transaction with an attachment from one node to another, and -// then accessing the attachment on the remote node. -// -// The different roles in the scenario this program can adopt are: - -enum class Role(val legalName: String, val port: Int) { - SENDER("Bank A", 31337), - RECIPIENT("Bank B", 31340); - - val other: Role - get() = when (this) { - SENDER -> RECIPIENT - RECIPIENT -> SENDER - } -} - -// And this is the directory under the current working directory where each node will create its own server directory, -// which holds things like checkpoints, keys, databases, message logs etc. -val DEFAULT_BASE_DIRECTORY = "./build/attachment-demo" - -val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") - -private val log: Logger = LoggerFactory.getLogger("AttachmentDemo") - -fun main(args: Array) { - val parser = OptionParser() - - val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required() - val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost") - val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost") - val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost") - val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY) - - val options = try { - parser.parse(*args) - } catch (e: Exception) { - log.error(e.message) - printHelp(parser) - exitProcess(1) - } - - val role = options.valueOf(roleArg)!! - - val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(role.port) - val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(role.other.port) - val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1) - - val baseDirectory = options.valueOf(baseDirectoryArg)!! - - // Suppress the Artemis MQ noise, and activate the demo logging. - // - // The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging - // for protocols will change in future. - LogHelper.setLevel("-org.apache.activemq") - - val directory = Paths.get(baseDirectory, role.name.toLowerCase()) - log.info("Using base demo directory $directory") - - - - // Override the default config file (which you can find in the file "reference.conf") to give each node a name. - val config = run { - val myLegalName = role.legalName - val configOverrides = mapOf( - "myLegalName" to myLegalName, - "artemisAddress" to myNetAddr.toString(), - "webAddress" to apiNetAddr.toString() - ) - FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides)) - } - - // Which services will this instance of the node provide to the network? - val advertisedServices: Set - - // One of the two servers needs to run the network map and notary services. In such a trivial two-node network - // the map is not very helpful, but we need one anyway. So just make the recipient side run the network map as it's - // the side that sticks around waiting for the sender. - val networkMapId = if (role == Role.SENDER) { - advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)) - null - } else { - advertisedServices = emptySet() - NodeMessagingClient.makeNetworkMapAddress(theirNetAddr) - } - - // And now construct then start the node object. It takes a little while. - val node = logElapsedTime("Node startup", log) { - Node(config, networkMapId, advertisedServices).setup().start() - } - - // What happens next depends on the role. The recipient sits around waiting for a transaction. The sender role - // will contact the recipient and actually make something happen. - when (role) { - Role.RECIPIENT -> runRecipient(node) - Role.SENDER -> { - node.networkMapRegistrationFuture.success { - // Pause a moment to give the network map time to update - Thread.sleep(100L) - val party = node.netMapCache.getNodeByLegalName(Role.RECIPIENT.legalName)?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!") - runSender(node, party) - } - } - } - - node.run() -} - -private fun runRecipient(node: Node) { - val serviceHub = node.services - - // Normally we would receive the transaction from a more specific protocol, but in this case we let [FinalityProtocol] - // handle receiving it for us. - serviceHub.storageService.validatedTransactions.updates.subscribe { event -> - // When the transaction is received, it's passed through [ResolveTransactionsProtocol], which first fetches any - // attachments for us, then verifies the transaction. As such, by the time it hits the validated transaction store, - // we have a copy of the attachment. - val tx = event.tx - if (tx.attachments.isNotEmpty()) { - val attachment = serviceHub.storageService.attachments.openAttachment(tx.attachments.first()) - assertEquals(PROSPECTUS_HASH, attachment?.id) - - println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(event.tx)}") - thread { - node.stop() - } - } - } -} - -private fun runSender(node: Node, otherSide: Party) { - val serviceHub = node.services - // Make sure we have the file in storage - // TODO: We should have our own demo file, not share the trader demo file - if (serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH) == null) { - Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use { - val id = node.storage.attachments.importAttachment(it) - assertEquals(PROSPECTUS_HASH, id) - } - } - - // Create a trivial transaction that just passes across the attachment - in normal cases there would be - // inputs, outputs and commands that refer to this attachment. - val ptx = TransactionType.General.Builder(notary = null) - ptx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) - - // Despite not having any states, we have to have at least one signature on the transaction - ptx.signWith(ALICE_KEY) - - // Send the transaction to the other recipient - val tx = ptx.toSignedTransaction() - serviceHub.startProtocol(FinalityProtocol(tx, emptySet(), setOf(otherSide))).success { - thread { - Thread.sleep(1000L) // Give the other side time to request the attachment - node.stop() - } - }.failure { - println("Failed to relay message ") - } -} - -private fun printHelp(parser: OptionParser) { - println(""" - Usage: attachment-demo --role [RECIPIENT|SENDER] [options] - Please refer to the documentation in docs/build/index.html for more info. - - """.trimIndent()) - parser.printHelpOn(System.out) -} diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt index f7758b86e8..d771937a19 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/http/HttpApi.kt @@ -5,8 +5,8 @@ import com.google.common.net.HostAndPort import java.net.URL class HttpApi(val root: URL) { - fun putJson(path: String, data: Any) = HttpUtils.putJson(URL(root, path), toJson(data)) - fun postJson(path: String, data: Any) = HttpUtils.postJson(URL(root, path), toJson(data)) + fun putJson(path: String, data: Any = Unit) = HttpUtils.putJson(URL(root, path), toJson(data)) + fun postJson(path: String, data: Any = Unit) = HttpUtils.postJson(URL(root, path), toJson(data)) private fun toJson(any: Any) = ObjectMapper().writeValueAsString(any) From d9f0a161e42a6a60b6131546684a761596cd39f1 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 1 Nov 2016 12:05:48 +0000 Subject: [PATCH 15/31] Addressed comments in PR review. --- core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt | 2 ++ .../r3corda/node/services/persistence/DataVendingService.kt | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt b/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt index 7f6d6c2894..6814ab1c25 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/PluginServiceHub.kt @@ -19,6 +19,8 @@ interface PluginServiceHub : ServiceHub { * form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol) * @param protocolFactory The protocol factory generating the initiated protocol. */ + + // TODO: remove dependency on Kotlin relfection (Kotlin KClass -> Java Class). fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) /** diff --git a/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt b/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt index 149ce3d21f..66a5c13639 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/persistence/DataVendingService.kt @@ -32,9 +32,6 @@ object DataVending { * Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null. */ @ThreadSafe - // TODO: I don't like that this needs ServiceHubInternal, but passing in a state machine breaks MockServices because -// the state machine isn't set when this is constructed. [NodeSchedulerService] has the same problem, and both -// should be fixed at the same time. class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { companion object { From dea9d663ff5338bd10c99e21b53915fe9fa27c54 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 1 Nov 2016 14:11:42 +0000 Subject: [PATCH 16/31] Fixed node config file being written to the wrong place in Cordformation templates. --- .../com/r3corda/plugins/Cordform.groovy | 5 +-- .../groovy/com/r3corda/plugins/Node.groovy | 34 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Cordform.groovy index e082834d12..3604b903b7 100644 --- a/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Cordform.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Cordform.groovy @@ -1,5 +1,6 @@ package com.r3corda.plugins +import org.apache.tools.ant.filters.FixCrLfFilter import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction import java.nio.file.Path @@ -53,7 +54,7 @@ class Cordform extends DefaultTask { */ protected Node getNodeByName(String name) { for(Node node : nodes) { - if(node.name.equals(networkMapNodeName)) { + if(node.name == networkMapNodeName) { return node } } @@ -69,7 +70,7 @@ class Cordform extends DefaultTask { from Cordformation.getPluginFile(project, "com/r3corda/plugins/runnodes") filter { String line -> line.replace("JAR_NAME", Node.JAR_NAME) } // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. - filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf")) + filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf")) into "${directory}/" } } diff --git a/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Node.groovy index 019c0d02ab..06bfd5ab98 100644 --- a/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/com/r3corda/plugins/Node.groovy @@ -35,7 +35,7 @@ class Node { private Config config = ConfigFactory.empty() //private Map config = new HashMap() private File nodeDir - private def project + private Project project /** * Set the name of the node. @@ -150,7 +150,7 @@ class Node { * Installs this project's cordapp to this directory. */ private void installBuiltPlugin() { - def pluginsDir = getAndCreateDirectory(nodeDir, "plugins") + def pluginsDir = new File(nodeDir, "plugins") project.copy { from project.jar into pluginsDir @@ -161,7 +161,7 @@ class Node { * Installs other cordapps to this node's plugins directory. */ private void installCordapps() { - def pluginsDir = getAndCreateDirectory(nodeDir, "plugins") + def pluginsDir = new File(nodeDir, "plugins") def cordapps = getCordappList() project.copy { from cordapps @@ -175,7 +175,7 @@ class Node { private void installDependencies() { def cordaJar = verifyAndGetCordaJar() def cordappList = getCordappList() - def depsDir = getAndCreateDirectory(nodeDir, "dependencies") + def depsDir = new File(nodeDir, "dependencies") def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappList.contains(it) } project.copy { from appDeps @@ -190,9 +190,17 @@ class Node { // Adding required default values config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromAnyRef(advertisedServices.join(','))) - def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList() - Files.write(new File(nodeDir, 'node.conf').toPath(), configFileText, StandardCharsets.UTF_8) + + // Need to write a temporary file first to use the project.copy, which resolves directories correctly. + def tmpDir = new File(project.buildDir, "tmp") + def tmpConfFile = new File(tmpDir, 'node.conf') + Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8) + + project.copy { + from tmpConfFile + into nodeDir + } } /** @@ -223,18 +231,4 @@ class Node { return (it != cordaJar) && cordapps.contains(jarName) } } - - /** - * Create a directory if it doesn't exist and return the file representation of it. - * - * @param baseDir The base directory to create the directory at. - * @param subDirName A valid name of the subdirectory to get and create if not exists. - * @return A file representing the subdirectory. - */ - private static File getAndCreateDirectory(File baseDir, String subDirName) { - File dir = new File(baseDir, subDirName) - assert(!dir.exists() || dir.isDirectory()) - dir.mkdirs() - return dir - } } From 613a86c5d92d6df57d20865ec083c63cb65fccd5 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 14 Oct 2016 16:32:10 +0100 Subject: [PATCH 17/31] Remove deposit and issuanceDef fields Remove deposit field from the FungibleAsset interface, and moved it into a fixed reference to amount.token.issuer. Remove issuanceDef field and replace it with amount.token. --- .../com/r3corda/contracts/asset/Cash.kt | 12 ++++---- .../contracts/asset/CommodityContract.kt | 6 ++-- .../com/r3corda/contracts/asset/Obligation.kt | 29 +++++++------------ .../clause/AbstractConserveAmount.kt | 7 +---- .../com/r3corda/contracts/asset/CashTests.kt | 20 ++++++------- .../contracts/asset/ObligationTests.kt | 22 +++++++------- .../r3corda/core/contracts/FungibleAsset.kt | 7 ----- .../node/services/vault/NodeVaultService.kt | 6 ++-- 8 files changed, 42 insertions(+), 67 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index 423509073a..1a92c5c881 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -66,7 +66,7 @@ class Cash : OnLedgerAsset() { ) ) { override fun groupStates(tx: TransactionForContract): List>> - = tx.groupStates> { it.issuanceDef } + = tx.groupStates> { it.amount.token } } class Issue : AbstractIssue( @@ -90,16 +90,14 @@ class Cash : OnLedgerAsset() { constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val deposit = amount.token.issuer - override val exitKeys = setOf(owner, deposit.party.owningKey) + override val exitKeys = setOf(owner, amount.token.issuer.party.owningKey) override val contract = CASH_PROGRAM_ID - override val issuanceDef = amount.token override val participants = listOf(owner) override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) - override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" + override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by ${owner.toStringShort()})" override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) @@ -197,8 +195,8 @@ fun Iterable.sumCashOrZero(currency: Issued): Amount> { it.issuanceDef } + = tx.groupStates> { it.amount.token } } /** @@ -101,16 +101,14 @@ class CommodityContract : OnLedgerAsset, owner: PublicKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val deposit = amount.token.issuer override val contract = COMMODITY_PROGRAM_ID override val exitKeys = Collections.singleton(owner) - override val issuanceDef = amount.token override val participants = listOf(owner) override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) - override fun toString() = "Commodity($amount at $deposit owned by ${owner.toStringShort()})" + override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by ${owner.toStringShort()})" override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index bfa7b43713..7299e2f897 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -60,7 +60,7 @@ class Obligation

: Contract { ) ) { override fun groupStates(tx: TransactionForContract): List, Issued>>> - = tx.groupStates, Issued>> { it.issuanceDef } + = tx.groupStates, Issued>> { it.amount.token } } /** @@ -152,7 +152,7 @@ class Obligation

: Contract { .filter { it.contract.legalContractReference in template.acceptableContracts } // Restrict the states to those of the correct issuance definition (this normally // covers issued product and obligor, but is opaque to us) - .filter { it.issuanceDef in template.acceptableIssuedProducts } + .filter { it.amount.token in template.acceptableIssuedProducts } // Catch that there's nothing useful here, so we can dump out a useful error requireThat { "there are fungible asset state outputs" by (assetStates.size > 0) @@ -164,7 +164,7 @@ class Obligation

: Contract { // this one. val moveCommands = tx.commands.select() var totalPenniesSettled = 0L - val requiredSigners = inputs.map { it.deposit.party.owningKey }.toSet() + val requiredSigners = inputs.map { it.amount.token.issuer.party.owningKey }.toSet() for ((beneficiary, obligations) in inputs.groupBy { it.owner }) { val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() @@ -268,21 +268,12 @@ class Obligation

: Contract { /** The public key of the entity the contract pays to */ val beneficiary: PublicKey ) : FungibleAsset>, NettableState, MultilateralNetState

> { - override val amount: Amount>> - get() = Amount(quantity, issuanceDef) + override val amount: Amount>> = Amount(quantity, Issued(obligor.ref(0), template)) override val contract = OBLIGATION_PROGRAM_ID - override val deposit: PartyAndReference - get() = amount.token.issuer - override val exitKeys: Collection - get() = setOf(owner) - val dueBefore: Instant - get() = template.dueBefore - override val issuanceDef: Issued> - get() = Issued(obligor.ref(0), template) - override val participants: List - get() = listOf(obligor.owningKey, beneficiary) - override val owner: PublicKey - get() = beneficiary + override val exitKeys: Collection = setOf(beneficiary) + val dueBefore: Instant = template.dueBefore + override val participants: List = listOf(obligor.owningKey, beneficiary) + override val owner: PublicKey = beneficiary override fun move(newAmount: Amount>>, newOwner: PublicKey): State

= copy(quantity = newAmount.quantity, beneficiary = newOwner) @@ -522,7 +513,7 @@ class Obligation

: Contract { require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be $existingLifecycle for all input states" } // Produce a new set of states - val groups = statesAndRefs.groupBy { it.state.data.issuanceDef } + val groups = statesAndRefs.groupBy { it.state.data.amount.token } for ((aggregateState, stateAndRefs) in groups) { val partiesUsed = ArrayList() stateAndRefs.forEach { stateAndRef -> @@ -608,7 +599,7 @@ class Obligation

: Contract { /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ private fun getIssuanceDefinitionOrThrow(states: Iterable>): Issued> = - states.map { it.issuanceDef }.distinct().single() + states.map { it.amount.token }.distinct().single() /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ private fun getTermsOrThrow(states: Iterable>) = diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index 3313ff4215..9bc938a364 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -1,12 +1,7 @@ package com.r3corda.contracts.clause -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 import com.r3corda.core.transactions.TransactionBuilder import java.security.PublicKey import java.util.* @@ -68,7 +63,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency)) val takeChangeFrom = gathered.lastOrNull() val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) + Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) } else { null } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index 7a9ca1c085..4e0b5ce720 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -43,7 +43,7 @@ class CashTests { val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2) fun Cash.State.editDepositRef(ref: Byte) = copy( - amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref)))) + amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) lateinit var services: MockServices @@ -173,7 +173,7 @@ class CashTests { assertTrue(tx.inputs.isEmpty()) val s = tx.outputs[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) - assertEquals(MINI_CORP, s.deposit.party) + assertEquals(MINI_CORP, s.amount.token.issuer.party) assertEquals(DUMMY_PUBKEY_1, s.owner) assertTrue(tx.commands[0].value is Cash.Commands.Issue) assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0]) @@ -650,22 +650,22 @@ class CashTests { val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY) // Obviously it must be possible to aggregate states with themselves - assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef) + assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token) // Owner is not considered when calculating whether it is possible to aggregate states - assertEquals(fiveThousandDollarsFromMega.issuanceDef, twoThousandDollarsFromMega.issuanceDef) + assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token) // States cannot be aggregated if the deposit differs - assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) - assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) + assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token) + assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token) // States cannot be aggregated if the currency differs - assertNotEquals(oneThousandDollarsFromMini.issuanceDef, - Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).issuanceDef) + assertNotEquals(oneThousandDollarsFromMini.amount.token, + Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).amount.token) // States cannot be aggregated if the reference differs - assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef) - assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) + assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token) + assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token) } @Test diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index e265a5bbb8..d73042f7ad 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -245,7 +245,7 @@ class ObligationTests { val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(BOB_KEY) signWith(DUMMY_NOTARY_KEY) @@ -259,7 +259,7 @@ class ObligationTests { val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY) val tx = TransactionType.General.Builder(null).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(BOB_KEY) }.toSignedTransaction().tx @@ -453,7 +453,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() } @@ -467,7 +467,7 @@ class ObligationTests { input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) } output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.issuanceDef)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() } @@ -480,7 +480,7 @@ class ObligationTests { input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this `fails with` "all inputs are in the normal state" } @@ -493,7 +493,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.issuanceDef)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this `fails with` "amount in settle command" } @@ -517,7 +517,7 @@ class ObligationTests { input("Alice's 1 FCOJ obligation to Bob") input("Alice's 1 FCOJ") output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.issuanceDef)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) } command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation().legalContractReference) } verifies() } @@ -648,13 +648,13 @@ class ObligationTests { output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.issuanceDef)) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) } command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef)) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) } this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command" tweak { @@ -679,10 +679,10 @@ class ObligationTests { this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef.copy(product = megaCorpDollarSettlement))) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) } this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.issuanceDef.copy(product = megaCorpPoundSettlement))) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) } this.verifies() } } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt index 5f6be7908d..4ea83808fc 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt @@ -21,12 +21,6 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ interface FungibleAsset : OwnableState { - /** - * Where the underlying asset backing this ledger entry can be found. The reference - * is only intended for use by the issuer, and is not intended to be meaningful to others. - */ - val deposit: PartyAndReference - val issuanceDef: Issued val amount: Amount> /** * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their @@ -55,7 +49,6 @@ interface FungibleAsset : OwnableState { } } - // Small DSL extensions. /** Sums the asset states in the list, returning null if there are none. */ diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt index 2e50747e49..425ac15549 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt @@ -147,7 +147,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT 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 } + ofCurrency.filter { it.state.data.amount.token.issuer.party in onlyFromParties } else ofCurrency } @@ -160,13 +160,13 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT 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) + Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) } else { null } val keysUsed = gathered.map { it.state.data.owner }.toSet() - val states = gathered.groupBy { it.state.data.deposit }.map { + val states = gathered.groupBy { it.state.data.amount.token.issuer }.map { val coins = it.value val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() deriveState(coins.first().state, totalAmount, to) From 2744d8abaaa0574e0cce16f3a06c92a462b9f11d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 13 Oct 2016 15:01:13 +0200 Subject: [PATCH 18/31] Tech white paper: new sections on the data model, identity lookups, attachments, dispute resolution, compound keys, timestamps. --- docs/source/whitepaper/Ref.bib | 20 ++ .../whitepaper/corda-technical-whitepaper.tex | 299 +++++++++++++++++- 2 files changed, 308 insertions(+), 11 deletions(-) diff --git a/docs/source/whitepaper/Ref.bib b/docs/source/whitepaper/Ref.bib index 430ed7b68f..e1f595e37c 100644 --- a/docs/source/whitepaper/Ref.bib +++ b/docs/source/whitepaper/Ref.bib @@ -88,6 +88,13 @@ year = 2013 } +@misc{BIP32, + title = "Hierarchical deterministic wallets", + author = "{{Pieter Wiulle}}", + howpublished = "{\url{https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki}}", + year = 2013 +} + @misc{HBBFT, author = {Andrew Miller and Yu Xia and Kyle Croman and Elaine Shi and Dawn Song}, title = "{{The Honey Badger of BFT Protocols}}", @@ -136,4 +143,17 @@ publisher = {ACM}, address = {New York, NY, USA}, keywords = {Large-Scale Distributed Storage}, +} + +@misc{JavaTimeScale, + title = "{{java.time.Instant documentation}}", + howpublished = "{\url{https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html}}", + year = 2014 +} + +@misc{ZipFormat, + title = {Zip file format}, + howpublished = {\url{https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT}}, + year = 1989, + author = {PKWARE} } \ No newline at end of file diff --git a/docs/source/whitepaper/corda-technical-whitepaper.tex b/docs/source/whitepaper/corda-technical-whitepaper.tex index faad8630a7..7ccb97322f 100644 --- a/docs/source/whitepaper/corda-technical-whitepaper.tex +++ b/docs/source/whitepaper/corda-technical-whitepaper.tex @@ -16,6 +16,8 @@ \usepackage[nottoc]{tocbibind} \usepackage[parfill]{parskip} \usepackage{textcomp} +\usepackage{scrextend} +\addtokomafont{labelinglabel}{\sffamily} %\usepackage[natbibapa]{apacite} \renewcommand{\thefootnote}{\alph{footnote}} @@ -159,8 +161,8 @@ A Corda network consists of the following components: \item A network map service that publishes information about nodes on the network. \item One or more notary services. A notary may itself be distributed over multiple nodes. \item Zero or more oracle services. An oracle is a well known service that signs transactions if they state a fact -and that fact is considered to be true. This is how the ledger can be connected to the real world, despite being -fully deterministic. +and that fact is considered to be true. They may also optionally also provide the facts. This is how the ledger can be +connected to the real world, despite being fully deterministic. \end{itemize} A purely in-memory implementation of the messaging subsystem is provided which can inject simulated latency between @@ -181,11 +183,11 @@ identities it signs are globally unique. Thus an entirely anonymous Corda networ IP obfuscation system like Tor is also used. Whilst simple string identities are likely sufficient for some networks, the financial industry typically requires some -level of \emph{know your customer} checking, and differentiation between different legal entities that may share -the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to identities and thus -names are actually X.500 names. When a single string is sufficient the \emph{common name} field can be used alone, -similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful to -differentiate between entities with the same name. For example there are at least five different companies called +level of \emph{know your customer} checking, and differentiation between different legal entities, branches and desks +that may share the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to +identities and thus names are actually X.500 names. When a single string is sufficient the \emph{common name} field can +be used alone, similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful +to differentiate between entities with the same name. For example there are at least five different companies called \emph{American Savings Bank} and in the past there may have been more than 40 independent banks with that name. More complex notions of identity that may attest to many time-varying attributes are not handled at this layer of the @@ -194,10 +196,10 @@ themselves may still contain anonymous public keys. \subsection{The network map} -Every network require a network map service, which may itself be composed of multiple cooperating nodes. This is +Every network requires a network map service, which may itself be composed of multiple cooperating nodes. This is similar to Tor's concept of \emph{directory authorities}. The network map publishes the IP addresses through which every node on the network can be reached, along with the identity certificates of those nodes and the services they -provide. On receiving a connection nodes check that the connecting node is in the network map. +provide. On receiving a connection, nodes check that the connecting node is in the network map. The network map abstracts the underlying IP addresses of the nodes from more useful business concepts like identities and services. Each participant on the network, called a \emph{party}, publishes one or more IP addresses in the @@ -312,20 +314,295 @@ with a solution. The ability to request manual solutions is useful for cases whe are contacting them, for example, the specified reason for sending a payment is not recognised, or when the asset used for a payment is not considered acceptable. +Flows are named using reverse DNS notation and several are defined by the base protocol. Note that the framework is +not required to implement the wire protocols, it is just a development aid. + +\subsection{Data visibility and dependency resolution} + +When a transaction is presented to a node as part of a flow it may need to be checked. Simply sending you +a message saying that I am paying you \pounds1000 is only useful if youa are sure I own the money I'm using to pay me. +Checking transaction validity is the responsibility of the \texttt{ResolveTransactions} flow. This flow performs +a breadth-first search over the transaction graph, downloading any missing transactions into local storage and +validating them. The search bottoms out at the issuance transactions. A transaction is not considered valid if +any of its transitive dependencies are invalid. + +It is required that a node be able to present the entire dependency graph for a transaction it is asking another +node to accept. Thus there is never any confusion about where to find transaction data. Because transactions are +always communicated inside a flow, and flows embed the resolution flow, the necessary dependencies are fetched +and checked automatically from the correct peer. Transactions propagate around the network lazily and there is +no need for distributed hash tables. + +This approach has several consequences. One is that transactions that move highly liquid assets like cash may +end up becoming a part of a very long chain of transactions. The act of resolving the tip of such a graph can +involve many round-trips and thus take some time to fully complete. How quickly a Corda network can send payments +is thus difficult to characterise: it depends heavily on usage and distance between nodes. Whilst nodes could +pre-push transactions in anticipation of them being fetched anyway, such optimisations are left for future work. + +A more important consequence is that in the absence of additional privacy measures it is difficult to reason +about who may get to see transaction data. We can say it's definitely better than a system that uses global +broadcast, but how much better is hard to characterise. This uncertainty is mitigated by several factors. + +\paragraph{Small-subgraph transactions.}Some uses of the ledger do not involve widely circulated asset states. +For example, two institutions that wish to keep their view of a particular deal synchronised but who are making +related payments off-ledger may use transactions that never go outside the involved parties. A discussion of +on-ledger vs off-ledger cash can be found in a later section. + +\paragraph{Transaction privacy techniques.}Corda supports a variety of transaction data hiding techniques. For +example, public keys can be randomised to make it difficult to link transactions to an identity. ``Tear-offs'' +allow some parts of a transaction to be presented without the others. In future versions of the system secure hardware +and/or zero knowledge proofs could be used to convince a party of the validity of a transaction without revealing the +underlying data. + +\paragraph{State re-issuance.}In cases where a state represents an asset that is backed by a particular issuer, +and the issuer is trusted to behave atomically even when the ledger isn't forcing atomicity, the state can +simply be `exited' from the ledger and then re-issued. Because there are no links between the exit and reissue +transactions this shortens the chain. In practice most issuers of highly liquid assets are already trusted with +far more sensitive tasks than reliably issuing pairs of signed data structures, so this approach is unlikely to +be an issue. + \section{Data model} -\subsection{Commands} + +Transactions consist of the following components: + +\begin{labeling}{Input references} +\item [Input references] These are \texttt{(hash, output index)} pairs that point to the states a +transaction is consuming. +\item [Output states] Each state specifies the notary for the new state, the contract(s) that define its allowed +transition functions and finally the data itself. +\item [Attachments] Transactions specify an ordered list of zip file hashes. Each zip file may contain +code, data, certificates or supporting documentation for the transaction. Contract code has access to the contents +of the attachments when checking the transaction for validity. +\item [Commands] There may be multiple allowed output states from any given input state. For instance +an asset can be moved to a new owner on the ledger, or issued, or exited from the ledger if the asset has been +redeemed by the owner and no longer needs to be tracked. A command is essentially a parameter to the contract +that specifies more information than is obtainable from examination of the states by themselves (e.g. data from an oracle +service). Each command has an associated list of public keys. Like states, commands are object graphs. +\item [Signatures] The set of required signatures is equal to the union of the commands' public keys. +\item [Type] Transactions can either be normal or notary-changing. The validation rules for each are +different. +\item [Timestamp] When present, a timestamp defines a time range in which the transaction is considered to +have occurrred. This is discussed in more detail below. +\end{labeling} + +% TODO: Update this one transaction types are separated. +% TODO: This description ignores the participants field in states, because it probably needs a rethink. +% TODO: Specify the curve used here once we decide how much we care about BIP32 public derivation. + +Signatures are appended to the end of a transaction and transactions are identified by the hash used for signing, so +signature malleability is not a problem. There is never a need to identify a transaction including its accompanying +signatures by hash. Signatures can be both checked and generated in parallel, and they are not directly exposed to +contract code. Instead contracts check that the set of public keys specified by a command is appropriate, knowing that +the transaction will not be valid unless every key listed in every command has a matching signature. Public key +structures are themselves opaque. In this way algorithmic agility is retained: new signature algorithms can be deployed +without adjusting the code of the smart contracts themselves. + +\subsection{Compound keys} + +The term ``public key'' in the description above actually refers to a \emph{compound key}. Compound keys are trees in +which leafs are regular cryptographic public keys with an accompanying algorithm identifiers. Nodes in the tree specify +both the weights of each child and a threshold weight that must be met. The validty of a set of signatures can be +determined by walking the tree bottom-up, summing the weights of the keys that have a valid signature and comparing +against the threshold. By using weights and thresholds a variety of conditions can be encoded, including boolean +formulas with AND and OR. + +Compound keys are useful in multiple scenarios. For example, assets can be placed under the control of a 2-of-2 +compound key where one leaf key is owned by a user, and the other by an independent risk analysis system. The +risk analysis system refuses to sign if the transaction seems suspicious, like if too much value has been +transferred in too short a time window. Another example involves encoding corporate structures into the key, +allowing a CFO to sign a large transaction alone but his subordinates are required to work together. Compound keys +are also useful for notaries. Each participant in a distributed notary is represented by a leaf, and the threshold +is set such that some participants can be offline or refusing to sign yet the signature of the group is still valid. + +Whilst there are threshold signature schemes in the literature that allow compound keys and signatures to be produced +mathematically, we choose the less space efficient explicit form in order to allow a mixture of keys using different +algorithms. In this way old algorithms can be phased out and new algorithms phased in without requiring all +participants in a group to upgrade simultaneously. + +\subsection{Timestamps} + +Transaction timestamps specify a \texttt{[start, end]} time window within which the transaction is asserted to have +occurred. Timestamps are expressed as windows because in a distributed system there is no true time, only a large number +of desynchronised clocks. This is not only implied by the laws of physics but also by the nature of shared transactions +- especially if the signing of a transaction requires multiple human authorisations, the process of constructing +a joint transaction could take hours or even days. + +It is important to note that the purpose of a transaction timestamp is to communicate the transaction's position +on the timeline to the smart contract code for the enforcement of contractual logic. Whilst such timestamps may +also be used for other purposes, such as regulatory reporting or ordering of events in a user interface, there is +no requirement to use them like that and locally observed timestamps may sometimes be preferable even if they will +not exactly match the time observed by other parties. Alternatively if a precise point on the timeline is required +and it must also be agreed by multiple parties, the midpoint of the time window may be used by convention. Even +though this may not precisely align to any particular action (like a keystroke or verbal agreement) it is often +useful nonetheless. + +Timestamp windows may be open ended in order to communicate that the transaction occurred before a certain +time or after a certain time, but how much before or after is unimportant. This can be used in a similar +way to Bitcoin's \texttt{nLockTime} transaction field, which specifies a \emph{happens-after} constraint. + +Timestamps are checked and enforced by notary services. As the participants in a notary service will themselves +not have precisely aligned clocks, whether a transaction is considered valid or not at the moment it is submitted +to a notary may be unpredictable if submission occurs right on a boundary of the given window. However, from the +perspective of all other observers the notaries signature is decisive: if the signature is present, the transaction +is assumed to have occurred within that time. + +\paragraph{Reference clocks.}In order to allow for relatively tight time windows to be used when transactions are fully +under the control of a single party, notaries are expected to be synchronised to the atomic clocks at the US Naval +Observatory. Accurate feeds of this clock can be obtained from GPS satellites. Note that Corda uses the Java +timeline\cite{JavaTimeScale} which is UTC with leap seconds spread over the last 1000 seconds of the day, thus each day +always has exactly 86400 seconds. Care should be taken to ensure that changes in the GPS leap second counter are +correctly smeared in order to stay synchronised with Java time. When setting a transaction time window care must be +taken to account for network propagation delays between the user and the notary service, and messaging within the notary +service. + +\subsection{Attachments and contract bytecodes} + +Transactions may have a number of \emph{attachments}, identified by the hash of the file. Attachments are stored +and transmitted separately to transaction data and are fetched by the standard resolution flow only when the +attachment has not previously been seen before. + +Attachments are always zip files\cite{ZipFormat} and cannot be referred to individually by contract code. The files +within the zips are collapsed together into a single logical file system, with overlapping files being resolved in +favour of the first mentioned. Not coincidentally, this is the mechanism used by Java classpaths. + +Smart contracts in Corda are defined using JVM bytecode as specified in \emph{``The Java Virtual Machine Specification SE 8 Edition''}\cite{JVM}, +with some small differences that are described in a later section. A contract is simply a class that implements +the \texttt{Contract} interface, which in turn exposes a single function called \texttt{verify}. The verify +function is passed a transaction and either throws an exception if the transaction is considered to be invalid, +or returns with no result if the transaction is valid. Embedding the JVM specification in the Corda specification +enables developers to write code in a variety of languages, use well developed toolchains, and to reuse code +already authored in Java or other JVM compatible languages. + +The Java standards also specify a comprehensive type system for expressing common business data. Time and calendar +handling is provided by an implementation of the JSR 310 specification, decimal calculations can be performed either +using portable (`\texttt{strictfp}') floating point arithmetic or the provided bignum library, and so on. These +libraries have been carefully engineered by the business Java community over a period of many years and it makes +sense to build on this investment. + +Contract bytecode also defines the states themselves, which may be arbitrary object graphs. Because JVM classes +are not a convenient form to work with from non-JVM platforms the allowed types are restricted and a standardised +binary encoding scheme is provided. States may label their properties with a small set of standardised annotations. +These can be useful for controlling how states are serialised to JSON and XML (using JSR 367 and JSR 222 respectively), +for expressing static validation constraints (JSR 349) and for controlling how states are inserted into relational +databases (JSR 338). This feature is discussed later. + +Attachments may also contain data files that support the contract code. These may be in the same zip as the +bytecode files, or in a different zip that must be provided for the transaction to be valid. Examples of such +data files might include currency definitions, timezone data and public holiday calendars. Any public data may +be referenced in this way. Attachments are intended for data on the ledger that many parties may wish to reuse +over and over again. Data files are accessed by contract code using the same APIs as any file on the classpath +would be accessed. The platform imposes some restrictions on what kinds of data can be included in attachments +along with size limits, to avoid people placing inappropriate files on the global ledger (videos, PowerPoints etc). + +Note that the creator of a transaction gets to choose which files are attached. Therefore, it is typical that +states place constraints on the data they're willing to accept. Attachments \emph{provide} data but do not +\emph{authenticate} it, so if there's a risk of someone providing bad data to gain an economic advantage +there must be a constraints mechanism to prevent that from happening. This is rooted at the contract constraints +encoded in the states themselves: a state can not only name a class that implements the \texttt{Contract} +interface but also place constraints on the zip/jar file that provides it. That constraint can in turn be used to +ensure that the contract checks the authenticity of the data - either by checking the hash of the data directly, +or by requiring the data to be signed by some trusted third party. + +% TODO: The code doesn't match this description yet. + +\subsection{Hard forks, specifications and dispute resolution} + +Decentralised ledger systems often differ in their underlying political ideology as well as their technical +choices. The Ethereum project originally promised ``unstoppable apps'' which would implement ``code as law''. After +a prominent smart contract was hacked, an argument took place over whether what had occurred could be described +as a hack at all given the lack of any non-code specification of what the program was meant to do. The disagreement +eventually led to a split in the community. + +As Corda contracts are simply zip files, it is easy to include a PDF or other documents describing what a contract +is meant to actually do. There is no requirement to use this mechanism, and there is no requirement that these +documents have any legal weight. However in financial use cases it's expected that they would be legal contracts that +take precedence over the software implementations in case of disagreement. + +It is technically possible to write a contract that cannot be upgraded. If such a contract governed an asset that +existed only on the ledger, like a cryptocurrency, then that would provide an approximation of ``code as law''. We +leave discussion of this wisdom of this concept to political scientists and reddit. + +\paragraph{Platform logging}There is no direct equivalent in Corda of a block chain ``hard fork'', so the only solution +to discarding buggy or fraudulent transaction chains would be to mutually agree out of band to discard an entire +transaction subgraph. As there is no global visibility either this mutual agreement would not need to encompass all +network participants: only those who may have received and processed such transactions. The flip side of lacking global +visibility is that there is no single point that records who exactly has seen which transactions. Determining the set +of entities that'd have to agree to discard a subgraph means correlating node activity logs. Corda nodes log sufficient +information to ensure this correlation can take place. The platform defines a flow to assist with this, which can be +used by anyone. A tool is provided that generates an ``investigation request'' and sends it to a seed node. The flow +signals to the node administrator that a decision is required, and sufficient information is transmitted to the node to +try and convince the administrator to take part (e.g. a signed court order). If the administrator accepts the request +through the node explorer interface, the next hops in the transaction chain are returned. In this way the tool can +semi-automatically crawl the network to find all parties that would be affected by a proposed rollback. The platform +does not take a position on what types of transaction rollback are justified and provides only minimal support for +implementing rollbacks beyond locating the parties that would have to agree. + +% TODO: DB logging of tx transmits is COR-544. + +Once involved parties are identified there are at least two strategies for editing the ledger. One is to extend +the transaction chain with new transactions that simply correct the database to match the intended reality. For +this to be possible the smart contract must have been written to allow arbitrary changes outside its normal +business logic when a sufficient threshold of signatures is present. This strategy is simple and makes the most +sense when the number of parties involved in a state is small and parties have no incentive to leave bad information +in the ledger. For asset states that are the result of theft or fraud the only party involved in a state may +resist attempts to patch things up in this way, as they may be able to benefit in the real world from the time +lag between the ledger becoming inaccurate and it catching up with reality. In this case a more complex approach +can be used in which the involved parties minus the uncooperative party agree to mark the relevant states as +no longer consumed/spent. This is essentially a limited form of database rollback. + \subsection{Identity lookups} -\subsection{Attachments, legal prose and bytecode} + +In all block chain inspired systems there exists a tension between wanting to know who you are dealing with and +not wanting others to know. A standard technique is to use randomised public keys in the shared data, and keep +the knowledge of the identity that key maps to private. For instance, it is considered good practice to generate +a fresh key for every received payment. This technique exploits the fact that verifying the integrity of the ledger +does not require knowing exactly who took part in the transactions, only that they followed the agreed upon +rules of the system. + +Platforms such as Bitcoin and Ethereum have relatively ad-hoc mechanisms for linking identities and keys. Typically +it is the user's responsibility to manually label public keys in their wallet software using knowledge gleaned from +websites, shop signs and so on. Because these mechanisms are ad hoc and tedious many users don't bother, which +can make it hard to figure out where money went later. It also complicates the deployment of secure signing devices +and risk analysis engines. Bitcoin has BIP 70\cite{BIP70} which specifies a way of signed a ``payment +request'' using X.509 certificates linked to the web PKI, giving a cryptographically secured and standardised way +of knowing who you are dealing with. Identities in this system are the same as used in the web PKI: a domain name, +email address or EV (extended validation) organisation name. + +Corda takes this concept further. States may define fields of type \texttt{Party}, which encapsulates an identity +and a public key. When a state is deserialised from a transaction in its raw form, the identity field of the +\texttt{Party} object is null and only the public (compound) key is present. If a transaction is deserialised +in conjunction with X.509 certificate chains linking the transient public keys to long term identity keys the +identity field is set. In this way a single data representation can be used for both the anonymised case, such +as when validating dependencies of a transaction, and the identified case, such as when trading directly with +a counterparty. Trading flows incorporate sub-flows to transmit certificates for the keys used, which are then +stored in the local database. However the transaction resolution flow does not transmit such data, keeping the +transactions in the chain of custody pseudonymous. + +\paragraph{Deterministic key derivation} Corda allows for but does not mandate the use of determinstic key +derivation schemes such as BIP 32\cite{BIP32}. The infrastructure does not assume any mathematical relationship +between public keys because some cryptographic schemes are not compatible with such systems. Thus we take the +efficiency hit of always linking transient public keys to longer term keys with X.509 certificates. + +% TODO: Discuss the crypto suites used in Corda. + \subsection{Merkle-structured transactions} \subsection{Encumbrances} \subsection{Contract constraints} + +% TODO: Contract constraints aren't designed yet. + \section{Cash and Obligations} +\section{Non-asset instruments} \section{Integration with existing infrastructure} \section{Deterministic JVM} \section{Notaries} +\section{Clauses} \section{Secure signing devices} \section{Client RPC and reactive collections} \section{Event scheduling} +\section{Future work} + +\paragraph Secure hardware +\paragraph Zero knowledge proofs \section{Conclusion} From 1e6cca4d5dfaddd62b0d1330aa285a1e9e427aff Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Tue, 1 Nov 2016 16:09:13 +0000 Subject: [PATCH 19/31] Removed dead installDist configurations and moved Jolokia access to the correct resources dir. --- .idea/runConfigurations/Clean___Install.xml | 1 + build.gradle | 43 ------------------- .../src/test}/resources/jolokia-access.xml | 0 3 files changed, 1 insertion(+), 43 deletions(-) rename {src/main => node/src/test}/resources/jolokia-access.xml (100%) diff --git a/.idea/runConfigurations/Clean___Install.xml b/.idea/runConfigurations/Clean___Install.xml index 57e99db323..7401f4120c 100644 --- a/.idea/runConfigurations/Clean___Install.xml +++ b/.idea/runConfigurations/Clean___Install.xml @@ -11,6 +11,7 @@