From f4f2d353751b6d9942c974ddda8778997bb5de9b Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 3 Aug 2017 17:17:17 +0100 Subject: [PATCH 01/11] Make a generic generate spend. Tests pass. Fixup after rebase Fixup after rebase Make node have only test compile dependency against finance module. Use original query code. Fixup after rebase Update docs Edit docs Add to changelog Follow recommendations from PR Follow recommendations from PR Make a re-usable helper function to create MockServices with database for tests Tweak a few comments Don't include tryLockFungibleStateForSpending in soft lock docs. Respond to PR comments Fix whitespace error Fix compile error Fixup after rebase --- .../corda/core/node/services/VaultService.kt | 63 ++--- docs/source/changelog.rst | 4 + .../corda/docs/FxTransactionBuildTutorial.kt | 42 ++-- docs/source/flow-state-machines.rst | 2 +- .../source/tutorial-building-transactions.rst | 5 +- docs/source/tutorial-contract.rst | 6 +- .../corda/contracts/JavaCommercialPaper.java | 10 +- .../net/corda/contracts/CommercialPaper.kt | 7 +- .../kotlin/net/corda/contracts/asset/Cash.kt | 170 +++++++++++++ .../corda/contracts/asset/OnLedgerAsset.kt | 2 - .../kotlin/net/corda/flows/CashExitFlow.kt | 4 +- .../kotlin/net/corda/flows/CashPaymentFlow.kt | 5 +- .../net/corda/flows/TwoPartyTradeFlow.kt | 8 +- .../corda/contracts/CommercialPaperTests.kt | 52 ++-- .../net/corda/contracts/asset/CashTests.kt | 44 ++-- node/build.gradle | 2 +- .../vault/HibernateQueryCriteriaParser.kt | 4 +- .../node/services/vault/NodeVaultService.kt | 226 +++++++++--------- .../services/vault/VaultQueryJavaTests.java | 107 ++++----- .../node/messaging/TwoPartyTradeFlowTests.kt | 16 +- .../database/HibernateConfigurationTest.kt | 7 +- .../services/vault/NodeVaultServiceTest.kt | 104 ++++---- .../node/services/vault/VaultQueryTests.kt | 72 ++---- .../node/services/vault/VaultWithCashTest.kt | 35 +-- .../notarydemo/flows/DummyIssueAndMove.kt | 2 +- .../corda/testing/contracts/VaultFiller.kt | 5 +- .../net/corda/testing/node/MockServices.kt | 37 ++- 27 files changed, 577 insertions(+), 464 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index bb3b44c17d..183557f355 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -5,18 +5,13 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.CoreTransaction -import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.OpaqueBytes import rx.Observable import rx.subjects.PublishSubject -import java.security.PublicKey import java.time.Instant import java.util.* @@ -224,29 +219,7 @@ interface VaultService { fun getTransactionNotes(txnId: SecureHash): Iterable - /** - * Generate a transaction that moves an amount of currency to the given pubkey. - * - * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] - * - * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed - * to move the cash will be added on top. - * @param amount How much currency to send. - * @param to a key of the recipient. - * @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. - * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign - * the resulting transaction for it to be valid. - * @throws InsufficientBalanceException when a cash spending transaction fails because - * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). - */ - @Throws(InsufficientBalanceException::class) - @Suspendable - fun generateSpend(tx: TransactionBuilder, - amount: Amount, - to: AbstractParty, - onlyFromParties: Set? = null): Pair> + // DOCEND VaultStatesQuery /** * Soft locking is used to prevent multiple transactions trying to use the same output simultaneously. @@ -257,7 +230,10 @@ interface VaultService { /** * Reserve a set of [StateRef] for a given [UUID] unique identifier. - * Typically, the unique identifier will refer to a Flow lockId associated with a [Transaction] in an in-flight flow. + * Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow. + * In this case if the flow terminates the locks will automatically be freed, even if there is an error. + * However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows, + * or from other thread contexts e.g. [CordaService] instances. * In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs. * * @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef] @@ -273,21 +249,32 @@ interface VaultService { * are consumed as part of cash spending. */ fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet? = null) - // DOCEND SoftLockAPI /** - * TODO: this function should be private to the vault, but currently Cash Exit functionality - * is implemented in a separate module (finance) and requires access to it. + * Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them. + * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending` + * However, this is fully generic and can operate with custom [FungibleAsset] states. + * @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states. + * @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the + * [contractType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED, + * soft lock and contract type requirements. + * @param amount The required amount of the asset, but with the issuer stripped off. + * It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery]. + * @param contractType class type of the result set. + * @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount, + * or else an empty list and no change in the stored lock states when their are insufficient resources available. */ @Suspendable - fun unconsumedStatesForSpending(amount: Amount, - onlyFromIssuerParties: Set? = null, - notary: Party? = null, - lockId: UUID, - withIssuerRefs: Set? = null): List> + @Throws(StatesNotAvailableException::class) + fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount, + contractType: Class): List> + } + class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) { override fun toString() = "Soft locking error: $message" } \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e889a99d13..68e1d3f166 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -60,6 +60,10 @@ UNRELEASED * ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should use to exclude core Corda JARs from being built into Cordapp fat JARs. +* Move the original ``Cash`` specific ``generateSpend`` and ``unconsumedStatesForSpending`` methods from ``:core`` + and onto ``Cash`` contract in the ``:finance`` module. Provide a genuinely generic ``tryLockFungibleStatesForSpending`` + on ``VaultService``, which in future could be optimised for performance. + .. Milestone 15: * Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 9b9682cd25..14d0efb9fa 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -5,6 +5,7 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.withoutIssuer import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic @@ -13,7 +14,6 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.ServiceHub -import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.builder import net.corda.core.serialization.CordaSerializable @@ -31,9 +31,10 @@ private data class FxRequest(val tradeId: String, val notary: Party? = null) // DOCSTART 1 -// This is equivalent to the VaultService.generateSpend +// This is equivalent to the Cash.generateSpend // Which is brought here to make the filtering logic more visible in the example private fun gatherOurInputs(serviceHub: ServiceHub, + lockId: UUID, amountRequired: Amount>, notary: Party?): Pair>, Long> { // extract our identity for convenience @@ -41,34 +42,25 @@ private fun gatherOurInputs(serviceHub: ServiceHub, val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") } val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties) + val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name + val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName)) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) } val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression) - // Collect cash type inputs - val suitableCashStates = serviceHub.vaultQueryService.queryBy(fungibleCriteria.and(cashCriteria)).states - require(!suitableCashStates.isEmpty()) { "Insufficient funds" } + val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria) - var remaining = amountRequired.quantity - // We will need all of the inputs to be on the same notary. - // For simplicity we just filter on the first notary encountered - // A production quality flow would need to migrate notary if the - // the amounts were not sufficient in any one notary - val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary + val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java) - val inputsList = mutableListOf>() - // Iterate over filtered cash states to gather enough to pay - for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) { - inputsList += cash - if (remaining <= cash.state.data.amount.quantity) { - return Pair(inputsList, cash.state.data.amount.quantity - remaining) - } - remaining -= cash.state.data.amount.quantity - } - throw IllegalStateException("Insufficient funds") + check(eligibleStates.isNotEmpty()) { "Insufficient funds" } + val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity } + val change = amount - amountRequired.quantity + + return Pair(eligibleStates, change) } // DOCEND 1 -private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): Pair>, List> { +private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, request: FxRequest): Pair>, List> { // Create amount with correct issuer details val sellAmount = request.amount @@ -78,7 +70,7 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques // we will use query manually in the helper function below. // Putting this into a non-suspendable function also prevents issues when // the flow is suspended. - val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) + val (inputs, residual) = gatherOurInputs(serviceHub, lockId, sellAmount, request.notary) // Build and an output state for the counterparty val transferedFundsOutput = Cash.State(sellAmount, request.counterparty) @@ -119,7 +111,7 @@ class ForeignExchangeFlow(val tradeId: String, } else throw IllegalArgumentException("Our identity must be one of the parties in the trade.") // Call the helper method to identify suitable inputs and make the outputs - val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, localRequest) + val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest) // identify the notary for our states val notary = outInputStates.first().state.notary @@ -231,7 +223,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { // we will use query manually in the helper function below. // Putting this into a non-suspendable function also prevent issues when // the flow is suspended. - val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, request) + val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, request) // Send back our proposed states and await the full transaction to verify val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single() diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index 1561740c03..d25ae31313 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -279,7 +279,7 @@ This code is longer but no more complicated. Here are some things to pay attenti 1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered what we expected to be offered. -2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this. +2. We create a cash spend using ``Cash.generateSpend``. You can read the vault documentation to learn more about this. 3. We access the *service hub* as needed to access things that are transient and may change or be recreated whilst a flow is suspended, such as the wallet or the network map. 4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so diff --git a/docs/source/tutorial-building-transactions.rst b/docs/source/tutorial-building-transactions.rst index c0ff9169ab..18734ca89f 100644 --- a/docs/source/tutorial-building-transactions.rst +++ b/docs/source/tutorial-building-transactions.rst @@ -116,11 +116,12 @@ standard ``CashState`` in the ``:financial`` Gradle module. The Cash contract uses ``FungibleAsset`` states to model holdings of interchangeable assets and allow the split/merge and summing of states to meet a contractual obligation. We would normally use the -``generateSpend`` method on the ``VaultService`` to gather the required +``Cash.generateSpend`` method to gather the required amount of cash into a ``TransactionBuilder``, set the outputs and move command. However, to elucidate more clearly example flow code is shown here that will manually carry out the inputs queries by specifying relevant -query criteria filters to the ``queryBy`` method of the ``VaultQueryService``. +query criteria filters to the ``tryLockFungibleStatesForSpending`` method +of the ``VaultService``. .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt :language: kotlin diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 154dfef8da..a1acf7997a 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -680,9 +680,9 @@ Finally, we can do redemption. .. sourcecode:: kotlin @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { // Add the cash movement using the states in our vault. - vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) + Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) } @@ -698,7 +698,7 @@ from the issuer of the commercial paper to the current owner. If we don't have e an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper. -.. warning:: The amount we pass to the ``generateSpend`` function has to be treated first with ``withoutIssuer``. +.. warning:: The amount we pass to the ``Cash.generateSpend`` function has to be treated first with ``withoutIssuer``. This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper contract requires payment in the form of a currency issued by a specific party (e.g. the central bank, or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index 584bd71926..d4910e67a9 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import kotlin.Pair; import kotlin.Unit; +import net.corda.contracts.asset.Cash; import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; import net.corda.core.crypto.SecureHash; @@ -12,15 +13,14 @@ import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.Party; -import net.corda.core.node.services.VaultService; +import net.corda.core.node.ServiceHub; import net.corda.core.transactions.LedgerTransaction; import net.corda.core.transactions.TransactionBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.time.Instant; -import java.util.Currency; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; @@ -255,8 +255,8 @@ public class JavaCommercialPaper implements Contract { } @Suspendable - public void generateRedeem(TransactionBuilder tx, StateAndRef paper, VaultService vault) throws InsufficientBalanceException { - vault.generateSpend(tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); + public void generateRedeem(TransactionBuilder tx, StateAndRef paper, ServiceHub services) throws InsufficientBalanceException { + Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET); tx.addInputState(paper); tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); } diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index c101c912b0..822b30d0bc 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -1,6 +1,7 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash @@ -9,7 +10,7 @@ import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.Emoji -import net.corda.core.node.services.VaultService +import net.corda.core.node.ServiceHub import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState @@ -185,9 +186,9 @@ class CommercialPaper : Contract { */ @Throws(InsufficientBalanceException::class) @Suspendable - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { // Add the cash movement using the states in our vault. - vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) + Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) } diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 23c7431c65..02832f6893 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -1,5 +1,7 @@ package net.corda.contracts.asset +import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair @@ -9,16 +11,27 @@ import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.Emoji +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString +import net.corda.core.utilities.toNonEmptySet +import net.corda.core.utilities.trace import net.corda.schemas.CashSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey +import java.sql.SQLException import java.util.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -212,6 +225,163 @@ class Cash : OnLedgerAsset() { "there is only a single issue command" using (cashCommands.count() == 1) } } + + companion object { + // coin selection retry loop counter, sleep (msecs) and lock for selecting states + private val MAX_RETRIES = 5 + private val RETRY_SLEEP = 100 + private val spendLock: ReentrantLock = ReentrantLock() + /** + * Generate a transaction that moves an amount of currency to the given pubkey. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to a key of the recipient. + * @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. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + onlyFromParties: Set = emptySet()): Pair> { + + fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) + + // Retrieve unspent and unlocked cash states that meet our spending criteria. + val acceptableCoins = Cash.unconsumedCashStatesForSpending(services, amount, onlyFromParties, tx.notary, tx.lockId) + return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins, + { state, quantity, owner -> deriveState(state, quantity, owner) }, + { Cash().generateMoveCommand() }) + + } + + /** + * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @JvmStatic + @Suspendable + fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> { + + val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) + val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) + + val stateAndRefs = mutableListOf>() + + // TODO: Need to provide a database provider independent means of performing this function. + // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: + // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the + // running total of such an accumulator + // 2) H2 uses session variables to perform this accumulator function: + // http://www.h2database.com/html/functions.html#set + // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + + for (retryCount in 1..MAX_RETRIES) { + + spendLock.withLock { + val statement = services.jdbcSession().createStatement() + try { + statement.execute("CALL SET(@t, 0);") + + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val selectJoin = """ + SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id + FROM vault_states AS vs, contract_cash_states AS ccs + WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index + AND vs.state_status = 0 + AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} + AND (vs.lock_id = '$lockId' OR vs.lock_id is null) + """ + + (if (notary != null) + " AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + + // Retrieve spendable state refs + val rs = statement.executeQuery(selectJoin) + stateAndRefs.clear() + log.debug(selectJoin) + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return stateAndRefs + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + stateAndRefs.clear() + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } finally { + statement.close() + } + } + + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { + Strand.sleep(RETRY_SLEEP * retryCount.toLong()) + } + } + + log.warn("Insufficient spendable states identified for $amount") + return stateAndRefs + } + } + } // Small DSL extensions. diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index 383ddab54d..c40649b789 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -226,8 +226,6 @@ abstract class OnLedgerAsset> : C * * @param tx transaction builder to add states and commands to. * @param amountIssued the amount to be exited, represented as a quantity of issued currency. - * @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not - * necessarily owned by us. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. * @return the public keys which must sign the transaction for it to be valid. diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 33c8b20b7b..abf0b511c8 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -11,8 +11,8 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import java.util.* @@ -41,7 +41,7 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - val exitStates = serviceHub.vaultService.unconsumedStatesForSpending(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) + val exitStates = Cash.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val signers = try { Cash().generateExit( builder, diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index 105bb113a5..28edc4a5f3 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -1,6 +1,7 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.flows.StartableByRPC @@ -26,7 +27,7 @@ open class CashPaymentFlow( val recipient: Party, val anonymous: Boolean, progressTracker: ProgressTracker, - val issuerConstraint: Set? = null) : AbstractCashFlow(progressTracker) { + val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) /** A straightforward constructor that constructs spends using cash states of any issuer. */ @@ -45,7 +46,7 @@ open class CashPaymentFlow( val builder: TransactionBuilder = TransactionBuilder(null as Party?) // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { - serviceHub.vaultService.generateSpend( + Cash.generateSpend(serviceHub, builder, amount, anonymousRecipient, diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index e2d788f161..584ade4106 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -1,8 +1,12 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash import net.corda.contracts.asset.sumCashBy -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.OwnableState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.withoutIssuer import net.corda.core.flows.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -174,7 +178,7 @@ object TwoPartyTradeFlow { val ptx = TransactionBuilder(notary) // 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.sellerOwner) + val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(assetForSale) diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 31edaa68ba..8adf55f286 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -1,21 +1,19 @@ package net.corda.contracts import net.corda.contracts.asset.* -import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* -import net.corda.core.utilities.days import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService -import net.corda.core.utilities.seconds import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.node.utilities.configureDatabase +import net.corda.core.utilities.days +import net.corda.core.utilities.seconds import net.corda.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.testing.node.makeTestDatabaseAndMockServices import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -212,40 +210,22 @@ class CommercialPaperTestsGeneric { @Test fun `issue move and then redeem`() { initialiseTestSerialization() - val dataSourcePropsAlice = makeTestDataSourceProperties() - val databaseAlice = configureDatabase(dataSourcePropsAlice, makeTestDatabaseProperties()) + val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY)) + val databaseAlice = aliceDatabaseAndServices.first + aliceServices = aliceDatabaseAndServices.second + aliceVaultService = aliceServices.vaultService + databaseAlice.transaction { - - aliceServices = object : MockServices(ALICE_KEY) { - override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 dataSourcePropsBigCorp = makeTestDataSourceProperties() - val databaseBigCorp = configureDatabase(dataSourcePropsBigCorp, makeTestDatabaseProperties()) + val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY)) + val databaseBigCorp = bigCorpDatabaseAndServices.first + bigCorpServices = bigCorpDatabaseAndServices.second + bigCorpVaultService = bigCorpServices.vaultService + databaseBigCorp.transaction { - - bigCorpServices = object : MockServices(BIG_CORP_KEY) { - override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 } @@ -266,7 +246,7 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { val builder = TransactionBuilder(DUMMY_NOTARY) - aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) @@ -288,7 +268,7 @@ class CommercialPaperTestsGeneric { fun makeRedeemTX(time: Instant): Pair { val builder = TransactionBuilder(DUMMY_NOTARY) builder.setTimeWindow(time, 30.seconds) - CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService) + CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = bigCorpServices.addSignature(ptx) val stx = notaryServices.addSignature(ptx2) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index c70d7f3cf8..cd9aa3d42d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -6,26 +6,19 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.vault.HibernateVaultQueryImpl +import net.corda.core.utilities.OpaqueBytes import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.fillWithSomeTestCash -import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.testing.node.makeTestDatabaseAndMockServices +import org.junit.After import org.junit.Before import org.junit.Test import java.security.KeyPair @@ -55,25 +48,11 @@ class CashTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(NodeVaultService::class) - val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) + val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)) + database = databaseAndServices.first + miniCorpServices = databaseAndServices.second + database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties()) - miniCorpServices = object : MockServices(MINI_CORP_KEY) { - override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) - override val vaultService: VaultService = makeVaultService(dataSourceProps) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 }) - } - - override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) - } - miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, @@ -88,6 +67,11 @@ class CashTests : TestDependencyInjectionBase() { resetTestSerialization() } + @After + fun tearDown() { + database.close() + } + @Test fun trivial() { transaction { @@ -485,7 +469,7 @@ class CashTests : TestDependencyInjectionBase() { fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - vault.generateSpend(tx, amount, dest) + Cash.generateSpend(miniCorpServices, tx, amount, dest) } return tx.toWireTransaction() } @@ -586,7 +570,7 @@ class CashTests : TestDependencyInjectionBase() { database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - vault.generateSpend(tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } diff --git a/node/build.gradle b/node/build.gradle index ffbe9df98b..616e217483 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -87,7 +87,6 @@ processSmokeTestResources { // build/reports/project/dependencies/index.html for green highlighted parts of the tree. dependencies { - compile project(':finance') compile project(':node-schemas') compile project(':node-api') compile project(':client:rpc') @@ -150,6 +149,7 @@ dependencies { testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version" testCompile project(':test-utils') testCompile project(':client:jfx') + testCompile project(':finance') // sample test schemas testCompile project(path: ':finance', configuration: 'testArtifacts') diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 46cc56a256..934193dc3c 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -8,13 +8,13 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria +import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.toHexString import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace -import net.corda.core.schemas.CommonSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.util.* import javax.persistence.Tuple diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 9de77c5a70..c173b55925 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -8,28 +8,32 @@ import io.requery.kotlin.eq import io.requery.kotlin.notNull import io.requery.query.RowExpression import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.OnLedgerAsset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.crypto.toBase58String -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox import net.corda.core.internal.tee import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService +import net.corda.core.node.services.vault.IQueryCriteriaParser +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.schemas.PersistentState import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction -import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toNonEmptySet +import net.corda.core.utilities.trace import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.database.parserTransactionIsolationLevel import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey -import java.sql.SQLException import java.util.* -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock +import javax.persistence.criteria.Predicate /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -79,6 +81,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P // For use during publishing only. val updatesPublisher: rx.Observer> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher) } + private val mutex = ThreadBox(InnerState()) private fun recordUpdate(update: Vault.Update): Vault.Update { @@ -334,123 +337,110 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P } } - // coin selection retry loop counter, sleep (msecs) and lock for selecting states - val MAX_RETRIES = 5 - val RETRY_SLEEP = 100 - val spendLock: ReentrantLock = ReentrantLock() + // TODO We shouldn't need to rewrite the query if we could modify the defaults. + private class QueryEditor(val services: ServiceHub, + val lockId: UUID, + val contractType: Class) : IQueryCriteriaParser { + var alreadyHasVaultQuery: Boolean = false + var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType), + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), + status = Vault.StateStatus.UNCONSUMED) - @Suspendable - override fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set?, notary: Party?, lockId: UUID, withIssuerRefs: Set?): List> { - - val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1) - val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1) - - val stateAndRefs = mutableListOf>() - - // TODO: Need to provide a database provider independent means of performing this function. - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: - // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the - // running total of such an accumulator - // 2) H2 uses session variables to perform this accumulator function: - // http://www.h2database.com/html/functions.html#set - // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - - for (retryCount in 1..MAX_RETRIES) { - - spendLock.withLock { - val statement = configuration.jdbcSession().createStatement() - try { - statement.execute("CALL SET(@t, 0);") - - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others - val selectJoin = """ - SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id - FROM vault_states AS vs, contract_cash_states AS ccs - WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index - AND vs.state_status = 0 - AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) - """ + - (if (notary != null) - " AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") + - (if (issuerKeysStr != null) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + - (if (issuerRefsStr != null) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") - - // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) - stateAndRefs.clear() - log.debug(selectJoin) - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } - - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // update database - softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return stateAndRefs - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { - stateAndRefs.clear() - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() - } - } - - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong()) - } + override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection { + modifiedCriteria = criteria + return emptyList() } - log.warn("Insufficient spendable states identified for $amount") - return stateAndRefs + override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection { + modifiedCriteria = criteria + return emptyList() + } + + override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection { + modifiedCriteria = criteria + return emptyList() + } + + override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { + modifiedCriteria = criteria + return emptyList() + } + + override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection { + modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType), + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), + status = Vault.StateStatus.UNCONSUMED) + alreadyHasVaultQuery = true + return emptyList() + } + + override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection { + parse(left) + val modifiedLeft = modifiedCriteria + parse(right) + val modifiedRight = modifiedCriteria + modifiedCriteria = modifiedLeft.or(modifiedRight) + return emptyList() + } + + override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection { + parse(left) + val modifiedLeft = modifiedCriteria + parse(right) + val modifiedRight = modifiedCriteria + modifiedCriteria = modifiedLeft.and(modifiedRight) + return emptyList() + } + + override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection { + val basicQuery = modifiedCriteria + criteria.visit(this) + modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery) + return emptyList() + } + + fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page { + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + parse(criteria, sorter) + + return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter) + } } - /** - * 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. - */ + @Suspendable - override fun generateSpend(tx: TransactionBuilder, - amount: Amount, - to: AbstractParty, - onlyFromParties: Set?): Pair> { - // Retrieve unspent and unlocked cash states that meet our spending criteria. - val acceptableCoins = unconsumedStatesForSpending(amount, onlyFromParties, tx.notary, tx.lockId) - return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins, - { state, quantity, owner -> deriveState(state, quantity, owner) }, - { Cash().generateMoveCommand() }) - } + @Throws(StatesNotAvailableException::class) + override fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount, + contractType: Class): List> { + if (amount.quantity == 0L) { + return emptyList() + } - private fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) - = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + // TODO This helper code re-writes the query to alter the defaults on things such as soft locks + // and then runs the query. Ideally we would not need to do this. + val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery) + + var claimedAmount = 0L + val claimedStates = mutableListOf>() + for (state in results.states) { + val issuedAssetToken = state.state.data.amount.token + if (issuedAssetToken.product == amount.token) { + claimedStates += state + claimedAmount += state.state.data.amount.quantity + if (claimedAmount > amount.quantity) { + break + } + } + } + if (claimedStates.isEmpty() || claimedAmount < amount.quantity) { + return emptyList() + } + softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet()) + return claimedStates + } // TODO : Persists this in DB. private val authorisedUpgrade = mutableMapOf>>() diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index bb2213bd1b..b5ed6dc440 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,42 +1,50 @@ package net.corda.node.services.vault; -import com.google.common.collect.*; -import net.corda.contracts.*; -import net.corda.contracts.asset.*; +import com.google.common.collect.ImmutableSet; +import kotlin.Pair; +import net.corda.contracts.DealState; +import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; -import net.corda.core.crypto.*; -import net.corda.core.identity.*; -import net.corda.core.messaging.*; -import net.corda.core.node.services.*; +import net.corda.core.crypto.EncodingUtils; +import net.corda.core.identity.AbstractParty; +import net.corda.core.messaging.DataFeed; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.VaultQueryException; +import net.corda.core.node.services.VaultQueryService; +import net.corda.core.node.services.VaultService; import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.*; -import net.corda.core.schemas.*; -import net.corda.core.transactions.*; -import net.corda.core.utilities.*; -import net.corda.node.services.database.*; -import net.corda.node.services.schema.*; -import net.corda.node.utilities.*; -import net.corda.schemas.*; -import net.corda.testing.*; -import net.corda.testing.contracts.*; -import net.corda.testing.node.*; -import net.corda.testing.schemas.*; -import org.jetbrains.annotations.*; -import org.junit.*; +import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; +import net.corda.core.utilities.OpaqueBytes; +import net.corda.node.utilities.CordaPersistence; +import net.corda.schemas.CashSchemaV1; +import net.corda.testing.TestConstants; +import net.corda.testing.TestDependencyInjectionBase; +import net.corda.testing.contracts.DummyLinearContract; +import net.corda.testing.contracts.VaultFiller; +import net.corda.testing.node.MockServices; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import rx.Observable; -import java.io.*; -import java.lang.reflect.*; +import java.io.IOException; +import java.lang.reflect.Field; +import java.security.KeyPair; import java.util.*; -import java.util.stream.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; -import static net.corda.contracts.asset.CashKt.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; -import static net.corda.core.utilities.ByteArrays.*; -import static net.corda.node.utilities.CordaPersistenceKt.*; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; +import static net.corda.core.utilities.ByteArrays.toHexString; import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.node.MockServicesKt.*; -import static org.assertj.core.api.Assertions.*; +import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; +import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { @@ -47,38 +55,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Before public void setUp() { - Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString()); - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()); - database.transaction(statement -> { - Set customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE)); - HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties()); - services = new MockServices(getMEGA_CORP_KEY()) { - @NotNull - @Override - public VaultService getVaultService() { - if (vaultSvc != null) return vaultSvc; - return makeVaultService(dataSourceProps, hibernateConfig); - } - - @NotNull - @Override - public VaultQueryService getVaultQueryService() { - return new HibernateVaultQueryImpl(hibernateConfig, vaultSvc.getUpdatesPublisher()); - } - - @Override - public void recordTransactions(@NotNull Iterable txs) { - for (SignedTransaction stx : txs) { - getValidatedTransactions().addTransaction(stx); - } - Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx); - vaultSvc.notifyAll(wtxn.collect(Collectors.toList())); - } - }; - vaultSvc = services.getVaultService(); - vaultQuerySvc = services.getVaultQueryService(); - return services; - }); + ArrayList keys = new ArrayList<>(); + keys.add(getMEGA_CORP_KEY()); + Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys); + database = databaseAndServices.getFirst(); + services = databaseAndServices.getSecond(); + vaultSvc = services.getVaultService(); + vaultQuerySvc = services.getVaultQueryService(); } @After diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 3910859d4d..03f8ea9c62 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -148,8 +148,8 @@ class TwoPartyTradeFlowTests { bobNode.disableDBCloseOnStop() val cashStates = bobNode.database.transaction { - bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) - } + bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) + } val alicesFakePaper = aliceNode.database.transaction { fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, @@ -168,7 +168,7 @@ class TwoPartyTradeFlowTests { } val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, - "alice's paper".outputStateAndRef()) + "alice's paper".outputStateAndRef()) assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow()) @@ -533,11 +533,11 @@ class TwoPartyTradeFlowTests { override fun call(): SignedTransaction { send(buyer, Pair(notary.notaryIdentity, price)) return subFlow(Seller( - buyer, - notary, - assetToSell, - price, - me.party)) + buyer, + notary, + assetToSell, + price, + me.party)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 27078de44b..0a37f8b04e 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -3,10 +3,12 @@ package net.corda.node.services.database import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DummyFungibleContract +import net.corda.contracts.asset.sumCash import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef @@ -14,6 +16,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence @@ -38,6 +41,7 @@ import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test +import java.math.BigDecimal import java.time.Instant import java.util.* import javax.persistence.EntityManager @@ -126,7 +130,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList - assertThat(queryResults.size).isEqualTo(6) + val coins = queryResults.map { it.contractState.deserialize>().data }.sumCash() + assertThat(coins.toDecimal() >= BigDecimal("50.00")) } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 9f456fa44d..893d8eff3d 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -1,39 +1,40 @@ package net.corda.node.services.vault +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.contracts.asset.sumCash import net.corda.contracts.getCashBalance import net.corda.core.contracts.* import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet -import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.testing.node.makeTestDatabaseAndMockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.After import org.junit.Before import org.junit.Test import rx.observers.TestSubscriber +import java.math.BigDecimal import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors @@ -50,23 +51,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(NodeVaultService::class) - val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) - database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties()) - services = object : MockServices() { - override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 }) - } - override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) - } - } + val databaseAndServices = makeTestDatabaseAndMockServices() + database = databaseAndServices.first + services = databaseAndServices.second } @After @@ -75,6 +62,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { LogHelper.reset(NodeVaultService::class) } + @Suspendable + private fun VaultService.unconsumedCashStatesForSpending(amount: Amount, + onlyFromIssuerParties: Set? = null, + notary: Party? = null, + lockId: UUID = UUID.randomUUID(), + withIssuerRefs: Set? = null): List> { + + val notaryName = if (notary != null) listOf(notary.name) else null + var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName) + if (onlyFromIssuerParties != null || withIssuerRefs != null) { + baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria( + issuerPartyName = onlyFromIssuerParties?.toList(), + issuerRef = withIssuerRefs?.toList())) + } + + return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java) + } + + @Test fun `states not local to instance`() { database.transaction { @@ -308,7 +314,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(100.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100) @@ -324,12 +330,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY) - val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), - onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList() + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(2) - assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER) - assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1)) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1)) + assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1)) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer) } } @@ -345,12 +352,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(4) - val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), - onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList() + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, + onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))) assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) - assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference) - assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference) + assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer.reference) } } @@ -363,9 +371,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(110.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS) spendableStatesUSD.forEach(::println) - assertThat(spendableStatesUSD).hasSize(1) + assertThat(spendableStatesUSD).hasSize(0) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) } @@ -380,7 +388,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(2) - val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(1.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L) @@ -397,16 +405,30 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) + var unlockedStates = 30 val allStates = vaultQuery.queryBy().states - assertThat(allStates).hasSize(30) + assertThat(allStates).hasSize(unlockedStates) + var lockedCount = 0 for (i in 1..5) { - val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(20.DOLLARS, lockId = UUID.randomUUID()) + val lockId = UUID.randomUUID() + val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId) spendableStatesUSD.forEach(::println) + assertThat(spendableStatesUSD.size <= unlockedStates) + unlockedStates -= spendableStatesUSD.size + val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId))) + val lockedStates = vaultQuery.queryBy(criteriaLocked).states + if (spendableStatesUSD.isNotEmpty()) { + assertEquals(spendableStatesUSD.size, lockedStates.size) + val lockedTotal = lockedStates.map { it.state.data }.sumCash() + val foundAmount = spendableStatesUSD.map { it.state.data }.sumCash() + assertThat(foundAmount.toDecimal() >= BigDecimal("20.00")) + assertThat(lockedTotal == foundAmount) + lockedCount += lockedStates.size + } } - // note only 3 spend attempts succeed with a total of 8 states val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) - assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(8) + assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(lockedCount) } } @@ -484,7 +506,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { database.transaction { val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply { - service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) + Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) }.toWireTransaction() service.notify(moveTx) } @@ -530,7 +552,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { // Move cash val moveTx = database.transaction { TransactionBuilder(newNotary).apply { - service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) + Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity) }.toWireTransaction() } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 311794c0c9..84cc1b8742 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -1,23 +1,19 @@ package net.corda.node.services.vault -import net.corda.contracts.* +import net.corda.contracts.CommercialPaper +import net.corda.contracts.Commodity +import net.corda.contracts.DealState import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toBase58String -import net.corda.core.utilities.days import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.utilities.seconds -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.toHexString -import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.schema.NodeSchemaService +import net.corda.core.utilities.* import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.schemas.CashSchemaV1 @@ -27,7 +23,7 @@ import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.node.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions @@ -54,24 +50,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Before fun setUp() { - val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) - database.transaction { - val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1) - val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties()) - services = object : MockServices(MEGA_CORP_KEY) { - override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 }) - } - override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) - } - } + val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY)) + database = databaseAndServices.first + services = databaseAndServices.second } @After @@ -97,7 +78,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { _database.transaction { // create new states - services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") services.fillWithSomeTestLinearStates(3, "ABC") @@ -239,15 +220,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `unconsumed cash states sorted by state ref`() { database.transaction { - var stateRefs : MutableList = mutableListOf() + val stateRefs: MutableList = mutableListOf() val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) val issuedStateRefs = issuedStates.states.map { it.ref }.toList() stateRefs.addAll(issuedStateRefs) val spentStates = services.consumeCash(25.DOLLARS) - var consumedStateRefs = spentStates.consumed.map { it.ref }.toList() - var producedStateRefs = spentStates.produced.map { it.ref }.toList() + val consumedStateRefs = spentStates.consumed.map { it.ref }.toList() + val producedStateRefs = spentStates.produced.map { it.ref }.toList() stateRefs.addAll(consumedStateRefs.plus(producedStateRefs)) val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) @@ -271,8 +252,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `unconsumed cash states sorted by state ref txnId and index`() { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) - services.consumeCash(10.DOLLARS) - services.consumeCash(10.DOLLARS) + val consumed = mutableSetOf() + services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash } + services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash } val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX) @@ -283,13 +265,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { results.statesMetadata.forEach { println(" ${it.ref}") + assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED) } - - // explicit sort order asc by txnId and then index: - // order by - // vaultschem1_.transaction_id asc, - // vaultschem1_.output_index asc - assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change) + val sorted = results.states.sortedBy { it.ref.toString() } + assertThat(results.states).isEqualTo(sorted) + assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) } } } @@ -411,7 +391,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } } - val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } + val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) @Test @@ -870,7 +850,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `aggregate functions count by contract type and state status`() { database.transaction { // create new states - services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") services.fillWithSomeTestLinearStates(3, "ABC") @@ -896,14 +876,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeLinearStates(linearStatesXYZ.states.toList()) services.consumeLinearStates(linearStatesJKL.states.toList()) services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) - services.consumeCash(50.DOLLARS) + val cashUpdates = services.consumeCash(50.DOLLARS) // UNCONSUMED states (default) // count fungible assets val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long - assertThat(fungibleStateCountUnconsumed).isEqualTo(5L) + assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size) // count linear states val linearStateCountUnconsumed = vaultQuerySvc.queryBy(countCriteriaUnconsumed).otherResults.single() as Long @@ -918,7 +898,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // count fungible assets val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED) val fungibleStateCountConsumed = vaultQuerySvc.queryBy>(countCriteriaConsumed).otherResults.single() as Long - assertThat(fungibleStateCountConsumed).isEqualTo(6L) + assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size) // count linear states val linearStateCountConsumed = vaultQuerySvc.queryBy(countCriteriaConsumed).otherResults.single() as Long @@ -962,7 +942,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `states consumed after time`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 38743121ae..fe564712b5 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -11,17 +11,12 @@ import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.testing.node.makeTestDatabaseAndMockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After @@ -44,23 +39,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(VaultWithCashTest::class) - val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) - database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties()) - services = object : MockServices() { - override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) - - override fun recordTransactions(txs: Iterable) { - for (stx in txs) { - 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 }) - } - override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) - } - } + val databaseAndServices = makeTestDatabaseAndMockServices() + database = databaseAndServices.first + services = databaseAndServices.second } @After @@ -103,7 +84,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) val spendTX = notaryServices.addSignature(spendPTX) @@ -151,7 +132,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn1Builder = TransactionBuilder(DUMMY_NOTARY) - vault.generateSpend(txn1Builder, 60.DOLLARS, BOB) + Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB) val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") @@ -187,7 +168,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { try { val txn2Builder = TransactionBuilder(DUMMY_NOTARY) - vault.generateSpend(txn2Builder, 80.DOLLARS, BOB) + Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB) val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") @@ -299,7 +280,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index 38bfaf1595..4db198241d 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -34,7 +34,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: // Move ownership of the asset to the counterparty val moveTxBuilder = TransactionBuilder(notary = notary) - val (_, keys) = vaultService.generateSpend(moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode) + val (_, keys) = Cash.generateSpend(serviceHub, moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode) // We don't check signatures because we know that the notary's signature is missing signInitialTransaction(moveTxBuilder, keys) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index 5991a554b7..34f4f4dda4 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -13,9 +13,9 @@ import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.toFuture -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.CHARLIE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY_KEY @@ -228,10 +228,11 @@ fun ServiceHub.evolveLinearState(linearState: StateAndRef) : StateA @JvmOverloads fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE): Vault.Update { val update = vaultService.rawUpdates.toFuture() + val services = this // A tx that spends our money. val spendTX = TransactionBuilder(DUMMY_NOTARY).apply { - vaultService.generateSpend(this, amount, to) + Cash.generateSpend(services, this, amount, to) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction(checkSufficientSignatures = false) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index a6bfdb1190..689d578e6e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -9,6 +9,7 @@ import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* +import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction @@ -24,11 +25,14 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService +import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService -import net.corda.testing.DUMMY_CA -import net.corda.testing.MEGA_CORP -import net.corda.testing.MOCK_IDENTITIES -import net.corda.testing.getTestPartyAndCertificate +import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.configureDatabase +import net.corda.schemas.CashSchemaV1 +import net.corda.schemas.CommercialPaperSchemaV1 +import net.corda.testing.* +import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -212,4 +216,29 @@ fun makeTestDatabaseProperties(): Properties { return props } +fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), keys: List = listOf(MEGA_CORP_KEY)): Pair { + val dataSourceProps = makeTestDataSourceProperties() + val databaseProperties = makeTestDatabaseProperties() + val database = configureDatabase(dataSourceProps, databaseProperties) + val mockService = database.transaction { + val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties) + object : MockServices(*(keys.toTypedArray())) { + override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + 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 }) + } + + override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) + + override fun jdbcSession(): Connection = database.createSession() + } + } + return Pair(database, mockService) +} + val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") From 2b8b3298dcfed2bb6b9845841f837e624a269950 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 10 Aug 2017 15:50:44 +0100 Subject: [PATCH 02/11] Tweak scheduled flow tests to improve error diagnosis * Add logging of the specific differences rather than dumping a long list of states to stdout * Verify all flows complete successfully --- .../services/events/ScheduledFlowTests.kt | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index a9bb8beb13..97ff1b7182 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -1,6 +1,7 @@ package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable +import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.containsAny import net.corda.core.flows.* @@ -15,10 +16,11 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.SortAttribute import net.corda.core.transactions.TransactionBuilder -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.utilities.getOrThrow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork import org.junit.After @@ -30,6 +32,10 @@ import java.time.Instant import kotlin.test.assertEquals class ScheduledFlowTests { + companion object { + val PAGE_SIZE = 20 + val SORTING = Sort(listOf(Sort.SortColumn(SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID), Sort.Direction.DESC))) + } lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var nodeA: MockNetwork.MockNode @@ -133,33 +139,50 @@ class ScheduledFlowTests { @Test fun `run a whole batch of scheduled flows`() { val N = 100 + val futures = mutableListOf>() for (i in 0..N - 1) { - nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) - nodeB.services.startFlow(InsertInitialStateFlow(nodeA.info.legalIdentity)) + futures.add(nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)).resultFuture) + futures.add(nodeB.services.startFlow(InsertInitialStateFlow(nodeA.info.legalIdentity)).resultFuture) } mockNet.waitQuiescent() - val statesFromA = nodeA.database.transaction { + // Check all of the flows completed successfully + futures.forEach { it.getOrThrow() } + + // Convert the states into maps to make error reporting easier + val statesFromA: List> = nodeA.database.transaction { queryStatesWithPaging(nodeA.services.vaultQueryService) } - val statesFromB = nodeB.database.transaction { + val statesFromB: List> = nodeB.database.transaction { queryStatesWithPaging(nodeB.services.vaultQueryService) } assertEquals(2 * N, statesFromA.count(), "Expect all states to be present") + statesFromA.forEach { ref -> + if (ref !in statesFromB) { + throw IllegalStateException("State $ref is only present on node A.") + } + } + statesFromB.forEach { ref -> + if (ref !in statesFromA) { + throw IllegalStateException("State $ref is only present on node B.") + } + } assertEquals(statesFromA, statesFromB, "Expect identical data on both nodes") assertTrue("Expect all states have run the scheduled task", statesFromB.all { it.state.data.processed }) } - // Demonstrate Vault Query paging and sorting - val PAGE_SIZE = 20 - val sorting = Sort(listOf(Sort.SortColumn(SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID), Sort.Direction.DESC))) - + /** + * Query all states from the Vault, fetching results as a series of pages with ordered states in order to perform + * integration testing of that functionality. + * + * @return states ordered by the transaction ID. + */ private fun queryStatesWithPaging(vaultQueryService: VaultQueryService): List> { var pageNumber = DEFAULT_PAGE_NUM val states = mutableListOf>() do { val pageSpec = PageSpecification(pageSize = PAGE_SIZE, pageNumber = pageNumber) - val results = vaultQueryService.queryBy(VaultQueryCriteria(), pageSpec, sorting) + val results = vaultQueryService.queryBy(VaultQueryCriteria(), pageSpec, SORTING) states.addAll(results.states) pageNumber++ } while ((pageSpec.pageSize * (pageNumber)) <= results.totalStatesAvailable) From 2c4dd87d41025af7a0efdc8250c6ef495fd1e710 Mon Sep 17 00:00:00 2001 From: Alberto Arri Date: Thu, 10 Aug 2017 14:53:35 +0100 Subject: [PATCH 03/11] Minor: made the code in IOUState match the description of the changes underneath. --- docs/source/hello-world-state.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index b677366a08..3888d07ee9 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -78,8 +78,8 @@ define an ``IOUState``: class IOUState(val value: Int, val lender: Party, - val borrower: Party) : ContractState { - override val contract: IOUContract = IOUContract() + val borrower: Party, + override val contract: TemplateContract) : ContractState { override val participants get() = listOf(lender, borrower) } @@ -153,4 +153,4 @@ We've defined an ``IOUState`` that can be used to represent IOUs as shared facts Corda are simply JVM classes that implement the ``ContractState`` interface. They can have any additional properties and methods you like. -Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time. \ No newline at end of file +Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time. From c8bbe453f5ad9c623e04259323311ed4b6dd2f0b Mon Sep 17 00:00:00 2001 From: mkit Date: Thu, 10 Aug 2017 17:09:08 +0100 Subject: [PATCH 04/11] Removing clauses from the core module (#1203) --- .../net/corda/core/contracts/Structures.kt | 21 -- .../core/contracts/clauses/AllComposition.kt | 10 - .../net/corda/core/contracts/clauses/AllOf.kt | 38 --- .../core/contracts/clauses/AnyComposition.kt | 10 - .../net/corda/core/contracts/clauses/AnyOf.kt | 28 -- .../corda/core/contracts/clauses/Clause.kt | 73 ----- .../core/contracts/clauses/ClauseVerifier.kt | 29 -- .../core/contracts/clauses/CompositeClause.kt | 25 -- .../corda/core/contracts/clauses/FilterOn.kt | 25 -- .../contracts/clauses/FirstComposition.kt | 28 -- .../corda/core/contracts/clauses/FirstOf.kt | 41 --- .../contracts/clauses/GroupClauseVerifier.kt | 29 -- .../core/contracts/clauses/AllOfTests.kt | 32 -- .../core/contracts/clauses/AnyOfTests.kt | 45 --- .../core/contracts/clauses/ClauseTestUtils.kt | 30 -- .../contracts/clauses/VerifyClausesTests.kt | 40 --- .../net/corda/contracts/CommercialPaper.kt | 1 - .../main/kotlin/net/corda/irs/contract/IRS.kt | 295 +++++++----------- .../net/corda/vega/contracts/OGTrade.kt | 49 +-- .../net/corda/vega/contracts/PortfolioSwap.kt | 80 ++--- .../testing/contracts/DummyLinearContract.kt | 20 +- 21 files changed, 167 insertions(+), 782 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/ClauseVerifier.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/FilterOn.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/clauses/GroupClauseVerifier.kt delete mode 100644 core/src/test/kotlin/net/corda/core/contracts/clauses/AllOfTests.kt delete mode 100644 core/src/test/kotlin/net/corda/core/contracts/clauses/AnyOfTests.kt delete mode 100644 core/src/test/kotlin/net/corda/core/contracts/clauses/ClauseTestUtils.kt delete mode 100644 core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 370e3bd06a..f820cdd674 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -2,7 +2,6 @@ package net.corda.core.contracts -import net.corda.core.contracts.clauses.Clause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.secureRandomBytes import net.corda.core.flows.FlowLogicRef @@ -211,26 +210,6 @@ interface LinearState : ContractState { * True if this should be tracked by our vault(s). */ fun isRelevant(ourKeys: Set): Boolean - - /** - * Standard clause to verify the LinearState safety properties. - */ - @CordaSerializable - class ClauseVerifier : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - val inputIds = inputs.map { it.linearId }.distinct() - val outputIds = outputs.map { it.linearId }.distinct() - requireThat { - "LinearStates are not merged" using (inputIds.count() == inputs.count()) - "LinearStates are not split" using (outputIds.count() == outputs.count()) - } - return emptySet() - } - } } // DOCEND 2 diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt deleted file mode 100644 index 5be41988e5..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState - -/** - * Compose a number of clauses, such that all of the clauses must run for verification to pass. - */ -@Deprecated("Use AllOf") -class AllComposition(firstClause: Clause, vararg remainingClauses: Clause) : AllOf(firstClause, *remainingClauses) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt deleted file mode 100644 index cb338ec6d1..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import java.util.* - -/** - * Compose a number of clauses, such that all of the clauses must run for verification to pass. - */ -open class AllOf(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() { - override val clauses = ArrayList>() - - init { - clauses.add(firstClause) - clauses.addAll(remainingClauses) - } - - override fun matchedClauses(commands: List>): List> { - clauses.forEach { clause -> - check(clause.matches(commands)) { "Failed to match clause $clause" } - } - return clauses - } - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: K?): Set { - return matchedClauses(commands).flatMapTo(HashSet()) { clause -> - clause.verify(tx, inputs, outputs, commands, groupingKey) - } - } - - override fun toString() = "All: $clauses.toList()" -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt deleted file mode 100644 index fbad044ca3..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState - -/** - * Compose a number of clauses, such that any number of the clauses can run. - */ -@Deprecated("Use AnyOf instead, although note that any of requires at least one matched clause") -class AnyComposition(vararg rawClauses: Clause) : AnyOf(*rawClauses) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt deleted file mode 100644 index ee4b1d57dd..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import java.util.* - -/** - * Compose a number of clauses, such that one or more of the clauses can run. - */ -open class AnyOf(vararg rawClauses: Clause) : CompositeClause() { - override val clauses: List> = rawClauses.toList() - - override fun matchedClauses(commands: List>): List> { - val matched = clauses.filter { it.matches(commands) } - require(matched.isNotEmpty()) { "At least one clause must match" } - return matched - } - - override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set { - return matchedClauses(commands).flatMapTo(HashSet()) { clause -> - clause.verify(tx, inputs, outputs, commands, groupingKey) - } - } - - override fun toString(): String = "Any: ${clauses.toList()}" -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt deleted file mode 100644 index d003a186ed..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.loggerFor - -/** - * A clause of a contract, containing a chunk of verification logic. That logic may be delegated to other clauses, or - * provided directly by this clause. - * - * @param S the type of contract state this clause operates on. - * @param C a common supertype of commands this clause operates on. - * @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable. - * - * @see CompositeClause - */ -abstract class Clause { - companion object { - val log = loggerFor>() - } - - /** Determine whether this clause runs or not */ - open val requiredCommands: Set> = emptySet() - - /** - * Determine the subclauses which will be verified as a result of verifying this clause. - * - * @throws IllegalStateException if the given commands do not result in a valid execution (for example no match - * with [FirstOf]). - */ - @Throws(IllegalStateException::class) - open fun getExecutionPath(commands: List>): List> - = listOf(this) - - /** - * Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause - * would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException - * if any matched. - * - * @param tx the full transaction being verified. This is provided for cases where clauses need to access - * states or commands outside of their normal scope. - * @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause], - * but may be further reduced by clauses such as [GroupClauseVerifier]. - * @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause], - * but may be further reduced by clauses such as [GroupClauseVerifier]. - * @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause], - * but may be further reduced by clauses such as [GroupClauseVerifier]. - * @param groupingKey a grouping key applied to states and commands, where applicable. Taken from - * [LedgerTransaction.InOutGroup]. - * @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a - * later clause. This would normally be all commands matching "requiredCommands" for this clause, but some - * verify() functions may do further filtering on possible matches, and return a subset. This may also include - * commands that were not required (for example the Exit command for fungible assets is optional). - */ - @Throws(IllegalStateException::class) - abstract fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: K?): Set -} - -/** - * Determine if the given list of commands matches the required commands for a clause to trigger. - */ -fun Clause<*, C, *>.matches(commands: List>): Boolean { - return if (requiredCommands.isEmpty()) - true - else - commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands) -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/ClauseVerifier.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/ClauseVerifier.kt deleted file mode 100644 index 3583fe1779..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/ClauseVerifier.kt +++ /dev/null @@ -1,29 +0,0 @@ -@file:JvmName("ClauseVerifier") - -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction - -/** - * Verify a transaction against the given list of clauses. - * - * @param tx transaction to be verified. - * @param clauses the clauses to verify. - * @param commands commands extracted from the transaction, which are relevant to the - * clauses. - */ -fun verifyClause(tx: LedgerTransaction, - clause: Clause, - commands: List>) { - if (Clause.log.isTraceEnabled) { - clause.getExecutionPath(commands).forEach { - Clause.log.trace("Tx ${tx.id} clause: $clause") - } - } - val matchedCommands = clause.verify(tx, tx.inputStates, tx.outputStates, commands, null) - - check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) } -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt deleted file mode 100644 index be0e711731..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState - -/** - * Abstract supertype for clauses which compose other clauses together in some logical manner. - */ -abstract class CompositeClause : Clause() { - /** List of clauses under this composite clause */ - abstract val clauses: List> - - override fun getExecutionPath(commands: List>): List> - = matchedClauses(commands).flatMap { it.getExecutionPath(commands) } - - /** - * Determine which clauses are matched by the supplied commands. - * - * @throws IllegalStateException if the given commands do not result in a valid execution (for example no match - * with [FirstOf]). - */ - @Throws(IllegalStateException::class) - abstract fun matchedClauses(commands: List>): List> -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/FilterOn.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/FilterOn.kt deleted file mode 100644 index e7112ab6bc..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/FilterOn.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction - -/** - * Filter the states that are passed through to the wrapped clause, to restrict them to a specific type. - */ -class FilterOn(val clause: Clause, - val filterStates: (List) -> List) : Clause() { - override val requiredCommands: Set> - = clause.requiredCommands - - override fun getExecutionPath(commands: List>): List> - = clause.getExecutionPath(commands) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: K?): Set - = clause.verify(tx, filterStates(inputs), filterStates(outputs), commands, groupingKey) -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt deleted file mode 100644 index f52b61195a..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import java.util.* - -/** - * Compose a number of clauses, such that the first match is run, and it errors if none is run. - */ -@Deprecated("Use FirstOf instead") -class FirstComposition(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() { - override val clauses = ArrayList>() - override fun matchedClauses(commands: List>): List> = listOf(clauses.first { it.matches(commands) }) - - init { - clauses.add(firstClause) - clauses.addAll(remainingClauses) - } - - override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set { - val clause = matchedClauses(commands).singleOrNull() ?: throw IllegalStateException("No delegate clause matched in first composition") - return clause.verify(tx, inputs, outputs, commands, groupingKey) - } - - override fun toString() = "First: ${clauses.toList()}" -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt deleted file mode 100644 index 2de9fc2b98..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.loggerFor -import java.util.* - -/** - * Compose a number of clauses, such that the first match is run, and it errors if none is run. - */ -class FirstOf(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() { - companion object { - val logger = loggerFor>() - } - - override val clauses = ArrayList>() - - /** - * Get the single matched clause from the set this composes, based on the given commands. This is provided as - * helper method for internal use, rather than using the exposed [matchedClauses] function which unnecessarily - * wraps the clause in a list. - */ - private fun matchedClause(commands: List>): Clause { - return clauses.firstOrNull { it.matches(commands) } ?: throw IllegalStateException("No delegate clause matched in first composition") - } - - override fun matchedClauses(commands: List>) = listOf(matchedClause(commands)) - - init { - clauses.add(firstClause) - clauses.addAll(remainingClauses) - } - - override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set { - return matchedClause(commands).verify(tx, inputs, outputs, commands, groupingKey) - } - - override fun toString() = "First: ${clauses.toList()}" -} diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/GroupClauseVerifier.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/GroupClauseVerifier.kt deleted file mode 100644 index 45ce9610e3..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/clauses/GroupClauseVerifier.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import java.util.* - -abstract class GroupClauseVerifier(val clause: Clause) : Clause() { - abstract fun groupStates(tx: LedgerTransaction): List> - - override fun getExecutionPath(commands: List>): List> - = clause.getExecutionPath(commands) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - val groups = groupStates(tx) - val matchedCommands = HashSet() - - for ((groupInputs, groupOutputs, groupToken) in groups) { - matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken)) - } - - return matchedCommands - } -} diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/AllOfTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/AllOfTests.kt deleted file mode 100644 index 7916790f44..0000000000 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/AllOfTests.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.PrivacySalt -import net.corda.core.crypto.SecureHash -import net.corda.core.transactions.LedgerTransaction -import org.junit.Test -import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class AllOfTests { - - @Test - fun minimal() { - val counter = AtomicInteger(0) - val clause = AllOf(matchedClause(counter), matchedClause(counter)) - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - verifyClause(tx, clause, emptyList>()) - - // Check that we've run the verify() function of two clauses - assertEquals(2, counter.get()) - } - - @Test - fun `not all match`() { - val clause = AllOf(matchedClause(), unmatchedClause()) - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - assertFailsWith { verifyClause(tx, clause, emptyList>()) } - } -} diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/AnyOfTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/AnyOfTests.kt deleted file mode 100644 index 52a4ac1be5..0000000000 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/AnyOfTests.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.PrivacySalt -import net.corda.core.crypto.SecureHash -import net.corda.core.transactions.LedgerTransaction -import org.junit.Test -import java.util.concurrent.atomic.AtomicInteger -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class AnyOfTests { - @Test - fun minimal() { - val counter = AtomicInteger(0) - val clause = AnyOf(matchedClause(counter), matchedClause(counter)) - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - verifyClause(tx, clause, emptyList>()) - - // Check that we've run the verify() function of two clauses - assertEquals(2, counter.get()) - } - - @Test - fun `not all match`() { - val counter = AtomicInteger(0) - val clause = AnyOf(matchedClause(counter), unmatchedClause(counter)) - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - verifyClause(tx, clause, emptyList>()) - - // Check that we've run the verify() function of one clause - assertEquals(1, counter.get()) - } - - @Test - fun `none match`() { - val counter = AtomicInteger(0) - val clause = AnyOf(unmatchedClause(counter), unmatchedClause(counter)) - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - assertFailsWith(IllegalArgumentException::class) { - verifyClause(tx, clause, emptyList>()) - } - } -} diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/ClauseTestUtils.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/ClauseTestUtils.kt deleted file mode 100644 index fb0f023f3c..0000000000 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/ClauseTestUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.AuthenticatedObject -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractState -import net.corda.core.transactions.LedgerTransaction -import java.util.concurrent.atomic.AtomicInteger - -internal fun matchedClause(counter: AtomicInteger? = null) = object : Clause() { - override val requiredCommands: Set> = emptySet() - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, groupingKey: Unit?): Set { - counter?.incrementAndGet() - return emptySet() - } -} - -/** A clause that can never be matched */ -internal fun unmatchedClause(counter: AtomicInteger? = null) = object : Clause() { - override val requiredCommands: Set> = setOf(object : CommandData {}.javaClass) - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, groupingKey: Unit?): Set { - counter?.incrementAndGet() - return emptySet() - } -} diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt deleted file mode 100644 index 90a57bfe92..0000000000 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.core.contracts.clauses - -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.transactions.LedgerTransaction -import net.corda.testing.contracts.DummyContract -import org.junit.Test -import kotlin.test.assertFailsWith - -/** - * Tests for the clause verifier. - */ -class VerifyClausesTests { - /** Very simple check that the function doesn't error when given any clause */ - @Test - fun minimal() { - val clause = object : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, groupingKey: Unit?): Set = emptySet() - } - val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - verifyClause(tx, clause, emptyList>()) - } - - @Test - fun errorSuperfluousCommands() { - val clause = object : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, groupingKey: Unit?): Set = emptySet() - } - val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create()) - val tx = LedgerTransaction(emptyList(), emptyList(), listOf(command), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt()) - // The clause is matched, but doesn't mark the command as consumed, so this should error - assertFailsWith { verifyClause(tx, clause, listOf(command)) } - } -} diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 822b30d0bc..f94ff09dd5 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -145,7 +145,6 @@ class CommercialPaper : Contract { "output values sum to more than the inputs" using (output.faceValue.quantity > 0) "the maturity date is not in the past" using (time < output.maturityDate) // Don't allow an existing CP state to be replaced by this issuance. - // TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch. "output values sum to more than the inputs" using inputs.isEmpty() } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 10eee75526..2c45c805c7 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -3,7 +3,6 @@ package net.corda.irs.contract import com.fasterxml.jackson.annotation.JsonIgnoreProperties import net.corda.contracts.* import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogicRefFactory @@ -460,189 +459,137 @@ class InterestRateSwap : Contract { fixingCalendar, index, indexSource, indexTenor) } - override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindow(), Clauses.Group()), tx.commands.select()) + // These functions may make more sense to use for basket types, but for now let's leave them here + private fun checkLegDates(legs: List) { + requireThat { + "Effective date is before termination date" using legs.all { it.effectiveDate < it.terminationDate } + "Effective dates are in alignment" using legs.all { it.effectiveDate == legs[0].effectiveDate } + "Termination dates are in alignment" using legs.all { it.terminationDate == legs[0].terminationDate } + } + } - interface Clauses { - /** - * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides - * helper functions for the clauses. - */ - abstract class AbstractIRSClause : Clause() { - // These functions may make more sense to use for basket types, but for now let's leave them here - fun checkLegDates(legs: List) { + private fun checkLegAmounts(legs: List) { + requireThat { + "The notional is non zero" using legs.any { it.notional.quantity > (0).toLong() } + "The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional } + } + for (leg: CommonLeg in legs) { + if (leg is FixedLeg) { requireThat { - "Effective date is before termination date" using legs.all { it.effectiveDate < it.terminationDate } - "Effective dates are in alignment" using legs.all { it.effectiveDate == legs[0].effectiveDate } - "Termination dates are in alignment" using legs.all { it.terminationDate == legs[0].terminationDate } - } - } - - fun checkLegAmounts(legs: List) { - requireThat { - "The notional is non zero" using legs.any { it.notional.quantity > (0).toLong() } - "The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional } - } - for (leg: CommonLeg in legs) { - if (leg is FixedLeg) { - requireThat { - // TODO: Confirm: would someone really enter a swap with a negative fixed rate? - "Fixed leg rate must be positive" using leg.fixedRate.isPositive() - } - } - } - } - - // TODO: After business rules discussion, add further checks to the schedules and rates - fun checkSchedules(@Suppress("UNUSED_PARAMETER") legs: List): Boolean = true - - fun checkRates(@Suppress("UNUSED_PARAMETER") legs: List): Boolean = true - - /** - * Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values). - */ - fun getFloatingLegPaymentsDifferences(payments1: Map, payments2: Map): List>> { - val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] } - val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] } - return (diff1.keys + diff2.keys).map { - it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent) + // TODO: Confirm: would someone really enter a swap with a negative fixed rate? + "Fixed leg rate must be positive" using leg.fixedRate.isPositive() } } } + } - class Group : GroupClauseVerifier(AnyOf(Agree(), Fix(), Pay(), Mature())) { - // Group by Trade ID for in / out states - override fun groupStates(tx: LedgerTransaction): List> { - return tx.groupStates { state -> state.linearId } - } + /** + * Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values). + */ + private fun getFloatingLegPaymentsDifferences(payments1: Map, payments2: Map): List>> { + val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] } + val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] } + return (diff1.keys + diff2.keys).map { + it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent) + } + } + + private fun verifyAgreeCommand(inputs: List, outputs: List) { + val irs = outputs.filterIsInstance().single() + requireThat { + "There are no in states for an agreement" using inputs.isEmpty() + "There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty()) + "There are events in the float schedule" using (irs.calculation.floatingLegPaymentSchedule.isNotEmpty()) + "All notionals must be non zero" using (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0) + "The fixed leg rate must be positive" using (irs.fixedLeg.fixedRate.isPositive()) + "The currency of the notionals must be the same" using (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token) + "All leg notionals must be the same" using (irs.fixedLeg.notional == irs.floatingLeg.notional) + "The effective date is before the termination date for the fixed leg" using (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate) + "The effective date is before the termination date for the floating leg" using (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate) + "The effective dates are aligned" using (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate) + "The termination dates are aligned" using (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate) + "The fixing period date offset cannot be negative" using (irs.floatingLeg.fixingPeriodOffset >= 0) + + // TODO: further tests + } + checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg)) + checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg)) + } + + private fun verifyFixCommand(inputs: List, outputs: List, command: AuthenticatedObject) { + val irs = outputs.filterIsInstance().single() + val prevIrs = inputs.filterIsInstance().single() + val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule) + + // Having both of these tests are "redundant" as far as verify() goes, however, by performing both + // we can relay more information back to the user in the case of failure. + requireThat { + "There is at least one difference in the IRS floating leg payment schedules" using !paymentDifferences.isEmpty() + "There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1) } - class TimeWindow : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - requireNotNull(tx.timeWindow) { "must be have a time-window)" } - // We return an empty set because we don't process any commands - return emptySet() + val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier). + val fixValue = command.value.fix + // Need to check that everything is the same apart from the new fixed rate entry. + requireThat { + "The fixed leg parties are constant" using (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue + "The fixed leg is constant" using (irs.fixedLeg == prevIrs.fixedLeg) + "The floating leg is constant" using (irs.floatingLeg == prevIrs.floatingLeg) + "The common values are constant" using (irs.common == prevIrs.common) + "The fixed leg payment schedule is constant" using (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule) + "The expression is unchanged" using (irs.calculation.expression == prevIrs.calculation.expression) + "There is only one changed payment in the floating leg" using (paymentDifferences.size == 1) + "There changed payment is a floating payment" using (oldFloatingRatePaymentEvent.rate is ReferenceRate) + "The new payment is a fixed payment" using (newFixedRatePaymentEvent.rate is FixedRate) + "The changed payments dates are aligned" using (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date) + "The new payment has the correct rate" using (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value) + "The fixing is for the next required date" using (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay) + "The fix payment has the same currency as the notional" using (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token) + // "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this . + } + } + + private fun verifyPayCommand() { + requireThat { + "Payments not supported / verifiable yet" using false + } + } + + private fun verifyMatureCommand(inputs: List, outputs: List) { + val irs = inputs.filterIsInstance().single() + requireThat { + "No more fixings to be applied" using (irs.calculation.nextFixingDate() == null) + "The irs is fully consumed and there is no id matched output state" using outputs.isEmpty() + } + } + + override fun verify(tx: LedgerTransaction) { + requireNotNull(tx.timeWindow) { "must be have a time-window)" } + val groups: List> = tx.groupStates { state -> state.linearId } + var atLeastOneCommandProcessed = false + for ((inputs, outputs, key) in groups) { + val agreeCommand = tx.commands.select().firstOrNull() + if (agreeCommand != null) { + verifyAgreeCommand(inputs, outputs) + atLeastOneCommandProcessed = true + } + val fixCommand = tx.commands.select().firstOrNull() + if (fixCommand != null) { + verifyFixCommand(inputs, outputs, fixCommand) + atLeastOneCommandProcessed = true + } + val payCommand = tx.commands.select().firstOrNull() + if (payCommand != null) { + verifyPayCommand() + atLeastOneCommandProcessed = true + } + val matureCommand = tx.commands.select().firstOrNull() + if (matureCommand != null) { + verifyMatureCommand(inputs, outputs) + atLeastOneCommandProcessed = true } } - - class Agree : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Agree::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance().single() - requireThat { - "There are no in states for an agreement" using inputs.isEmpty() - "There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty()) - "There are events in the float schedule" using (irs.calculation.floatingLegPaymentSchedule.isNotEmpty()) - "All notionals must be non zero" using (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0) - "The fixed leg rate must be positive" using (irs.fixedLeg.fixedRate.isPositive()) - "The currency of the notionals must be the same" using (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token) - "All leg notionals must be the same" using (irs.fixedLeg.notional == irs.floatingLeg.notional) - - "The effective date is before the termination date for the fixed leg" using (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate) - "The effective date is before the termination date for the floating leg" using (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate) - "The effective dates are aligned" using (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate) - "The termination dates are aligned" using (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate) - "The rates are valid" using checkRates(listOf(irs.fixedLeg, irs.floatingLeg)) - "The schedules are valid" using checkSchedules(listOf(irs.fixedLeg, irs.floatingLeg)) - "The fixing period date offset cannot be negative" using (irs.floatingLeg.fixingPeriodOffset >= 0) - - // TODO: further tests - } - checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg)) - checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg)) - - return setOf(command.value) - } - } - - class Fix : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Refix::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance().single() - val prevIrs = inputs.filterIsInstance().single() - val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule) - - // Having both of these tests are "redundant" as far as verify() goes, however, by performing both - // we can relay more information back to the user in the case of failure. - requireThat { - "There is at least one difference in the IRS floating leg payment schedules" using !paymentDifferences.isEmpty() - "There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1) - } - - val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier). - val fixValue = command.value.fix - // Need to check that everything is the same apart from the new fixed rate entry. - requireThat { - "The fixed leg parties are constant" using (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue - "The fixed leg is constant" using (irs.fixedLeg == prevIrs.fixedLeg) - "The floating leg is constant" using (irs.floatingLeg == prevIrs.floatingLeg) - "The common values are constant" using (irs.common == prevIrs.common) - "The fixed leg payment schedule is constant" using (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule) - "The expression is unchanged" using (irs.calculation.expression == prevIrs.calculation.expression) - "There is only one changed payment in the floating leg" using (paymentDifferences.size == 1) - "There changed payment is a floating payment" using (oldFloatingRatePaymentEvent.rate is ReferenceRate) - "The new payment is a fixed payment" using (newFixedRatePaymentEvent.rate is FixedRate) - "The changed payments dates are aligned" using (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date) - "The new payment has the correct rate" using (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value) - "The fixing is for the next required date" using (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay) - "The fix payment has the same currency as the notional" using (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token) - // "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this . - } - - return setOf(command.value) - } - } - - class Pay : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Pay::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - requireThat { - "Payments not supported / verifiable yet" using false - } - return setOf(command.value) - } - } - - class Mature : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Mature::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = inputs.filterIsInstance().single() - requireThat { - "No more fixings to be applied" using (irs.calculation.nextFixingDate() == null) - "The irs is fully consumed and there is no id matched output state" using outputs.isEmpty() - } - - return setOf(command.value) - } - } - + require(atLeastOneCommandProcessed) { "At least one command needs to present" } } interface Commands : CommandData { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt index d91a886832..9d6cfa56d9 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt @@ -1,7 +1,6 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.* import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction import java.math.BigDecimal @@ -10,39 +9,13 @@ import java.math.BigDecimal * Specifies the contract between two parties that trade an OpenGamma IRS. Currently can only agree to trade. */ data class OGTrade(override val legalContractReference: SecureHash = SecureHash.sha256("OGTRADE.KT")) : Contract { - override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select()) - - interface Commands : CommandData { - class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade - } - - interface Clauses { - class TimeWindowed : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - requireNotNull(tx.timeWindow) { "must have a time-window" } - // We return an empty set because we don't process any commands - return emptySet() - } - } - - class Group : GroupClauseVerifier(AnyOf(Agree())) { - override fun groupStates(tx: LedgerTransaction): List> - // Group by Trade ID for in / out states - = tx.groupStates { state -> state.linearId } - } - - class Agree : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - + override fun verify(tx: LedgerTransaction) { + requireNotNull(tx.timeWindow) { "must have a time-window" } + val groups: List> = tx.groupStates { state -> state.linearId } + var atLeastOneCommandProcessed = false + for ((inputs, outputs, key) in groups) { + val command = tx.commands.select().firstOrNull() + if (command != null) { require(inputs.isEmpty()) { "Inputs must be empty" } require(outputs.size == 1) { "" } require(outputs[0].buyer != outputs[0].seller) @@ -51,9 +24,13 @@ data class OGTrade(override val legalContractReference: SecureHash = SecureHash. require(outputs[0].swap.startDate.isBefore(outputs[0].swap.endDate)) require(outputs[0].swap.notional > BigDecimal(0)) require(outputs[0].swap.tradeDate.isBefore(outputs[0].swap.endDate)) - - return setOf(command.value) + atLeastOneCommandProcessed = true } } + require(atLeastOneCommandProcessed) { "At least one command needs to present" } + } + + interface Commands : CommandData { + class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade } } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt index 0c8706c2ac..d417111e7d 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt @@ -1,7 +1,6 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.* import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction @@ -11,71 +10,34 @@ import net.corda.core.transactions.LedgerTransaction * of the portfolio arbitrarily. */ data class PortfolioSwap(override val legalContractReference: SecureHash = SecureHash.sha256("swordfish")) : Contract { - override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select()) - - interface Commands : CommandData { - class Agree : TypeOnlyCommandData(), Commands // Both sides agree to portfolio - class Update : TypeOnlyCommandData(), Commands // Both sides re-agree to portfolio - } - - interface Clauses { - class TimeWindowed : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - requireNotNull(tx.timeWindow) { "must have a time-window)" } - // We return an empty set because we don't process any commands - return emptySet() - } - } - - class Group : GroupClauseVerifier(FirstOf(Agree(), Update())) { - override fun groupStates(tx: LedgerTransaction): List> - // Group by Trade ID for in / out states - = tx.groupStates { state -> state.linearId } - } - - class Update : Clause() { - override val requiredCommands: Set> = setOf(Commands.Update::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - - requireThat { - "there is only one input" using (inputs.size == 1) - "there is only one output" using (outputs.size == 1) - "the valuer hasn't changed" using (inputs[0].valuer == outputs[0].valuer) - "the linear id hasn't changed" using (inputs[0].linearId == outputs[0].linearId) - } - - return setOf(command.value) - } - } - - class Agree : Clause() { - override val requiredCommands: Set> = setOf(Commands.Agree::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - + override fun verify(tx: LedgerTransaction) { + requireNotNull(tx.timeWindow) { "must have a time-window)" } + val groups: List> = tx.groupStates { state -> state.linearId } + for ((inputs, outputs, key) in groups) { + val agreeCommand = tx.commands.select().firstOrNull() + if (agreeCommand != null) { requireThat { "there are no inputs" using (inputs.isEmpty()) "there is one output" using (outputs.size == 1) "valuer must be a party" using (outputs[0].participants.contains(outputs[0].valuer)) } + } else { + val updateCommand = tx.commands.select().firstOrNull() + if (updateCommand != null) { + requireThat { + "there is only one input" using (inputs.size == 1) + "there is only one output" using (outputs.size == 1) + "the valuer hasn't changed" using (inputs[0].valuer == outputs[0].valuer) + "the linear id hasn't changed" using (inputs[0].linearId == outputs[0].linearId) + } - return setOf(command.value) + } } } } + + interface Commands : CommandData { + class Agree : TypeOnlyCommandData(), Commands // Both sides agree to portfolio + class Update : TypeOnlyCommandData(), Commands // Both sides re-agree to portfolio + } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt index c733587965..2ed7cc7a9d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt @@ -1,12 +1,9 @@ package net.corda.testing.contracts -import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract import net.corda.core.contracts.LinearState import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.contracts.clauses.Clause -import net.corda.core.contracts.clauses.FilterOn -import net.corda.core.contracts.clauses.verifyClause +import net.corda.core.contracts.requireThat import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.identity.AbstractParty @@ -22,10 +19,17 @@ import java.time.ZoneOffset.UTC class DummyLinearContract : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("Test") - val clause: Clause = LinearState.ClauseVerifier() - override fun verify(tx: LedgerTransaction) = verifyClause(tx, - FilterOn(clause, { states -> states.filterIsInstance() }), - emptyList()) + override fun verify(tx: LedgerTransaction) { + val inputs = tx.inputs.map { it.state.data }.filterIsInstance() + val outputs = tx.outputs.map { it.data }.filterIsInstance() + + val inputIds = inputs.map { it.linearId }.distinct() + val outputIds = outputs.map { it.linearId }.distinct() + requireThat { + "LinearStates are not merged" using (inputIds.count() == inputs.count()) + "LinearStates are not split" using (outputIds.count() == outputs.count()) + } + } data class State( override val linearId: UniqueIdentifier = UniqueIdentifier(), From 54cf46952c9d13072f831a5ca38965e9c4d46e31 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 10 Aug 2017 17:17:12 +0100 Subject: [PATCH 05/11] JPA Hibernate AbstractParty converter (#1205) * Added JPA AbstractParty converter (using IdentityService to resolve anonymous parties). * Use partyFromX500Name. Add meaningful exception messages. * AutoApply the JPA AbstractParty converter. * Entity attribute still needs the Convert annotation. * Fix incorrect registration of custom attribute converter. * Deal with non-resolvable anonymous parties (eg. store as null and ignore) * Updates following PR review feedback. * Added documentation. * Added entry to changelog. * Added code documentation as per RN PR feedback request. * Updates required following rebase from master. * Renamed converter for clarity. --- ...bstractPartyToX500NameAsStringConverter.kt | 30 +++++++++++++++++++ docs/source/api-persistence.rst | 7 +++++ docs/source/changelog.rst | 3 ++ .../net/corda/contracts/asset/CashTests.kt | 6 ++++ .../net/corda/node/internal/AbstractNode.kt | 4 +-- .../database/HibernateConfiguration.kt | 8 ++++- .../corda/node/services/vault/VaultSchema.kt | 6 ++-- .../services/vault/VaultQueryJavaTests.java | 3 ++ .../database/HibernateConfigurationTest.kt | 4 ++- .../services/schema/HibernateObserverTests.kt | 6 +++- .../services/vault/NodeVaultServiceTest.kt | 3 ++ .../node/services/vault/VaultQueryTests.kt | 8 +++++ .../node/services/vault/VaultWithCashTest.kt | 3 ++ .../net/corda/testing/node/MockServices.kt | 5 ++-- 14 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt diff --git a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt new file mode 100644 index 0000000000..3d88947dbd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt @@ -0,0 +1,30 @@ +package net.corda.core.schemas.converters + +import net.corda.core.identity.AbstractParty +import net.corda.core.node.services.IdentityService +import org.bouncycastle.asn1.x500.X500Name +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +/** + * Converter to persist a party as its's well known identity (where resolvable) + * Completely anonymous parties are stored as null (to preserve privacy) + */ +@Converter(autoApply = true) +class AbstractPartyToX500NameAsStringConverter(val identitySvc: IdentityService) : AttributeConverter { + + override fun convertToDatabaseColumn(party: AbstractParty?): String? { + party?.let { + return identitySvc.partyFromAnonymous(party)?.toString() + } + return null // non resolvable anonymous parties + } + + override fun convertToEntityAttribute(dbData: String?): AbstractParty? { + dbData?.let { + val party = identitySvc.partyFromX500Name(X500Name(dbData)) + return party as AbstractParty + } + return null // non resolvable anonymous parties are stored as nulls + } +} diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 06609ca828..9fee067b2c 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -93,6 +93,13 @@ Several examples of entities and mappings are provided in the codebase, includin .. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt :language: kotlin +Identity mapping +---------------- +Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically +processed to ensure only the ``X500Name`` of the identity is persisted where an identity is well known, otherwise a null +value is stored in the associated column. To preserve privacy, identity keys are never persisted. Developers should use +the ``IdentityService`` to resolve keys from well know X500 identity names. + JDBC session ------------ Apps may also interact directly with the underlying Node's database by using a standard diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 68e1d3f166..4c0e27f7d6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -91,6 +91,9 @@ UNRELEASED of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) +* Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type + (well known party, resolvable anonymous party, completely anonymous party). + Milestone 13 ------------ diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index cd9aa3d42d..5b51d8fc1b 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -6,11 +6,17 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index bcae6876b1..d29b113465 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -479,7 +479,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeVaultObservers() { VaultSoftLockManager(services.vaultService, smm) ScheduledActivityObserver(services) - HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties())) + HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties(), services.identityService)) } private fun makeInfo(): NodeInfo { @@ -766,7 +766,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) } override val vaultQueryService by lazy { - HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties()), vaultService.updatesPublisher) + HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), identityService), vaultService.updatesPublisher) } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with diff --git a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt index c2076db13d..4ab3dc248f 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt @@ -1,7 +1,9 @@ package net.corda.node.services.database import net.corda.core.internal.castIfPossible +import net.corda.core.node.services.IdentityService import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverter import net.corda.core.utilities.loggerFor import net.corda.node.services.api.SchemaService import net.corda.node.utilities.DatabaseTransactionManager @@ -18,7 +20,7 @@ import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap -class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties) { +class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties, val identitySvc: IdentityService) { companion object { val logger = loggerFor() } @@ -58,6 +60,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") + schemas.forEach { schema -> // TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session schema.mappedTypes.forEach { config.addAnnotatedClass(it) } @@ -76,6 +79,9 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted) } }) + // register custom converters + applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identitySvc)) + build() } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index af52eb645f..abf2e44c99 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -105,8 +105,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var participants: Set, /** [OwnableState] attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var owner: CommonSchemaV1.Party, + @Column(name = "owner_id") + var owner: AbstractParty, /** [FungibleAsset] attributes * @@ -126,7 +126,7 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var issuerRef: ByteArray ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : - this(owner = CommonSchemaV1.Party(_owner), + this(owner = _owner, quantity = _quantity, issuerParty = CommonSchemaV1.Party(_issuerParty), issuerRef = _issuerRef.bytes, diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index b5ed6dc440..72b70bb8a0 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -18,6 +18,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.core.utilities.OpaqueBytes; import net.corda.node.utilities.CordaPersistence; +import net.corda.node.services.identity.*; import net.corda.schemas.CashSchemaV1; import net.corda.testing.TestConstants; import net.corda.testing.TestDependencyInjectionBase; @@ -44,6 +45,7 @@ import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZ import static net.corda.core.utilities.ByteArrays.toHexString; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; +import static net.corda.testing.TestConstants.*; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { @@ -57,6 +59,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { public void setUp() { ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); + InMemoryIdentityService identityService = new InMemoryIdentityService(getMOCK_IDENTITIES(), Collections.emptyMap(), getDUMMY_CA().getCertificate()); Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 0a37f8b04e..e19300c60b 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -14,6 +14,7 @@ import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.HibernateVaultQueryImpl @@ -71,7 +72,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { database = configureDatabase(dataSourceProps, defaultDatabaseProperties) val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) database.transaction { - hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties()) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), identityService) services = object : MockServices(BOB_KEY) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 64a5b9d791..ba470599c6 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -10,9 +10,12 @@ import net.corda.core.schemas.QueryableState import net.corda.testing.LogHelper import net.corda.node.services.api.SchemaService import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase +import net.corda.testing.DUMMY_CA import net.corda.testing.MEGA_CORP +import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import org.hibernate.annotations.Cascade @@ -102,7 +105,8 @@ class HibernateObserverTests { } @Suppress("UNUSED_VARIABLE") - val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties())) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties(), identityService)) database.transaction { rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 893d8eff3d..a55801327b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -23,6 +23,9 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 84cc1b8742..5f145243ca 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -13,6 +13,14 @@ import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* +import net.corda.core.utilities.seconds +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.core.utilities.* import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index fe564712b5..ad16be499f 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -12,6 +12,9 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.schema.NodeSchemaService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.* diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 689d578e6e..001007f968 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -88,7 +88,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { lateinit var hibernatePersister: HibernateObserver - fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())): VaultService { + fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), identityService)): VaultService { val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties()) hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService @@ -221,7 +221,8 @@ fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(Com val databaseProperties = makeTestDatabaseProperties() val database = configureDatabase(dataSourceProps, databaseProperties) val mockService = database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties) + val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identityService) object : MockServices(*(keys.toTypedArray())) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) From 0977ffca54fdaa93f32849674b6ddedf5c457012 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 10 Aug 2017 17:39:14 +0100 Subject: [PATCH 06/11] Sync changelog and release notes from M14 (#1213) --- docs/source/changelog.rst | 85 ++++++++++++++++++++++++----------- docs/source/release-notes.rst | 38 +++++++++++++++- 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 4c0e27f7d6..edb2f6589a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,8 +7,33 @@ from the previous milestone release. UNRELEASED ---------- -* The concept of ``TransactionType`` has been removed. Transactions no longer carry a `type` property. All usages of - ``TransactionType.General.Builder()`` have to be replaced with ``TransactionBuilder()``. +* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` + +* Following deprecated methods have been removed: + * In ``DataFeed`` + * ``first`` and ``current``, replaced by ``snapshot`` + * ``second`` and ``future``, replaced by ``updates`` + * In ``CordaRPCOps`` + * ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed`` + * ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed`` + * ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed`` + * ``networkMapUpdates``, replaced by ``networkMapFeed`` + +* Due to security concerns and the need to remove the concept of state relevancy (which isn't needed in Corda), + ``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent + sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty + must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for + dealing with ``StateAndRef``s. + + +* Vault query soft locking enhancements and deprecations + * removed original ``VaultService`` ``softLockedStates` query mechanism. + * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification + of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified + by set of lock ids) + +Milestone 14 +------------ * Changes in ``NodeInfo``: @@ -37,7 +62,7 @@ UNRELEASED * Moved the core flows previously found in ``net.corda.flows`` into ``net.corda.core.flows``. This is so that all packages in the ``core`` module begin with ``net.corda.core``. -* ``FinalityFlow`` now has can be subclassed, and the ``broadcastTransaction`` and ``lookupParties`` function can be +* ``FinalityFlow`` can now be subclassed, and the ``broadcastTransaction`` and ``lookupParties`` function can be overriden in order to handle cases where no single transaction participant is aware of all parties, and therefore the transaction must be relayed between participants rather than sent from a single node. @@ -60,36 +85,42 @@ UNRELEASED * ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should use to exclude core Corda JARs from being built into Cordapp fat JARs. -* Move the original ``Cash`` specific ``generateSpend`` and ``unconsumedStatesForSpending`` methods from ``:core`` - and onto ``Cash`` contract in the ``:finance`` module. Provide a genuinely generic ``tryLockFungibleStatesForSpending`` - on ``VaultService``, which in future could be optimised for performance. +* ``database`` field in ``AbstractNode`` class has changed the type from ``org.jetbrains.exposed.sql.Database`` to + ‘net.corda.node.utilities.CordaPersistence’ - no change is needed for the typical use + (i.e. services.database.transaction { code block } ) however a change is required when Database was explicitly declared -.. Milestone 15: +* ``DigitalSignature.LegallyIdentifiable``, previously used to identify a signer (e.g. in Oracles), has been removed. + One can use the public key to derive the corresponding identity. -* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` +* Vault Query improvements and fixes: -* Following deprecated methods have been removed: - * In ``DataFeed`` - * ``first`` and ``current``, replaced by ``snapshot`` - * ``second`` and ``future``, replaced by ``updates`` - * In ``CordaRPCOps`` - * ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed`` - * ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed`` - * ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed`` - * ``networkMapUpdates``, replaced by ``networkMapFeed`` + * FIX inconsistent behaviour: Vault Query defaults to UNCONSUMED in all QueryCriteria types -* Due to security concerns and the need to remove the concept of state relevancy (which isn't needed in Corda), - ``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent - sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty - must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for - dealing with ``StateAndRef``s. + * FIX serialization error: Vault Query over RPC when using custom attributes using VaultCustomQueryCriteria. + * Aggregate function support: extended VaultCustomQueryCriteria and associated DSL to enable specification of + Aggregate Functions (sum, max, min, avg, count) with, optional, group by clauses and sorting (on calculated aggregate) -* Vault query soft locking enhancements and deprecations - * removed original ``VaultService`` ``softLockedStates` query mechanism. - * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification - of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified - by set of lock ids) + * Pagination simplification + Pagination continues to be optional, but with following changes: + - If no PageSpecification provided then a maximum of MAX_PAGE_SIZE (200) results will be returned, otherwise we fail-fast with a ``VaultQueryException`` to alert the API user to the need to specify a PageSpecification. + Internally, we no longer need to calculate a results count (thus eliminating an expensive SQL query) unless a PageSpecification is supplied (note: that a value of -1 is returned for total_results in this scenario). + Internally, we now use the AggregateFunction capability to perform the count. + - Paging now starts from 1 (was previously 0). + + * Additional Sort criteria: by StateRef (or constituents: txId, index) + +* Confidential identities API improvements + + * Registering anonymous identities now takes in AnonymousPartyAndPath + * AnonymousParty.toString() now uses toStringShort() to match other toString() functions + * Add verifyAnonymousIdentity() function to verify without storing an identity + * Replace pathForAnonymous() with anonymousFromKey() which matches actual use-cases better + * Add unit test for fetching the anonymous identity from a key + * Update verifyAnonymousIdentity() function signature to match registerAnonymousIdentity() + * Rename AnonymisedIdentity to AnonymousPartyAndPath + * Remove certificate from AnonymousPartyAndPath as it's not actually used. + * Rename registerAnonymousIdentity() to verifyAndRegisterAnonymousIdentity() * Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type (well known party, resolvable anonymous party, completely anonymous party). diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6b5ab654b4..9c9b91ab43 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,9 +6,45 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- -The transaction finalisation flow (``FinalityFlow``) has had hooks for alternative implementations, for example in +Milestone 14 +------------ + +This release continues with the goal to improve API stability and developer friendliness. There have also been more +bug fixes and other improvements across the board. + +The CorDapp template repository has been replaced with a specific repository for +`Java `_ and `Kotlin `_ +to improve the experience of starting a new project and to simplify the build system. + +It is now possible to specify multiple IP addresses and legal identities for a single node, allowing node operators +more flexibility in setting up nodes. + +A format has been introduced for CorDapp JARs that standardises the contents of CorDapps across nodes. This new format +now requires CorDapps to contain their own external dependencies. This paves the way for significantly improved +dependency management for CorDapps with the release of `Jigsaw (Java Modules) `_. For those using non-gradle build systems it is important +to read :doc:`cordapp-build-systems` to learn more. Those using our ``cordformation`` plugin simply need to update +to the latest version (``0.14.0``) to get the fixes. + +We've now begun the process of demarcating which classes are part of our public API and which ones are internal. +Everything found in ``net.corda.core.internal`` and other packages in the ``net.corda`` namespace which has ``.internal`` in it are +considered internal and not for public use. In a future release any CorDapp using these packages will fail to load, and +when we migrate to Jigsaw these will not be exported. + +The transaction finalisation flow (``FinalityFlow``) has had hooks added for alternative implementations, for example in scenarios where no single participant in a transaction is aware of the well known identities of all parties. +DemoBench has a fix for a rare but inconvenient crash that can occur when sharing your display across multiple devices, +e.g. a projector while performing demonstrations in front of an audience. + +Guava types are being removed because Guava does not have backwards compatibility across versions, which has serious +issues when multiple libraries depend on different versions of the library. + +The identity service API has been tweaked, primarily so anonymous identity registration now takes in +AnonymousPartyAndPath rather than the individual components of the identity, as typically the caller will have +an AnonymousPartyAndPath instance. See change log for further detail. + +Upgrading to this release is strongly recommended in order to keep up with the API changes, removal and additions. + Milestone 13 ------------ From 4602739e930d12b3c278db3851998e7e73badb29 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 11 Aug 2017 10:20:27 +0100 Subject: [PATCH 07/11] Issue commercial paper from the Bank of Corda node (#1196) * Remove hard coded commercial paper issuer from trader demo * Issue commercial paper from the Bank of Corda node, as per previous hard-coded issuer name * Issue cash from the Bank of Corda node rather than in response to the buyer node requesting issuance --- docs/source/changelog.rst | 3 + docs/source/running-the-demos.rst | 2 +- .../net/corda/flows/TwoPartyTradeFlow.kt | 7 +- samples/trader-demo/build.gradle | 11 ++- .../net/corda/traderdemo/TraderDemoTest.kt | 29 ++++--- .../kotlin/net/corda/traderdemo/TraderDemo.kt | 26 ++++--- .../corda/traderdemo/TraderDemoClientApi.kt | 37 ++++++--- .../flow/CommercialPaperIssueFlow.kt | 76 +++++++++++++++++++ .../net/corda/traderdemo/flow/SellerFlow.kt | 48 +----------- 9 files changed, 153 insertions(+), 86 deletions(-) create mode 100644 samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index edb2f6589a..b2e41c9ba3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -32,6 +32,9 @@ UNRELEASED of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified by set of lock ids) +* Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing + commercial paper but labelling it as if issued by the bank. + Milestone 14 ------------ diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 6440872320..884484aa95 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -32,7 +32,7 @@ To run from the command line in Unix: 1. Run ``./gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples/trader-demo/build/nodes`` 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes -3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node +3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively. 4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index 584ade4106..e63be156e3 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -142,7 +142,8 @@ object TwoPartyTradeFlow { // Put together a proposed transaction that performs the trade, and sign it. progressTracker.currentStep = SIGNING val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest) - val partSignedTx = signWithOurKeys(cashSigningPubKeys, ptx) + // Now sign the transaction with whatever keys we need to move the cash. + val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) // Send the signed transaction to the seller, who must then sign it themselves and commit // it to the ledger by sending it to the notary. @@ -168,10 +169,6 @@ object TwoPartyTradeFlow { } } - private fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedTransaction { - // Now sign the transaction with whatever keys we need to move the cash. - return serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) - } @Suspendable private fun assembleSharedTX(assetForSale: StateAndRef, tradeRequest: SellerTradeInfo): Pair> { diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 5fc1ab4f0d..5cec069227 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -40,8 +40,9 @@ dependencies { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [ - 'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester', - "StartFlow.net.corda.traderdemo.flow.SellerFlow" + 'StartFlow.net.corda.flows.CashIssueFlow', + 'StartFlow.net.corda.traderdemo.flow.CommercialPaperIssueFlow', + 'StartFlow.net.corda.traderdemo.flow.SellerFlow' ]]] directory "./build/nodes" @@ -74,7 +75,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "CN=BankOfCorda,O=R3,L=New York,C=US" advertisedServices = [] p2pPort 10011 + rpcPort 10012 cordapps = [] + rpcUsers = ext.rpcUsers } } @@ -102,11 +105,11 @@ publishing { } } -task runBuyer(type: JavaExec) { +task runBank(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'net.corda.traderdemo.TraderDemoKt' args '--role' - args 'BUYER' + args 'BANK' } task runSeller(type: JavaExec) { diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 4f0d1cbd32..4955c54207 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -6,6 +6,7 @@ import net.corda.core.utilities.millis import net.corda.core.node.services.ServiceInfo import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow +import net.corda.flows.CashIssueFlow import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY @@ -17,6 +18,7 @@ import net.corda.testing.BOC import net.corda.testing.driver.poll import net.corda.testing.node.NodeBasedTest import net.corda.traderdemo.flow.BuyerFlow +import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -25,15 +27,13 @@ import java.util.concurrent.Executors class TraderDemoTest : NodeBasedTest() { @Test fun `runs trader demo`() { - val permissions = setOf( - startFlowPermission(), - startFlowPermission()) - val demoUser = listOf(User("demo", "demo", permissions)) - val user = User("user1", "test", permissions = setOf(startFlowPermission())) - val (nodeA, nodeB) = listOf( - startNode(DUMMY_BANK_A.name, rpcUsers = demoUser), - startNode(DUMMY_BANK_B.name, rpcUsers = demoUser), - startNode(BOC.name, rpcUsers = listOf(user)), + val demoUser = User("demo", "demo", setOf(startFlowPermission())) + val bankUser = User("user1", "test", permissions = setOf(startFlowPermission(), + startFlowPermission())) + val (nodeA, nodeB, bankNode, notaryNode) = listOf( + startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)), + startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)), + startNode(BOC.name, rpcUsers = listOf(bankUser)), startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) ).transpose().getOrThrow() @@ -41,19 +41,24 @@ class TraderDemoTest : NodeBasedTest() { val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.configuration.rpcAddress!!, initialiseSerialization = false) - client.start(demoUser[0].username, demoUser[0].password).proxy + client.start(demoUser.username, demoUser.password).proxy + } + val nodeBankRpc = let { + val client = CordaRPCClient(bankNode.configuration.rpcAddress!!, initialiseSerialization = false) + client.start(bankUser.username, bankUser.password).proxy } val clientA = TraderDemoClientApi(nodeARpc) val clientB = TraderDemoClientApi(nodeBRpc) + val clientBank = TraderDemoClientApi(nodeBankRpc) val originalACash = clientA.cashCount // A has random number of issued amount val expectedBCash = clientB.cashCount + 1 val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount) // TODO: Enable anonymisation - clientA.runBuyer(amount = 100.DOLLARS, anonymous = false) - clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) + clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name, notaryName = notaryNode.info.legalIdentity.name) + clientB.runSeller(buyerName = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) assertThat(clientA.cashCount).isGreaterThan(originalACash) assertThat(clientB.cashCount).isEqualTo(expectedBCash) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index 7fba92d602..f7accdc4ae 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -4,8 +4,10 @@ import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.DOLLARS import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.DUMMY_BANK_A import net.corda.core.utilities.loggerFor +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import org.slf4j.Logger import kotlin.system.exitProcess @@ -18,12 +20,18 @@ fun main(args: Array) { private class TraderDemo { enum class Role { - BUYER, + BANK, SELLER } companion object { val logger: Logger = loggerFor() + val buyerName = DUMMY_BANK_A.name + val sellerName = DUMMY_BANK_B.name + val notaryName = DUMMY_NOTARY.name + val buyerRpcPort = 10006 + val sellerRpcPort = 10009 + val bankRpcPort = 10012 } fun main(args: Array) { @@ -41,15 +49,15 @@ private class TraderDemo { // 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 role = options.valueOf(roleArg)!! - if (role == Role.BUYER) { - val host = NetworkHostAndPort("localhost", 10006) - CordaRPCClient(host).start("demo", "demo").use { - TraderDemoClientApi(it.proxy).runBuyer() + if (role == Role.BANK) { + val bankHost = NetworkHostAndPort("localhost", bankRpcPort) + CordaRPCClient(bankHost).use("demo", "demo") { + TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName, notaryName) } } else { - val host = NetworkHostAndPort("localhost", 10009) - CordaRPCClient(host).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name) + val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort) + CordaRPCClient(sellerHost).use("demo", "demo") { + TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName) } } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index cbc47cb521..e8e5d0d924 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -16,11 +16,11 @@ import net.corda.core.node.services.vault.builder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor -import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.flows.CashIssueFlow import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.testing.BOC import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.calculateRandomlySizedAmounts +import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.bouncycastle.asn1.x500.X500Name import java.util.* @@ -47,25 +47,42 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long } - fun runBuyer(amount: Amount = 30000.DOLLARS, anonymous: Boolean = false) { - val bankOfCordaParty = rpc.partyFromX500Name(BOC.name) - ?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service") + fun runIssuer(amount: Amount = 1100.0.DOLLARS, buyerName: X500Name, sellerName: X500Name, notaryName: X500Name) { + val ref = OpaqueBytes.of(1) + val buyer = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName") + val seller = rpc.partyFromX500Name(sellerName) ?: throw IllegalStateException("Don't know $sellerName") val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name) ?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service") val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) ?: throw IllegalStateException("Unable to locate notary node in network map cache") - val me = rpc.nodeIdentity() val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) - // issuer random amounts of currency totaling 30000.DOLLARS in parallel + val anonymous = false + // issue random amounts of currency up to the requested amount, in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue + rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), OpaqueBytes.of(1), buyer, notaryNode.notaryIdentity, anonymous).returnValue } resultFutures.transpose().getOrThrow() + println("Cash issued to buyer") + + // 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 (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) { + javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use { + val id = rpc.uploadAttachment(it) + check(SellerFlow.PROSPECTUS_HASH == id) + } + } + + // The line below blocks and waits for the future to resolve. + val stx = rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryNode.notaryIdentity).returnValue.getOrThrow() + println("Commercial paper issued to seller") } - fun runSeller(amount: Amount = 1000.0.DOLLARS, counterparty: X500Name) { - val otherParty = rpc.partyFromX500Name(counterparty) ?: throw IllegalStateException("Don't know $counterparty") + fun runSeller(amount: Amount = 1000.0.DOLLARS, buyerName: X500Name) { + val otherParty = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName") // 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 diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt new file mode 100644 index 0000000000..56a4b4696d --- /dev/null +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -0,0 +1,76 @@ +package net.corda.traderdemo.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.CommercialPaper +import net.corda.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.core.contracts.Amount +import net.corda.core.contracts.`issued by` +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.NodeInfo +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.days +import net.corda.core.utilities.seconds +import java.time.Instant +import java.util.* + +/** + * Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer. + */ +@InitiatingFlow +@StartableByRPC +class CommercialPaperIssueFlow(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val notary: Party, + override val progressTracker: ProgressTracker) : FlowLogic() { + constructor(amount: Amount, issueRef: OpaqueBytes, recipient: Party, notary: Party) : this(amount, issueRef, recipient, notary, tracker()) + + companion object { + val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") + object ISSUING : ProgressTracker.Step("Issuing and timestamping some commercial paper") + fun tracker() = ProgressTracker(ISSUING) + } + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = ISSUING + + val me = serviceHub.myInfo.legalIdentity + val issuance: SignedTransaction = run { + val tx = CommercialPaper().generateIssue(me.ref(issueRef), amount `issued by` me.ref(issueRef), + Instant.now() + 10.days, notary) + + // TODO: Consider moving these two steps below into generateIssue. + + // Attach the prospectus. + tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id) + + // Requesting a time-window to be set, all CP must have a validation window. + tx.setTimeWindow(Instant.now(), 30.seconds) + + // Sign it as ourselves. + val stx = serviceHub.signInitialTransaction(tx) + + subFlow(FinalityFlow(stx)).single() + } + + // 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 = TransactionBuilder(notary) + CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient) + val stx = serviceHub.signInitialTransaction(builder) + subFlow(FinalityFlow(stx)).single() + } + + return move + } + +} diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index c82a5da116..c51edf30ca 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -2,25 +2,17 @@ package net.corda.traderdemo.flow import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper -import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount import net.corda.core.crypto.SecureHash -import net.corda.core.utilities.days -import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo -import net.corda.core.utilities.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.flows.TwoPartyTradeFlow -import org.bouncycastle.asn1.x500.X500Name -import java.time.Instant import java.util.* @InitiatingFlow @@ -51,7 +43,8 @@ class SellerFlow(val otherParty: Party, val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] val cpOwnerKey = serviceHub.keyManagementService.freshKey() - val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary) + val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first() + progressTracker.currentStep = TRADING @@ -66,39 +59,4 @@ class SellerFlow(val otherParty: Party, progressTracker.getChildProgressTracker(TRADING)!!) return subFlow(seller) } - - @Suspendable - fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef { - // Make a fake company that's issued its own paper. - val party = Party(X500Name("CN=BankOfCorda,O=R3,L=New York,C=US"), serviceHub.legalIdentityKey) - - 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.attachments.openAttachment(PROSPECTUS_HASH)!!.id) - - // Requesting a time-window to be set, all CP must have a validation window. - tx.setTimeWindow(Instant.now(), 30.seconds) - - // Sign it as ourselves. - val stx = serviceHub.signInitialTransaction(tx) - - subFlow(FinalityFlow(stx)).single() - } - - // 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 = TransactionBuilder(notaryNode.notaryIdentity) - CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) - val stx = serviceHub.signInitialTransaction(builder) - subFlow(FinalityFlow(stx)).single() - } - - return move.tx.outRef(0) - } - } From af1337151063020986a15ed92ad9e1347757dff8 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 10 Aug 2017 10:11:16 +0100 Subject: [PATCH 08/11] Move certificate builder code from core to node utilities. Address PR comments Fixup dead reference --- .../kotlin/net/corda/core/crypto/Crypto.kt | 103 +--------- .../net/corda/core/crypto/X500NameUtils.kt | 77 ++++++++ .../corda/core/crypto/CompositeKeyTests.kt | 4 +- .../core/crypto/X509NameConstraintsTest.kt | 10 +- .../internal/serialization/KryoTests.kt | 26 --- .../messaging/MQSecurityAsNodeTest.kt | 4 +- .../node/services/config/ConfigUtilities.kt | 3 +- .../net/corda/node/services/keys/KMSUtils.kt | 8 +- .../messaging/ArtemisMessagingServer.kt | 13 +- .../corda/node/utilities}/X509Utilities.kt | 182 ++++++++++-------- .../HTTPNetworkRegistrationService.kt | 2 +- .../registration/NetworkRegistrationHelper.kt | 8 +- .../network/InMemoryIdentityServiceTests.kt | 9 +- .../node/utilities}/X509UtilitiesTest.kt | 95 ++++++--- .../NetworkisRegistrationHelperTest.kt | 6 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 10 +- .../kotlin/net/corda/testing/TestConstants.kt | 5 +- .../kotlin/net/corda/testing/driver/Driver.kt | 11 +- .../testing/node/InMemoryMessagingNetwork.kt | 4 +- .../net/corda/testing/node/NodeBasedTest.kt | 11 +- .../corda/demobench/model/NodeController.kt | 2 +- .../demobench/model/NodeControllerTest.kt | 4 +- .../net/corda/verifier/GeneratedLedger.kt | 4 +- .../net/corda/verifier/VerifierDriver.kt | 9 +- 24 files changed, 325 insertions(+), 285 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt rename {core/src/main/kotlin/net/corda/core/crypto => node/src/main/kotlin/net/corda/node/utilities}/X509Utilities.kt (55%) rename {core/src/test/kotlin/net/corda/core/crypto => node/src/test/kotlin/net/corda/node/utilities}/X509UtilitiesTest.kt (79%) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 80ce7842ae..6db86fb563 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -14,19 +14,19 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec -import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.DERNull +import org.bouncycastle.asn1.DLSequence import org.bouncycastle.asn1.bc.BCObjectIdentifiers import org.bouncycastle.asn1.nist.NISTObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.sec.SECObjectIdentifiers -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x9.X9ObjectIdentifiers import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.bc.BcX509ExtensionUtils -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey @@ -40,10 +40,6 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec import org.bouncycastle.math.ec.ECConstants import org.bouncycastle.math.ec.FixedPointCombMultiplier import org.bouncycastle.math.ec.WNafUtil -import org.bouncycastle.operator.ContentSigner -import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder -import org.bouncycastle.pkcs.PKCS10CertificationRequest -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey @@ -53,7 +49,6 @@ import java.security.* import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec -import java.util.* import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -196,7 +191,7 @@ object Crypto { // that could cause unexpected and suspicious behaviour. // i.e. if someone removes a Provider and then he/she adds a new one with the same name. // The val is private to avoid any harmful state changes. - private val providerMap: Map = mapOf( + val providerMap: Map = mapOf( BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(), "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. @@ -770,90 +765,6 @@ object Crypto { return mac.doFinal(seed) } - /** - * Build a partial X.509 certificate ready for signing. - * - * @param issuer name of the issuing entity. - * @param subject name of the certificate subject. - * @param subjectPublicKey public key of the certificate subject. - * @param validityWindow the time period the certificate is valid for. - * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. - */ - fun createCertificate(certificateType: CertificateType, issuer: X500Name, - subject: X500Name, subjectPublicKey: PublicKey, - validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509v3CertificateBuilder { - - val serial = BigInteger.valueOf(random63BitValue()) - val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) - val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded)) - - val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) - .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo)) - .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) - .addExtension(Extension.keyUsage, false, certificateType.keyUsage) - .addExtension(Extension.extendedKeyUsage, false, keyPurposes) - - if (nameConstraints != null) { - builder.addExtension(Extension.nameConstraints, true, nameConstraints) - } - return builder - } - - /** - * Build and sign an X.509 certificate with the given signer. - * - * @param issuer name of the issuing entity. - * @param issuerSigner content signer to sign the certificate with. - * @param subject name of the certificate subject. - * @param subjectPublicKey public key of the certificate subject. - * @param validityWindow the time period the certificate is valid for. - * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. - */ - fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerSigner: ContentSigner, - subject: X500Name, subjectPublicKey: PublicKey, - validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509CertificateHolder { - val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) - return builder.build(issuerSigner).apply { - require(isValidOn(Date())) - } - } - - /** - * Build and sign an X.509 certificate with CA cert private key. - * - * @param issuer name of the issuing entity. - * @param issuerKeyPair the public & private key to sign the certificate with. - * @param subject name of the certificate subject. - * @param subjectPublicKey public key of the certificate subject. - * @param validityWindow the time period the certificate is valid for. - * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. - */ - fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, - subject: X500Name, subjectPublicKey: PublicKey, - validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509CertificateHolder { - - val signatureScheme = findSignatureScheme(issuerKeyPair.private) - val provider = providerMap[signatureScheme.providerName] - val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) - - val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) - return builder.build(signer).apply { - require(isValidOn(Date())) - require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))) - } - } - - /** - * Create certificate signing request using provided information. - */ - fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { - val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, providerMap[signatureScheme.providerName]) - return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer) - } - private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter { override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt new file mode 100644 index 0000000000..0043988ba4 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt @@ -0,0 +1,77 @@ +@file:JvmName("X500NameUtils") +package net.corda.core.crypto + +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.X500NameBuilder +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import java.security.KeyPair +import java.security.cert.X509Certificate + +/** + * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. + */ +fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } + +/** + * Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this + * adds one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. + */ +fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } + +/** + * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. + * + * @param mutator a function to generate the new value from the previous one. + * @throws IllegalArgumentException if the distinguished name does not contain a common name element. + */ +private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name { + val builder = X500NameBuilder(BCStyle.INSTANCE) + var matched = false + this.rdNs.forEach { rdn -> + rdn.typesAndValues.forEach { typeAndValue -> + when (typeAndValue.type) { + BCStyle.CN -> { + matched = true + builder.addRDN(typeAndValue.type, mutator(typeAndValue.value)) + } + else -> { + builder.addRDN(typeAndValue) + } + } + } + } + require(matched) { "Input X.500 name must include a common name (CN) attribute: ${this}" } + return builder.build() +} + +val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() +val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() +val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() +val X500Name.locationOrNull: String? get() = try { + location +} catch (e: Exception) { + null +} +val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject +val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) + +/** + * Generate a distinguished name from the provided values. + */ +@JvmOverloads +fun getX509Name(myLegalName: String, nearestCity: String, email: String, country: String? = null): X500Name { + return X500NameBuilder(BCStyle.INSTANCE).let { builder -> + builder.addRDN(BCStyle.CN, myLegalName) + builder.addRDN(BCStyle.L, nearestCity) + country?.let { builder.addRDN(BCStyle.C, it) } + builder.addRDN(BCStyle.E, email) + builder.build() + } +} + +data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair) diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index f02f028fb4..b34d2cc976 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -8,9 +8,7 @@ import net.corda.core.internal.declaredField import net.corda.core.internal.div import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes -import net.corda.node.utilities.loadKeyStore -import net.corda.node.utilities.loadOrCreateKeyStore -import net.corda.node.utilities.save +import net.corda.node.utilities.* import net.corda.testing.TestDependencyInjectionBase import org.bouncycastle.asn1.x500.X500Name import org.junit.Rule diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index ef6f879409..8d2589119c 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -1,9 +1,7 @@ package net.corda.core.crypto import net.corda.core.internal.toTypedArray -import net.corda.node.utilities.KEYSTORE_TYPE -import net.corda.node.utilities.addOrReplaceCertificate -import net.corda.node.utilities.addOrReplaceKey +import net.corda.node.utilities.* import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -20,13 +18,13 @@ class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Root CA","London","demo@r3.com",null), rootKeys) + val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Root CA", "London", "demo@r3.com", null), rootKeys) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getX509Name("Corda Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, getX509Name("Corda Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public) val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getX509Name("Corda Client CA","London","demo@r3.com",null), clientCAKeyPair.public, nameConstraints = nameConstraints) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, getX509Name("Corda Client CA", "London", "demo@r3.com", null), clientCAKeyPair.public, nameConstraints = nameConstraints) val keyPass = "password" val trustStore = KeyStore.getInstance(KEYSTORE_TYPE) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 41c9dd0466..702c66b9b7 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -11,20 +11,14 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.sequence import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.testing.ALICE import net.corda.testing.ALICE_PUBKEY -import net.corda.testing.BOB -import net.corda.testing.BOB_PUBKEY import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.bouncycastle.cert.X509CertificateHolder import org.junit.Before import org.junit.Test import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream -import java.security.cert.CertPath -import java.security.cert.CertificateFactory import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -150,26 +144,6 @@ class KryoTests { assertEquals(-1, readRubbishStream.read()) } - @Test - fun `serialize - deserialize X509CertififcateHolder`() { - val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - val serialized = expected.serialize(factory, context).bytes - val actual: X509CertificateHolder = serialized.deserialize(factory, context) - assertEquals(expected, actual) - } - - @Test - fun `serialize - deserialize X509CertPath`() { - val certFactory = CertificateFactory.getInstance("X509") - val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) - val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY) - val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert)) - val serialized = expected.serialize(factory, context).bytes - val actual: CertPath = serialized.deserialize(factory, context) - assertEquals(expected, actual) - } - @CordaSerializable private data class Person(val name: String, val birthday: Instant?) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index 59649518bd..d053552c1c 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -1,10 +1,8 @@ package net.corda.services.messaging +import net.corda.core.crypto.Crypto import net.corda.core.internal.copyTo import net.corda.core.internal.createDirectories -import net.corda.core.crypto.CertificateType -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.X509Utilities import net.corda.core.internal.exists import net.corda.node.utilities.* import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 76d7ec5792..f66e53a7be 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -4,9 +4,10 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigRenderOptions +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignatureScheme import net.corda.core.internal.copyTo import net.corda.core.internal.createDirectories -import net.corda.core.crypto.* import net.corda.core.internal.div import net.corda.core.internal.exists import net.corda.core.utilities.loggerFor diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 9b50ac1572..88ae5560bf 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -1,10 +1,14 @@ package net.corda.node.services.keys -import net.corda.core.crypto.* +import net.corda.core.crypto.ContentSignerBuilder +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.cert import net.corda.core.identity.AnonymousPartyAndPath import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.X509Utilities import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PublicKey @@ -31,7 +35,7 @@ fun freshCertificate(identityService: IdentityService, revocationEnabled: Boolean = false): AnonymousPartyAndPath { val issuerCertificate = issuer.certificate val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCertificate) - val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) + val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) val certFactory = CertificateFactory.getInstance("X509") val ourCertPath = certFactory.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) val anonymisedIdentity = AnonymousPartyAndPath(subjectPublicKey, ourCertPath) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 5ef9d5f906..919a744191 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -2,13 +2,13 @@ package net.corda.node.services.messaging import com.google.common.util.concurrent.ListenableFuture import io.netty.handler.ssl.SslHandler -import net.corda.core.* import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.* -import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS -import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA -import net.corda.core.internal.concurrent.openFuture +import net.corda.core.crypto.AddressFormatException +import net.corda.core.crypto.newSecureRandom +import net.corda.core.crypto.parsePublicKeyBase58 +import net.corda.core.crypto.random63BitValue import net.corda.core.internal.ThreadBox +import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle import net.corda.core.node.NodeInfo @@ -22,6 +22,9 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE +import net.corda.node.utilities.X509Utilities +import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS +import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import net.corda.node.utilities.getX509Certificate import net.corda.node.utilities.loadKeyStore import net.corda.nodeapi.* diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt similarity index 55% rename from core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt rename to node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index db7717aa33..53ed5580eb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -1,25 +1,33 @@ -package net.corda.core.crypto +package net.corda.node.utilities +import net.corda.core.crypto.* import net.corda.core.utilities.days import net.corda.core.utilities.millis -import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.X500NameBuilder -import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.asn1.x509.KeyPurposeId -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.bc.BcX509ExtensionUtils +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder +import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.util.io.pem.PemReader import java.io.FileReader import java.io.FileWriter import java.io.InputStream +import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey import java.security.cert.* +import java.security.cert.Certificate import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -69,44 +77,13 @@ object X509Utilities { return Pair(notBefore, notAfter) } - /** - * Return a bogus X509 for dev purposes. Use [getX509Name] for something more real. - */ - @Deprecated("Full legal names should be specified in all configurations") - fun getDevX509Name(commonName: String): X500Name { - val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) - nameBuilder.addRDN(BCStyle.CN, commonName) - nameBuilder.addRDN(BCStyle.O, "R3") - nameBuilder.addRDN(BCStyle.OU, "corda") - nameBuilder.addRDN(BCStyle.L, "London") - nameBuilder.addRDN(BCStyle.C, "GB") - return nameBuilder.build() - } - - /** - * Generate a distinguished name from the provided values. - * - * @see [CoreTestUtils.getTestX509Name] for generating distinguished names for test cases. - */ - @JvmOverloads - @JvmStatic - fun getX509Name(myLegalName: String, nearestCity: String, email: String, country: String? = null): X500Name { - return X500NameBuilder(BCStyle.INSTANCE).let { builder -> - builder.addRDN(BCStyle.CN, myLegalName) - builder.addRDN(BCStyle.L, nearestCity) - country?.let { builder.addRDN(BCStyle.C, it) } - builder.addRDN(BCStyle.E, email) - builder.build() - } - } - /* * Create a de novo root self-signed X509 v3 CA cert. */ @JvmStatic fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) - return Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) + return createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) } /** @@ -126,7 +103,7 @@ object X509Utilities { validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, nameConstraints: NameConstraints? = null): X509CertificateHolder { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate) - return Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) + return createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) } fun validateCertificateChain(trustedRoot: X509CertificateHolder, vararg certificates: Certificate) { @@ -168,54 +145,93 @@ object X509Utilities { } } - fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) = Crypto.createCertificateSigningRequest(subject, keyPair, signatureScheme) -} + /** + * Build a partial X.509 certificate ready for signing. + * + * @param issuer name of the issuing entity. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + */ + fun createCertificate(certificateType: CertificateType, issuer: X500Name, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509v3CertificateBuilder { -/** - * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } + val serial = BigInteger.valueOf(random63BitValue()) + val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded)) -/** - * Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this - * adds one. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } + val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) + .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo)) + .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) + .addExtension(Extension.keyUsage, false, certificateType.keyUsage) + .addExtension(Extension.extendedKeyUsage, false, keyPurposes) -/** - * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. - * - * @param mutator a function to generate the new value from the previous one. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name { - val builder = X500NameBuilder(BCStyle.INSTANCE) - var matched = false - this.rdNs.forEach { rdn -> - rdn.typesAndValues.forEach { typeAndValue -> - when (typeAndValue.type) { - BCStyle.CN -> { - matched = true - builder.addRDN(typeAndValue.type, mutator(typeAndValue.value)) - } - else -> { - builder.addRDN(typeAndValue) - } - } + if (nameConstraints != null) { + builder.addExtension(Extension.nameConstraints, true, nameConstraints) + } + return builder + } + + /** + * Build and sign an X.509 certificate with the given signer. + * + * @param issuer name of the issuing entity. + * @param issuerSigner content signer to sign the certificate with. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + */ + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerSigner: ContentSigner, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { + val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) + return builder.build(issuerSigner).apply { + require(isValidOn(Date())) } } - require(matched) { "Input X.500 name must include a common name (CN) attribute: ${this}" } - return builder.build() + + /** + * Build and sign an X.509 certificate with CA cert private key. + * + * @param issuer name of the issuing entity. + * @param issuerKeyPair the public & private key to sign the certificate with. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + */ + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { + + val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) + val provider = Crypto.providerMap[signatureScheme.providerName] + val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) + + val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) + return builder.build(signer).apply { + require(isValidOn(Date())) + require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))) + } + } + + /** + * Create certificate signing request using provided information. + */ + fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { + val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.providerMap[signatureScheme.providerName]) + return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer) + } + + fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair) = createCertificateSigningRequest(subject, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME) } -val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() -val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() -val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() -val X500Name.locationOrNull: String? get() = try { location } catch (e: Exception) { null } -val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject -val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) class CertificateStream(val input: InputStream) { private val certificateFactory = CertificateFactory.getInstance("X.509") @@ -223,8 +239,6 @@ class CertificateStream(val input: InputStream) { fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate } -data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair) - enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index 7d5ccae2b5..6904f91258 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -1,7 +1,7 @@ package net.corda.node.utilities.registration import com.google.common.net.MediaType -import net.corda.core.crypto.CertificateStream +import net.corda.node.utilities.CertificateStream import org.apache.commons.io.IOUtils import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.io.IOException diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 863bee702c..268b89fa53 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -1,17 +1,15 @@ package net.corda.node.utilities.registration -import net.corda.core.crypto.CertificateType import net.corda.core.crypto.Crypto -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA -import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS -import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.crypto.cert import net.corda.core.internal.* import net.corda.core.utilities.seconds import net.corda.core.utilities.validateX500Name import net.corda.node.services.config.NodeConfiguration import net.corda.node.utilities.* +import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA +import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS +import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 9fa1d0e0e5..b425fdcc8e 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -1,12 +1,17 @@ package net.corda.node.services.network -import net.corda.core.crypto.* -import net.corda.core.identity.AnonymousPartyAndPath +import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.cert +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.AnonymousPartyAndPath import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.X509Utilities import net.corda.testing.* import org.bouncycastle.asn1.x500.X500Name import org.junit.Test diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt similarity index 79% rename from core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt rename to node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt index d1da2f6a27..2c528cbfd5 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt @@ -1,19 +1,28 @@ -package net.corda.core.crypto +package net.corda.node.utilities +import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.generateKeyPair -import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME -import net.corda.core.crypto.X509Utilities.createSelfSignedCACertificate +import net.corda.core.crypto.cert +import net.corda.core.crypto.commonName +import net.corda.core.crypto.getX509Name import net.corda.core.internal.div import net.corda.core.internal.toTypedArray +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.config.createKeystoreForCordaNode -import net.corda.node.utilities.* -import net.corda.testing.MEGA_CORP -import net.corda.testing.getTestX509Name +import net.corda.nodeapi.internal.serialization.AllWhitelist +import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 +import net.corda.nodeapi.internal.serialization.SerializationContextImpl +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.testing.* import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder import org.junit.Rule import org.junit.Test @@ -27,7 +36,9 @@ import java.nio.file.Path import java.security.KeyStore import java.security.PrivateKey import java.security.SecureRandom +import java.security.cert.CertPath import java.security.cert.Certificate +import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.* import java.util.stream.Stream @@ -42,8 +53,8 @@ class X509UtilitiesTest { @Test fun `create valid self-signed CA certificate`() { - val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) assertTrue { caCert.subject.commonName == "Test Cert" } // using our subject common name assertEquals(caCert.issuer, caCert.subject) //self-signed caCert.isValidOn(Date()) // throws on verification problems @@ -57,8 +68,8 @@ class X509UtilitiesTest { @Test fun `load and save a PEM file certificate`() { val tmpCertificateFile = tempFile("cacert.pem") - val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) assertEquals(caCert, readCertificate) @@ -66,10 +77,10 @@ class X509UtilitiesTest { @Test fun `create valid server certificate chain`() { - val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) + val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) val subject = getTestX509Name("Server Cert") - val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) + val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public) assertTrue { serverCert.subject.toString().contains("CN=Server Cert") } // using our subject common name assertEquals(caCert.issuer, serverCert.issuer) // Issued by our CA cert @@ -86,7 +97,7 @@ class X509UtilitiesTest { val tmpKeyStore = tempFile("keystore.jks") val keyPair = generateKeyPair(EDDSA_ED25519_SHA512) - val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) assertTrue(Arrays.equals(selfSignCert.subjectPublicKeyInfo.encoded, keyPair.public.encoded)) @@ -111,7 +122,7 @@ class X509UtilitiesTest { fun `signing EdDSA key with EcDSA certificate`() { val tmpKeyStore = tempFile("keystore.jks") val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val ecDSACert = createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey) + val ecDSACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey) val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512) val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, X500Name("CN=TestEdDSA"), edDSAKeypair.public) @@ -155,8 +166,8 @@ class X509UtilitiesTest { // Now sign something with private key and verify against certificate public key val testData = "12345".toByteArray() - val caSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) - assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } + val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData) + assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) } // Load back generated intermediate CA Cert and private key val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate @@ -165,8 +176,8 @@ class X509UtilitiesTest { intermediateCaCert.verify(rootCaCert.publicKey) // Now sign something with private key and verify against certificate public key - val intermediateSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) - assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } + val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData) + assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) } } @Test @@ -209,9 +220,9 @@ class X509UtilitiesTest { assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() - val signature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) + val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo) - assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) } + assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) } } @Test @@ -343,11 +354,11 @@ class X509UtilitiesTest { trustStoreFilePath: Path, trustStorePassword: String ): KeyStore { - val rootCAKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Node Root CA","London","demo@r3.com",null), rootCAKey) + val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Node Root CA", "London", "demo@r3.com", null), rootCAKey) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getX509Name("Corda Node Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, getX509Name("Corda Node Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public) val keyPass = keyPassword.toCharArray() val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) @@ -374,7 +385,7 @@ class X509UtilitiesTest { @Test fun `Get correct private key type from Keystore`() { val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) val keyStore = loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.cert)) @@ -384,4 +395,38 @@ class X509UtilitiesTest { assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey) } + + @Test + fun `serialize - deserialize X509CertififcateHolder`() { + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } + val context = SerializationContextImpl(KryoHeaderV0_1, + javaClass.classLoader, + AllWhitelist, + emptyMap(), + true, + SerializationContext.UseCase.P2P) + val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + val serialized = expected.serialize(factory, context).bytes + val actual: X509CertificateHolder = serialized.deserialize(factory, context) + assertEquals(expected, actual) + } + + @Test + fun `serialize - deserialize X509CertPath`() { + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } + val context = SerializationContextImpl(KryoHeaderV0_1, + javaClass.classLoader, + AllWhitelist, + emptyMap(), + true, + SerializationContext.UseCase.P2P) + val certFactory = CertificateFactory.getInstance("X509") + val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) + val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY) + val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert)) + val serialized = expected.serialize(factory, context).bytes + val actual: CertPath = serialized.deserialize(factory, context) + assertEquals(expected, actual) + } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 1eeae40142..51de115d1f 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -3,9 +3,13 @@ package net.corda.node.utilities.registration import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock -import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.cert +import net.corda.core.crypto.commonName import net.corda.core.internal.exists import net.corda.core.internal.toTypedArray +import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.loadKeyStore import net.corda.testing.ALICE import net.corda.testing.getTestX509Name diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 5b93da6c1c..5a573b90e3 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -11,13 +11,15 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.X509Utilities import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties @@ -67,9 +69,9 @@ val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public val BOB_PUBKEY: PublicKey get() = BOB_KEY.public val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public -val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MegaCorp","London","demo@r3.com",null), MEGA_CORP_PUBKEY) +val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MegaCorp", "London", "demo@r3.com", null), MEGA_CORP_PUBKEY) val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party -val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MiniCorp","London","demo@r3.com",null), MINI_CORP_PUBKEY) +val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MiniCorp", "London", "demo@r3.com", null), MINI_CORP_PUBKEY) val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party val BOC_KEY: KeyPair by lazy { generateKeyPair() } @@ -80,7 +82,7 @@ val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() } val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public -val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("BigCorporation","London","demo@r3.com",null), BIG_CORP_PUBKEY) +val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("BigCorporation", "London", "demo@r3.com", null), BIG_CORP_PUBKEY) val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index f6c236b330..2975a84ada 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -4,7 +4,9 @@ package net.corda.testing import net.corda.core.contracts.Command import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.crypto.* +import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.testing.DummyPublicKey import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -12,6 +14,7 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.services.ServiceInfo import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.node.utilities.X509Utilities import net.corda.nodeapi.User import net.corda.testing.driver.DriverDSLExposedInterface import org.bouncycastle.asn1.x500.X500Name diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index ab21d10c69..3a14d967e0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -11,12 +11,12 @@ import net.corda.cordform.CordformNode import net.corda.cordform.NodeDefinition import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf -import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName +import net.corda.core.crypto.getX509Name import net.corda.core.identity.Party -import net.corda.core.internal.concurrent.* import net.corda.core.internal.ThreadBox +import net.corda.core.internal.concurrent.* import net.corda.core.internal.div import net.corda.core.internal.times import net.corda.core.messaging.CordaRPCOps @@ -53,9 +53,12 @@ import java.time.Instant import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter import java.util.* -import java.util.concurrent.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread @@ -566,7 +569,7 @@ class DriverDSL( val rpcAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: X509Utilities.getX509Name("${oneOf(names).commonName}-${p2pAddress.port}","London","demo@r3.com",null) + val name = providedName ?: getX509Name("${oneOf(names).commonName}-${p2pAddress.port}", "London", "demo@r3.com", null) val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition { override fun getName() = name.toString() override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString()) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index c85f87f97d..440b4da95a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -3,8 +3,8 @@ package net.corda.testing.node import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture +import net.corda.core.crypto.getX509Name import net.corda.core.internal.ThreadBox -import net.corda.core.crypto.X509Utilities import net.corda.core.messaging.AllPossibleRecipients import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients @@ -128,7 +128,7 @@ class InMemoryMessagingNetwork( id: Int, executor: AffinityExecutor, advertisedServices: List, - description: X500Name = X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null), + description: X500Name = getX509Name("In memory node $id", "London", "demo@r3.com", null), database: CordaPersistence) : MessagingServiceBuilder { val peerHandle = PeerHandle(id, description) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index e9ba532ce4..7e8d3bedd1 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -1,10 +1,13 @@ package net.corda.testing.node import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName -import net.corda.core.internal.concurrent.* +import net.corda.core.crypto.getX509Name +import net.corda.core.internal.concurrent.flatMap +import net.corda.core.internal.concurrent.fork +import net.corda.core.internal.concurrent.map +import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.services.ServiceInfo @@ -120,13 +123,13 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } val masterNodeFuture = startNode( - X509Utilities.getX509Name("${notaryName.commonName}-0","London","demo@r3.com",null), + getX509Name("${notaryName.commonName}-0", "London", "demo@r3.com", null), advertisedServices = setOf(serviceInfo), configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0])) val remainingNodesFutures = (1 until clusterSize).map { startNode( - X509Utilities.getX509Name("${notaryName.commonName}-$it","London","demo@r3.com",null), + getX509Name("${notaryName.commonName}-$it", "London", "demo@r3.com", null), advertisedServices = setOf(serviceInfo), configOverrides = mapOf( "notaryNodeAddress" to nodeAddresses[it], diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index b593bc32ce..0b484aacda 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,6 +1,6 @@ package net.corda.demobench.model -import net.corda.core.crypto.X509Utilities.getX509Name +import net.corda.core.crypto.getX509Name import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty import tornadofx.* diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index 6c77fa3b05..d476ccc93a 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,8 +1,8 @@ package net.corda.demobench.model -import net.corda.core.crypto.X509Utilities.getX509Name -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.crypto.getX509Name import net.corda.nodeapi.User +import net.corda.testing.DUMMY_NOTARY import org.junit.Test import java.nio.file.Path import java.nio.file.Paths diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index a273485ea3..e178b25e71 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -3,7 +3,6 @@ package net.corda.verifier import net.corda.client.mock.* import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.sha256 import net.corda.core.identity.AbstractParty @@ -12,6 +11,7 @@ import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.testing.contracts.DummyContract +import net.corda.testing.getTestX509Name import java.io.ByteArrayInputStream import java.math.BigInteger import java.security.PublicKey @@ -179,7 +179,7 @@ fun commandGenerator(partiesToPickFrom: Collection): Generator = Generator.int().combine(publicKeyGenerator) { n, key -> - Party(X509Utilities.getDevX509Name("Party$n"), key) + Party(getTestX509Name("Party$n"), key) } fun pickOneOrMaybeNew(from: Collection, generator: Generator): Generator { diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index b2432f7e4d..b97f5845dc 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -2,15 +2,13 @@ package net.corda.verifier import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import net.corda.core.concurrent.* -import net.corda.core.crypto.X509Utilities +import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.commonName -import net.corda.core.internal.div import net.corda.core.crypto.random63BitValue import net.corda.core.internal.concurrent.* +import net.corda.core.internal.div import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.driver.ProcessUtilities import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -20,6 +18,7 @@ import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.driver.* +import net.corda.testing.getTestX509Name import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ClientProducer @@ -250,7 +249,7 @@ data class VerifierDriverDSL( val id = verifierCount.andIncrement val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null val processFuture = driverDSL.executorService.fork { - val verifierName = X509Utilities.getDevX509Name("verifier$id") + val verifierName = getTestX509Name("verifier$id") val baseDirectory = driverDSL.driverDirectory / verifierName.commonName val config = createConfiguration(baseDirectory, address) val configFilename = "verifier.conf" From 31df43bd761cb5f95c4f931e3f85e265a6dd039e Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Fri, 11 Aug 2017 13:23:00 +0100 Subject: [PATCH 09/11] Some preparation work for AMQP serialization integration & a refactor fix (#1214) --- .../net/corda/client/rpc/CordaRPCClient.kt | 2 + .../serialization/AMQPSerializationScheme.kt | 94 +++++++++++++++++++ .../serialization/SerializationScheme.kt | 17 ++-- .../amqp/DeserializationInput.kt | 23 ++--- .../amqp/JavaSerializationOutputTests.java | 0 .../kotlin/net/corda/node/internal/Node.kt | 1 + .../corda/testing/SerializationTestHelpers.kt | 2 + 7 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt rename {core/src/test/java => node-api/src/test/kotlin}/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java (100%) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 9857501ec3..a0a4a7ea1f 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.config.SSLConfiguration +import net.corda.nodeapi.internal.serialization.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl @@ -71,6 +72,7 @@ class CordaRPCClient( try { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) + registerScheme(AMQPClientSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt new file mode 100644 index 0000000000..9772bef28a --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -0,0 +1,94 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes +import net.corda.core.utilities.ByteSequence +import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 +import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput +import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.util.concurrent.ConcurrentHashMap + +private const val AMQP_ENABLED = false + +abstract class AbstractAMQPSerializationScheme : SerializationScheme { + private val serializerFactoriesForContexts = ConcurrentHashMap, SerializerFactory>() + + protected abstract fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory + protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory + + private fun getSerializerFactory(context: SerializationContext): SerializerFactory { + return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { + when (context.useCase) { + SerializationContext.UseCase.Checkpoint -> + throw IllegalStateException("AMQP should not be used for checkpoint serialization.") + SerializationContext.UseCase.RPCClient -> + rpcClientSerializerFactory(context) + SerializationContext.UseCase.RPCServer -> + rpcServerSerializerFactory(context) + else -> SerializerFactory(context.whitelist) // TODO pass class loader also + } + } + } + + override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { + val serializerFactory = getSerializerFactory(context) + return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz) + } + + override fun serialize(obj: T, context: SerializationContext): SerializedBytes { + val serializerFactory = getSerializerFactory(context) + return SerializationOutput(serializerFactory).serialize(obj) + } + + protected fun canDeserializeVersion(byteSequence: ByteSequence): Boolean = AMQP_ENABLED && byteSequence == AmqpHeaderV1_0 +} + +// TODO: This will eventually cover server RPC as well and move to node module, but for now this is not implemented +class AMQPServerSerializationScheme : AbstractAMQPSerializationScheme() { + override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { + throw UnsupportedOperationException() + } + + override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { + return (canDeserializeVersion(byteSequence) && + (target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)) + } + +} + +// TODO: This will eventually cover client RPC as well and move to client module, but for now this is not implemented +class AMQPClientSerializationScheme : AbstractAMQPSerializationScheme() { + override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { + throw UnsupportedOperationException() + } + + override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { + return (canDeserializeVersion(byteSequence) && + (target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)) + } + +} + +val AMQP_P2P_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0, + SerializationDefaults.javaClass.classLoader, + GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), + emptyMap(), + true, + SerializationContext.UseCase.P2P) +val AMQP_STORAGE_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0, + SerializationDefaults.javaClass.classLoader, + AllButBlacklisted, + emptyMap(), + true, + SerializationContext.UseCase.Storage) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index cd68bbb1bd..688dfacf7e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -54,6 +54,8 @@ data class SerializationContextImpl(override val preferedSerializationVersion: B } } +private const val HEADER_SIZE: Int = 8 + open class SerializationFactoryImpl : SerializationFactory { private val creator: List = Exception().stackTrace.asList() @@ -63,8 +65,8 @@ open class SerializationFactoryImpl : SerializationFactory { private val schemes: ConcurrentHashMap, SerializationScheme> = ConcurrentHashMap() private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme { - // truncate sequence to 8 bytes - return schemes.computeIfAbsent(byteSequence.take(8).copy() to target) { + // truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays + return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) { for (scheme in registeredSchemes) { if (scheme.canDeserializeVersion(it.first, it.second)) { return@computeIfAbsent scheme @@ -162,11 +164,12 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme { override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { val pool = getPool(context) - Input(byteSequence.bytes, byteSequence.offset, byteSequence.size).use { input -> - val header = OpaqueBytes(input.readBytes(8)) - if (header != KryoHeaderV0_1) { - throw KryoException("Serialized bytes header does not match expected format.") - } + val headerSize = KryoHeaderV0_1.size + val header = byteSequence.take(headerSize) + if (header != KryoHeaderV0_1) { + throw KryoException("Serialized bytes header does not match expected format.") + } + Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input -> return pool.run { kryo -> withContext(kryo, context) { @Suppress("UNCHECKED_CAST") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 907d39ec83..5654dfa20d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import net.corda.core.internal.getStackTraceAsString import net.corda.core.serialization.SerializedBytes +import net.corda.core.utilities.ByteSequence import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedByte @@ -26,17 +27,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S internal companion object { val BYTES_NEEDED_TO_PEEK: Int = 23 - private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean { - if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException() - var bytesRemaining = length - var aPos = aOffset - var bPos = bOffset - while (bytesRemaining-- > 0) { - if (a[aPos++] != b[bPos++]) return false - } - return true - } - fun peekSize(bytes: ByteArray): Int { // There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor val eighth = bytes[8].toInt() @@ -69,15 +59,16 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S @Throws(NotSerializableException::class) - private fun getEnvelope(bytes: SerializedBytes): Envelope { + private fun getEnvelope(bytes: ByteSequence): Envelope { // Check that the lead bytes match expected header - if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) { + val headerSize = AmqpHeaderV1_0.size + if (bytes.take(headerSize) != AmqpHeaderV1_0) { throw NotSerializableException("Serialization header does not match.") } val data = Data.Factory.create() - val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) - if (size.toInt() != bytes.size - 8) { + val size = data.decode(ByteBuffer.wrap(bytes.bytes, bytes.offset + headerSize, bytes.size - headerSize)) + if (size.toInt() != bytes.size - headerSize) { throw NotSerializableException("Unexpected size of data") } @@ -103,7 +94,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S * be deserialized and a schema describing the types of the objects. */ @Throws(NotSerializableException::class) - fun deserialize(bytes: SerializedBytes, clazz: Class): T { + fun deserialize(bytes: ByteSequence, clazz: Class): T { return des { val envelope = getEnvelope(bytes) clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) diff --git a/core/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java similarity index 100% rename from core/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java rename to node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 29acbc417b..eea378de27 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -332,6 +332,7 @@ open class Node(override val configuration: FullNodeConfiguration, private fun initialiseSerialization() { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPServerSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT diff --git a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index b6612af10e..82d7925fec 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -63,6 +63,8 @@ fun initialiseTestSerialization() { (SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = SerializationFactoryImpl().apply { registerScheme(KryoClientSerializationScheme()) registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPClientSerializationScheme()) + registerScheme(AMQPServerSerializationScheme()) } (SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = KRYO_P2P_CONTEXT (SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_SERVER_CONTEXT From f9552a0d3e97ef48565583abf1a2a3edb70829fa Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 11 Aug 2017 10:53:20 +0100 Subject: [PATCH 10/11] Correct exceptions thrown from checkSignaturesAreValid() checkSignaturesAreValid() calls TransactionSignature.verify(), which throws InvalidKeyException but checkSignaturesAreValid() did not indicate it could throw InvalidKeyException. --- .../net/corda/core/transactions/TransactionWithSignatures.kt | 4 +++- .../src/main/java/net/corda/docs/FlowCookbookJava.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index f4ef837cd4..5bc5f3ce0c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.transactions.SignedTransaction.SignaturesMissingException import net.corda.core.utilities.toNonEmptySet +import java.security.InvalidKeyException import java.security.PublicKey import java.security.SignatureException @@ -52,9 +53,10 @@ interface TransactionWithSignatures : NamedByHash { * corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you * want [verifySignatures] instead. * + * @throws InvalidKeyException if the key on a signature is invalid. * @throws SignatureException if a signature fails to verify. */ - @Throws(SignatureException::class) + @Throws(InvalidKeyException::class, SignatureException::class) fun checkSignaturesAreValid() { for (sig in sigs) { sig.verify(id) diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index e397f550e4..c3c6b626d7 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -26,6 +26,7 @@ import net.corda.testing.contracts.DummyState; import org.bouncycastle.asn1.x500.X500Name; import org.jetbrains.annotations.NotNull; +import java.security.GeneralSecurityException; import java.security.PublicKey; import java.security.SignatureException; import java.time.Duration; @@ -506,7 +507,7 @@ public class FlowCookbookJava { twiceSignedTx.checkSignaturesAreValid(); // DOCEND 37 - } catch (SignatureException e) { + } catch (GeneralSecurityException e) { // Handle this as required. } From 907ec9ff466e7430b15e3d79dda130c9f435206b Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 11 Aug 2017 14:26:52 +0100 Subject: [PATCH 11/11] Reduce logging severity for Hibernate SQL statements (in test). (#1217) --- test-common/src/main/resources/log4j2-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-common/src/main/resources/log4j2-test.xml b/test-common/src/main/resources/log4j2-test.xml index cf5d261292..222a4a1778 100644 --- a/test-common/src/main/resources/log4j2-test.xml +++ b/test-common/src/main/resources/log4j2-test.xml @@ -22,7 +22,7 @@ - 
 +