From f8ad5c9d10da8db042c771bd623a882e4afcf542 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 22 Jun 2017 10:35:49 +0100 Subject: [PATCH 01/25] Vault Query Service JPA implementation (#840) * Vault Query Service API implementation using JPA Hibernate Added queryBy(QueryCriteria) Vault API and Junit tests. Minor cosmetic API changes following rebase. Fixes following rebase from master Upgraded to requery 1.3.1 WIP - removed 'latestOnly' from LinearStateQueryCriteria WIP - CommercialSchemas V2, V3, V4 testing WIP - sort out generics handling. WIP - most general queries completed. WIP - join queries, contractStateType derivation WIP - refactoring Requery WIP - refactored VaultService to extract a VaultQueryService interface (and associated Requery implementation). WIP - HibernateVaultQuery implementation WIP - Re-structured all Schema definitions (requery/jpa) and make Hibernate Config reusable. WIP - Multi-version schema testing, hibernate query testing. WIP - Custom Criteria and Fungible Criteria impl & testing. WIP - Kotlin Comparable Generics error WIP - Party queries all working now WIP - All VaultQueryTests now working (refactored for AND / OR composition) WIP - added schema registration in CordaPluginRegistry to enable custom vault queries on arbitrary schemas. WIP - added new default Sort NULL order to be NONE + added lots more tests for Logical Operator testing. Mostly identity fixes following rebase from master. Exception handling and public API cleanup in prep for PR. Additional tests for Logical Operators; additional tests for NULLS sort ordering; additional logging; Additional parser to handle Nullable attribute values; added Unary and Collection logical expression handlers Lots of cleanup: participants; trackBy interfaces; additional fungible tests; parser cleanup and improved support for Java Removed all traces of Requery implementation. Further minor cleanup and Junit test fix. Final identity and schema related identity clean-up. Revert unrelated changes. PR review updates: blank lines, isRelevant. Fixed wiring of updatesPublisher for dynamic trackBy queries. PR review changes: multi-versioned schema samples and associated dummy contracts moved to test packages. Fixed problem with sorted queries (not specifying any filterable criteria). PR review: minor updates to address RP comments. Typesafe custom query criteria Cleanup: remove redundant tests. Further clean-up and make all Java test work successfully. Remove debugging print statements. Rebased from master - changes required due to DealState module change. fixed broken assertion caused by DealState ordering change (different package) Fixed transaction demarcation issue causing "java.lang.IllegalStateException: Was not expecting to find existing database transaction on current strand" trackBy() now filters on ContractType and StateStatus (CONSUMED, UNCONSUMED, ALL) Added tests to exercise RPCOps trackBy and queryBy (RPC smoke test and CordaRPCOps) Added additional @CordaSerializable annotations. Updated documentation and referenced sample code. Added deprecation annotations. Re-added missing deprecation annotation. Hibernate debug logging is now configurable and disabled by default. Introduced common Sort attributes based on the node schemas. Completely removed NULL_HANDLING sort parameter as this is not supported in JPA. Revisited and fixed usage of @CordaSerializable. * Minor fix following rebase from master. * Remove blank line as per RP PR feedback request. * Minor Java documentation and example clean-up. * Disable BFT Notary Service tests. --- build.gradle | 2 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 55 + core/build.gradle | 1 + .../core/contracts/DummyLinearContract.kt | 60 + .../net/corda/core/messaging/CordaRPCOps.kt | 10 +- .../corda/core/node/CordaPluginRegistry.kt | 13 + .../kotlin/net/corda/core/node/ServiceHub.kt | 1 + .../net/corda/core/node/services/Services.kt | 180 +- .../core/node/services/vault/QueryCriteria.kt | 78 +- .../node/services/vault/QueryCriteriaUtils.kt | 225 ++- .../net/corda/core/schemas/CommonSchema.kt | 96 ++ .../core/schemas/DummyDealStateSchemaV1.kt | 25 + .../core/schemas/DummyLinearStateSchemaV1.kt | 49 + .../core/schemas/DummyLinearStateSchemaV2.kt | 29 + .../net/corda/core/schemas/PersistentTypes.kt | 7 +- .../core/schemas/requery/PersistentState.kt | 3 +- .../AttachmentSerializationTest.kt | 2 +- docs/source/vault-query.rst | 155 +- finance/build.gradle | 13 + .../net/corda/contracts/CommercialPaper.kt | 2 + .../{testing => }/DummyDealContract.kt | 25 +- .../net/corda/contracts/FinanceTypes.kt | 2 +- .../kotlin/net/corda/contracts/asset/Cash.kt | 2 + .../contracts/testing/DummyLinearContract.kt | 30 - .../corda/contracts/testing/VaultFiller.kt | 110 +- .../kotlin/net/corda/schemas/CashSchemaV1.kt | 8 +- .../corda/schemas/CommercialPaperSchemaV1.kt | 6 +- .../corda/contracts/DummyFungibleContract.kt | 132 ++ .../net/corda/schemas/SampleCashSchemaV1.kt | 38 + .../net/corda/schemas/SampleCashSchemaV2.kt | 37 + .../net/corda/schemas/SampleCashSchemaV3.kt | 45 + .../schemas/SampleCommercialPaperSchemaV1.kt | 51 + .../schemas/SampleCommercialPaperSchemaV2.kt | 50 + .../{ => requery}/AttachmentsSchema.kt | 2 +- .../schemas/{ => requery}/VaultSchema.kt | 18 +- .../services/vault/schemas/VaultSchemaTest.kt | 38 +- node/build.gradle | 6 + .../node/services/BFTNotaryServiceTests.kt | 3 + .../net/corda/node/internal/AbstractNode.kt | 11 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 5 +- .../corda/node/services/api/SchemaService.kt | 6 +- .../database/HibernateConfiguration.kt | 119 ++ .../persistence/NodeAttachmentService.kt | 4 +- .../node/services/schema/HibernateObserver.kt | 107 +- .../node/services/schema/NodeSchemaService.kt | 49 +- .../vault/HibernateQueryCriteriaParser.kt | 368 +++++ .../services/vault/HibernateVaultQueryImpl.kt | 148 ++ .../node/services/vault/NodeVaultService.kt | 33 +- .../corda/node/services/vault/VaultSchema.kt | 134 ++ .../services/vault/VaultQueryJavaTests.java | 273 ++-- .../net/corda/node/CordaRPCOpsImplTest.kt | 28 +- .../corda/node/messaging/AttachmentTests.kt | 2 +- .../node/services/MockServiceHubInternal.kt | 3 + .../database/HibernateConfigurationTest.kt | 698 ++++++++ .../database/RequeryConfigurationTest.kt | 62 +- .../persistence/NodeAttachmentStorageTest.kt | 2 +- .../services/schema/HibernateObserverTests.kt | 16 +- .../node/services/vault/VaultQueryTests.kt | 1453 ++++++++++++++--- .../node/services/vault/VaultWithCashTest.kt | 1 + .../net/corda/testing/node/MockServices.kt | 7 +- 60 files changed, 4355 insertions(+), 783 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt create mode 100644 core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt create mode 100644 core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt create mode 100644 core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt create mode 100644 core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt rename finance/src/main/kotlin/net/corda/contracts/{testing => }/DummyDealContract.kt (55%) delete mode 100644 finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt create mode 100644 finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt create mode 100644 finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt create mode 100644 finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt create mode 100644 finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt create mode 100644 finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt create mode 100644 finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt rename node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/{ => requery}/AttachmentsSchema.kt (87%) rename node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/{ => requery}/VaultSchema.kt (83%) create mode 100644 node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt diff --git a/build.gradle b/build.gradle index c975db734a..972ff7c346 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ buildscript { ext.hibernate_version = '5.2.6.Final' ext.h2_version = '1.4.194' ext.rxjava_version = '1.2.4' - ext.requery_version = '1.2.1' + ext.requery_version = '1.3.1' ext.dokka_version = '0.9.14' ext.eddsa_version = '0.2.0' diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 50d6b90d9e..8863b6c559 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -4,6 +4,7 @@ import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.notUsed +import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.POUNDS import net.corda.core.contracts.SWISS_FRANCS @@ -14,12 +15,18 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.messaging.startTrackedFlow +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.PageSpecification +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.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.sizedInputStreamAndHash import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.loggerFor import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess @@ -156,6 +163,54 @@ class StandaloneCordaRPClientTest { assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) } + @Test + fun `test vault track by`() { + val (vault, vaultUpdates) = rpcProxy.vaultTrackBy() + assertEquals(0, vault.totalStatesAvailable) + + var updateCount = 0 + vaultUpdates.subscribe { update -> + log.info("Vault>> FlowId=${update.flowId}") + ++updateCount + } + + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(timeout) + assertNotEquals(0, updateCount) + + // Check that this cash exists in the vault + val cashBalance = rpcProxy.getCashBalances() + log.info("Cash Balances: $cashBalance") + assertEquals(1, cashBalance.size) + assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) + } + + @Test + fun `test vault query by`() { + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(timeout) + + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(0, 10) + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC))) + + val queryResults = rpcProxy.vaultQueryBy(criteria, paging, sorting) + assertEquals(1, queryResults.totalStatesAvailable) + assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity) + + rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryIdentity).returnValue.getOrThrow() + + val moreResults = rpcProxy.vaultQueryBy(criteria, paging, sorting) + assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100 + + // Check that this cash exists in the vault + val cashBalance = rpcProxy.getCashBalances() + log.info("Cash Balances: $cashBalance") + assertEquals(1, cashBalance.size) + assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) + } private fun fetchNotaryIdentity(): Party { val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates() diff --git a/core/build.gradle b/core/build.gradle index 4f972786e7..e7dc4fa641 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'kotlin' +apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt new file mode 100644 index 0000000000..a2bffbe78d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/DummyLinearContract.kt @@ -0,0 +1,60 @@ +package net.corda.core.contracts + +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.crypto.SecureHash +import net.corda.core.crypto.containsAny +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.* +import java.security.PublicKey +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset + +class DummyLinearContract : Contract { + override val legalContractReference: SecureHash = SecureHash.sha256("Test") + + val clause: Clause = LinearState.ClauseVerifier() + override fun verify(tx: TransactionForContract) = verifyClause(tx, + FilterOn(clause, { states -> states.filterIsInstance() }), + emptyList()) + + data class State( + override val linearId: UniqueIdentifier = UniqueIdentifier(), + override val contract: Contract = DummyLinearContract(), + override val participants: List = listOf(), + val linearString: String = "ABC", + val linearNumber: Long = 123L, + val linearTimestamp: Instant = LocalDateTime.now().toInstant(ZoneOffset.UTC), + val linearBoolean: Boolean = true, + val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState { + + override fun isRelevant(ourKeys: Set): Boolean { + return participants.any { it.owningKey.containsAny(ourKeys) } + } + + override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( + externalId = linearId.externalId, + uuid = linearId.id, + linearString = linearString, + linearNumber = linearNumber, + linearTimestamp = linearTimestamp, + linearBoolean = linearBoolean + ) + is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState( + uid = linearId, + linearString = linearString, + linearNumber = linearNumber, + linearTimestamp = linearTimestamp, + linearBoolean = linearBoolean + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index ebdb291174..e58efb36f0 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -111,12 +111,12 @@ interface CordaRPCOps : RPCOps { // Java Helpers // DOCSTART VaultQueryAPIJavaHelpers - fun vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page = vaultQueryBy(criteria = criteria) - fun vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = vaultQueryBy(criteria, paging = paging) + fun vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page = vaultQueryBy(criteria) + fun vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = vaultQueryBy(criteria, paging) fun vaultQueryByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.Page = vaultQueryBy(criteria, sorting = sorting) - fun vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates = vaultTrackBy(criteria = criteria) - fun vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = vaultTrackBy(criteria, paging = paging) + fun vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates = vaultTrackBy(criteria) + fun vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = vaultTrackBy(criteria, paging) fun vaultTrackByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = vaultTrackBy(criteria, sorting = sorting) // DOCEND VaultQueryAPIJavaHelpers @@ -125,7 +125,7 @@ interface CordaRPCOps : RPCOps { */ @RPCReturnsObservables // TODO: Remove this from the interface - // @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())")) + @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())")) fun vaultAndUpdates(): Pair>, Observable> /** diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt index 14835e1e1f..89daa9939d 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -1,8 +1,12 @@ package net.corda.core.node import net.corda.core.messaging.CordaRPCOps +import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomization import java.util.function.Function +import net.corda.core.schemas.QueryableState +import net.corda.core.contracts.ContractState +import net.corda.core.node.services.VaultQueryService /** * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file @@ -46,4 +50,13 @@ abstract class CordaPluginRegistry { * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. */ open fun customizeSerialization(custom: SerializationCustomization): Boolean = false + + /** + * Optionally, custom schemas to be used for contract state persistence and vault custom querying + * + * For example, if you implement the [QueryableState] interface on a new [ContractState] + * it needs to be registered here if you wish to perform custom queries on schema entity attributes using the + * [VaultQueryService] API + */ + open val requiredSchemas: Set get() = emptySet() } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 0c0af907f8..13038dc558 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -38,6 +38,7 @@ interface ServicesForResolution { */ interface ServiceHub : ServicesForResolution { val vaultService: VaultService + val vaultQueryService: VaultQueryService val keyManagementService: KeyManagementService override val storageService: StorageService val networkMapCache: NetworkMapCache diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 7899a50094..daf2c2cfbe 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,6 +3,7 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash @@ -12,7 +13,6 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -22,6 +22,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import org.bouncycastle.cert.X509CertificateHolder import rx.Observable +import rx.subjects.PublishSubject import java.io.InputStream import java.security.PublicKey import java.security.cert.CertPath @@ -68,6 +69,15 @@ class Vault(val states: Iterable>) { /** Checks whether the update contains a state of the specified type. */ inline fun containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T } + /** Checks whether the update contains a state of the specified type and state status */ + fun containsType(clazz: Class, status: StateStatus) = + when(status) { + StateStatus.UNCONSUMED -> produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } + StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } + else -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } + || produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } + } + /** * Combine two updates into a single update with the combined inputs and outputs of the two updates but net * any outputs of the left-hand-side (this) that are consumed by the inputs of the right-hand-side (rhs). @@ -98,6 +108,7 @@ class Vault(val states: Iterable>) { val NoUpdate = Update(emptySet(), emptySet()) } + @CordaSerializable enum class StateStatus { UNCONSUMED, CONSUMED, ALL } @@ -114,9 +125,10 @@ class Vault(val states: Iterable>) { */ @CordaSerializable data class Page(val states: List>, - val statesMetadata: List, + val statesMetadata: List, val pageable: PageSpecification, - val totalStatesAvailable: Long) + val totalStatesAvailable: Int, + val stateTypes: StateStatus) @CordaSerializable data class StateMetadata(val ref: StateRef, @@ -130,7 +142,7 @@ class Vault(val states: Iterable>) { val lockUpdateTime: Instant?) @CordaSerializable - data class PageAndUpdates (val current: Vault.Page, val future: Observable? = null) + data class PageAndUpdates (val current: Vault.Page, val future: Observable) } /** @@ -160,6 +172,11 @@ interface VaultService { */ val updates: Observable + /** + * Enable creation of observables of updates. + */ + val updatesPublisher: PublishSubject + /** * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null (not present in map), not 0. @@ -171,57 +188,14 @@ interface VaultService { * first subscriber is registered so as to avoid racing with early updates. */ // TODO: Remove this from the interface - // @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())")) + @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())")) fun track(): Pair, Observable> - // DOCSTART VaultQueryAPI - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.Page] object containing the following: - * 1. states as a List of (page number and size defined by [PageSpecification]) - * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. - * 3. the [PageSpecification] used in the query - * 4. a total number of results available (for subsequent paging if necessary) - * - * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. - * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. - */ - fun queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.Page - /** - * Generic vault query function which takes a [QueryCriteria] object to define filters, - * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.PageAndUpdates] object containing - * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) - * 2) an [Observable] of [Vault.Update] - * - * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. - * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). - */ - fun trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates - // DOCEND VaultQueryAPI - - // Note: cannot apply @JvmOverloads to interfaces nor interface implementations - // Java Helpers - fun queryBy(): Vault.Page = queryBy() - fun queryBy(criteria: QueryCriteria): Vault.Page = queryBy(criteria) - fun queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = queryBy(criteria, paging) - fun queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page = queryBy(criteria, sorting) - - fun trackBy(): Vault.PageAndUpdates = trackBy() - fun trackBy(criteria: QueryCriteria): Vault.PageAndUpdates = trackBy(criteria) - fun trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = trackBy(criteria, paging) - fun trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = trackBy(criteria, Sort(emptySet())) - /** * Return unconsumed [ContractState]s for a given set of [StateRef]s */ // TODO: Remove this from the interface - // @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(stateRefs = listOf()))")) + @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(stateRefs = listOf()))")) fun statesForRefs(refs: List): Map?> /** @@ -300,7 +274,7 @@ interface VaultService { * Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true) */ // TODO: Remove this from the interface - // @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) + @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) fun states(clazzes: Set>, statuses: EnumSet, includeSoftLockedStates: Boolean = true): Iterable> // DOCEND VaultStatesQuery @@ -347,18 +321,18 @@ interface VaultService { } // TODO: Remove this from the interface -// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria())")) +@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria())")) inline fun VaultService.unconsumedStates(includeSoftLockedStates: Boolean = true): Iterable> = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED), includeSoftLockedStates) // TODO: Remove this from the interface -// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))")) +@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))")) inline fun VaultService.consumedStates(): Iterable> = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.CONSUMED)) /** Returns the [linearState] heads only when the type of the state would be considered an 'instanceof' the given type. */ // TODO: Remove this from the interface -// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(linearId = listOf()))")) +@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(linearId = listOf()))")) inline fun VaultService.linearHeadsOfType() = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED)) .associateBy { it.state.data.linearId }.mapValues { it.value } @@ -367,6 +341,106 @@ class StatesNotAvailableException(override val message: String?, override val ca override fun toString() = "Soft locking error: $message" } +interface VaultQueryService { + + // DOCSTART VaultQueryAPI + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.Page] object containing the following: + * 1. states as a List of (page number and size defined by [PageSpecification]) + * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. + * 3. the [PageSpecification] used in the query + * 4. a total number of results available (for subsequent paging if necessary) + * + * @throws VaultQueryException if the query cannot be executed for any reason + * (missing criteria or parsing error, invalid operator, unsupported query, underlying database error) + * + * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. + * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. + * Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting + */ + @Throws(VaultQueryException::class) + fun _queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet()), + contractType: Class): Vault.Page + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.PageAndUpdates] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * 2) an [Observable] of [Vault.Update] + * + * @throws VaultQueryException if the query cannot be executed for any reason + * + * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). + */ + @Throws(VaultQueryException::class) + fun _trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet()), + contractType: Class): Vault.PageAndUpdates + // DOCEND VaultQueryAPI + + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + fun queryBy(contractType: Class): Vault.Page = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + fun queryBy(contractType: Class, criteria: QueryCriteria): Vault.Page = _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page = _queryBy(criteria, paging, Sort(emptySet()), contractType) + fun queryBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page = _queryBy(criteria, PageSpecification(), sorting, contractType) + fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page = _queryBy(criteria, paging, sorting, contractType) + + fun trackBy(contractType: Class): Vault.Page = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + fun trackBy(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates = _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = _trackBy(criteria, paging, Sort(emptySet()), contractType) + fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = _trackBy(criteria, PageSpecification(), sorting, contractType) + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates = _trackBy(criteria, paging, sorting, contractType) +} + +inline fun VaultQueryService.queryBy(): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.queryBy(criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, T::class.java) +} + +inline fun VaultQueryService.trackBy(): Vault.PageAndUpdates { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.trackBy(criteria: QueryCriteria): Vault.PageAndUpdates { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) +} + +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + return _trackBy(criteria, PageSpecification(), sorting, T::class.java) +} + +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { + return _trackBy(criteria, paging, sorting, T::class.java) +} + +class VaultQueryException(description: String) : FlowException("$description") + /** * The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example, * call out to a hardware security module that enforces various auditing and frequency-of-use requirements. diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 815b5e2465..9dd0e974d1 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -3,14 +3,17 @@ package net.corda.core.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.QueryCriteria.AndComposition import net.corda.core.node.services.vault.QueryCriteria.OrComposition +import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* +import javax.persistence.criteria.Predicate /** * Indexing assumptions: @@ -18,27 +21,39 @@ import java.util.* */ @CordaSerializable sealed class QueryCriteria { + abstract fun visit(parser: IQueryCriteriaParser): Collection + + @CordaSerializable + data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate) /** * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] */ data class VaultQueryCriteria @JvmOverloads constructor ( val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - val stateRefs: List? = null, val contractStateTypes: Set>? = null, + val stateRefs: List? = null, val notaryName: List? = null, - val includeSoftlockedStates: Boolean? = true, - val timeCondition: Logical>? = null, - val participantIdentities: List? = null) : QueryCriteria() + val includeSoftlockedStates: Boolean = true, + val timeCondition: TimeCondition? = null) : QueryCriteria() { + + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this) + } + } /** * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] */ data class LinearStateQueryCriteria @JvmOverloads constructor( + val participants: List? = null, val linearId: List? = null, - val latestOnly: Boolean? = true, - val dealRef: List? = null, - val dealPartyName: List? = null) : QueryCriteria() + val dealRef: List? = null) : QueryCriteria() { + + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this) + } + } /** * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] @@ -48,13 +63,16 @@ sealed class QueryCriteria { * [Commodity] as used in [CommodityContract] state */ data class FungibleAssetQueryCriteria @JvmOverloads constructor( - val ownerIdentity: List? = null, - val quantity: Logical<*,Long>? = null, - val tokenType: List>? = null, - val tokenValue: List? = null, - val issuerPartyName: List? = null, - val issuerRef: List? = null, - val exitKeyIdentity: List? = null) : QueryCriteria() + val participants: List? = null, + val owner: List? = null, + val quantity: ColumnPredicate? = null, + val issuerPartyName: List? = null, + val issuerRef: List? = null) : QueryCriteria() { + + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this) + } + } /** * VaultCustomQueryCriteria: provides query by custom attributes defined in a contracts @@ -62,16 +80,28 @@ sealed class QueryCriteria { * (see Persistence documentation for more information) * * Params - * [indexExpression] refers to a (composable) JPA Query like WHERE expression clauses of the form: - * [JPA entityAttributeName] [Operand] [Value] + * [expression] refers to a (composable) type safe [CriteriaExpression] * * Refer to [CommercialPaper.State] for a concrete example. */ - data class VaultCustomQueryCriteria(val indexExpression: Logical? = null) : QueryCriteria() + data class VaultCustomQueryCriteria(val expression: CriteriaExpression) : QueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this) + } + } // enable composition of [QueryCriteria] - data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() - data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() + data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseAnd(this.a, this.b) + } + } + + data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseOr(this.a, this.b) + } + } // timestamps stored in the vault states table [VaultSchema.VaultStates] @CordaSerializable @@ -81,5 +111,15 @@ sealed class QueryCriteria { } } +interface IQueryCriteriaParser { + fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection + fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection + fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection + fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection +} + infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index fc53fc2d02..4dcd9e6ed9 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -1,61 +1,93 @@ package net.corda.core.node.services.vault +import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable +import java.lang.reflect.Field +import kotlin.reflect.KProperty1 +import kotlin.reflect.jvm.javaField @CordaSerializable -enum class Operator { +enum class BinaryLogicalOperator { AND, - OR, + OR +} + +@CordaSerializable +enum class EqualityComparisonOperator { EQUAL, - NOT_EQUAL, + NOT_EQUAL +} + +@CordaSerializable +enum class BinaryComparisonOperator { LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, - IN, - NOT_IN, - LIKE, - NOT_LIKE, - BETWEEN, +} + +@CordaSerializable +enum class NullOperator { IS_NULL, NOT_NULL } -interface Condition { - val leftOperand: L - val operator: Operator - val rightOperand: R -} - -interface AndOr { - infix fun and(condition: Condition): Q - infix fun or(condition: Condition): Q +@CordaSerializable +enum class LikenessOperator { + LIKE, + NOT_LIKE } @CordaSerializable -sealed class Logical : Condition, AndOr> - -class LogicalExpression(leftOperand: L, - operator: Operator, - rightOperand: R? = null) : Logical() { - init { - if (rightOperand == null) { - check(operator in setOf(Operator.NOT_NULL, Operator.IS_NULL), - { "Must use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is null"} ) - } - else { - check(operator !in setOf(Operator.NOT_NULL, Operator.IS_NULL), - { "Cannot use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is not null"} ) - } - } - override fun and(condition: Condition): Logical<*, *> = LogicalExpression(this, Operator.AND, condition) - override fun or(condition: Condition): Logical<*, *> = LogicalExpression(this, Operator.OR, condition) - - override val operator: Operator = operator - override val rightOperand: R = rightOperand as R - override val leftOperand: L = leftOperand +enum class CollectionOperator { + IN, + NOT_IN } +@CordaSerializable +sealed class CriteriaExpression { + data class BinaryLogical(val left: CriteriaExpression, val right: CriteriaExpression, val operator: BinaryLogicalOperator) : CriteriaExpression() + data class Not(val expression: CriteriaExpression) : CriteriaExpression() + data class ColumnPredicateExpression(val column: Column, val predicate: ColumnPredicate) : CriteriaExpression() +} + +@CordaSerializable +sealed class Column { + data class Java(val field: Field) : Column() + data class Kotlin(val property: KProperty1) : Column() +} + +@CordaSerializable +sealed class ColumnPredicate { + data class EqualityComparison(val operator: EqualityComparisonOperator, val rightLiteral: C) : ColumnPredicate() + data class BinaryComparison>(val operator: BinaryComparisonOperator, val rightLiteral: C) : ColumnPredicate() + data class Likeness(val operator: LikenessOperator, val rightLiteral: String) : ColumnPredicate() + data class CollectionExpression(val operator: CollectionOperator, val rightLiteral: Collection) : ColumnPredicate() + data class Between>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate() + data class NullExpression(val operator: NullOperator) : ColumnPredicate() +} + +fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression): Class { + return when (expression) { + is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left) + is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression) + is CriteriaExpression.ColumnPredicateExpression -> resolveEnclosingObjectFromColumn(expression.column) + } +} + +fun resolveEnclosingObjectFromColumn(column: Column): Class { + return when (column) { + is Column.Java -> column.field.declaringClass as Class + is Column.Kotlin -> column.property.javaField!!.declaringClass as Class + } +} + +fun getColumnName(column: Column): String { + return when (column) { + is Column.Java -> column.field.name + is Column.Kotlin -> column.property.name + } +} /** * Pagination and Ordering @@ -70,21 +102,21 @@ class LogicalExpression(leftOperand: L, * paging and sorting capability: * https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html */ -val DEFAULT_PAGE_NUM = 0L -val DEFAULT_PAGE_SIZE = 200L +val DEFAULT_PAGE_NUM = 0 +val DEFAULT_PAGE_SIZE = 200 /** * Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations) * Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE]. */ -val MAX_PAGE_SIZE = 512L +val MAX_PAGE_SIZE = 512 /** * PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to - * [DEFAULT_PAGE_SIZE] with a maximum page size of [DEFAULT_PAGE_SIZE] + * [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE] */ @CordaSerializable -data class PageSpecification(val pageNumber: Long = DEFAULT_PAGE_NUM, val pageSize: Long = DEFAULT_PAGE_SIZE) +data class PageSpecification(val pageNumber: Int = DEFAULT_PAGE_NUM, val pageSize: Int = DEFAULT_PAGE_SIZE) /** * Sort allows specification of a set of entity attribute names and their associated directionality @@ -97,17 +129,106 @@ data class Sort(val columns: Collection) { ASC, DESC } + @CordaSerializable - enum class NullHandling { - NULLS_FIRST, - NULLS_LAST + interface Attribute + + enum class VaultStateAttribute(val columnName: String) : Attribute { + /** Vault States */ + NOTARY_NAME("notaryName"), + CONTRACT_TYPE("contractStateClassName"), + STATE_STATUS("stateStatus"), + RECORDED_TIME("recordedTime"), + CONSUMED_TIME("consumedTime"), + LOCK_ID("lockId"), } - /** - * [columnName] should reference a persisted entity attribute name as defined by the associated mapped schema - * (for example, [VaultSchema.VaultStates::txId.name]) - */ + + enum class LinearStateAttribute(val columnName: String) : Attribute { + /** Vault Linear States */ + UUID("uuid"), + EXTERNAL_ID("externalId"), + DEAL_REFERENCE("dealReference"), + } + + enum class FungibleStateAttribute(val columnName: String) : Attribute { + /** Vault Fungible States */ + QUANTITY("quantity"), + ISSUER_REF("issuerRef") + } + @CordaSerializable - data class SortColumn(val columnName: String, val direction: Sort.Direction = Sort.Direction.ASC, - val nullHandling: Sort.NullHandling = if (direction == Sort.Direction.ASC) Sort.NullHandling.NULLS_LAST else Sort.NullHandling.NULLS_FIRST) + data class SortColumn( + val sortAttribute: SortAttribute, + val direction: Sort.Direction = Sort.Direction.ASC) } +@CordaSerializable +sealed class SortAttribute { + /** + * [sortAttribute] refers to common table attributes defined in the node schemas: + * VaultState, VaultLinearStates, VaultFungibleStates + */ + data class Standard(val attribute: Sort.Attribute) : SortAttribute() + + /** + * [entityStateClass] should reference a persistent state entity + * [entityStateColumnName] should reference an entity attribute name as defined by the associated mapped schema + * (for example, [CashSchemaV1.PersistentCashState::currency.name]) + */ + data class Custom(val entityStateClass: Class, + val entityStateColumnName: String) : SortAttribute() +} + +@CordaSerializable +object Builder { + + fun > compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value) + + fun KProperty1.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate) + fun Field.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Java(this), predicate) + + fun > KProperty1.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) + fun > Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) + + fun KProperty1.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + fun KProperty1.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + fun > KProperty1.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + fun > KProperty1.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > KProperty1.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + fun > KProperty1.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > KProperty1.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + + fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + + fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) + fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) + fun > lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value) + fun > lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value) + fun > greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > between(from: R, to: R) = ColumnPredicate.Between(from, to) + fun > `in`(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection) + fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) + + fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + + fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + fun KProperty1.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) +} + +inline fun builder(block: Builder.() -> A) = block(Builder) diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt new file mode 100644 index 0000000000..1fe600300a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -0,0 +1,96 @@ +package net.corda.node.services.vault.schemas.jpa + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.StatePersistable +import java.util.* +import javax.persistence.* + +/** + * JPA representation of the common schema entities + */ +object CommonSchema + +/** + * First version of the Vault ORM schema + */ +object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) { + + @MappedSuperclass + open class LinearState( + /** + * Represents a [LinearState] [UniqueIdentifier] + */ + @Column(name = "external_id") + var externalId: String?, + + @Column(name = "uuid", nullable = false) + var uuid: UUID + + ) : PersistentState() { + constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id) + } + + @MappedSuperclass + open class FungibleState( + /** [ContractState] attributes */ + @OneToMany(cascade = arrayOf(CascadeType.ALL)) + var participants: Set, + + /** [OwnableState] attributes */ + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var ownerKey: CommonSchemaV1.Party, + + /** [FungibleAsset] attributes + * + * Note: the underlying Product being issued must be modelled into the + * custom contract itself (eg. see currency in Cash contract state) + */ + + /** Amount attributes */ + @Column(name = "quantity") + var quantity: Long, + + /** Issuer attributes */ + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var issuerParty: CommonSchemaV1.Party, + + @Column(name = "issuer_reference") + var issuerRef: ByteArray + ) : PersistentState() { + constructor(_participants: Set, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray) + : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), + ownerKey = CommonSchemaV1.Party(_ownerKey), + quantity = _quantity, + issuerParty = CommonSchemaV1.Party(_issuerParty), + issuerRef = _issuerRef) + } + + /** + * Party entity (to be replaced by referencing final Identity Schema) + */ + @Entity + @Table(name = "vault_party", + indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name"))) + class Party( + @Id + @GeneratedValue + @Column(name = "party_id") + var id: Int, + + /** + * [Party] attributes + */ + @Column(name = "party_name") + var name: String, + + @Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?) + var key: String + ) { + constructor(party: net.corda.core.identity.AbstractParty) + : this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String()) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt new file mode 100644 index 0000000000..e26382143f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt @@ -0,0 +1,25 @@ +package net.corda.core.schemas + +/** + * An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version). + */ +object DummyDealStateSchema + +/** + * First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood + * at the time of writing. + */ +object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) { + @javax.persistence.Entity + @javax.persistence.Table(name = "dummy_deal_states") + class PersistentDummyDealState( + + @javax.persistence.Column(name = "deal_reference") + var dealReference: String, + + /** parent attributes */ + @javax.persistence.Transient + val uid: net.corda.core.contracts.UniqueIdentifier + + ) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid) +} diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt new file mode 100644 index 0000000000..a0c3c82649 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt @@ -0,0 +1,49 @@ +package net.corda.core.schemas + +import java.time.Instant +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table + +/** + * An object used to fully qualify the [DummyLinearStateSchema] family name (i.e. independent of version). + */ +object DummyLinearStateSchema + +/** + * First version of a cash contract ORM schema that maps all fields of the [DummyLinearState] contract state as it stood + * at the time of writing. + */ +object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyLinearState::class.java)) { + @Entity + @Table(name = "dummy_linear_states", + indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), + Index(name = "uuid_idx", columnList = "uuid"))) + class PersistentDummyLinearState( + /** + * UniqueIdentifier + */ + @Column(name = "external_id") + var externalId: String?, + + @Column(name = "uuid", nullable = false) + var uuid: UUID, + + /** + * Dummy attributes + */ + @Column(name = "linear_string") + var linearString: String, + + @Column(name = "linear_number") + var linearNumber: Long, + + @Column(name = "linear_timestamp") + var linearTimestamp: Instant, + + @Column(name = "linear_boolean") + var linearBoolean: Boolean + ) : PersistentState() +} diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt b/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt new file mode 100644 index 0000000000..247c061a0f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt @@ -0,0 +1,29 @@ +package net.corda.core.schemas + +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +/** + * Second version of a cash contract ORM schema that extends the common + * [VaultLinearState] abstract schema + */ +object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, + mappedTypes = listOf(PersistentDummyLinearState::class.java)) { + @Entity + @Table(name = "dummy_linear_states_v2") + class PersistentDummyLinearState( + @Column(name = "linear_string") var linearString: String, + + @Column(name = "linear_number") var linearNumber: Long, + + @Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant, + + @Column(name = "linear_boolean") var linearBoolean: Boolean, + + /** parent attributes */ + @Transient + val uid: net.corda.core.contracts.UniqueIdentifier + ) : CommonSchemaV1.LinearState(uid = uid) +} diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index 514c3384bf..b615e75b64 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -49,7 +49,7 @@ open class MappedSchema(schemaFamily: Class<*>, * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). */ -@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : Persistable +@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable /** * Embedded [StateRef] representation used in state mapping. @@ -64,3 +64,8 @@ data class PersistentStateRef( ) : Serializable { constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index) } + +/** + * Marker interface to denote a persistable Corda state entity that will always have a transaction id and index + */ +interface StatePersistable : Persistable \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt index 45db4afe24..787b681807 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt @@ -4,6 +4,7 @@ import io.requery.Key import io.requery.Persistable import io.requery.Superclass import net.corda.core.contracts.StateRef +import net.corda.core.schemas.StatePersistable import javax.persistence.Column @@ -14,7 +15,7 @@ object Requery { */ // TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt // once we cut-over all existing Hibernate ContractState persistence to Requery - @Superclass interface PersistentState : Persistable { + @Superclass interface PersistentState : StatePersistable { @get:Key @get:Column(name = "transaction_id", length = 64) var txId: String diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 297304d9d9..9756ba57fb 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -16,7 +16,7 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.persistence.schemas.AttachmentEntity +import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.statemachine.SessionInit import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork diff --git a/docs/source/vault-query.rst b/docs/source/vault-query.rst index 2cbe5c31e6..697fee22f4 100644 --- a/docs/source/vault-query.rst +++ b/docs/source/vault-query.rst @@ -9,7 +9,7 @@ Corda provides a number of flexible query mechanisms for accessing the Vault: - custom JPA_/JPQL_ queries - custom 3rd party Data Access frameworks such as `Spring Data `_ -The majority of query requirements can be satified by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows: +The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultQueryService`` for use directly by flows: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/Services.kt :language: kotlin @@ -39,52 +39,64 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd - Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``) - Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``) + +.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) -Simple pagination (page number and size) and sorting (directional ordering, null handling, custom property sort) is also specifiable. -Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC, nullHandling = NULLS_LAST). +Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. +Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of logical operators. There are four implementations of this interface which can be chained together to define advanced filters. +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). + +There are four implementations of this interface which can be chained together to define advanced filters. 1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). - 2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). + 2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: Contract states that extend the ``FungibleAsset`` interface now automatically persist associated state attributes. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. - 3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo) + 3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s). - .. note:: Contract states that extend the ``LinearState`` or ``DealState`` interfaces now automatically persist associated state attributes. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. - 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the Persistence_ documentation and associated examples. - Custom criteria expressions are expressed as JPA Query like WHERE clauses as follows: [JPA entityAttributeName] [Operand] [Value] + 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the api-persistence_ documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. - An example is illustrated here: + .. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`) + + An example is illustrated here: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin - :start-after: DOCSTART VaultQueryExample16 - :end-before: DOCEND VaultQueryExample16 + :start-after: DOCSTART VaultQueryExample20 + :end-before: DOCEND VaultQueryExample20 All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. -Additional notes - - .. note:: Custom contract states that implement the ``Queryable`` interface may now extend the ``FungiblePersistentState``, ``LinearPersistentState`` or ``DealPersistentState`` classes when implementing their ``MappedSchema``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset``, ``LinearState`` and ``DealState`` attributes. +.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. -The Vault Query API leverages the rich semantics of the underlying Requery_ persistence framework adopted by Corda. +.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). -.. _Requery: https://github.com/requery/requery/wiki -.. _Persistence: https://docs.corda.net/persistence.html +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based Persistence_ framework adopted by Corda. + +.. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ +.. _Persistence: https://docs.corda.net/api-persistence.html .. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. -.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. +.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types. -.. note:: Current queries by ``Party`` specify only a party name as the underlying identity framework is being re-designed. In future it may be possible to query on specific elements of a parties identity such as a ``CompositeKey`` hierarchy (parent and child nodes, weightings). +An example of a custom query in Java is illustrated here: + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample3 + :end-before: DOCEND VaultJavaQueryExample3 + +.. note:: Current queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not have an associated X500Name, then no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. Ongoing design work on identity manangement is likely to enhance identity based queries (including composite key criteria selection). Example usage ------------- @@ -94,7 +106,7 @@ Kotlin **General snapshot queries using** ``VaultQueryCriteria`` -Query for all unconsumed states: +Query for all unconsumed states (simplest query possible): .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin @@ -122,6 +134,8 @@ Query for unconsumed states for a given notary: :start-after: DOCSTART VaultQueryExample4 :end-before: DOCEND VaultQueryExample4 +.. note:: We are using the notaries X500Name as our search identifier. + Query for unconsumed states for a given set of participants: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -136,13 +150,17 @@ Query for unconsumed states recorded between two time intervals: :start-after: DOCSTART VaultQueryExample6 :end-before: DOCEND VaultQueryExample6 -Query for all states with pagination specification: +.. note:: This example illustrates usage of a Between ColumnPredicate. + +Query for all states with pagination specification (10 results per page): .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample7 :end-before: DOCEND VaultQueryExample7 +.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly. + **LinearState and DealState queries using** ``LinearStateQueryCriteria`` Query for unconsumed linear states for given linear ids: @@ -152,12 +170,12 @@ Query for unconsumed linear states for given linear ids: :start-after: DOCSTART VaultQueryExample8 :end-before: DOCEND VaultQueryExample8 -.. note:: This example was previously executed using the deprecated extension method: +This example was previously executed using the deprecated extension method: - .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt - :language: kotlin - :start-after: DOCSTART VaultDeprecatedQueryExample1 - :end-before: DOCEND VaultDeprecatedQueryExample1 +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultDeprecatedQueryExample1 + :end-before: DOCEND VaultDeprecatedQueryExample1 Query for all linear states associated with a linear id: @@ -166,12 +184,12 @@ Query for all linear states associated with a linear id: :start-after: DOCSTART VaultQueryExample9 :end-before: DOCEND VaultQueryExample9 -.. note:: This example was previously executed using the deprecated method: +This example was previously executed using the deprecated method: - .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt - :language: kotlin - :start-after: DOCSTART VaultDeprecatedQueryExample2 - :end-before: DOCEND VaultDeprecatedQueryExample2 +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultDeprecatedQueryExample2 + :end-before: DOCEND VaultDeprecatedQueryExample2 Query for unconsumed deal states with deals references: @@ -196,13 +214,15 @@ Query for fungible assets for a given currency: :start-after: DOCSTART VaultQueryExample12 :end-before: DOCEND VaultQueryExample12 -Query for fungible assets for a given currency and minimum quantity: +Query for fungible assets for a minimum quantity: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample13 :end-before: DOCEND VaultQueryExample13 +.. note:: This example uses the builder DSL. + Query for fungible assets for a specifc issuer party: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -210,45 +230,84 @@ Query for fungible assets for a specifc issuer party: :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 -Query for consumed fungible assets with a specific exit key: +**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. + +Track unconsumed cash states: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin :start-after: DOCSTART VaultQueryExample15 :end-before: DOCEND VaultQueryExample15 +Track unconsumed linear states: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample16 + :end-before: DOCEND VaultQueryExample16 + +.. note:: This will return both Deal and Linear states. + +Track unconsumed deal states: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample17 + :end-before: DOCEND VaultQueryExample17 + +.. note:: This will return only Deal states. + Java examples ^^^^^^^^^^^^^ -Query for all consumed contract states: +Query for all unconsumed linear states: + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample0 + :end-before: DOCEND VaultJavaQueryExample0 + +This example was previously executed using the deprecated method: + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultDeprecatedJavaQueryExample0 + :end-before: DOCEND VaultDeprecatedJavaQueryExample0 + +Query for all consumed cash states: .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java :language: java :start-after: DOCSTART VaultJavaQueryExample1 :end-before: DOCEND VaultJavaQueryExample1 -.. note:: This example was previously executed using the deprecated method: +This example was previously executed using the deprecated method: - .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java - :language: java - :start-after: DOCSTART VaultDeprecatedJavaQueryExample1 - :end-before: DOCEND VaultDeprecatedJavaQueryExample1 +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultDeprecatedJavaQueryExample1 + :end-before: DOCEND VaultDeprecatedJavaQueryExample1 -Query for all deal states: +Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier: .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java :language: java :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 -.. note:: This example was previously executed using the deprecated method: +Track unconsumed cash states: - .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java - :language: java - :start-after: DOCSTART VaultDeprecatedJavaQueryExample2 - :end-before: DOCEND VaultDeprecatedJavaQueryExample2 +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample4 + :end-before: DOCEND VaultJavaQueryExample4 -**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. +Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier): + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample4 + :end-before: DOCEND VaultJavaQueryExample4 Other use case scenarios ------------------------ diff --git a/finance/build.gradle b/finance/build.gradle index 6b9870ed39..6a554c33ec 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -30,6 +30,19 @@ configurations.testCompile { exclude group: 'javassist', module: 'javassist' } +configurations { + testArtifacts.extendsFrom testRuntime +} + +task testJar(type: Jar) { + classifier "tests" + from sourceSets.test.output +} + +artifacts { + testArtifacts testJar +} + jar { baseName 'corda-finance' } diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 5f95d95aec..0bfeb4ea50 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -84,6 +84,7 @@ class CommercialPaper : Contract { // DOCSTART VaultIndexedQueryCriteria /** Object Relational Mapping support. */ override fun supportedSchemas(): Iterable = listOf(CommercialPaperSchemaV1) + /** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */ /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { @@ -98,6 +99,7 @@ class CommercialPaper : Contract { faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(), faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes ) + /** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */ else -> throw IllegalArgumentException("Unrecognised schema $schema") } } diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt b/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt similarity index 55% rename from finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt rename to finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt index 5158b85eb4..04ca026671 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/DummyDealContract.kt @@ -1,13 +1,15 @@ -package net.corda.contracts.testing +package net.corda.contracts -import net.corda.contracts.DealState import net.corda.core.contracts.Contract import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.containsAny +import net.corda.core.crypto.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.schemas.DummyDealStateSchemaV1 +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -20,7 +22,8 @@ class DummyDealContract : Contract { override val contract: Contract = DummyDealContract(), override val participants: List = listOf(), override val linearId: UniqueIdentifier = UniqueIdentifier(), - override val ref: String) : DealState { + override val ref: String) : DealState, QueryableState + { override fun isRelevant(ourKeys: Set): Boolean { return participants.any { it.owningKey.containsAny(ourKeys) } } @@ -28,5 +31,17 @@ class DummyDealContract : Contract { override fun generateAgreement(notary: Party): TransactionBuilder { throw UnsupportedOperationException("not implemented") } + + override fun supportedSchemas(): Iterable = listOf(DummyDealStateSchemaV1) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is DummyDealStateSchemaV1 -> DummyDealStateSchemaV1.PersistentDummyDealState( + uid = linearId, + dealReference = ref + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } } } diff --git a/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt index 2968cbf7d1..82b1ff1a41 100644 --- a/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt @@ -410,7 +410,7 @@ interface DealState : LinearState { } // TODO: Remove this from the interface -// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf()))")) +@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf()))")) inline fun VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType().values.filter { it.state.data.participants.any { it == party } } 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 0abf2f3bce..62458b59da 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -115,12 +115,14 @@ class Cash : OnLedgerAsset() { issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), issuerRef = this.amount.token.issuer.reference.bytes ) + /** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ else -> throw IllegalArgumentException("Unrecognised schema $schema") } } /** Object Relational Mapping support. */ override fun supportedSchemas(): Iterable = listOf(CashSchemaV1) + /** Additional used schemas would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ } // DOCEND 1 diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt b/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt deleted file mode 100644 index 6809dec4fd..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.contracts.testing - -import net.corda.core.contracts.* -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.crypto.SecureHash -import net.corda.core.crypto.containsAny -import net.corda.core.identity.AbstractParty -import java.security.PublicKey - -class DummyLinearContract : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("Test") - - val clause: Clause = LinearState.ClauseVerifier() - override fun verify(tx: TransactionForContract) = verifyClause(tx, - FilterOn(clause, { states -> states.filterIsInstance() }), - emptyList()) - - data class State( - override val linearId: UniqueIdentifier = UniqueIdentifier(), - override val contract: Contract = DummyLinearContract(), - override val participants: List = listOf(), - val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState { - - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } - } -} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt index d935646ca0..b7fdd8c06f 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt @@ -2,10 +2,10 @@ package net.corda.contracts.testing +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.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.contracts.DummyDealContract +import net.corda.contracts.asset.* import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -14,22 +14,25 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.CHARLIE import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY import java.security.KeyPair import java.security.PublicKey +import java.time.Instant +import java.time.Instant.now import java.util.* @JvmOverloads fun ServiceHub.fillWithSomeTestDeals(dealIds: List, participants: List = emptyList()) : Vault { - val freshKey = keyManagementService.freshKey() - val recipient = AnonymousParty(freshKey) + val myKey: PublicKey = myInfo.legalIdentity.owningKey + val me = AnonymousParty(myKey) val transactions: List = dealIds.map { // Issue a deal state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(recipient))) + addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(me))) signWith(DUMMY_NOTARY_KEY) } return@map signInitialTransaction(dummyIssue) @@ -47,15 +50,24 @@ fun ServiceHub.fillWithSomeTestDeals(dealIds: List, @JvmOverloads fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, - uid: UniqueIdentifier = UniqueIdentifier(), - participants: List = emptyList()) : Vault { - val freshKey = keyManagementService.freshKey() - val recipient = AnonymousParty(freshKey) + externalId: String? = null, + participants: List = emptyList(), + linearString: String = "", + linearNumber: Long = 0L, + linearBoolean: Boolean = false, + linearTimestamp: Instant = now()) : Vault { + val myKey: PublicKey = myInfo.legalIdentity.owningKey + val me = AnonymousParty(myKey) val transactions: List = (1..numberToCreate).map { // Issue a Linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = uid, participants = participants.plus(recipient))) + addOutputState(DummyLinearContract.State(linearId = UniqueIdentifier(externalId), + participants = participants.plus(me), + linearString = linearString, + linearNumber = linearNumber, + linearBoolean = linearBoolean, + linearTimestamp = linearTimestamp)) signWith(DUMMY_NOTARY_KEY) } @@ -116,6 +128,27 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, return Vault(states) } +// TODO: need to make all FungibleAsset commands (issue, move, exit) generic +fun ServiceHub.fillWithSomeTestCommodity(amount: Amount, + outputNotary: Party = DUMMY_NOTARY, + ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), + ownedBy: AbstractParty? = null, + issuedBy: PartyAndReference = DUMMY_OBLIGATION_ISSUER.ref(1), + issuerKey: KeyPair = DUMMY_OBLIGATION_ISSUER_KEY): Vault { + val myKey: PublicKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey + val me = AnonymousParty(myKey) + + val commodity = CommodityContract() + val issuance = TransactionType.General.Builder(null as Party?) + commodity.generateIssue(issuance, Amount(amount.quantity, Issued(issuedBy.copy(reference = ref), amount.token)), me, outputNotary) + issuance.signWith(issuerKey) + val transaction = issuance.toSignedTransaction(true) + + recordTransactions(transaction) + + return Vault(setOf(transaction.tx.outRef(0))) +} + fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray { val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt() val baseSize = howMuch.quantity / numSlots @@ -143,3 +176,58 @@ fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, return amounts } + +fun ServiceHub.consume(states: List>) { + // Create a txn consuming different contract types + states.forEach { + val consumedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addInputState(it) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + recordTransactions(consumedTx) + } +} + +fun ServiceHub.consumeAndProduce(stateAndRef: StateAndRef): StateAndRef { + // Create a txn consuming different contract types + val consumedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addInputState(stateAndRef) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + recordTransactions(consumedTx) + + // Create a txn consuming different contract types + val producedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId, + participants = stateAndRef.state.data.participants)) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + recordTransactions(producedTx) + + return producedTx.tx.outRef(0) +} + +fun ServiceHub.consumeAndProduce(states: List>) { + states.forEach { + consumeAndProduce(it) + } +} + +fun ServiceHub.consumeDeals(dealStates: List>) = consume(dealStates) +fun ServiceHub.consumeLinearStates(linearStates: List>) = consume(linearStates) +fun ServiceHub.evolveLinearStates(linearStates: List>) = consumeAndProduce(linearStates) +fun ServiceHub.evolveLinearState(linearState: StateAndRef) : StateAndRef = consumeAndProduce(linearState) + +@JvmOverloads +fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE) { + // A tx that spends our money. + val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { + vaultService.generateSpend(this, amount, to) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction(checkSufficientSignatures = false) + + recordTransactions(spendTX) +} diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index 27b4b3aed6..05bc37c1c3 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -2,10 +2,8 @@ package net.corda.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Index -import javax.persistence.Table +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.* /** * An object used to fully qualify the [CashSchema] family name (i.e. independent of version). @@ -18,7 +16,7 @@ object CashSchema */ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity - @Table(name = "cash_states", + @Table(name = "contract_cash_states", indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( diff --git a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt index 3a1fcb8a21..2b24f6be8b 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt @@ -20,9 +20,9 @@ object CommercialPaperSchema object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states", - indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), - Index(name = "maturity_idx", columnList = "maturity_instant"), - Index(name = "face_value_idx", columnList = "face_value"))) + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt new file mode 100644 index 0000000000..e73cce41ce --- /dev/null +++ b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt @@ -0,0 +1,132 @@ +package net.corda.contracts.asset + +import net.corda.contracts.clause.AbstractConserveAmount +import net.corda.contracts.clause.AbstractIssue +import net.corda.contracts.clause.NoZeroSizedOutputs +import net.corda.core.contracts.* +import net.corda.core.contracts.clauses.AllOf +import net.corda.core.contracts.clauses.FirstOf +import net.corda.core.contracts.clauses.GroupClauseVerifier +import net.corda.core.contracts.clauses.verifyClause +import net.corda.core.crypto.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.Emoji +import net.corda.schemas.SampleCashSchemaV1 +import net.corda.schemas.SampleCashSchemaV2 +import net.corda.schemas.SampleCashSchemaV3 +import java.util.* + +class DummyFungibleContract : OnLedgerAsset() { + override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") + + override fun extractCommands(commands: Collection>): List> + = commands.select() + + interface Clauses { + class Group : GroupClauseVerifier>(AllOf>( + NoZeroSizedOutputs(), + FirstOf>( + Issue(), + ConserveAmount()) + ) + ) { + override fun groupStates(tx: TransactionForContract): List>> + = tx.groupStates> { it.amount.token } + } + + class Issue : AbstractIssue( + sum = { sumCash() }, + sumOrZero = { sumCashOrZero(it) } + ) { + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) + } + + @CordaSerializable + class ConserveAmount : AbstractConserveAmount() + } + + data class State( + override val amount: Amount>, + + override val owner: AbstractParty + ) : FungibleAsset, QueryableState { + constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + + override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey) + override val contract = CASH_PROGRAM_ID + override val participants = listOf(owner) + + override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset + = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) + + override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" + + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) + + /** Object Relational Mapping support. */ + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is SampleCashSchemaV1 -> SampleCashSchemaV1.PersistentCashState( + owner = this.owner.owningKey.toBase58String(), + pennies = this.amount.quantity, + currency = this.amount.token.product.currencyCode, + issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), + issuerRef = this.amount.token.issuer.reference.bytes + ) + is SampleCashSchemaV2 -> SampleCashSchemaV2.PersistentCashState( + _participants = this.participants.toSet(), + _owner = this.owner, + _quantity = this.amount.quantity, + currency = this.amount.token.product.currencyCode, + _issuerParty = this.amount.token.issuer.party, + _issuerRef = this.amount.token.issuer.reference.bytes + ) + is SampleCashSchemaV3 -> SampleCashSchemaV3.PersistentCashState( + _participants = this.participants.toSet(), + _owner = this.owner, + _quantity = this.amount.quantity, + _currency = this.amount.token.product.currencyCode, + _issuerParty = this.amount.token.issuer.party, + _issuerRef = this.amount.token.issuer.reference.bytes + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + + /** Object Relational Mapping support. */ + override fun supportedSchemas(): Iterable = listOf(SampleCashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) + } + + interface Commands : FungibleAsset.Commands { + + data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + + data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands + + data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit + } + + fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: AbstractParty, notary: Party) + = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) + + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) + = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) + + override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + + override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) + override fun generateIssueCommand() = Commands.Issue() + override fun generateMoveCommand() = Commands.Move() + + override fun verify(tx: TransactionForContract) + = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands)) +} + diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt new file mode 100644 index 0000000000..08cda83d92 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt @@ -0,0 +1,38 @@ +package net.corda.schemas + +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.* + +/** + * An object used to fully qualify the [CashSchema] family name (i.e. independent of version). + */ +object CashSchema + +/** + * First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood + * at the time of writing. + */ +object SampleCashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { + @Entity + @Table(name = "contract_cash_states", + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) + class PersistentCashState( + @Column(name = "owner_key") + var owner: String, + + @Column(name = "pennies") + var pennies: Long, + + @Column(name = "ccy_code", length = 3) + var currency: String, + + @Column(name = "issuer_key") + var issuerParty: String, + + @Column(name = "issuer_ref") + var issuerRef: ByteArray + ) : PersistentState() +} diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt new file mode 100644 index 0000000000..a48b664023 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt @@ -0,0 +1,37 @@ +package net.corda.schemas + +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table + +/** + * Second version of a cash contract ORM schema that extends the common + * [VaultFungibleState] abstract schema + */ +object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2, + mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { + @Entity + @Table(name = "cash_states_v2", + indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) + class PersistentCashState ( + /** product type */ + @Column(name = "ccy_code", length = 3) + var currency: String, + + /** parent attributes */ + @Transient + val _participants: Set, + @Transient + val _owner: AbstractParty, + @Transient + val _quantity: Long, + @Transient + val _issuerParty: AbstractParty, + @Transient + val _issuerRef: ByteArray + ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) +} diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt new file mode 100644 index 0000000000..33bd6e2a19 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt @@ -0,0 +1,45 @@ +package net.corda.schemas + +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import javax.persistence.* + +/** + * First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood + * at the time of writing. + */ +object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, + mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { + @Entity + @Table(name = "cash_states_v3") + class PersistentCashState( + /** [ContractState] attributes */ + @OneToMany(cascade = arrayOf(CascadeType.ALL)) + var participants: Set, + + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var owner: CommonSchemaV1.Party, + + @Column(name = "pennies") + var pennies: Long, + + @Column(name = "ccy_code", length = 3) + var currency: String, + + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var issuerParty: CommonSchemaV1.Party, + + @Column(name = "issuer_ref") + var issuerRef: ByteArray + ) : PersistentState() { + constructor(_participants: Set, _owner: AbstractParty, _quantity: Long, _currency: String, _issuerParty: AbstractParty, _issuerRef: ByteArray) + : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), + owner = CommonSchemaV1.Party(_owner), + pennies = _quantity, + currency = _currency, + issuerParty = CommonSchemaV1.Party(_issuerParty), + issuerRef = _issuerRef) + } +} diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt new file mode 100644 index 0000000000..b45075a9eb --- /dev/null +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt @@ -0,0 +1,51 @@ +package net.corda.schemas + +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import java.time.Instant +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table + +/** + * An object used to fully qualify the [CommercialPaperSchema] family name (i.e. independent of version). + */ +object CommercialPaperSchema + +/** + * First version of a commercial paper contract ORM schema that maps all fields of the [CommercialPaper] contract state + * as it stood at the time of writing. + */ +object SampleCommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { + @Entity + @Table(name = "cp_states", + indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"), + Index(name = "maturity_index", columnList = "maturity_instant"), + Index(name = "face_value_index", columnList = "face_value"))) + class PersistentCommercialPaperState( + @Column(name = "issuance_key") + var issuanceParty: String, + + @Column(name = "issuance_ref") + var issuanceRef: ByteArray, + + @Column(name = "owner_key") + var owner: String, + + @Column(name = "maturity_instant") + var maturity: Instant, + + @Column(name = "face_value") + var faceValue: Long, + + @Column(name = "ccy_code", length = 3) + var currency: String, + + @Column(name = "face_value_issuer_key") + var faceValueIssuerParty: String, + + @Column(name = "face_value_issuer_ref") + var faceValueIssuerRef: ByteArray + ) : PersistentState() +} diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt new file mode 100644 index 0000000000..e52df55695 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt @@ -0,0 +1,50 @@ +package net.corda.schemas + +import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import java.security.PublicKey +import java.time.Instant +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table + +/** + * Second version of a cash contract ORM schema that extends the common + * [VaultFungibleState] abstract schema + */ +object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, + mappedTypes = listOf(PersistentCommercialPaperState::class.java, CommonSchemaV1.Party::class.java)) { + @Entity + @Table(name = "cp_states_v2", + indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), + Index(name = "maturity_index2", columnList = "maturity_instant"))) + class PersistentCommercialPaperState( + @Column(name = "maturity_instant") + var maturity: Instant, + + @Column(name = "ccy_code", length = 3) + var currency: String, + + @Column(name = "face_value_issuer_key") + var faceValueIssuerParty: String, + + @Column(name = "face_value_issuer_ref") + var faceValueIssuerRef: ByteArray, + + /** parent attributes */ + @Transient + val _participants: Set, + @Transient + val _owner: AbstractParty, + @Transient + // face value + val _quantity: Long, + @Transient + val _issuerParty: AbstractParty, + @Transient + val _issuerRef: ByteArray + ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) +} diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/AttachmentsSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt similarity index 87% rename from node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/AttachmentsSchema.kt rename to node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt index 95ac8b89ca..1f756febfc 100644 --- a/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/AttachmentsSchema.kt +++ b/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.persistence.schemas +package net.corda.node.services.persistence.schemas.requery import io.requery.* import net.corda.core.crypto.SecureHash diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt similarity index 83% rename from node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt rename to node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt index dd3a43743b..a32d12ef23 100644 --- a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt +++ b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt @@ -1,4 +1,4 @@ -package net.corda.node.services.vault.schemas +package net.corda.node.services.vault.schemas.requery import io.requery.* import net.corda.core.node.services.Vault @@ -74,20 +74,4 @@ object VaultSchema { @get:Column(name = "lock_timestamp", nullable = true) var lockUpdateTime: Instant? } - - /** - * The following entity is for illustration purposes only as used by VaultQueryTests - */ - @Table(name = "vault_linear_states") - @Entity(model = "vault") - interface VaultLinearState : Persistable { - - @get:Index("external_id_index") - @get:Column(name = "external_id") - var externalId: String - - @get:Index("uuid_index") - @get:Column(name = "uuid", unique = true, nullable = false) - var uuid: UUID - } } \ No newline at end of file diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 29b19cb1e1..b54cb0d961 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -2,15 +2,13 @@ package net.corda.node.services.vault.schemas import io.requery.Persistable import io.requery.TransactionIsolation -import io.requery.kotlin.`in` -import io.requery.kotlin.eq -import io.requery.kotlin.invoke -import io.requery.kotlin.isNull +import io.requery.kotlin.* import io.requery.query.RowExpression import io.requery.rx.KotlinRxEntityStore import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* +import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair @@ -23,10 +21,8 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.core.utilities.* +import net.corda.node.services.vault.schemas.requery.* import org.h2.jdbcx.JdbcDataSource import org.junit.After import org.junit.Assert @@ -34,7 +30,6 @@ import org.junit.Before import org.junit.Test import rx.Observable import java.time.Instant -import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.test.assertEquals @@ -56,7 +51,7 @@ class VaultSchemaTest { fun setup() { val dataSource = JdbcDataSource() dataSource.setURL("jdbc:h2:mem:vault_persistence;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1") - val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping()) + val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping(), useDefaultLogging = true) instance = KotlinEntityDataStore(configuration) oinstance = KotlinRxEntityStore(KotlinEntityDataStore(configuration)) val tables = SchemaModifier(configuration) @@ -102,7 +97,10 @@ class VaultSchemaTest { } private fun setupDummyData() { - // dummy Transaction + // dummy Transaction comprised of 3 different Contract State types + // 1. SingleOwnerState + // 2. MultiOwnerState + // 3. VaultNoopState val notary: Party = DUMMY_NOTARY val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) val inState2 = TransactionState(DummyContract.MultiOwnerState(0, @@ -287,6 +285,24 @@ class VaultSchemaTest { } } + @Test + fun testDistinctContractStateTypes() { + val txn = createTxnWithTwoStateTypes() + dummyStatesInsert(txn) + + data.invoke { + transaction!!.inputs.forEach { + val stateEntity = createStateEntity(it) + insert(stateEntity) + } + + val query = select(VaultSchema.VaultStates::contractStateClassName).distinct() + val results = query.get() + + Assert.assertSame(3, results.count()) + } + } + private fun createStateEntity(stateAndRef: StateAndRef<*>, idx: Int? = null, txHash: String? = null): VaultStatesEntity { val stateRef = stateAndRef.ref val state = stateAndRef.state diff --git a/node/build.gradle b/node/build.gradle index d53d3c908c..1ca6b79f34 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -1,4 +1,7 @@ apply plugin: 'kotlin' +// Java Persistence API support: create no-arg constructor +// see: http://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell +apply plugin: 'kotlin-jpa' apply plugin: 'java' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' @@ -137,6 +140,9 @@ dependencies { testCompile project(':test-utils') testCompile project(':client:jfx') + // sample test schemas + testCompile project(path: ':finance', configuration: 'testArtifacts') + // For H2 database support in persistence compile "com.h2database:h2:$h2_version" diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 0aefe4de76..15b258d796 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -22,6 +22,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction import net.corda.testing.node.NodeBasedTest import org.bouncycastle.asn1.x500.X500Name +import org.junit.Ignore import org.junit.Test import java.nio.file.Files import kotlin.test.* @@ -52,11 +53,13 @@ class BFTNotaryServiceTests : NodeBasedTest() { } @Test + @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 1 faulty`() { detectDoubleSpend(1) } @Test + @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 2 faulty`() { detectDoubleSpend(2) } 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 f534ae4a38..8c1a680dea 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -32,6 +32,7 @@ import net.corda.node.services.api.* import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate +import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver import net.corda.node.services.identity.InMemoryIdentityService @@ -51,6 +52,7 @@ import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.flowVersionAndInitiatingClass import net.corda.node.services.transactions.* import net.corda.node.services.vault.CashBalanceAsMetricsObserver +import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.utilities.AddOrRemove.ADD @@ -121,6 +123,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val networkMapCache: NetworkMapCacheInternal get() = netMapCache override val storageService: TxWritableStorageService get() = storage override val vaultService: VaultService get() = vault + override val vaultQueryService: VaultQueryService get() = vaultQuery override val keyManagementService: KeyManagementService get() = keyManagement override val identityService: IdentityService get() = identity override val schedulerService: SchedulerService get() = scheduler @@ -164,6 +167,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, lateinit var checkpointStorage: CheckpointStorage lateinit var smm: StateMachineManager lateinit var vault: VaultService + lateinit var vaultQuery: VaultQueryService lateinit var keyManagement: KeyManagementService var inNodeNetworkMapService: NetworkMapService? = null lateinit var txVerifierService: TransactionVerifierService @@ -447,6 +451,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, network = makeMessagingService() schemas = makeSchemaService() vault = makeVaultService(configuration.dataSourceProperties) + vaultQuery = makeVaultQueryService(schemas) txVerifierService = makeTransactionVerifierService() auditService = DummyAuditService() @@ -525,7 +530,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, VaultSoftLockManager(vault, smm) CashBalanceAsMetricsObserver(services, database) ScheduledActivityObserver(services) - HibernateObserver(vault.rawUpdates, schemas) + HibernateObserver(vault.rawUpdates, HibernateConfiguration(schemas)) } private fun makeInfo(): NodeInfo { @@ -704,7 +709,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // TODO: sort out ordering of open & protected modifiers of functions in this class. protected open fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - protected open fun makeSchemaService(): SchemaService = NodeSchemaService() + protected open fun makeVaultQueryService(schemas: SchemaService): VaultQueryService = HibernateVaultQueryImpl(HibernateConfiguration(schemas), vault.updatesPublisher) + + protected open fun makeSchemaService(): SchemaService = NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) protected abstract fun makeTransactionVerifierService(): TransactionVerifierService diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index b0fced2732..0cb8ed074c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -14,6 +14,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort @@ -59,7 +60,7 @@ class CordaRPCOpsImpl( paging: PageSpecification, sorting: Sort): Vault.Page { return database.transaction { - services.vaultService.queryBy(criteria, paging, sorting) + services.vaultQueryService._queryBy(criteria, paging, sorting, ContractState::class.java as Class) } } @@ -68,7 +69,7 @@ class CordaRPCOpsImpl( paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { return database.transaction { - services.vaultService.trackBy(criteria, paging, sorting) + services.vaultQueryService._trackBy(criteria, paging, sorting, ContractState::class.java as Class) } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt index a297c99cd3..4a4d815708 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt @@ -1,8 +1,8 @@ package net.corda.node.services.api +import net.corda.core.contracts.ContractState import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.core.schemas.QueryableState //DOCSTART SchemaService /** @@ -23,12 +23,12 @@ interface SchemaService { * Given a state, select schemas to map it to that are supported by [generateMappedObject] and that are configured * for this node. */ - fun selectSchemas(state: QueryableState): Iterable + fun selectSchemas(state: ContractState): Iterable /** * Map a state to a [PersistentState] for the given schema, either via direct support from the state * or via custom logic in this service. */ - fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState + fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState } //DOCEND SchemaService 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 new file mode 100644 index 0000000000..4d2ba9f95c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt @@ -0,0 +1,119 @@ +package net.corda.node.services.database + +import net.corda.core.schemas.MappedSchema +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor +import net.corda.node.services.api.SchemaService +import org.hibernate.SessionFactory +import org.hibernate.boot.MetadataSources +import org.hibernate.boot.model.naming.Identifier +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder +import org.hibernate.cfg.Configuration +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment +import org.hibernate.service.UnknownUnwrapTypeException +import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.sql.Connection +import java.util.concurrent.ConcurrentHashMap + +class HibernateConfiguration(val schemaService: SchemaService, val useDefaultLogging: Boolean = false) { + constructor(schemaService: SchemaService) : this(schemaService, false) + + companion object { + val logger = loggerFor() + } + + // TODO: make this a guava cache or similar to limit ability for this to grow forever. + val sessionFactories = ConcurrentHashMap() + + init { + schemaService.schemaOptions.map { it.key }.forEach { mappedSchema -> + sessionFactories.computeIfAbsent(mappedSchema, { makeSessionFactoryForSchema(mappedSchema) }) + } + } + + fun sessionFactoryForRegisteredSchemas(): SessionFactory { + return sessionFactoryForSchemas(*schemaService.schemaOptions.map { it.key }.toTypedArray()) + } + + fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { + return sessionFactories.computeIfAbsent(schema, { sessionFactoryForSchemas(schema) }) + } + + fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory { + return makeSessionFactoryForSchemas(schemas.iterator()) + } + + private fun makeSessionFactoryForSchema(schema: MappedSchema): SessionFactory { + return makeSessionFactoryForSchemas(setOf(schema).iterator()) + } + + private fun makeSessionFactoryForSchemas(schemas: Iterator): SessionFactory { + logger.info("Creating session factory for schemas: $schemas") + val serviceRegistry = BootstrapServiceRegistryBuilder().build() + val metadataSources = MetadataSources(serviceRegistry) + // We set a connection provider as the auto schema generation requires it. The auto schema generation will not + // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. + // TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs. + val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) + .setProperty("hibernate.hbm2ddl.auto", "update") + .setProperty("hibernate.show_sql", "$useDefaultLogging") + .setProperty("hibernate.format_sql", "$useDefaultLogging") + schemas.forEach { schema -> + // TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session + schema.mappedTypes.forEach { config.addAnnotatedClass(it) } + } + val sessionFactory = buildSessionFactory(config, metadataSources, "") + logger.info("Created session factory for schemas: $schemas") + return sessionFactory + } + + private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory { + config.standardServiceRegistryBuilder.applySettings(config.properties) + val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run { + applyPhysicalNamingStrategy(object : PhysicalNamingStrategyStandardImpl() { + override fun toPhysicalTableName(name: Identifier?, context: JdbcEnvironment?): Identifier { + val default = super.toPhysicalTableName(name, context) + return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted) + } + }) + build() + } + + return metadata.sessionFactoryBuilder.run { + allowOutOfTransactionUpdateOperations(true) + applySecondLevelCacheSupport(false) + applyQueryCacheSupport(false) + enableReleaseResourcesOnCloseEnabled(true) + build() + } + } + + // Supply Hibernate with connections from our underlying Exposed database integration. Only used + // during schema creation / update. + class NodeDatabaseConnectionProvider : ConnectionProvider { + override fun closeConnection(conn: Connection) { + val tx = TransactionManager.current() + tx.commit() + tx.close() + } + + override fun supportsAggressiveRelease(): Boolean = true + + override fun getConnection(): Connection { + val tx = TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ) + return tx.connection + } + + override fun unwrap(unwrapType: Class): T { + try { + return unwrapType.cast(this) + } catch(e: ClassCastException) { + throw UnknownUnwrapTypeException(unwrapType) + } + } + + override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = (unwrapType == NodeDatabaseConnectionProvider::class.java) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 4f69c4dc7b..0fecc0049f 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -21,8 +21,8 @@ import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.services.database.RequeryConfiguration -import net.corda.node.services.persistence.schemas.AttachmentEntity -import net.corda.node.services.persistence.schemas.Models +import net.corda.node.services.persistence.schemas.requery.AttachmentEntity +import net.corda.node.services.persistence.schemas.requery.Models import java.io.ByteArrayInputStream import java.io.FilterInputStream import java.io.IOException diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 2a92d0fe4d..d53b29d5b1 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -9,70 +9,25 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.schemas.QueryableState import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.node.services.api.SchemaService +import net.corda.node.services.database.HibernateConfiguration import org.hibernate.FlushMode -import org.hibernate.SessionFactory -import org.hibernate.boot.MetadataSources -import org.hibernate.boot.model.naming.Identifier -import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl -import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder -import org.hibernate.cfg.Configuration -import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment -import org.hibernate.service.UnknownUnwrapTypeException import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.Observable -import java.sql.Connection -import java.util.concurrent.ConcurrentHashMap /** * A vault observer that extracts Object Relational Mappings for contract states that support it, and persists them with Hibernate. */ // TODO: Manage version evolution of the schemas via additional tooling. -class HibernateObserver(vaultUpdates: Observable, val schemaService: SchemaService) { +class HibernateObserver(vaultUpdates: Observable, val config: HibernateConfiguration) { + companion object { val logger = loggerFor() } - // TODO: make this a guava cache or similar to limit ability for this to grow forever. - val sessionFactories = ConcurrentHashMap() - init { - schemaService.schemaOptions.map { it.key }.forEach { - makeSessionFactoryForSchema(it) - } vaultUpdates.subscribe { persist(it.produced) } } - private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return sessionFactories.computeIfAbsent(schema, { makeSessionFactoryForSchema(it) }) - } - - private fun makeSessionFactoryForSchema(schema: MappedSchema): SessionFactory { - logger.info("Creating session factory for schema $schema") - val serviceRegistry = BootstrapServiceRegistryBuilder().build() - val metadataSources = MetadataSources(serviceRegistry) - // We set a connection provider as the auto schema generation requires it. The auto schema generation will not - // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. - // TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs. - val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name) - .setProperty("hibernate.hbm2ddl.auto", "update") - .setProperty("hibernate.show_sql", "false") - .setProperty("hibernate.format_sql", "true") - val options = schemaService.schemaOptions[schema] - val databaseSchema = options?.databaseSchema - if (databaseSchema != null) { - logger.debug { "Database schema = $databaseSchema" } - config.setProperty("hibernate.default_schema", databaseSchema) - } - val tablePrefix = options?.tablePrefix ?: "contract_" // We always have this as the default for aesthetic reasons. - logger.debug { "Table prefix = $tablePrefix" } - schema.mappedTypes.forEach { config.addAnnotatedClass(it) } - val sessionFactory = buildSessionFactory(config, metadataSources, tablePrefix) - logger.info("Created session factory for schema $schema") - return sessionFactory - } - private fun persist(produced: Set>) { produced.forEach { persistState(it) } } @@ -81,69 +36,21 @@ class HibernateObserver(vaultUpdates: Observable, val schemaServic val state = stateAndRef.state.data if (state is QueryableState) { logger.debug { "Asked to persist state ${stateAndRef.ref}" } - schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } + config.schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } } } - private fun persistStateWithSchema(state: QueryableState, stateRef: StateRef, schema: MappedSchema) { - val sessionFactory = sessionFactoryForSchema(schema) + fun persistStateWithSchema(state: QueryableState, stateRef: StateRef, schema: MappedSchema) { + val sessionFactory = config.sessionFactoryForSchema(schema) val session = sessionFactory.withOptions(). connection(TransactionManager.current().connection). flushMode(FlushMode.MANUAL). openSession() session.use { - val mappedObject = schemaService.generateMappedObject(state, schema) + val mappedObject = config.schemaService.generateMappedObject(state, schema) mappedObject.stateRef = PersistentStateRef(stateRef) it.persist(mappedObject) it.flush() } } - - private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory { - config.standardServiceRegistryBuilder.applySettings(config.properties) - val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run { - applyPhysicalNamingStrategy(object : PhysicalNamingStrategyStandardImpl() { - override fun toPhysicalTableName(name: Identifier?, context: JdbcEnvironment?): Identifier { - val default = super.toPhysicalTableName(name, context) - return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted) - } - }) - build() - } - - return metadata.sessionFactoryBuilder.run { - allowOutOfTransactionUpdateOperations(true) - applySecondLevelCacheSupport(false) - applyQueryCacheSupport(false) - enableReleaseResourcesOnCloseEnabled(true) - build() - } - } - - // Supply Hibernate with connections from our underlying Exposed database integration. Only used - // during schema creation / update. - class NodeDatabaseConnectionProvider : ConnectionProvider { - override fun closeConnection(conn: Connection) { - val tx = TransactionManager.current() - tx.commit() - tx.close() - } - - override fun supportsAggressiveRelease(): Boolean = true - - override fun getConnection(): Connection { - val tx = TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ) - return tx.connection - } - - override fun unwrap(unwrapType: Class): T { - try { - return unwrapType.cast(this) - } catch(e: ClassCastException) { - throw UnknownUnwrapTypeException(unwrapType) - } - } - - override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = (unwrapType == NodeDatabaseConnectionProvider::class.java) - } } diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index ffbad9a60e..03c9f35a7d 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -1,10 +1,16 @@ package net.corda.node.services.schema +import net.corda.contracts.DealState +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.LinearState import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.api.SchemaService +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import net.corda.schemas.CashSchemaV1 /** @@ -15,21 +21,46 @@ import net.corda.schemas.CashSchemaV1 * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { + // Currently does not support configuring schema options. - // Whitelisted tables are those required by internal Corda services - // For example, cash is used by the vault for coin selection - // This whitelist will grow as we add further functionality (eg. other fungible assets) - override val schemaOptions: Map = mapOf(Pair(CashSchemaV1, SchemaService.SchemaOptions())) + // Required schemas are those used by internal Corda services + // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future) + val requiredSchemas: Map = + mapOf(Pair(CashSchemaV1, SchemaService.SchemaOptions()), + Pair(CommonSchemaV1, SchemaService.SchemaOptions()), + Pair(VaultSchemaV1, SchemaService.SchemaOptions())) + + override val schemaOptions: Map = requiredSchemas.plus(customSchemas.map { + mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) + }) // Currently returns all schemas supported by the state, with no filtering or enrichment. - override fun selectSchemas(state: QueryableState): Iterable { - return state.supportedSchemas() + override fun selectSchemas(state: ContractState): Iterable { + val schemas = mutableSetOf() + if (state is QueryableState) + schemas += state.supportedSchemas() + if (state is LinearState) + schemas += VaultSchemaV1 // VaultLinearStates + // TODO: DealState to be deprecated (collapsed into LinearState) + if (state is DealState) + schemas += VaultSchemaV1 // VaultLinearStates + if (state is FungibleAsset<*>) + schemas += VaultSchemaV1 // VaultFungibleStates + + return schemas } // Because schema is always one supported by the state, just delegate. - override fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState { - return state.generateMappedObject(schema) + override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState { + // TODO: DealState to be deprecated (collapsed into LinearState) + if ((schema is VaultSchemaV1) && (state is DealState)) + return VaultSchemaV1.VaultLinearStates(state.linearId, state.ref, state.participants) + if ((schema is VaultSchemaV1) && (state is LinearState)) + return VaultSchemaV1.VaultLinearStates(state.linearId, "", state.participants) + if ((schema is VaultSchemaV1) && (state is FungibleAsset<*>)) + return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) + return (state as QueryableState).generateMappedObject(schema) } } 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 new file mode 100644 index 0000000000..9698ed58ad --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -0,0 +1,368 @@ +package net.corda.node.services.vault + +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +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.schemas.PersistentState +import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.serialization.toHexString +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import org.bouncycastle.asn1.x500.X500Name +import java.util.* +import javax.persistence.Tuple +import javax.persistence.criteria.* + + +class HibernateQueryCriteriaParser(val contractType: Class, + val contractTypeMappings: Map>, + val criteriaBuilder: CriteriaBuilder, + val criteriaQuery: CriteriaQuery, + val vaultStates: Root) : IQueryCriteriaParser { + private companion object { + val log = loggerFor() + } + + // incrementally build list of join predicates + private val joinPredicates = mutableListOf() + // incrementally build list of root entities (for later use in Sort parsing) + private val rootEntities = mutableMapOf, Root<*>>() + + var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED + + override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria) : Collection { + log.trace { "Parsing VaultQueryCriteria: $criteria" } + val predicateSet = mutableSetOf() + + // state status + stateTypes = criteria.status + if (criteria.status == Vault.StateStatus.ALL) + predicateSet.add(vaultStates.get("stateStatus").`in`(setOf(Vault.StateStatus.UNCONSUMED, Vault.StateStatus.CONSUMED))) + else + predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) + + // contract State Types + val combinedContractTypeTypes = criteria.contractStateTypes?.plus(contractType) ?: setOf(contractType) + combinedContractTypeTypes.filter { it.name != ContractState::class.java.name }.let { + val interfaces = it.flatMap { contractTypeMappings[it.name] ?: emptyList() } + val concrete = it.filter { !it.isInterface }.map { it.name } + val all = interfaces.plus(concrete) + if (all.isNotEmpty()) + predicateSet.add(criteriaBuilder.and(vaultStates.get("contractStateClassName").`in`(all))) + } + + // soft locking + if (!criteria.includeSoftlockedStates) + predicateSet.add(criteriaBuilder.and(vaultStates.get("lockId").isNull)) + + // notary names + criteria.notaryName?.let { + val notaryNames = (criteria.notaryName as List).map { it.toString() } + predicateSet.add(criteriaBuilder.and(vaultStates.get("notaryName").`in`(notaryNames))) + } + + // state references + criteria.stateRefs?.let { + val persistentStateRefs = (criteria.stateRefs as List).map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = vaultStates.get("stateRef") + predicateSet.add(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))) + } + + // time constraints (recorded, consumed) + criteria.timeCondition?.let { + val timeCondition = criteria.timeCondition + val timeInstantType = timeCondition!!.type + val timeColumn = when (timeInstantType) { + QueryCriteria.TimeInstantType.RECORDED -> Column.Kotlin(VaultSchemaV1.VaultStates::recordedTime) + QueryCriteria.TimeInstantType.CONSUMED -> Column.Kotlin(VaultSchemaV1.VaultStates::consumedTime) + } + val expression = CriteriaExpression.ColumnPredicateExpression(timeColumn, timeCondition.predicate) + predicateSet.add(expressionToPredicate(vaultStates, expression)) + } + return predicateSet + } + + private fun columnPredicateToPredicate(column: Path, columnPredicate: ColumnPredicate<*>): Predicate { + return when (columnPredicate) { + is ColumnPredicate.EqualityComparison -> { + val literal = columnPredicate.rightLiteral + when (columnPredicate.operator) { + EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal) + EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal) + } + } + is ColumnPredicate.BinaryComparison -> { + column as Path?> + val literal = columnPredicate.rightLiteral as Comparable? + when (columnPredicate.operator) { + BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) + BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal) + BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal) + BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal) + } + } + is ColumnPredicate.Likeness -> { + column as Path + when (columnPredicate.operator) { + LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral) + LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral) + } + } + is ColumnPredicate.CollectionExpression -> { + when (columnPredicate.operator) { + CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral) + CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral)) + } + } + is ColumnPredicate.Between -> { + column as Path?> + val fromLiteral = columnPredicate.rightFromLiteral as Comparable? + val toLiteral = columnPredicate.rightToLiteral as Comparable? + criteriaBuilder.between(column, fromLiteral, toLiteral) + } + is ColumnPredicate.NullExpression -> { + when (columnPredicate.operator) { + NullOperator.IS_NULL -> criteriaBuilder.isNull(column) + NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column) + } + } + } + } + + /** + * @return : Expression -> : Predicate + */ + private fun expressionToExpression(root: Root, expression: CriteriaExpression): Expression { + return when (expression) { + is CriteriaExpression.BinaryLogical -> { + val leftPredicate = expressionToExpression(root, expression.left) + val rightPredicate = expressionToExpression(root, expression.right) + when (expression.operator) { + BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) as Expression + BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) as Expression + } + } + is CriteriaExpression.Not -> criteriaBuilder.not(expressionToExpression(root, expression.expression)) as Expression + is CriteriaExpression.ColumnPredicateExpression -> { + val column = root.get(getColumnName(expression.column)) + columnPredicateToPredicate(column, expression.predicate) as Expression + } + } + } + + private fun expressionToPredicate(root: Root, expression: CriteriaExpression): Predicate { + return expressionToExpression(root, expression) as Predicate + } + + override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria) : Collection { + log.trace { "Parsing FungibleAssetQueryCriteria: $criteria" } + + var predicateSet = mutableSetOf() + + val vaultFungibleStates = criteriaQuery.from(VaultSchemaV1.VaultFungibleStates::class.java) + rootEntities.putIfAbsent(VaultSchemaV1.VaultFungibleStates::class.java, vaultFungibleStates) + + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultFungibleStates.get("stateRef")) + predicateSet.add(joinPredicate) + + // owner + criteria.owner?.let { + val ownerKeys = criteria.owner as List + val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") + val owners = ownerKeys.map { it.nameOrNull()?.toString() ?: it.toString()} + predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(owners))) + } + + // quantity + criteria.quantity?.let { + predicateSet.add(columnPredicateToPredicate(vaultFungibleStates.get("quantity"), it)) + } + + // issuer party + criteria.issuerPartyName?.let { + val issuerParties = criteria.issuerPartyName as List + val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") + val dealPartyKeys = issuerParties.map { it.nameOrNull().toString() } + predicateSet.add(criteriaBuilder.equal(joinFungibleStateToParty.get("name"), dealPartyKeys)) + } + + // issuer reference + criteria.issuerRef?.let { + val issuerRefs = (criteria.issuerRef as List).map { it.bytes } + predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get("issuerRef").`in`(issuerRefs))) + } + + // participants + criteria.participants?.let { + val participants = criteria.participants as List + val joinFungibleStateToParty = vaultFungibleStates.join("participants") + val participantKeys = participants.map { it.nameOrNull().toString() } + predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(participantKeys))) + criteriaQuery.distinct(true) + } + return predicateSet + } + + override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria) : Collection { + log.trace { "Parsing LinearStateQueryCriteria: $criteria" } + + val predicateSet = mutableSetOf() + + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + joinPredicates.add(joinPredicate) + + // linear ids + criteria.linearId?.let { + val uniqueIdentifiers = criteria.linearId as List + val externalIds = uniqueIdentifiers.mapNotNull { it.externalId } + if (externalIds.isNotEmpty()) + predicateSet.add(criteriaBuilder.and(vaultLinearStates.get("externalId").`in`(externalIds))) + predicateSet.add(criteriaBuilder.and(vaultLinearStates.get("uuid").`in`(uniqueIdentifiers.map { it.id }))) + } + + // deal refs + criteria.dealRef?.let { + val dealRefs = criteria.dealRef as List + predicateSet.add(criteriaBuilder.and(vaultLinearStates.get("dealReference").`in`(dealRefs))) + } + + // deal participants + criteria.participants?.let { + val participants = criteria.participants as List + val joinLinearStateToParty = vaultLinearStates.join("participants") + val participantKeys = participants.map { it.nameOrNull().toString() } + predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.get("name").`in`(participantKeys))) + criteriaQuery.distinct(true) + } + return predicateSet + } + + override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { + log.trace { "Parsing VaultCustomQueryCriteria: $criteria" } + + val predicateSet = mutableSetOf() + val entityClass = resolveEnclosingObjectFromExpression(criteria.expression) + + try { + val entityRoot = criteriaQuery.from(entityClass) + rootEntities.putIfAbsent(entityClass, entityRoot) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) + joinPredicates.add(joinPredicate) + + predicateSet.add(expressionToPredicate(entityRoot, criteria.expression)) + } + catch (e: Exception) { + e.message?.let { message -> + if (message.contains("Not an entity")) + throw VaultQueryException(""" + Please register the entity '${entityClass.name.substringBefore('$')}' class in your CorDapp's CordaPluginRegistry configuration (requiredSchemas attribute) + and ensure you have declared (in supportedSchemas()) and mapped (in generateMappedObject()) the schema in the associated contract state's QueryableState interface implementation. + See https://docs.corda.net/persistence.html?highlight=persistence for more information""") + } + throw VaultQueryException("Parsing error: ${e.message}") + } + return predicateSet + } + + override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection { + log.trace { "Parsing OR QueryCriteria composition: $left OR $right" } + + var predicateSet = mutableSetOf() + val leftPredicates = parse(left) + val rightPredicates = parse(right) + + val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) + predicateSet.add(orPredicate) + + return predicateSet + } + + override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection { + log.trace { "Parsing AND QueryCriteria composition: $left AND $right" } + + var predicateSet = mutableSetOf() + val leftPredicates = parse(left) + val rightPredicates = parse(right) + + val andPredicate = criteriaBuilder.and(criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray())) + predicateSet.add(andPredicate) + + return predicateSet + } + + override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection { + val predicateSet = criteria.visit(this) + + sorting?.let { + if (sorting.columns.isNotEmpty()) + parse(sorting) + } + + val selections = listOf(vaultStates).plus(rootEntities.map { it.value }) + criteriaQuery.multiselect(selections) + val combinedPredicates = joinPredicates.plus(predicateSet) + criteriaQuery.where(*combinedPredicates.toTypedArray()) + + return predicateSet + } + + private fun parse(sorting: Sort) { + log.trace { "Parsing sorting specification: $sorting" } + + var orderCriteria = mutableListOf() + + sorting.columns.map { (sortAttribute, direction) -> + val (entityStateClass, entityStateColumnName) = + when(sortAttribute) { + is SortAttribute.Standard -> parse(sortAttribute.attribute) + is SortAttribute.Custom -> Pair(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName) + } + val sortEntityRoot = + rootEntities.getOrElse(entityStateClass) { + // scenario where sorting on attributes not parsed as criteria + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities.put(entityStateClass, entityRoot) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) + joinPredicates.add(joinPredicate) + entityRoot + } + when (direction) { + Sort.Direction.ASC -> { + orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateColumnName))) + } + Sort.Direction.DESC -> + orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateColumnName))) + } + } + if (orderCriteria.isNotEmpty()) { + criteriaQuery.orderBy(orderCriteria) + criteriaQuery.where(*joinPredicates.toTypedArray()) + } + } + + private fun parse(sortAttribute: Sort.Attribute): Pair, String> { + val entityClassAndColumnName : Pair, String> = + when(sortAttribute) { + is Sort.VaultStateAttribute -> { + Pair(VaultSchemaV1.VaultStates::class.java, sortAttribute.columnName) + } + is Sort.LinearStateAttribute -> { + Pair(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.columnName) + } + is Sort.FungibleStateAttribute -> { + Pair(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.columnName) + } + else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute") + } + return entityClassAndColumnName + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt new file mode 100644 index 0000000000..b5a34ff5f0 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -0,0 +1,148 @@ +package net.corda.node.services.vault + +import net.corda.core.ThreadBox +import net.corda.core.bufferUntilSubscribed +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +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.vault.MAX_PAGE_SIZE +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.storageKryo +import net.corda.core.utilities.loggerFor +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.node.utilities.wrapWithDatabaseTransaction +import org.jetbrains.exposed.sql.transactions.TransactionManager +import rx.subjects.PublishSubject +import java.lang.Exception +import javax.persistence.EntityManager +import javax.persistence.Tuple + + +class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, + val updatesPublisher: PublishSubject) : SingletonSerializeAsToken(), VaultQueryService { + + companion object { + val log = loggerFor() + } + + private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() + private val criteriaBuilder = sessionFactory.criteriaBuilder + + @Throws(VaultQueryException::class) + override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { + log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") + + val session = sessionFactory.withOptions(). + connection(TransactionManager.current().connection). + openSession() + + session.use { + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + val contractTypeMappings = resolveUniqueContractStateTypes(session) + // TODO: revisit (use single instance of parser for all queries) + val criteriaParser = HibernateQueryCriteriaParser(contractType, contractTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) + + try { + // parse criteria and build where predicates + criteriaParser.parse(criteria, sorting) + + // prepare query for execution + val query = session.createQuery(criteriaQuery) + + // pagination + if (paging.pageNumber < 0) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from 0]") + if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]") + + // count total results available + val countQuery = criteriaBuilder.createQuery(Long::class.java) + countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java))) + val totalStates = session.createQuery(countQuery).singleResult.toInt() + + if ((paging.pageNumber != 0) && (paging.pageSize * paging.pageNumber >= totalStates)) + throw VaultQueryException("Requested more results than available [${paging.pageSize} * ${paging.pageNumber} >= ${totalStates}]") + + query.firstResult = paging.pageNumber * paging.pageSize + query.maxResults = paging.pageSize + + // execution + val results = query.resultList + val statesAndRefs: MutableList> = mutableListOf() + val statesMeta: MutableList = mutableListOf() + + results.asSequence() + .forEach { it -> + val it = it[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!) + val state = it.contractState.deserialize>(storageKryo()) + statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime)) + statesAndRefs.add(StateAndRef(state, stateRef)) + } + + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates) as Vault.Page + + } catch (e: Exception) { + log.error(e.message) + throw e.cause ?: e + } + } + } + + private val mutex = ThreadBox ({ updatesPublisher }) + + @Throws(VaultQueryException::class) + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.PageAndUpdates { + return mutex.locked { + val snapshotResults = _queryBy(criteria, paging, sorting, contractType) + Vault.PageAndUpdates(snapshotResults, + updatesPublisher.bufferUntilSubscribed() + .filter { it.containsType(contractType, snapshotResults.stateTypes) } ) + } + } + + /** + * Maintain a list of contract state interfaces to concrete types stored in the vault + * for usage in generic queries of type queryBy or queryBy> + */ + fun resolveUniqueContractStateTypes(session: EntityManager) : Map> { + val criteria = criteriaBuilder.createQuery(String::class.java) + val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) + criteria.select(vaultStates.get("contractStateClassName")).distinct(true) + val query = session.createQuery(criteria) + val results = query.resultList + val distinctTypes = results.map { it } + + val contractInterfaceToConcreteTypes = mutableMapOf>() + distinctTypes.forEach { it -> + val concreteType = Class.forName(it) as Class + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableListOf() }) + contractInterface.add(concreteType.name) + } + } + return contractInterfaceToConcreteTypes + } + + private fun deriveContractInterfaces(clazz: Class): Set> { + val myInterfaces: MutableSet> = mutableSetOf() + clazz.interfaces.forEach { + if (!it.equals(ContractState::class.java)) { + myInterfaces.add(it as Class) + myInterfaces.addAll(deriveContractInterfaces(it)) + } + } + return myInterfaces + } +} \ No newline at end of file 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 75f20fec92..0f014302c1 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 @@ -20,13 +20,7 @@ import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party 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.unconsumedStates -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.* import net.corda.core.serialization.* import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder @@ -35,7 +29,7 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.vault.schemas.* +import net.corda.node.services.vault.schemas.requery.* import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import rx.Observable @@ -173,6 +167,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P override val updates: Observable get() = mutex.locked { _updatesInDbTx } + override val updatesPublisher: PublishSubject + get() = mutex.locked { _updatesPublisher } + override fun track(): Pair, Observable> { return mutex.locked { Pair(Vault(unconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) @@ -222,26 +219,6 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P return stateAndRefs.associateBy({ it.ref }, { it.state }) } - override fun queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { - - TODO("Under construction") - - // If [VaultQueryCriteria.PageSpecification] specified - // must return (CloseableIterator) result.get().iterator(skip, take) - // where - // skip = Max[(pageNumber - 1),0] * pageSize - // take = pageSize - } - - override fun trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { - TODO("Under construction") - -// return mutex.locked { -// Vault.PageAndUpdates(queryBy(criteria), -// _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) -// } - } - override fun notifyAll(txns: Iterable) { val ourKeys = services.keyManagementService.keys val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) } 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 new file mode 100644 index 0000000000..24cd7baf0f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -0,0 +1,134 @@ +package net.corda.node.services.vault.schemas.jpa + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.node.services.Vault +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.serialization.OpaqueBytes +import java.security.PublicKey +import java.time.Instant +import java.util.* +import javax.persistence.* + +/** + * JPA representation of the core Vault Schema + */ +object VaultSchema + +/** + * First version of the Vault ORM schema + */ +object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, + mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, CommonSchemaV1.Party::class.java)) { + @Entity + @Table(name = "vault_states", + indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status"))) + class VaultStates( + /** refers to the notary a state is attached to */ + @Column(name = "notary_name") + var notaryName: String, + + @Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey? + var notaryKey: String, + + /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ + @Column(name = "contract_state_class_name") + var contractStateClassName: String, + + /** refers to serialized transaction Contract State */ + // TODO: define contract state size maximum size and adjust length accordingly + @Column(name = "contract_state", length = 100000) + var contractState: ByteArray, + + /** state lifecycle: unconsumed, consumed */ + @Column(name = "state_status") + var stateStatus: Vault.StateStatus, + + /** refers to timestamp recorded upon entering UNCONSUMED state */ + @Column(name = "recorded_timestamp") + var recordedTime: Instant, + + /** refers to timestamp recorded upon entering CONSUMED state */ + @Column(name = "consumed_timestamp", nullable = true) + var consumedTime: Instant?, + + /** used to denote a state has been soft locked (to prevent double spend) + * will contain a temporary unique [UUID] obtained from a flow session */ + @Column(name = "lock_id", nullable = true) + var lockId: String, + + /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ + @Column(name = "lock_timestamp", nullable = true) + var lockUpdateTime: Instant? + ) : PersistentState() + + @Entity + @Table(name = "vault_linear_states", + indexes = arrayOf(Index(name = "external_id_index", columnList = "external_id"), + Index(name = "uuid_index", columnList = "uuid"), + Index(name = "deal_reference_index", columnList = "deal_reference"))) + class VaultLinearStates( + /** [ContractState] attributes */ + @OneToMany(cascade = arrayOf(CascadeType.ALL)) + var participants: Set, + + /** + * Represents a [LinearState] [UniqueIdentifier] + */ + @Column(name = "external_id") + var externalId: String?, + + @Column(name = "uuid", nullable = false) + var uuid: UUID, + + // TODO: DealState to be deprecated (collapsed into LinearState) + + /** Deal State attributes **/ + @Column(name = "deal_reference") + var dealReference: String + ) : PersistentState() { + constructor(uid: UniqueIdentifier, _dealReference: String, _participants: List) : + this(externalId = uid.externalId, + uuid = uid.id, + dealReference = _dealReference, + participants = _participants.map{ CommonSchemaV1.Party(it) }.toSet() ) + } + + @Entity + @Table(name = "vault_fungible_states") + class VaultFungibleStates( + /** [ContractState] attributes */ + @OneToMany(cascade = arrayOf(CascadeType.ALL)) + var participants: Set, + + /** [OwnableState] attributes */ + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var owner: CommonSchemaV1.Party, + + /** [FungibleAsset] attributes + * + * Note: the underlying Product being issued must be modelled into the + * custom contract itself (eg. see currency in Cash contract state) + */ + + /** Amount attributes */ + @Column(name = "quantity") + var quantity: Long, + + /** Issuer attributes */ + @OneToOne(cascade = arrayOf(CascadeType.ALL)) + var issuerParty: CommonSchemaV1.Party, + + @Column(name = "issuer_reference") + var issuerRef: ByteArray + ) : PersistentState() { + constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : + this(owner = CommonSchemaV1.Party(_owner), + quantity = _quantity, + issuerParty = CommonSchemaV1.Party(_issuerParty), + issuerRef = _issuerRef.bytes, + participants = _participants.map { CommonSchemaV1.Party(it) }.toSet()) + } +} \ No newline at end of file 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 b596868ac2..542c98a5db 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,86 +1,88 @@ package net.corda.node.services.vault; -import com.google.common.collect.ImmutableSet; -import kotlin.Pair; -import net.corda.contracts.DealState; -import net.corda.contracts.asset.Cash; +import com.google.common.collect.*; +import kotlin.*; +import net.corda.contracts.*; +import net.corda.contracts.asset.*; import net.corda.core.contracts.*; -import net.corda.core.crypto.SecureHash; -import net.corda.core.node.services.Vault; -import net.corda.core.node.services.VaultService; -import net.corda.core.node.services.vault.PageSpecification; -import net.corda.core.node.services.vault.QueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import net.corda.core.node.services.vault.Sort; -import net.corda.core.serialization.OpaqueBytes; -import net.corda.core.transactions.SignedTransaction; -import net.corda.core.transactions.WireTransaction; -import net.corda.node.services.vault.schemas.VaultLinearStateEntity; -import net.corda.testing.node.MockServices; -import org.bouncycastle.asn1.x500.X500Name; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.exposed.sql.Database; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import net.corda.core.crypto.*; +import net.corda.core.identity.*; +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.schemas.*; +import net.corda.core.serialization.*; +import net.corda.core.transactions.*; +import net.corda.node.services.database.*; +import net.corda.node.services.schema.*; +import net.corda.schemas.*; +import net.corda.testing.node.*; +import org.jetbrains.annotations.*; +import org.jetbrains.exposed.sql.*; +import org.junit.*; import rx.Observable; -import java.io.Closeable; -import java.io.IOException; +import java.io.*; +import java.lang.reflect.*; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.stream.*; -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.contracts.asset.CashKt.*; import static net.corda.contracts.testing.VaultFiller.*; -import static net.corda.core.node.services.vault.QueryCriteriaKt.and; -import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE; -import static net.corda.core.utilities.TestConstants.getDUMMY_NOTARY; -import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; +import static net.corda.core.node.services.vault.QueryCriteriaKt.*; +import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*; +import static net.corda.core.utilities.TestConstants.*; +import static net.corda.node.utilities.DatabaseSupportKt.*; import static net.corda.node.utilities.DatabaseSupportKt.transaction; -import static net.corda.testing.CoreTestUtils.getMEGA_CORP; -import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties; -import static org.assertj.core.api.Assertions.assertThat; +import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.node.MockServicesKt.*; +import static org.assertj.core.api.Assertions.*; -@Ignore public class VaultQueryJavaTests { private MockServices services; - private VaultService vaultSvc; + VaultService vaultSvc; + private VaultQueryService vaultQuerySvc; private Closeable dataSource; private Database database; @Before public void setUp() { - Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString()); Pair dataSourceAndDatabase = configureDatabase(dataSourceProps); dataSource = dataSourceAndDatabase.getFirst(); database = dataSourceAndDatabase.getSecond(); - transaction(database, statement -> services = new MockServices() { - @NotNull - @Override - public VaultService getVaultService() { - return makeVaultService(dataSourceProps); - } + Set customSchemas = new HashSet<>(Arrays.asList(DummyLinearStateSchemaV1.INSTANCE)); + HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas)); + transaction(database, + statement -> { services = new MockServices(getMEGA_CORP_KEY()) { + @NotNull + @Override + public VaultService getVaultService() { + return makeVaultService(dataSourceProps, hibernateConfig); + } - @Override - public void recordTransactions(@NotNull Iterable txs) { - for (SignedTransaction stx : txs ) { - getStorageService().getValidatedTransactions().addTransaction(stx); - } + @Override + public VaultQueryService getVaultQueryService() { + return new HibernateVaultQueryImpl(hibernateConfig, getVaultService().getUpdatesPublisher()); + } - Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx()); - getVaultService().notifyAll(wtxn.collect(Collectors.toList())); - } + @Override + public void recordTransactions(@NotNull Iterable txs) { + for (SignedTransaction stx : txs) { + getStorageService().getValidatedTransactions().addTransaction(stx); + } + + Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx()); + getVaultService().notifyAll(wtxn.collect(Collectors.toList())); + } + }; + vaultSvc = services.getVaultService(); + vaultQuerySvc = services.getVaultQueryService(); + + return services; }); - - vaultSvc = services.getVaultService(); } @After @@ -97,8 +99,27 @@ public class VaultQueryJavaTests { */ @Test - public void consumedStates() { + public void unconsumedLinearStates() throws VaultQueryException { transaction(database, tx -> { + + fillWithSomeTestLinearStates(services, 3); + + // DOCSTART VaultJavaQueryExample0 + Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); + // DOCEND VaultJavaQueryExample0 + + assertThat(results.getStates()).hasSize(3); + + return tx; + }); + } + + @Test + public void consumedCashStates() { + transaction(database, tx -> { + + Amount amount = new Amount<>(100, Currency.getInstance("USD")); + fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), getDUMMY_NOTARY(), @@ -110,13 +131,11 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - // DOCSTART VaultJavaQueryExample1 - @SuppressWarnings("unchecked") - Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); - Vault.StateStatus status = Vault.StateStatus.CONSUMED; + consumeCash(services, amount); - VaultQueryCriteria criteria = new VaultQueryCriteria(status, null, contractStateTypes); - Vault.Page results = vaultSvc.queryBy(criteria); + // DOCSTART VaultJavaQueryExample1 + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample1 assertThat(results.getStates()).hasSize(3); @@ -126,32 +145,38 @@ public class VaultQueryJavaTests { } @Test - public void consumedDealStatesPagedSorted() { + public void consumedDealStatesPagedSorted() throws VaultQueryException { transaction(database, tx -> { - UniqueIdentifier uid = new UniqueIdentifier(); - fillWithSomeTestLinearStates(services, 10, uid); + Vault states = fillWithSomeTestLinearStates(services, 10, null); + StateAndRef linearState = states.getStates().iterator().next(); + UniqueIdentifier uid = linearState.component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); - fillWithSomeTestDeals(services, dealIds); + Vault dealStates = fillWithSomeTestDeals(services, dealIds); + + // consume states + consumeDeals(services, (List>) dealStates.getStates()); + consumeLinearStates(services, Arrays.asList(linearState)); // DOCSTART VaultJavaQueryExample2 Vault.StateStatus status = Vault.StateStatus.CONSUMED; @SuppressWarnings("unchecked") - Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); + Set> contractStateTypes = new HashSet(Collections.singletonList(LinearState.class)); - QueryCriteria vaultCriteria = new VaultQueryCriteria(status, null, contractStateTypes); + QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes); List linearIds = Arrays.asList(uid); - List dealPartyNames = Arrays.asList(getMEGA_CORP().getName()); - QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames); + QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds); + QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds); - QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria); + QueryCriteria compositeCriteria1 = or(dealCriteriaAll, linearCriteriaAll); + QueryCriteria compositeCriteria2 = and(vaultCriteria, compositeCriteria1); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); - Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST); + Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.Page results = vaultSvc.queryBy(compositeCriteria, pageSpec, sorting); + Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); // DOCEND VaultJavaQueryExample2 assertThat(results.getStates()).hasSize(4); @@ -160,13 +185,52 @@ public class VaultQueryJavaTests { }); } + @Test + public void customQueryForCashStatesWithAmountOfCurrencyGreaterOrEqualThanQuantity() { + transaction(database, tx -> { + + Amount pounds = new Amount<>(100, Currency.getInstance("GBP")); + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars10 = new Amount<>(10, Currency.getInstance("USD")); + Amount dollars1 = new Amount<>(1, Currency.getInstance("USD")); + + fillWithSomeTestCash(services, pounds, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + fillWithSomeTestCash(services, dollars100, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + fillWithSomeTestCash(services, dollars10, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + fillWithSomeTestCash(services, dollars1, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + + try { + // DOCSTART VaultJavaQueryExample3 + QueryCriteria generalCriteria = new VaultQueryCriteria(Vault.StateStatus.ALL); + + Field attributeCurrency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); + Field attributeQuantity = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + + CriteriaExpression currencyIndex = Builder.INSTANCE.equal(attributeCurrency, "USD"); + CriteriaExpression quantityIndex = Builder.INSTANCE.greaterThanOrEqual(attributeQuantity, 10L); + + QueryCriteria customCriteria2 = new VaultCustomQueryCriteria(quantityIndex); + QueryCriteria customCriteria1 = new VaultCustomQueryCriteria(currencyIndex); + + + QueryCriteria criteria = QueryCriteriaKt.and(QueryCriteriaKt.and(generalCriteria, customCriteria1), customCriteria2); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + // DOCEND VaultJavaQueryExample3 + + assertThat(results.getStates()).hasSize(2); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } + /** * Dynamic trackBy() tests */ @Test public void trackCashStates() { - transaction(database, tx -> { fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), @@ -179,17 +243,17 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - // DOCSTART VaultJavaQueryExample1 + // DOCSTART VaultJavaQueryExample4 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); - VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes); - Vault.PageAndUpdates results = vaultSvc.trackBy(criteria); + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); + Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, criteria); Vault.Page snapshot = results.getCurrent(); Observable updates = results.getFuture(); - // DOCEND VaultJavaQueryExample1 + // DOCEND VaultJavaQueryExample4 assertThat(snapshot.getStates()).hasSize(3); return tx; @@ -200,31 +264,36 @@ public class VaultQueryJavaTests { public void trackDealStatesPagedSorted() { transaction(database, tx -> { - UniqueIdentifier uid = new UniqueIdentifier(); - fillWithSomeTestLinearStates(services, 10, uid); + Vault states = fillWithSomeTestLinearStates(services, 10, null); + UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); fillWithSomeTestDeals(services, dealIds); - // DOCSTART VaultJavaQueryExample2 + // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") - Set> contractStateTypes = new HashSet(Collections.singletonList(DealState.class)); - QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes); + Set> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class)); + QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); List linearIds = Arrays.asList(uid); - List dealPartyNames = Arrays.asList(getMEGA_CORP().getName()); - QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames); + List dealParty = Arrays.asList(getMEGA_CORP()); + QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); - QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria); + QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); + + + QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria); + + QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); - Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST); + Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.PageAndUpdates results = vaultSvc.trackBy(compositeCriteria, pageSpec, sorting); + Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); Vault.Page snapshot = results.getCurrent(); Observable updates = results.getFuture(); - // DOCEND VaultJavaQueryExample2 + // DOCEND VaultJavaQueryExample5 assertThat(snapshot.getStates()).hasSize(4); @@ -239,6 +308,7 @@ public class VaultQueryJavaTests { @Test public void consumedStatesDeprecated() { transaction(database, tx -> { + Amount amount = new Amount<>(100, Currency.getInstance("USD")); fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), getDUMMY_NOTARY(), @@ -250,6 +320,8 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); + consumeCash(services, amount); + // DOCSTART VaultDeprecatedJavaQueryExample1 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); @@ -269,24 +341,21 @@ public class VaultQueryJavaTests { public void consumedStatesForLinearIdDeprecated() { transaction(database, tx -> { - UniqueIdentifier trackUid = new UniqueIdentifier(); - fillWithSomeTestLinearStates(services, 1, trackUid); - fillWithSomeTestLinearStates(services, 4, new UniqueIdentifier()); + Vault linearStates = fillWithSomeTestLinearStates(services, 4,null); + UniqueIdentifier trackUid = linearStates.getStates().iterator().next().component1().getData().getLinearId(); - // DOCSTART VaultDeprecatedJavaQueryExample2 + consumeLinearStates(services, (List>) linearStates.getStates()); + + // DOCSTART VaultDeprecatedJavaQueryExample0 @SuppressWarnings("unchecked") - Set> contractStateTypes = new HashSet(Collections.singletonList(LinearState.class)); + Set> contractStateTypes = new HashSet(Collections.singletonList(DummyLinearContract.State.class)); EnumSet status = EnumSet.of(Vault.StateStatus.CONSUMED); // WARNING! unfortunately cannot use inlined reified Kotlin extension methods. - Iterable> results = vaultSvc.states(contractStateTypes, status, true); - - Stream> trackedLinearState = StreamSupport.stream(results.spliterator(), false).filter( - state -> ((LinearState) state.component1().getData()).getLinearId() == trackUid); - // DOCEND VaultDeprecatedJavaQueryExample2 + Iterable> results = vaultSvc.states(contractStateTypes, status, true); + // DOCEND VaultDeprecatedJavaQueryExample0 assertThat(results).hasSize(4); - assertThat(trackedLinearState).hasSize(1); return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 1a87c42034..f47a4355d6 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -53,7 +53,8 @@ class CordaRPCOpsImplTest { lateinit var rpc: CordaRPCOpsImpl lateinit var stateMachineUpdates: Observable lateinit var transactions: Observable - lateinit var vaultUpdates: Observable + lateinit var vaultUpdates: Observable // TODO: deprecated + lateinit var vaultTrackCash: Observable @Before fun setup() { @@ -71,6 +72,7 @@ class CordaRPCOpsImplTest { stateMachineUpdates = rpc.stateMachinesAndUpdates().second transactions = rpc.verifiedTransactions().second vaultUpdates = rpc.vaultAndUpdates().second + vaultTrackCash = rpc.vaultTrackBy().future } } @@ -112,12 +114,20 @@ class CordaRPCOpsImplTest { } } + // TODO: deprecated vaultUpdates.expectEvents { expect { update -> val actual = update.produced.single().state.data assertEquals(expectedState, actual) } } + + vaultTrackCash.expectEvents { + expect { update -> + val actual = update.produced.single().state.data + assertEquals(expectedState, actual) + } + } } @Test @@ -180,6 +190,7 @@ class CordaRPCOpsImplTest { ) } + // TODO: deprecated vaultUpdates.expectEvents { sequence( // ISSUE @@ -194,6 +205,21 @@ class CordaRPCOpsImplTest { } ) } + + vaultTrackCash.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.isEmpty()) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + }, + // MOVE + expect { update -> + require(update.consumed.size == 1) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + } + ) + } } @Test diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index bb6f59c78f..78408f57dd 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -12,7 +12,7 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.persistence.schemas.AttachmentEntity +import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.node.MockNetwork import net.corda.node.utilities.transaction diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 061655f22b..5b3194df6b 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -22,6 +22,7 @@ import java.time.Clock open class MockServiceHubInternal( val customVault: VaultService? = null, + val customVaultQuery: VaultQueryService? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, @@ -32,6 +33,8 @@ open class MockServiceHubInternal( val schemas: SchemaService? = NodeSchemaService(), val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) ) : ServiceHubInternal() { + override val vaultQueryService: VaultQueryService + get() = customVaultQuery ?: throw UnsupportedOperationException() override val transactionVerifierService: TransactionVerifierService get() = customTransactionVerifierService ?: throw UnsupportedOperationException() override val vaultService: VaultService 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 new file mode 100644 index 0000000000..cebee41ce1 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -0,0 +1,698 @@ +package net.corda.node.services.database + +import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.DummyFungibleContract +import net.corda.contracts.testing.consumeCash +import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.contracts.testing.fillWithSomeTestDeals +import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.core.contracts.* +import net.corda.core.crypto.toBase58String +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultService +import net.corda.core.schemas.DummyLinearStateSchemaV1 +import net.corda.core.schemas.DummyLinearStateSchemaV2 +import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.storageKryo +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.BOB_KEY +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.node.services.schema.HibernateObserver +import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.services.vault.NodeVaultService +import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.node.utilities.configureDatabase +import net.corda.node.utilities.transaction +import net.corda.schemas.CashSchemaV1 +import net.corda.schemas.SampleCashSchemaV2 +import net.corda.schemas.SampleCashSchemaV3 +import net.corda.testing.BOB_PUBKEY +import net.corda.testing.node.MockServices +import net.corda.testing.node.makeTestDataSourceProperties +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.hibernate.SessionFactory +import org.jetbrains.exposed.sql.Database +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.Closeable +import java.time.Instant +import java.util.* +import javax.persistence.EntityManager +import javax.persistence.Tuple +import javax.persistence.criteria.CriteriaBuilder + +class HibernateConfigurationTest { + + lateinit var services: MockServices + lateinit var dataSource: Closeable + lateinit var database: Database + val vault: VaultService get() = services.vaultService + + // Hibernate configuration objects + lateinit var hibernateConfig: HibernateConfiguration + lateinit var hibernatePersister: HibernateObserver + lateinit var sessionFactory: SessionFactory + lateinit var entityManager: EntityManager + lateinit var criteriaBuilder: CriteriaBuilder + + // test States + lateinit var cashStates: List> + + @Before + fun setUp() { + val dataSourceProps = makeTestDataSourceProperties() + val dataSourceAndDatabase = configureDatabase(dataSourceProps) + val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) + + dataSource = dataSourceAndDatabase.first + database = dataSourceAndDatabase.second + database.transaction { + + hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas)) + + services = object : MockServices(BOB_KEY) { + override val vaultService: VaultService get() { + val vaultService = NodeVaultService(this, dataSourceProps) + hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) + return vaultService + } + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + vaultService.notifyAll(txs.map { it.tx }) + } + } + } + setUpDb() + + sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray()) + entityManager = sessionFactory.createEntityManager() + criteriaBuilder = sessionFactory.criteriaBuilder + } + + @After + fun cleanUp() { + dataSource.close() + } + + private fun setUpDb() { + database.transaction { + cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)).states.toList() + } + } + + @Test + fun `count rows`() { + // structure query + val countQuery = criteriaBuilder.createQuery(Long::class.java) + countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java))) + + // execute query + val countResult = entityManager.createQuery(countQuery).singleResult + + assertThat(countResult).isEqualTo(10) + } + + @Test + fun `consumed states`() { + database.transaction { + services.consumeCash(50.DOLLARS) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + criteriaQuery.where(criteriaBuilder.equal( + vaultStates.get("stateStatus"), Vault.StateStatus.CONSUMED)) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults.size).isEqualTo(6) + } + + @Test + fun `select by composite primary key`() { + val issuedStates = + database.transaction { + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestLinearStates(2) + } + val persistentStateRefs = issuedStates.states.map { PersistentStateRef(it.ref) }.toList() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val compositeKey = vaultStates.get("stateRef") + criteriaQuery.where(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + assertThat(queryResults).hasSize(2) + assertThat(queryResults.first().stateRef?.txId).isEqualTo(issuedStates.states.first().ref.txhash.toString()) + assertThat(queryResults.first().stateRef?.index).isEqualTo(issuedStates.states.first().ref.index) + assertThat(queryResults.last().stateRef?.txId).isEqualTo(issuedStates.states.last().ref.txhash.toString()) + assertThat(queryResults.last().stateRef?.index).isEqualTo(issuedStates.states.last().ref.index) + } + + @Test + fun `distinct contract types`() { + database.transaction { + // add 2 more contract types + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(String::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + criteriaQuery.select(vaultStates.get("contractStateClassName")).distinct(true) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + Assertions.assertThat(queryResults.size).isEqualTo(3) + } + + @Test + fun `with sorting`() { + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(vaultStates.get("recordedTime"))) + val queryResults = entityManager.createQuery(criteriaQuery).resultList + queryResults.map { println(it.recordedTime) } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(vaultStates.get("recordedTime"))) + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + queryResultsAsc.map { println(it.recordedTime) } + } + + @Test + fun `with pagination`() { + // add 100 additional cash entries + database.transaction { + services.fillWithSomeTestCash(1000.POUNDS, DUMMY_NOTARY, 100, 100, Random(0L)) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + // set pagination + val query = entityManager.createQuery(criteriaQuery) + query.firstResult = 10 + query.maxResults = 15 + + // execute query + val queryResults = query.resultList + Assertions.assertThat(queryResults.size).isEqualTo(15) + + // try towards end + query.firstResult = 100 + query.maxResults = 15 + + val lastQueryResults = query.resultList + + Assertions.assertThat(lastQueryResults.size).isEqualTo(10) + } + + /** + * VaultLinearState is a concrete table, extendible by any Contract extending a LinearState + */ + @Test + fun `select by composite primary key on LinearStates`() { + database.transaction { + services.fillWithSomeTestLinearStates(10) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + + criteriaQuery.select(vaultStates) + criteriaQuery.where(criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef"))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + /** + * VaultFungibleState is an abstract entity, which should be extended any Contract extending a FungibleAsset + */ + + /** + * CashSchemaV1 = original Cash schema (extending PersistentState) + */ + @Test + fun `count CashStates`() { + // structure query + val countQuery = criteriaBuilder.createQuery(Long::class.java) + countQuery.select(criteriaBuilder.count(countQuery.from(CashSchemaV1.PersistentCashState::class.java))) + + // execute query + val countResult = entityManager.createQuery(countQuery).singleResult + + assertThat(countResult).isEqualTo(10) + } + + @Test + fun `select by composite primary key on CashStates`() { + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + vaultStates.join("stateRef") + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + @Test + fun `select and join by composite primary key on CashStates`() { + database.transaction { + services.fillWithSomeTestLinearStates(5) + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultCashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + criteriaQuery.select(vaultStates) + criteriaQuery.where(criteriaBuilder.equal(vaultStates.get("stateRef"), vaultCashStates.get("stateRef"))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + } + + /** + * CashSchemaV2 = optimised Cash schema (extending FungibleState) + */ + @Test + fun `count CashStates in V2`() { + database.transaction { + // persist cash states explicitly with V2 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2) + } + } + + // structure query + val countQuery = criteriaBuilder.createQuery(Long::class.java) + countQuery.select(criteriaBuilder.count(countQuery.from(SampleCashSchemaV2.PersistentCashState::class.java))) + + // execute query + val countResult = entityManager.createQuery(countQuery).singleResult + + assertThat(countResult).isEqualTo(10) + } + + @Test + fun `select by composite primary key on CashStates in V2`() { + database.transaction { + services.fillWithSomeTestLinearStates(5) + + // persist cash states explicitly with V2 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2) + } + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultCashStates = criteriaQuery.from(SampleCashSchemaV2.PersistentCashState::class.java) + + criteriaQuery.select(vaultStates) + criteriaQuery.where(criteriaBuilder.equal(vaultStates.get("stateRef"), vaultCashStates.get("stateRef"))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + /** + * Represents a 3-way join between: + * - VaultStates + * - VaultLinearStates + * - a concrete LinearState implementation (eg. DummyLinearState) + */ + + /** + * DummyLinearStateV1 = original DummyLinearState schema (extending PersistentState) + */ + @Test + fun `select by composite primary between VaultStates, VaultLinearStates and DummyLinearStates`() { + database.transaction { + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + services.fillWithSomeTestLinearStates(2) + } + + val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java) + + criteriaQuery.select(vaultStates) + val joinPredicate1 = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get("stateRef"), dummyLinearStates.get("stateRef"))) + criteriaQuery.where(joinPredicate1, joinPredicate2) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + /** + * DummyLinearSchemaV2 = optimised DummyLinear schema (extending LinearState) + */ + + @Test + fun `three way join by composite primary between VaultStates, VaultLinearStates and DummyLinearStates`() { + database.transaction { + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + services.fillWithSomeTestLinearStates(2) + } + + val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV2.PersistentDummyLinearState::class.java) + + criteriaQuery.select(vaultStates) + val joinPredicate1 = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get("stateRef"), dummyLinearStates.get("stateRef"))) + criteriaQuery.where(joinPredicate1, joinPredicate2) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + /** + * Test a OneToOne table mapping + */ + @Test + fun `select fungible states by owner party`() { + database.transaction { + // persist original cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(SampleCashSchemaV3.PersistentCashState::class.java) + criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(10) + } + + /** + * Test Query by Party (OneToOne table mapping) + */ + @Test + fun `query fungible states by owner party`() { + database.transaction { + // persist original cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = ALICE) + val cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), + issuedBy = BOB.ref(0), issuerKey = BOB_KEY, ownedBy = (BOB)).states + // persist additional cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + } + + val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + + // select + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + criteriaQuery.select(vaultStates) + + // search predicate + val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) + + val joinCashToParty = cashStatesSchema.join("owner") + val queryOwnerKey = BOB_PUBKEY.toBase58String() + criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryOwnerKey)) + + val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) + criteriaQuery.where(joinVaultStatesToCash) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { + val contractState = it.contractState.deserialize>(storageKryo()) + val cashState = contractState.data as Cash.State + println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") } + + assertThat(queryResults).hasSize(12) + } + + /** + * Test a OneToMany table mapping + */ + @Test + fun `select fungible states by participants`() { + database.transaction { + // persist cash states explicitly with V2 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(SampleCashSchemaV3.PersistentCashState::class.java) + criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + assertThat(queryResults).hasSize(10) + } + + /** + * Test Query by participants (OneToMany table mapping) + */ + @Test + fun `query fungible states by participants`() { + val firstCashState = + database.transaction { + // persist original cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + + val moreCash = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), + issuedBy = BOB.ref(0), issuerKey = BOB_KEY, ownedBy = BOB).states + // persist additional cash states explicitly with V3 schema + moreCash.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + + val cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states + // persist additional cash states explicitly with V3 schema + cashStates.forEach { + val cashState = it.state.data + val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner) + hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3) + } + cashStates.first() + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + + // select + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + criteriaQuery.select(vaultStates) + + // search predicate + val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) + + val joinCashToParty = cashStatesSchema.join("participants") + val queryParticipantKeys = firstCashState.state.data.participants.map { it.owningKey.toBase58String() } + criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryParticipantKeys)) + + val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) + criteriaQuery.where(joinVaultStatesToCash) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + queryResults.forEach { + val contractState = it.contractState.deserialize>(storageKryo()) + val cashState = contractState.data as Cash.State + println("${it.stateRef} with owner ${cashState.owner.owningKey.toBase58String()} and participants ${cashState.participants.map { it.owningKey.toBase58String() }}") + } + + assertThat(queryResults).hasSize(12) + } + + /** + * Query with sorting on Common table attribute + */ + @Test + fun `with sorting on attribute from common table`() { + + database.transaction { + services.fillWithSomeTestLinearStates(1, externalId = "111") + services.fillWithSomeTestLinearStates(2, externalId = "222") + services.fillWithSomeTestLinearStates(3, externalId = "333") + } + + val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + + // join + criteriaQuery.multiselect(vaultStates, vaultLinearStates) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + criteriaQuery.where(joinPredicate) + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(vaultLinearStates.get("externalId"))) + criteriaQuery.orderBy(criteriaBuilder.desc(vaultLinearStates.get("uuid"))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + queryResults.map { + val vaultState = it[0] as VaultSchemaV1.VaultStates + val vaultLinearState = it[1] as VaultSchemaV1.VaultLinearStates + println("${vaultState.stateRef} : ${vaultLinearState.externalId} ${vaultLinearState.uuid}") + } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(vaultLinearStates.get("externalId"))) + criteriaQuery.orderBy(criteriaBuilder.asc(vaultLinearStates.get("uuid"))) + + // execute query + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + queryResultsAsc.map { + val vaultState = it[0] as VaultSchemaV1.VaultStates + val vaultLinearState = it[1] as VaultSchemaV1.VaultLinearStates + println("${vaultState.stateRef} : ${vaultLinearState.externalId} ${vaultLinearState.uuid}") + } + + assertThat(queryResults).hasSize(6) + } + + /** + * Query with sorting on Custom table attribute + */ + @Test + fun `with sorting on attribute from custom table`() { + + database.transaction { + services.fillWithSomeTestLinearStates(1, externalId = "111") + services.fillWithSomeTestLinearStates(2, externalId = "222") + services.fillWithSomeTestLinearStates(3, externalId = "333") + } + + val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java) + + // join + criteriaQuery.multiselect(vaultStates, vaultLinearStates, dummyLinearStates) + val joinPredicate1 = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get("stateRef"), dummyLinearStates.get("stateRef"))) + criteriaQuery.where(joinPredicate1, joinPredicate2) + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(dummyLinearStates.get("externalId"))) + criteriaQuery.orderBy(criteriaBuilder.desc(dummyLinearStates.get("uuid"))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + queryResults.map { + val vaultState = it[0] as VaultSchemaV1.VaultStates + val _vaultLinearStates = it[1] as VaultSchemaV1.VaultLinearStates + val _dummyLinearStates = it[2] as DummyLinearStateSchemaV1.PersistentDummyLinearState + println("${vaultState.stateRef} : [${_dummyLinearStates.externalId} ${_dummyLinearStates.uuid}] : [${_vaultLinearStates.externalId} ${_vaultLinearStates.uuid}]") + } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(dummyLinearStates.get("externalId"))) + criteriaQuery.orderBy(criteriaBuilder.asc(dummyLinearStates.get("uuid"))) + + // execute query + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + queryResultsAsc.map { + val vaultState = it[0] as VaultSchemaV1.VaultStates + val _vaultLinearStates = it[1] as VaultSchemaV1.VaultLinearStates + val _dummyLinearStates = it[2] as DummyLinearStateSchemaV1.PersistentDummyLinearState + println("${vaultState.stateRef} : [${_dummyLinearStates.externalId} ${_dummyLinearStates.uuid}] : [${_vaultLinearStates.externalId} ${_vaultLinearStates.uuid}]") + } + + assertThat(queryResults).hasSize(6) + } + +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index 6d892f456c..b1b482261a 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -19,10 +19,10 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_PUBKEY_1 import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.vault.schemas.Models -import net.corda.node.services.vault.schemas.VaultCashBalancesEntity -import net.corda.node.services.vault.schemas.VaultSchema -import net.corda.node.services.vault.schemas.VaultStatesEntity +import net.corda.node.services.vault.schemas.requery.Models +import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity +import net.corda.node.services.vault.schemas.requery.VaultSchema +import net.corda.node.services.vault.schemas.requery.VaultStatesEntity import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.node.makeTestDataSourceProperties @@ -30,6 +30,7 @@ import org.assertj.core.api.Assertions import org.jetbrains.exposed.sql.Database import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.io.Closeable @@ -123,6 +124,55 @@ class RequeryConfigurationTest { } } + @Test + fun `bounded iteration`() { + // insert 100 entities + database.transaction { + requerySession.withTransaction { + (1..100) + .map { newTransaction(it) } + .forEach { insert(createVaultStateEntity(it)) } + } + } + + // query entities 41..45 + database.transaction { + requerySession.withTransaction { + // Note: cannot specify a limit explicitly when using iterator skip & take + val query = select(VaultSchema.VaultStates::class) + val count = query.get().count() + Assertions.assertThat(count).isEqualTo(100) + val result = query.get().iterator(40, 5) + Assertions.assertThat(result.asSequence().count()).isEqualTo(5) + } + } + } + + @Test + fun `test calling an arbitrary JDBC native query`() { + val txn = newTransaction() + + database.transaction { + transactionStorage.addTransaction(txn) + requerySession.withTransaction { + insert(createVaultStateEntity(txn)) + } + } + + val dataSourceProperties = makeTestDataSourceProperties() + val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0" + + database.transaction { + val configuration = RequeryConfiguration(dataSourceProperties, true) + val jdbcSession = configuration.jdbcSession() + val prepStatement = jdbcSession.prepareStatement(nativeQuery) + val rs = prepStatement.executeQuery() + assertTrue(rs.next()) + assertEquals(rs.getString(1), txn.tx.inputs[0].txhash.toString()) + assertEquals(rs.getInt(2), txn.tx.inputs[0].index) + } + } + private fun createVaultStateEntity(txn: SignedTransaction): VaultStatesEntity { val txnState = txn.tx.inputs[0] val state = VaultStatesEntity().apply { @@ -158,9 +208,9 @@ class RequeryConfigurationTest { } } - private fun newTransaction(): SignedTransaction { + private fun newTransaction(index: Int = 0): SignedTransaction { val wtx = WireTransaction( - inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)), + inputs = listOf(StateRef(SecureHash.randomSHA256(), index)), attachments = emptyList(), outputs = emptyList(), commands = emptyList(), diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 1233f22951..0246f69f64 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -11,7 +11,7 @@ import net.corda.core.utilities.LogHelper import net.corda.core.write import net.corda.core.writeLines import net.corda.node.services.database.RequeryConfiguration -import net.corda.node.services.persistence.schemas.AttachmentEntity +import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction 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 08dffaa371..8c087f2b1c 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 @@ -1,9 +1,6 @@ package net.corda.node.services.schema -import net.corda.core.contracts.Contract -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty @@ -13,6 +10,7 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.utilities.LogHelper import net.corda.node.services.api.SchemaService +import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.HibernateObserver import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction @@ -99,9 +97,9 @@ class HibernateObserverTests { val schemaService = object : SchemaService { override val schemaOptions: Map = emptyMap() - override fun selectSchemas(state: QueryableState): Iterable = setOf(testSchema) + override fun selectSchemas(state: ContractState): Iterable = setOf(testSchema) - override fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState { + override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState { val parent = Parent() parent.children.add(Child()) parent.children.add(Child()) @@ -110,14 +108,14 @@ class HibernateObserverTests { } @Suppress("UNUSED_VARIABLE") - val observer = HibernateObserver(rawUpdatesPublisher, schemaService) + val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService)) 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 contract_Parents").executeQuery() + val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() parentRowCountResult.next() val parentRows = parentRowCountResult.getInt(1) parentRowCountResult.close() - val childrenRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from contract_Children").executeQuery() + val childrenRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery() childrenRowCountResult.next() val childrenRows = childrenRowCountResult.getInt(1) childrenRowCountResult.close() 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 74df33c1a9..6db977b677 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,50 +1,63 @@ package net.corda.node.services.vault import net.corda.contracts.CommercialPaper +import net.corda.contracts.Commodity import net.corda.contracts.DealState +import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.fillWithSomeTestCash -import net.corda.contracts.testing.fillWithSomeTestDeals -import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.contracts.testing.* import net.corda.core.contracts.* import net.corda.core.crypto.entropyToKeyPair import net.corda.core.days import net.corda.core.identity.Party -import net.corda.core.node.services.Vault -import net.corda.core.node.services.VaultService -import net.corda.core.node.services.linearHeadsOfType +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.schemas.DummyLinearStateSchemaV1 import net.corda.core.seconds import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.* -import net.corda.node.services.vault.schemas.VaultLinearStateEntity -import net.corda.node.services.vault.schemas.VaultSchema +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.core.utilities.TEST_TX_TIME +import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction +import net.corda.schemas.CashSchemaV1 import net.corda.schemas.CashSchemaV1.PersistentCashState -import net.corda.schemas.CommercialPaperSchemaV1.PersistentCommercialPaperState +import net.corda.schemas.CommercialPaperSchemaV1 +import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties +import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Test import java.io.Closeable +import java.lang.Thread.sleep import java.math.BigInteger import java.security.KeyPair +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.* +import kotlin.test.assertFails -@Ignore class VaultQueryTests { + lateinit var services: MockServices val vaultSvc: VaultService get() = services.vaultService + val vaultQuerySvc: VaultQueryService get() = services.vaultQueryService lateinit var dataSource: Closeable lateinit var database: Database @@ -55,8 +68,10 @@ class VaultQueryTests { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second database.transaction { - services = object : MockServices() { - override val vaultService: VaultService = makeVaultService(dataSourceProps) + val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1) + val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas)) + services = object : MockServices(MEGA_CORP_KEY) { + override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) override fun recordTransactions(txs: Iterable) { for (stx in txs) { @@ -65,6 +80,7 @@ class VaultQueryTests { // 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) } } } @@ -74,6 +90,56 @@ class VaultQueryTests { dataSource.close() } + /** + * Helper method for generating a Persistent H2 test database + */ + @Ignore //@Test + fun createPersistentTestDb() { + + val dataSourceAndDatabase = configureDatabase(makePersistentDataSourceProperties()) + val dataSource = dataSourceAndDatabase.first + val database = dataSourceAndDatabase.second + + setUpDb(database, 5000) + + dataSource.close() + } + + private fun setUpDb(_database: Database, delay: Long = 0) { + + _database.transaction { + + // create new states + services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L)) + val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") + val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") + services.fillWithSomeTestLinearStates(3, "ABC") + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // Total unconsumed states = 10 + 1 + 2 + 3 + 3 = 19 + + sleep(delay) + + // consume some states + services.consumeLinearStates(linearStatesXYZ.states.toList()) + services.consumeLinearStates(linearStatesJKL.states.toList()) + services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) + services.consumeCash(50.DOLLARS) + + // Total unconsumed states = 4 + 3 + 2 + 1 (new cash change) = 10 + // Total consumed states = 6 + 1 + 2 + 1 = 10 + } + } + + private fun makePersistentDataSourceProperties(): Properties { + val props = Properties() + props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") + props.setProperty("dataSource.url", "jdbc:h2:~/test/vault_query_persistence;DB_CLOSE_ON_EXIT=TRUE") + props.setProperty("dataSource.user", "sa") + props.setProperty("dataSource.password", "") + return props + } + /** * Query API tests */ @@ -82,7 +148,7 @@ class VaultQueryTests { (combining both FungibleState and LinearState contract types) */ @Test - fun `unconsumed states`() { + fun `unconsumed states simple`() { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) @@ -90,8 +156,7 @@ class VaultQueryTests { services.fillWithSomeTestDeals(listOf("123", "456", "789")) // DOCSTART VaultQueryExample1 - val criteria = VaultQueryCriteria() // default is UNCONSUMED - val result = vaultSvc.queryBy(criteria) + val result = vaultQuerySvc.queryBy() /** * Query result returns a [Vault.Page] which contains: @@ -110,16 +175,63 @@ class VaultQueryTests { } @Test - fun `unconsumed states for state refs`() { + fun `unconsumed states verbose`() { database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val criteria = VaultQueryCriteria() // default is UNCONSUMED + val result = vaultQuerySvc.queryBy(criteria) + + assertThat(result.states).hasSize(16) + assertThat(result.statesMetadata).hasSize(16) + } + } + + @Test + fun `unconsumed cash states simple`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val result = vaultQuerySvc.queryBy() + + assertThat(result.states).hasSize(3) + assertThat(result.statesMetadata).hasSize(3) + } + } + + @Test + fun `unconsumed cash states verbose`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val criteria = VaultQueryCriteria() // default is UNCONSUMED + val result = vaultQuerySvc.queryBy(criteria) + + assertThat(result.states).hasSize(3) + assertThat(result.statesMetadata).hasSize(3) + } + } + + @Test + fun `unconsumed states for state refs`() { + + database.transaction { + services.fillWithSomeTestLinearStates(8) val issuedStates = services.fillWithSomeTestLinearStates(2) val stateRefs = issuedStates.states.map { it.ref }.toList() - services.fillWithSomeTestLinearStates(8) // DOCSTART VaultQueryExample2 val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample2 assertThat(results.states).hasSize(2) @@ -131,15 +243,15 @@ class VaultQueryTests { @Test fun `unconsumed states for contract state types`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) + // default State.Status is UNCONSUMED // DOCSTART VaultQueryExample3 val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java, DealState::class.java)) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample3 assertThat(results.states).hasSize(6) } @@ -148,18 +260,17 @@ class VaultQueryTests { @Test fun `consumed states`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID + val linearStates = services.fillWithSomeTestLinearStates(2, "TEST") // create 2 states with same externalId services.fillWithSomeTestLinearStates(8) - services.fillWithSomeTestDeals(listOf("123", "456", "789")) + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) -// services.consumeLinearStates(UniqueIdentifier("TEST")) -// services.consumeDeals("456") -// services.consumeCash(80.DOLLARS) + services.consumeLinearStates(linearStates.states.toList()) + services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) + services.consumeCash(50.DOLLARS) val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(5) } } @@ -167,25 +278,24 @@ class VaultQueryTests { @Test fun `all states`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 results with same UID + val linearStates = services.fillWithSomeTestLinearStates(2, "TEST") // create 2 results with same UID services.fillWithSomeTestLinearStates(8) - services.fillWithSomeTestDeals(listOf("123", "456", "789")) + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) -// services.consumeLinearStates(UniqueIdentifier("TEST")) -// services.consumeDeals("456") -// services.consumeCash(80.DOLLARS) + services.consumeLinearStates(linearStates.states.toList()) + services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) + services.consumeCash(50.DOLLARS) // generates a new change state! val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultSvc.queryBy(criteria) - assertThat(results.states).hasSize(16) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(17) } } val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } - val CASH_NOTARY: Party get() = Party(DUMMY_NOTARY.name, CASH_NOTARY_KEY.public) + val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) @Test fun `unconsumed states by notary`() { @@ -197,23 +307,38 @@ class VaultQueryTests { // DOCSTART VaultQueryExample4 val criteria = VaultQueryCriteria(notaryName = listOf(CASH_NOTARY.name)) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample4 assertThat(results.states).hasSize(3) } } @Test - fun `unconsumed states by participants`() { + fun `unconsumed linear states for single participant`() { database.transaction { - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST"), participants = listOf(MEGA_CORP, MINI_CORP)) + services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) - services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP, MINI_CORP)) + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP)) + + val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP)) + val results = vaultQuerySvc.queryBy(criteria) + + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed linear states for two participants`() { + database.transaction { + + services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) + services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP)) // DOCSTART VaultQueryExample5 - val criteria = VaultQueryCriteria(participantIdentities = listOf(MEGA_CORP.name, MINI_CORP.name)) - val results = vaultSvc.queryBy(criteria) + val criteria = LinearStateQueryCriteria(participants = listOf(MEGA_CORP, MINI_CORP)) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample5 assertThat(results.states).hasSize(3) @@ -228,28 +353,233 @@ class VaultQueryTests { vaultSvc.softLockReserve(UUID.randomUUID(), setOf(issuedStates.states.first().ref, issuedStates.states.last().ref)) val criteria = VaultQueryCriteria(includeSoftlockedStates = false) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(1) } } + @Test + fun `logical operator EQUAL`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator NOT EQUAL`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual(GBP.currencyCode) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator GREATER_THAN`() { + database.transaction { + + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThan(1000L) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator GREATER_THAN_OR_EQUAL`() { + database.transaction { + + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThanOrEqual(1000L) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator LESS_THAN`() { + database.transaction { + + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThan(1000L) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator LESS_THAN_OR_EQUAL`() { + database.transaction { + + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThanOrEqual(1000L) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator BETWEEN`() { + database.transaction { + + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.between(500L, 1500L) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator IN`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val currencies = listOf(CHF.currencyCode, GBP.currencyCode) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator NOT IN`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val currencies = listOf(CHF.currencyCode, GBP.currencyCode) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator LIKE`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%BP") } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator NOT LIKE`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%BP") } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator IS_NULL`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.isNull() } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(0) + } + } + + @Test + fun `logical operator NOT_NULL`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.notNull() } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(3) + } + } + + private val TODAY = LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC) + @Test fun `unconsumed states recorded between two time intervals`() { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestLinearStates(10) - services.fillWithSomeTestDeals(listOf("123", "456", "789")) // DOCSTART VaultQueryExample6 - val start = TEST_TX_TIME - val end = TEST_TX_TIME.plus(30, ChronoUnit.DAYS) - val recordedBetweenExpression = LogicalExpression( - QueryCriteria.TimeInstantType.RECORDED, Operator.BETWEEN, arrayOf(start, end)) + val start = TODAY + val end = TODAY.plus(30, ChronoUnit.DAYS) + val recordedBetweenExpression = TimeCondition( + QueryCriteria.TimeInstantType.RECORDED, + ColumnPredicate.Between(start, end)) val criteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample6 assertThat(results.states).hasSize(3) + + // Future + val startFuture = TODAY.plus(1, ChronoUnit.DAYS) + val recordedBetweenExpressionFuture = TimeCondition( + QueryCriteria.TimeInstantType.RECORDED, ColumnPredicate.Between(startFuture, end)) + val criteriaFuture = VaultQueryCriteria(timeCondition = recordedBetweenExpressionFuture) + assertThat(vaultQuerySvc.queryBy(criteriaFuture).states).isEmpty() } } @@ -261,12 +591,14 @@ class VaultQueryTests { services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val asOfDateTime = TEST_TX_TIME - val consumedAfterExpression = LogicalExpression( - QueryCriteria.TimeInstantType.CONSUMED, Operator.GREATER_THAN, arrayOf(asOfDateTime)) + services.consumeCash(100.DOLLARS) + + val asOfDateTime = TODAY + val consumedAfterExpression = TimeCondition( + QueryCriteria.TimeInstantType.CONSUMED, ColumnPredicate.BinaryComparison(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, asOfDateTime)) val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, - timeCondition = consumedAfterExpression) - val results = vaultSvc.queryBy(criteria) + timeCondition = consumedAfterExpression) + val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(3) } @@ -282,9 +614,10 @@ class VaultQueryTests { // DOCSTART VaultQueryExample7 val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultSvc.queryBy(criteria, paging = pagingSpec) + val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) // DOCEND VaultQueryExample7 assertThat(results.states).hasSize(10) + assertThat(results.totalStatesAvailable).isEqualTo(100) } } @@ -293,28 +626,102 @@ class VaultQueryTests { fun `all states with paging specification - last`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + services.fillWithSomeTestCash(95.DOLLARS, DUMMY_NOTARY, 95, 95, Random(0L)) // Last page implies we need to perform a row count for the Query first, // and then re-query for a given offset defined by (count - pageSize) + val pagingSpec = PageSpecification(9, 10) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + assertThat(results.states).hasSize(5) // should retrieve states 90..94 + assertThat(results.totalStatesAvailable).isEqualTo(95) + } + } + + // pagination: invalid page number + @Test(expected = VaultQueryException::class) + fun `invalid page number`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + val pagingSpec = PageSpecification(-1, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultSvc.queryBy(criteria, paging = pagingSpec) + val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) assertThat(results.states).hasSize(10) // should retrieve states 90..99 } } + // pagination: invalid page size + @Test(expected = VaultQueryException::class) + fun `invalid page size`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + + val pagingSpec = PageSpecification(0, MAX_PAGE_SIZE + 1) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + assertFails { } + } + } + + // pagination: out or range request (page number * page size) > total rows available + @Test(expected = VaultQueryException::class) + fun `out of range page request`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + + val pagingSpec = PageSpecification(10, 10) // this requests results 101 .. 110 + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) + assertFails { println("Query should throw an exception [${results.states.count()}]") } + } + } + + // sorting + @Test + fun `sorting - all states sorted by contract type, state status, consumed time`() { + + setUpDb(database) + + database.transaction { + + val sortCol1 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.CONTRACT_TYPE), Sort.Direction.DESC) + val sortCol2 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.STATE_STATUS), Sort.Direction.ASC) + val sortCol3 = Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.CONSUMED_TIME), Sort.Direction.DESC) + val sorting = Sort(setOf(sortCol1, sortCol2, sortCol3)) + val result = vaultQuerySvc.queryBy(VaultQueryCriteria(status = Vault.StateStatus.ALL), sorting = sorting) + + val states = result.states + val metadata = result.statesMetadata + + for (i in 0..states.size - 1) { + println("${states[i].ref} : ${metadata[i].contractStateClassName}, ${metadata[i].status}, ${metadata[i].consumedTime}") + } + + assertThat(states).hasSize(20) + assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.core.contracts.DummyLinearContract\$State") + assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED + assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.DummyDealContract\$State") + assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED + } + } + @Test fun `unconsumed fungible assets`() { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) -// services.fillWithSomeTestCommodity() + services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!)) services.fillWithSomeTestLinearStates(10) - val criteria = VaultQueryCriteria(contractStateTypes = setOf(FungibleAsset::class.java)) // default is UNCONSUMED - val results = vaultSvc.queryBy>(criteria) + val results = vaultQuerySvc.queryBy>() assertThat(results.states).hasSize(4) } } @@ -324,16 +731,13 @@ class VaultQueryTests { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) -// services.consumeCash(2) -// services.fillWithSomeTestCommodity() -// services.consumeCommodity() + services.consumeCash(50.DOLLARS) + services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!)) services.fillWithSomeTestLinearStates(10) -// services.consumeLinearStates(8) - val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, - contractStateTypes = setOf(FungibleAsset::class.java)) - val results = vaultSvc.queryBy>(criteria) - assertThat(results.states).hasSize(3) + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val results = vaultQuerySvc.queryBy>(criteria) + assertThat(results.states).hasSize(2) } } @@ -344,8 +748,7 @@ class VaultQueryTests { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) - val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java)) // default is UNCONSUMED - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy() assertThat(results.states).hasSize(3) } } @@ -355,13 +758,13 @@ class VaultQueryTests { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) -// services.consumeCash(2) - services.fillWithSomeTestLinearStates(10) -// services.consumeLinearStates(8) + services.consumeCash(50.DOLLARS) + val linearStates = services.fillWithSomeTestLinearStates(10) + services.consumeLinearStates(linearStates.states.toList()) val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val results = vaultSvc.queryBy(criteria) - assertThat(results.states).hasSize(1) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) } } @@ -371,10 +774,10 @@ class VaultQueryTests { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val criteria = VaultQueryCriteria(contractStateTypes = setOf(LinearState::class.java)) // default is UNCONSUMED - val results = vaultSvc.queryBy(criteria) - assertThat(results.states).hasSize(10) + val results = vaultQuerySvc.queryBy() + assertThat(results.states).hasSize(13) } } @@ -383,20 +786,24 @@ class VaultQueryTests { database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestLinearStates(10) -// services.consumeLinearStates(8) + val linearStates = services.fillWithSomeTestLinearStates(2, "TEST") // create 2 states with same externalId + services.fillWithSomeTestLinearStates(8) + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, - contractStateTypes = setOf(LinearState::class.java)) - val results = vaultSvc.queryBy(criteria) - assertThat(results.states).hasSize(2) + services.consumeLinearStates(linearStates.states.toList()) + services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) + services.consumeCash(50.DOLLARS) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(3) } } /** LinearState tests */ @Test - fun `unconsumed linear heads for linearId`() { + fun `unconsumed linear heads for linearId without external Id`() { database.transaction { val issuedStates = services.fillWithSomeTestLinearStates(10) @@ -404,83 +811,181 @@ class VaultQueryTests { // DOCSTART VaultQueryExample8 val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first(), linearIds.last())) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample8 assertThat(results.states).hasSize(2) } } @Test - fun `latest unconsumed linear heads for linearId`() { + fun `unconsumed linear heads for linearId with external Id`() { database.transaction { - val issuedStates = services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID - services.fillWithSomeTestLinearStates(8) + val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") + services.fillWithSomeTestLinearStates(1, "ID2") + val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") - val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() - val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first()), - latestOnly = true) - val results = vaultSvc.queryBy(criteria) - assertThat(results.states).hasSize(1) + val linearIds = listOf(linearState1.states.first().state.data.linearId, linearState3.states.first().state.data.linearId) + val criteria = LinearStateQueryCriteria(linearId = linearIds) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) } } @Test - fun `return chain of linear state for a given id`() { + fun `all linear states for a given id`() { database.transaction { - val id = UniqueIdentifier("TEST") - services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearId = linearState.state.data.linearId + services.evolveLinearState(linearState) // consume current and produce new state reference + services.evolveLinearState(linearState) // consume current and produce new state reference + services.evolveLinearState(linearState) // consume current and produce new state reference - // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 - val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(id)) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId)) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val sorting = Sort(setOf(Sort.SortColumn(VaultSchema.VaultLinearState::uuid.name, Sort.Direction.DESC))) - val results = vaultSvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting) + val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria)) // DOCEND VaultQueryExample9 assertThat(results.states).hasSize(4) } } @Test - fun `DEPRECATED return linear states for a given id`() { + fun `all linear states for a given id sorted by uuid`() { database.transaction { - val linearUid = UniqueIdentifier("TEST") - services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference + val txns = services.fillWithSomeTestLinearStates(2, "TEST") + val linearStates = txns.states.toList() + services.evolveLinearStates(linearStates) // consume current and produce new state reference + services.evolveLinearStates(linearStates) // consume current and produce new state reference + services.evolveLinearStates(linearStates) // consume current and produce new state reference - // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" + val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }) + val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) - // DOCSTART VaultDeprecatedQueryExample1 - val states = vaultSvc.linearHeadsOfType().filter { it.key == linearUid } - // DOCEND VaultDeprecatedQueryExample1 - - assertThat(states).hasSize(4) + val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + results.states.forEach { println("${it.state.data.linearId.id}") } + assertThat(results.states).hasSize(8) } } @Test - fun `DEPRECATED return consumed linear states for a given id`() { + fun `unconsumed linear states sorted by linear state attribute`() { database.transaction { - val linearUid = UniqueIdentifier("TEST") - services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference -// services.processLinearState(id) // consume current and produce new state reference + services.fillWithSomeTestLinearStates(1, externalId = "111") + services.fillWithSomeTestLinearStates(2, externalId = "222") + services.fillWithSomeTestLinearStates(3, externalId = "333") - // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + val vaultCriteria = VaultQueryCriteria() + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) + + val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + results.states.forEach { println("${it.state.data.linearString}") } + assertThat(results.states).hasSize(6) + } + } + + @Test + fun `unconsumed deal states paged and sorted`() { + database.transaction { + + val linearStates = services.fillWithSomeTestLinearStates(10) + val uid = linearStates.states.first().state.data.linearId + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(uid)) + val dealStateCriteria = LinearStateQueryCriteria(dealRef = listOf("123", "456", "789")) + val compositeCriteria = vaultCriteria.and(linearStateCriteria).or(dealStateCriteria) + + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.DEAL_REFERENCE), Sort.Direction.DESC))) + + val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) + results.states.forEach { + if (it.state.data is DummyDealContract.State) + println("${(it.state.data as DealState).ref}, ${it.state.data.linearId}") } + assertThat(results.states).hasSize(4) + } + } + + @Test + fun `unconsumed linear states sorted by custom attribute`() { + database.transaction { + + services.fillWithSomeTestLinearStates(1, linearString = "111") + services.fillWithSomeTestLinearStates(2, linearString = "222") + services.fillWithSomeTestLinearStates(3, linearString = "333") + + val vaultCriteria = VaultQueryCriteria() + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Custom(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java, "linearString"), Sort.Direction.DESC))) + + val results = vaultQuerySvc.queryBy((vaultCriteria), sorting = sorting) + results.states.forEach { println("${it.state.data.linearString}") } + assertThat(results.states).hasSize(6) + } + } + + @Test + fun `return consumed linear states for a given id`() { + database.transaction { + + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearState2 = services.evolveLinearState(linearState) // consume current and produce new state reference + val linearState3 = services.evolveLinearState(linearState2) // consume current and produce new state reference + services.evolveLinearState(linearState3) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" + val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }) + val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) + val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `DEPRECATED unconsumed linear states for a given id`() { + database.transaction { + + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearId = linearState.state.data.linearId + val linearState2 = services.evolveLinearState(linearState) // consume current and produce new state reference + val linearState3 = services.evolveLinearState(linearState2) // consume current and produce new state reference + services.evolveLinearState(linearState3) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" + + // DOCSTART VaultDeprecatedQueryExample1 + val states = vaultSvc.linearHeadsOfType().filter { it.key == linearId } + // DOCEND VaultDeprecatedQueryExample1 + + assertThat(states).hasSize(1) + } + } + + @Test + fun `DEPRECATED consumed linear states for a given id`() { + database.transaction { + + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearId = linearState.state.data.linearId + val linearState2 = services.evolveLinearState(linearState) // consume current and produce new state reference + val linearState3 = services.evolveLinearState(linearState2) // consume current and produce new state reference + services.evolveLinearState(linearState3) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultDeprecatedQueryExample2 - val states = vaultSvc.states(setOf(LinearState::class.java), - EnumSet.of(Vault.StateStatus.CONSUMED)).filter { it.state.data.linearId == linearUid } + val states = vaultSvc.consumedStates().filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample2 assertThat(states).hasSize(3) @@ -488,28 +993,37 @@ class VaultQueryTests { } @Test - fun `latest unconsumed linear heads for state refs`() { + fun `DEPRECATED all linear states for a given id`() { database.transaction { - val issuedStates = services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID - services.fillWithSomeTestLinearStates(8) - val stateRefs = issuedStates.states.map { it.ref }.toList() + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearId = linearState.state.data.linearId + services.evolveLinearState(linearState) // consume current and produce new state reference + services.evolveLinearState(linearState) // consume current and produce new state reference + services.evolveLinearState(linearState) // consume current and produce new state reference - val vaultCriteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val linearStateCriteria = LinearStateQueryCriteria(latestOnly = true) - val results = vaultSvc.queryBy(vaultCriteria.and(linearStateCriteria)) - assertThat(results.states).hasSize(2) + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" + + // DOCSTART VaultDeprecatedQueryExample3 + val states = vaultSvc.states(setOf(DummyLinearContract.State::class.java), + EnumSet.of(Vault.StateStatus.CONSUMED, Vault.StateStatus.UNCONSUMED)).filter { it.state.data.linearId == linearId } + // DOCEND VaultDeprecatedQueryExample3 + + assertThat(states).hasSize(4) } } + /** + * Deal Contract state to be removed as is duplicate of LinearState + */ @Test fun `unconsumed deals`() { database.transaction { services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val criteria = LinearStateQueryCriteria() - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy() assertThat(results.states).hasSize(3) } } @@ -522,7 +1036,7 @@ class VaultQueryTests { // DOCSTART VaultQueryExample10 val criteria = LinearStateQueryCriteria(dealRef = listOf("456", "789")) - val results = vaultSvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample10 assertThat(results.states).hasSize(2) @@ -533,27 +1047,33 @@ class VaultQueryTests { fun `latest unconsumed deals for ref`() { database.transaction { - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) - services.fillWithSomeTestDeals(listOf("456")) // create 3 revisions with same ID + services.fillWithSomeTestLinearStates(2, "TEST") + services.fillWithSomeTestDeals(listOf("456")) services.fillWithSomeTestDeals(listOf("123", "789")) - val criteria = LinearStateQueryCriteria(dealRef = listOf("456"), latestOnly = true) - val results = vaultSvc.queryBy(criteria) + val all = vaultQuerySvc.queryBy() + all.states.forEach { println(it.state) } + + val criteria = LinearStateQueryCriteria(dealRef = listOf("456")) + val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(1) } } @Test fun `latest unconsumed deals with party`() { + database.transaction { - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) - services.fillWithSomeTestDeals(listOf("456")) // specify party + val parties = listOf(MEGA_CORP) + + services.fillWithSomeTestLinearStates(2, "TEST") + services.fillWithSomeTestDeals(listOf("456"), parties) services.fillWithSomeTestDeals(listOf("123", "789")) // DOCSTART VaultQueryExample11 - val criteria = LinearStateQueryCriteria(dealPartyName = listOf(MEGA_CORP.name, MINI_CORP.name)) - val results = vaultSvc.queryBy(criteria) + val criteria = LinearStateQueryCriteria(participants = parties) + val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample11 assertThat(results.states).hasSize(1) @@ -561,21 +1081,58 @@ class VaultQueryTests { } /** FungibleAsset tests */ + @Test - fun `unconsumed fungible assets of token type`() { + fun `unconsumed fungible assets for specific issuer party and refs`() { database.transaction { - services.fillWithSomeTestLinearStates(10) - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) + 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, ref = OpaqueBytes.of(1)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(2)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(3)) - val criteria = FungibleAssetQueryCriteria(tokenType = listOf(Currency::class.java)) - val results = vaultSvc.queryBy>(criteria) - assertThat(results.states).hasSize(9) + val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC), + issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) + val results = vaultQuerySvc.queryBy>(criteria) + assertThat(results.states).hasSize(2) } } + @Test + fun `unconsumed fungible assets by owner`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), + issuedBy = MEGA_CORP.ref(0), issuerKey = MEGA_CORP_KEY, ownedBy = (MEGA_CORP)) + + val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) + val results = vaultQuerySvc.queryBy>(criteria) + assertThat(results.states).hasSize(1) + } + } + + + @Test + fun `unconsumed fungible states for owners`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), + issuedBy = MEGA_CORP.ref(0), issuerKey = MEGA_CORP_KEY, ownedBy = (MEGA_CORP)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), + issuedBy = MINI_CORP.ref(0), issuerKey = MINI_CORP_KEY, ownedBy = (MINI_CORP)) // irrelevant to this vault + + // DOCSTART VaultQueryExample5.2 + val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP,MINI_CORP)) + val results = vaultQuerySvc.queryBy(criteria) + // DOCEND VaultQueryExample5.2 + + assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) + } + } + + /** Cash Fungible State specific */ @Test fun `unconsumed fungible assets for single currency`() { database.transaction { @@ -586,8 +1143,9 @@ class VaultQueryTests { services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) // DOCSTART VaultQueryExample12 - val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(USD.currencyCode)) - val results = vaultSvc.queryBy>(criteria) + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } + val criteria = VaultCustomQueryCriteria(ccyIndex) + val results = vaultQuerySvc.queryBy>(criteria) // DOCEND VaultQueryExample12 assertThat(results.states).hasSize(3) @@ -595,36 +1153,20 @@ class VaultQueryTests { } @Test - fun `unconsumed fungible assets for single currency and quantity greater than`() { + fun `unconsumed fungible assets for quantity greater than`() { database.transaction { - services.fillWithSomeTestLinearStates(10) - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) // DOCSTART VaultQueryExample13 - val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(GBP.currencyCode), - quantity = LogicalExpression(this, Operator.GREATER_THAN, 50)) - val results = vaultSvc.queryBy(criteria) + val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(2500L) }) + val results = vaultQuerySvc.queryBy(fungibleAssetCriteria) // DOCEND VaultQueryExample13 - assertThat(results.states).hasSize(1) - } - } - - @Test - fun `unconsumed fungible assets for several currencies`() { - database.transaction { - - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) - services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) - - val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(CHF.currencyCode, GBP.currencyCode)) - val results = vaultSvc.queryBy>(criteria) - assertThat(results.states).hasSize(3) + assertThat(results.states).hasSize(4) // POUNDS, SWISS_FRANCS } } @@ -636,8 +1178,8 @@ class VaultQueryTests { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY) // DOCSTART VaultQueryExample14 - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC.name)) - val results = vaultSvc.queryBy>(criteria) + val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC)) + val results = vaultQuerySvc.queryBy>(criteria) // DOCEND VaultQueryExample14 assertThat(results.states).hasSize(1) @@ -645,48 +1187,20 @@ class VaultQueryTests { } @Test - fun `unconsumed fungible assets for specific issuer party and refs`() { + fun `unconsumed fungible assets for single currency and quantity greater than`() { database.transaction { - 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, ref = OpaqueBytes.of(1)) - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(2)) - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(3)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC.name), - issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) - val results = vaultSvc.queryBy>(criteria) - assertThat(results.states).hasSize(2) - } - } + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } + val customCriteria = VaultCustomQueryCriteria(ccyIndex) + val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(5000L) }) + val results = vaultQuerySvc.queryBy(fungibleAssetCriteria.and(customCriteria)) - @Test - fun `unconsumed fungible assets with exit keys`() { - database.transaction { - - 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) - - // DOCSTART VaultQueryExample15 - val criteria = FungibleAssetQueryCriteria(exitKeyIdentity = listOf(DUMMY_CASH_ISSUER.party.toString())) - val results = vaultSvc.queryBy>(criteria) - // DOCEND VaultQueryExample15 - - assertThat(results.states).hasSize(1) - } - } - - @Test - fun `unconsumed fungible assets by owner`() { - database.transaction { - - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) - // issue some cash to BOB - // issue some cash to ALICE - - val criteria = FungibleAssetQueryCriteria(ownerIdentity = listOf(BOB.name, ALICE.name)) - val results = vaultSvc.queryBy>(criteria) - assertThat(results.states).hasSize(1) + assertThat(results.states).hasSize(1) // POUNDS > 50 } } @@ -694,12 +1208,13 @@ class VaultQueryTests { // specifying Query on Commercial Paper contract state attributes @Test - fun `commercial paper custom query`() { + fun `custom query using JPA - commercial paper schema V1 single attribute`() { database.transaction { + val issuance = MEGA_CORP.ref(1) + // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER - val issuance = MEGA_CORP.ref(1) val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { addTimeWindow(TEST_TX_TIME, 30.seconds) @@ -708,15 +1223,87 @@ class VaultQueryTests { }.toSignedTransaction() services.recordTransactions(commercialPaper) - val ccyIndex = LogicalExpression(PersistentCommercialPaperState::currency, Operator.EQUAL, USD.currencyCode) - val maturityIndex = LogicalExpression(PersistentCommercialPaperState::maturity, Operator.GREATER_THAN_OR_EQUAL, TEST_TX_TIME + 30.days) - val faceValueIndex = LogicalExpression(PersistentCommercialPaperState::faceValue, Operator.GREATER_THAN_OR_EQUAL, 10000) + // MegaCorp™ now issues £10,000 of commercial paper, to mature in 30 days, owned by itself. + val faceValue2 = 10000.POUNDS `issued by` DUMMY_CASH_ISSUER + val commercialPaper2 = + CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + addTimeWindow(TEST_TX_TIME, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + services.recordTransactions(commercialPaper2) - val criteria = VaultCustomQueryCriteria(maturityIndex.and(faceValueIndex).or(ccyIndex)) - val result = vaultSvc.queryBy(criteria) + val ccyIndex = builder { CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) } + val criteria1 = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) - assertThat(result.states).hasSize(1) - assertThat(result.statesMetadata).hasSize(1) + val result = vaultQuerySvc.queryBy(criteria1) + + Assertions.assertThat(result.states).hasSize(1) + Assertions.assertThat(result.statesMetadata).hasSize(1) + } + } + + // specifying Query on Commercial Paper contract state attributes + @Test + fun `custom query using JPA - commercial paper schema V1 - multiple attributes`() { + database.transaction { + + val issuance = MEGA_CORP.ref(1) + + // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val commercialPaper = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + addTimeWindow(TEST_TX_TIME, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + services.recordTransactions(commercialPaper) + + // MegaCorp™ now issues £5,000 of commercial paper, to mature in 30 days, owned by itself. + val faceValue2 = 5000.POUNDS `issued by` DUMMY_CASH_ISSUER + val commercialPaper2 = + CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + addTimeWindow(TEST_TX_TIME, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + services.recordTransactions(commercialPaper2) + + val result = builder { + + val ccyIndex = CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) + val maturityIndex = CommercialPaperSchemaV1.PersistentCommercialPaperState::maturity.greaterThanOrEqual(TEST_TX_TIME + 30.days) + val faceValueIndex = CommercialPaperSchemaV1.PersistentCommercialPaperState::faceValue.greaterThanOrEqual(10000L) + + val criteria1 = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) + val criteria2 = QueryCriteria.VaultCustomQueryCriteria(maturityIndex) + val criteria3 = QueryCriteria.VaultCustomQueryCriteria(faceValueIndex) + + vaultQuerySvc.queryBy(criteria1.and(criteria3).and(criteria2)) + } + + + Assertions.assertThat(result.states).hasSize(1) + Assertions.assertThat(result.statesMetadata).hasSize(1) + } + } + + @Test + fun `query attempting to use unregistered schema`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 1, 1, Random(0L)) + + // CashSchemaV3 NOT registered with NodeSchemaService + val logicalExpression = builder { SampleCashSchemaV3.PersistentCashState::currency.equal(GBP.currencyCode) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + + assertThatThrownBy { + vaultQuerySvc.queryBy(criteria) + }.isInstanceOf(VaultQueryException::class.java).hasMessageContaining("Please register the entity") } } @@ -724,7 +1311,7 @@ class VaultQueryTests { // specifying Query on Cash contract state attributes @Test - fun `all cash states with amount of currency greater or equal than`() { + fun `custom - all cash states with amount of currency greater or equal than`() { database.transaction { @@ -733,45 +1320,413 @@ class VaultQueryTests { services.fillWithSomeTestCash(10.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) - // DOCSTART VaultQueryExample16 + // DOCSTART VaultQueryExample20 val generalCriteria = VaultQueryCriteria(Vault.StateStatus.ALL) - val currencyIndex = LogicalExpression(PersistentCashState::currency, Operator.EQUAL, USD.currencyCode) - val quantityIndex = LogicalExpression(PersistentCashState::pennies, Operator.GREATER_THAN_OR_EQUAL, 10) - val customCriteria = VaultCustomQueryCriteria(currencyIndex.and(quantityIndex)) + val results = builder { + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode) + val quantityIndex = PersistentCashState::pennies.greaterThanOrEqual(10L) - val criteria = generalCriteria.and(customCriteria) - val results = vaultSvc.queryBy(criteria) - // DOCEND VaultQueryExample16 + val customCriteria1 = VaultCustomQueryCriteria(currencyIndex) + val customCriteria2 = VaultCustomQueryCriteria(quantityIndex) - assertThat(results.states).hasSize(2) + val criteria = generalCriteria.and(customCriteria1.and(customCriteria2)) + vaultQuerySvc.queryBy(criteria) + } + // DOCEND VaultQueryExample20 + + assertThat(results.states).hasSize(3) } } // specifying Query on Linear state attributes @Test - fun `consumed linear heads for linearId between two timestamps`() { + fun `unconsumed linear heads for linearId between two timestamps`() { database.transaction { - val issuedStates = services.fillWithSomeTestLinearStates(10) - val externalIds = issuedStates.states.map { it.state.data.linearId }.map { it.externalId }[0] - val uuids = issuedStates.states.map { it.state.data.linearId }.map { it.id }[1] - val start = TEST_TX_TIME - val end = TEST_TX_TIME.plus(30, ChronoUnit.DAYS) - val recordedBetweenExpression = LogicalExpression(TimeInstantType.RECORDED, Operator.BETWEEN, arrayOf(start, end)) + val start = Instant.now() + val end = start.plus(1, ChronoUnit.SECONDS) + + services.fillWithSomeTestLinearStates(1, "TEST") + sleep(1000) + services.fillWithSomeTestLinearStates(1, "TEST") + + // 2 unconsumed states with same external ID + + val recordedBetweenExpression = TimeCondition(TimeInstantType.RECORDED, builder { between(start, end) }) val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) - val linearIdsExpression = LogicalExpression(VaultLinearStateEntity::externalId, Operator.IN, externalIds) - val linearIdCondition = LogicalExpression(VaultLinearStateEntity::uuid, Operator.EQUAL, uuids) - val customIndexCriteria = VaultCustomQueryCriteria(linearIdsExpression.or(linearIdCondition)) + val results = vaultQuerySvc.queryBy(basicCriteria) - val criteria = basicCriteria.and(customIndexCriteria) - val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + // specifying Query on Linear state attributes + @Test + fun `unconsumed linear heads for a given external id`() { + database.transaction { + + services.fillWithSomeTestLinearStates(1, "TEST1") + services.fillWithSomeTestLinearStates(1, "TEST2") + + // 2 unconsumed states with same external ID + + val externalIdCondition = builder { VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") } + val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) + + val results = vaultQuerySvc.queryBy(externalIdCustomCriteria) + + assertThat(results.states).hasSize(1) + } + } + + // specifying Query on Linear state attributes + @Test + fun `unconsumed linear heads for linearId between two timestamps for a given external id`() { + database.transaction { + + val start = Instant.now() + val end = start.plus(1, ChronoUnit.SECONDS) + + services.fillWithSomeTestLinearStates(1, "TEST1") + services.fillWithSomeTestLinearStates(1, "TEST2") + sleep(1000) + services.fillWithSomeTestLinearStates(1, "TEST3") + + // 2 unconsumed states with same external ID + + val results = builder { + val linearIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") + val customCriteria = VaultCustomQueryCriteria(linearIdCondition) + + val recordedBetweenExpression = TimeCondition(TimeInstantType.RECORDED, between(start, end)) + val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) + + val criteria = basicCriteria.and(customCriteria) + vaultQuerySvc.queryBy(criteria) + } + + assertThat(results.states).hasSize(1) + } + } + + // specifying Query on Linear state attributes + @Test + fun `unconsumed linear heads for a given external id or uuid`() { + database.transaction { + + services.fillWithSomeTestLinearStates(1, "TEST1") + services.fillWithSomeTestLinearStates(1, "TEST2") + val uuid = services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id + + // 2 unconsumed states with same external ID + + val results = builder { + val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") + val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) + + val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid) + val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) + + val criteria = externalIdCustomCriteria.or(uuidCustomCriteria) + vaultQuerySvc.queryBy(criteria) + } + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `unconsumed linear heads where external id is null`() { + database.transaction { + + services.fillWithSomeTestLinearStates(1, "TEST1") + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + + // 3 unconsumed states (one without an external ID) + + val results = builder { + val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.isNull() + val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) + + vaultQuerySvc.queryBy(externalIdCustomCriteria) + } + + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed linear heads where external id is not null`() { + database.transaction { + + services.fillWithSomeTestLinearStates(1, "TEST1") + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + + // 3 unconsumed states (two with an external ID) + + val results = builder { + val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.notNull() + val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) + + vaultQuerySvc.queryBy(externalIdCustomCriteria) + } assertThat(results.states).hasSize(2) } } + /** + * Dynamic trackBy() tests + */ + + @Test + fun trackCashStates_unconsumed() { + val updates = + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + + // DOCSTART VaultQueryExample15 + val (snapshot, updates) = vaultQuerySvc.trackBy() // UNCONSUMED default + // DOCEND VaultQueryExample15 + + assertThat(snapshot.states).hasSize(5) + assertThat(snapshot.statesMetadata).hasSize(5) + + // add more cash + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + + // consume stuff + services.consumeCash(100.DOLLARS) + services.consumeDeals(dealStates.toList()) + services.consumeLinearStates(linearStates.toList()) + + updates + } + + updates?.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 5) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 1) {} + } + ) + } + } + + @Test + fun trackCashStates_consumed() { + val updates = + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + + // add more cash + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + + // consume stuff + services.consumeCash(100.POUNDS) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val (snapshot, updates) = vaultQuerySvc.trackBy(criteria) + + assertThat(snapshot.states).hasSize(1) + assertThat(snapshot.statesMetadata).hasSize(1) + + // consume more stuff + services.consumeCash(100.DOLLARS) + services.consumeDeals(dealStates.toList()) + services.consumeLinearStates(linearStates.toList()) + + updates + } + + updates?.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 1) {} + require(produced.size == 0) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 5) {} + require(produced.size == 0) {} + } + ) + } + } + + @Test + fun trackCashStates_all() { + val updates = + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + + // add more cash + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + + // consume stuff + services.consumeCash(99.POUNDS) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (snapshot, updates) = vaultQuerySvc.trackBy(criteria) + + assertThat(snapshot.states).hasSize(7) + assertThat(snapshot.statesMetadata).hasSize(7) + + // consume more stuff + services.consumeCash(100.DOLLARS) + services.consumeDeals(dealStates.toList()) + services.consumeLinearStates(linearStates.toList()) + + updates + } + + updates?.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 5) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 1) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 1) {} + require(produced.size == 1) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 5) {} + require(produced.size == 0) {} + } + ) + } + } + + @Test + fun trackLinearStates() { + val updates = + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + + // DOCSTART VaultQueryExample16 + val (snapshot, updates) = vaultQuerySvc.trackBy() + // DOCEND VaultQueryExample16 + + + assertThat(snapshot.states).hasSize(13) + assertThat(snapshot.statesMetadata).hasSize(13) + + // add more cash + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + + // consume stuff + services.consumeCash(100.DOLLARS) + services.consumeDeals(dealStates.toList()) + services.consumeLinearStates(linearStates.toList()) + + updates + } + + updates?.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 10) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 3) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 1) {} + } + ) + } + } + + @Test + fun trackDealStates() { + val updates = + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + + // DOCSTART VaultQueryExample17 + val (snapshot, updates) = vaultQuerySvc.trackBy() + // DOCEND VaultQueryExample17 + + assertThat(snapshot.states).hasSize(3) + assertThat(snapshot.statesMetadata).hasSize(3) + + // add more cash + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + + // consume stuff + services.consumeCash(100.DOLLARS) + services.consumeDeals(dealStates.toList()) + services.consumeLinearStates(linearStates.toList()) + + updates + } + + updates?.expectEvents { + sequence( + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 3) {} + }, + expect { (consumed, produced, flowId) -> + require(flowId == null) {} + require(consumed.size == 0) {} + require(produced.size == 1) {} + } + ) + } + } + /** * USE CASE demonstrations (outside of mainline Corda) * @@ -780,4 +1735,4 @@ class VaultQueryTests { * 3) Template / Tutorial CorDapp service query extension executing Named Queries via JPA * 4) Advanced pagination queries using Spring Data (and/or Hibernate/JPQL) */ -} \ No newline at end of file +} 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 9e8fd69af9..7714a2186b 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 @@ -1,5 +1,6 @@ package net.corda.node.services.vault +import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.* 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 e390bb3fbe..2126f18897 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 @@ -13,6 +13,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate import net.corda.node.services.keys.getSigner @@ -69,15 +70,15 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) override val vaultService: VaultService get() = throw UnsupportedOperationException() + override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - fun makeVaultService(dataSourceProps: Properties): VaultService { + fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService())): VaultService { val vaultService = NodeVaultService(this, dataSourceProps) - // Vault cash spending requires access to contract_cash_states and their updates - HibernateObserver(vaultService.rawUpdates, NodeSchemaService()) + HibernateObserver(vaultService.rawUpdates, hibernateConfig) return vaultService } From 4f16512dcf41d58a37243971f1d8632210f7d29d Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 14 Jun 2017 18:51:30 +0100 Subject: [PATCH 02/25] Revert "Reverting "Upgrade Artemis to 2.1.0" as the new version seems to introduce a race condition that causes a RPC stability test to randomly hang." This reverts commit 022fb7b8cb1cee36611ac7dee5b6cd9b7fbc7bab. --- build.gradle | 2 +- node/src/main/kotlin/net/corda/node/internal/Node.kt | 3 ++- .../corda/node/services/messaging/ArtemisMessagingServer.kt | 4 ++-- .../net/corda/node/services/messaging/NodeMessagingClient.kt | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 972ff7c346..4515bb43da 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { ext.capsule_version = '1.0.1' ext.asm_version = '0.5.3' - ext.artemis_version = '1.5.3' + ext.artemis_version = '2.1.0' ext.jackson_version = '2.8.5' ext.jetty_version = '9.3.9.v20160517' ext.jersey_version = '2.25' 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 7cc8a14032..f25ed8e9bc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -40,6 +40,7 @@ import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException +import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ClientMessage import org.bouncycastle.asn1.x500.X500Name @@ -223,7 +224,7 @@ open class Node(override val configuration: FullNodeConfiguration, session.start() val queueName = "$IP_REQUEST_PREFIX$requestId" - session.createQueue(queueName, queueName, false) + session.createQueue(queueName, RoutingType.MULTICAST, queueName, false) val consumer = session.createConsumer(queueName) val artemisMessage: ClientMessage = consumer.receive(10.seconds.toMillis()) ?: 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 077f3d79d1..1fee1f6f2d 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 @@ -30,12 +30,12 @@ import org.apache.activemq.artemis.core.config.Configuration import org.apache.activemq.artemis.core.config.CoreQueueConfiguration import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration +import org.apache.activemq.artemis.core.message.impl.CoreMessage import org.apache.activemq.artemis.core.remoting.impl.netty.* import org.apache.activemq.artemis.core.security.Role import org.apache.activemq.artemis.core.server.ActiveMQServer import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl import org.apache.activemq.artemis.core.server.impl.RoutingContextImpl -import org.apache.activemq.artemis.core.server.impl.ServerMessageImpl import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings import org.apache.activemq.artemis.spi.core.remoting.* @@ -445,7 +445,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, } fun sendResponse(remoteAddress: String?) { - val responseMessage = ServerMessageImpl(random63BitValue(), 0).apply { + val responseMessage = CoreMessage(random63BitValue(), 0).apply { putStringProperty(ipDetectResponseProperty, remoteAddress) } val routingContext = RoutingContextImpl(null) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 19e1802dc4..56d56b8746 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -32,6 +32,7 @@ import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException import org.apache.activemq.artemis.api.core.Message.* +import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -513,7 +514,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, val queueQuery = session!!.queueQuery(SimpleString(queueName)) if (!queueQuery.isExists) { log.info("Create fresh queue $queueName bound on same address") - session!!.createQueue(queueName, queueName, true) + session!!.createQueue(queueName, RoutingType.MULTICAST, queueName, true) } } } From aaf7de0d0218a5badc21589d3183a270afddaa05 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 15 Jun 2017 15:19:33 +0100 Subject: [PATCH 03/25] RPC server: buffer response messages until the client queue is fully set up. The issue arises when the server restarts, and the client is sometimes not able to recreate its queue in time, so the server is unable to send back a response message and just drops it, causing the client to hang. --- .../net/corda/client/rpc/RPCStabilityTests.kt | 78 ++------ .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 4 + .../messaging/ArtemisMessagingServer.kt | 6 + .../node/services/messaging/RPCServer.kt | 166 +++++++++++++----- .../kotlin/net/corda/testing/RPCDriver.kt | 6 + 5 files changed, 157 insertions(+), 103 deletions(-) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 83d5965028..4cf43156d2 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -5,7 +5,6 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool -import com.google.common.base.Stopwatch import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import net.corda.client.rpc.internal.RPCClient @@ -17,7 +16,6 @@ import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCKryo import net.corda.testing.* -import org.apache.activemq.artemis.ArtemisConstants import org.apache.activemq.artemis.api.core.SimpleString import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -28,8 +26,6 @@ import rx.subjects.UnicastSubject import java.time.Duration import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger -import kotlin.concurrent.thread -import kotlin.test.fail class RPCStabilityTests { @@ -218,65 +214,27 @@ class RPCStabilityTests { @Test fun `client reconnects to rebooted server`() { - // TODO: Remove multiple trials when we fix the Artemis bug (which should have its own test(s)). - if (ArtemisConstants::class.java.`package`.implementationVersion == "1.5.3") { - // The test fails maybe 1 in 100 times, so to stay green until we upgrade Artemis, retry if it fails: - for (i in (1..3)) { - try { - `client reconnects to rebooted server`(1) - } catch (e: TimeoutException) { - continue - } - return - } - fail("Test failed 3 times, which is vanishingly unlikely unless something has changed.") - } else { - // We've upgraded Artemis so make the test fail reliably, in the 2.1.0 case that takes 25 trials: - `client reconnects to rebooted server`(25) - } - } - - private fun `client reconnects to rebooted server`(trials: Int) { rpcDriver { - val coreBurner = thread { - while (!Thread.interrupted()) { - // Spin. - } + val ops = object : ReconnectOps { + override val protocolVersion = 0 + override fun ping() = "pong" } - try { - val ops = object : ReconnectOps { - override val protocolVersion = 0 - override fun ping() = "pong" - } - var serverFollower = shutdownManager.follower() - val serverPort = startRpcServer(ops = ops).getOrThrow().broker.hostAndPort!! - serverFollower.unfollow() - val clientFollower = shutdownManager.follower() - val client = startRpcClient(serverPort).getOrThrow() - clientFollower.unfollow() - assertEquals("pong", client.ping()) - val background = Executors.newSingleThreadExecutor() - (1..trials).forEach { - System.err.println("Start trial $it of $trials.") - serverFollower.shutdown() - serverFollower = shutdownManager.follower() - startRpcServer(ops = ops, customPort = serverPort).getOrThrow() - serverFollower.unfollow() - val stopwatch = Stopwatch.createStarted() - val pingFuture = background.submit(Callable { - client.ping() // Would also hang in foreground, we need it in background so we can timeout. - }) - assertEquals("pong", pingFuture.getOrThrow(10.seconds)) - System.err.println("Took ${stopwatch.elapsed(TimeUnit.MILLISECONDS)} millis.") - } - background.shutdown() // No point in the hanging case. - clientFollower.shutdown() // Driver would do this after the current server, causing 'legit' failover hang. - } finally { - with(coreBurner) { - interrupt() - join() - } + val serverFollower = shutdownManager.follower() + val serverPort = startRpcServer(ops = ops).getOrThrow().broker.hostAndPort!! + serverFollower.unfollow() + // Set retry interval to 1s to reduce test duration + val clientConfiguration = RPCClientConfiguration.default.copy(connectionRetryInterval = 1.seconds) + val clientFollower = shutdownManager.follower() + val client = startRpcClient(serverPort, configuration = clientConfiguration).getOrThrow() + clientFollower.unfollow() + assertEquals("pong", client.ping()) + serverFollower.shutdown() + startRpcServer(ops = ops, customPort = serverPort).getOrThrow() + val pingFuture = future { + client.ping() } + assertEquals("pong", pingFuture.getOrThrow(10.seconds)) + clientFollower.shutdown() // Driver would do this after the new server, causing hang. } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index b3820c154b..836315752a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -65,10 +65,14 @@ object RPCApi { val RPC_SERVER_QUEUE_NAME = "rpc.server" val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client" val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals" + val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions" val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" + val RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION = + "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_ADDED.name}' AND " + + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" data class RpcRequestId(val toLong: Long) data class ObservableId(val toLong: Long) 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 1fee1f6f2d..5b2d5f9c3c 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 @@ -200,6 +200,12 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, address = NOTIFICATIONS_ADDRESS, filter = RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION, durable = false + ), + queueConfig( + name = RPCApi.RPC_CLIENT_BINDING_ADDITIONS, + address = NOTIFICATIONS_ADDRESS, + filter = RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION, + durable = false ) ) addressesSettings = mapOf( diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index e001d4a248..1306c4206b 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -42,8 +42,8 @@ import rx.Subscription import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration -import java.util.* import java.util.concurrent.* +import kotlin.collections.ArrayList data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ @@ -91,14 +91,22 @@ class RPCServer( STARTED, FINISHED } + + private sealed class BufferOrNone { + data class Buffer(val container: MutableCollection) : BufferOrNone() + object None : BufferOrNone() + } + + private data class MessageAndContext(val message: RPCApi.ServerToClient.RpcReply, val context: ObservableContext) + private val lifeCycle = LifeCycle(State.UNSTARTED) - // The methodname->Method map to use for dispatching. + /** The methodname->Method map to use for dispatching. */ private val methodTable: Map - // The observable subscription mapping. + /** The observable subscription mapping. */ private val observableMap = createObservableSubscriptionMap() - // A mapping from client addresses to IDs of associated Observables + /** A mapping from client addresses to IDs of associated Observables */ private val clientAddressToObservables = Multimaps.synchronizedSetMultimap(HashMultimap.create()) - // The scheduled reaper handle. + /** The scheduled reaper handle. */ private var reaperScheduledFuture: ScheduledFuture<*>? = null private var observationSendExecutor: ExecutorService? = null @@ -113,13 +121,16 @@ class RPCServer( ArtemisProducer(sessionFactory, session, session.createProducer()) } private var clientBindingRemovalConsumer: ClientConsumer? = null + private var clientBindingAdditionConsumer: ClientConsumer? = null private var serverControl: ActiveMQServerControl? = null + private val responseMessageBuffer = ConcurrentHashMap() + init { val groupedMethods = ops.javaClass.declaredMethods.groupBy { it.name } groupedMethods.forEach { name, methods -> if (methods.size > 1) { - throw IllegalArgumentException("Encountered more than one method called ${name} on ${ops.javaClass.name}") + throw IllegalArgumentException("Encountered more than one method called $name on ${ops.javaClass.name}") } } methodTable = groupedMethods.mapValues { it.value.single() } @@ -155,17 +166,8 @@ class RPCServer( rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS ) - val sessions = ArrayList() - for (i in 1 .. rpcConfiguration.consumerPoolSize) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) - consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) - sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) - sessions.add(session) - } - clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) - clientBindingRemovalConsumer!!.setMessageHandler(this::bindingRemovalArtemisMessageHandler) + val sessions = createConsumerSessions() + createNotificationConsumers() serverControl = activeMqServerControl lifeCycle.transition(State.UNSTARTED, State.STARTED) // We delay the consumer session start because Artemis starts delivering messages immediately, so we need to be @@ -179,6 +181,26 @@ class RPCServer( } } + private fun createConsumerSessions(): ArrayList { + val sessions = ArrayList() + for (i in 1..rpcConfiguration.consumerPoolSize) { + val sessionFactory = serverLocator.createSessionFactory() + val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) + sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) + sessions.add(session) + } + return sessions + } + + private fun createNotificationConsumers() { + clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) + clientBindingRemovalConsumer!!.setMessageHandler(this::bindingRemovalArtemisMessageHandler) + clientBindingAdditionConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_ADDITIONS) + clientBindingAdditionConsumer!!.setMessageHandler(this::bindingAdditionArtemisMessageHandler) + } + fun close() { reaperScheduledFuture?.cancel(false) rpcExecutor?.shutdownNow() @@ -203,12 +225,38 @@ class RPCServer( invalidateClient(SimpleString(clientAddress)) } + private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) { + lifeCycle.requireState(State.STARTED) + val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE) + require(notificationType == CoreNotificationType.BINDING_ADDED.name) + val clientAddress = SimpleString(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)) + log.debug("RPC client queue created on address $clientAddress") + + val buffer = stopBuffering(clientAddress) + buffer?.let { drainBuffer(it) } + } + + /** + * Disables message buffering for [clientAddress] and returns the existing buffer + * or `null` if no requests were ever received. + */ + private fun stopBuffering(clientAddress: SimpleString): BufferOrNone.Buffer? { + return responseMessageBuffer.put(clientAddress, BufferOrNone.None) as? BufferOrNone.Buffer + } + + private fun drainBuffer(buffer: BufferOrNone.Buffer) { + buffer.container.forEach { + it.context.sendMessage(it.message) + } + } + // Note that this function operates on the *current* view of client observables. During invalidation further // Observables may be serialised and thus registered. private fun invalidateClient(clientAddress: SimpleString) { lifeCycle.requireState(State.STARTED) val observableIds = clientAddressToObservables.removeAll(clientAddress) observableMap.invalidateAll(observableIds) + responseMessageBuffer.remove(clientAddress) } private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) { @@ -221,17 +269,7 @@ class RPCServer( currentUser = getUser(artemisMessage) ) rpcExecutor!!.submit { - val result = ErrorOr.catch { - try { - CURRENT_RPC_CONTEXT.set(rpcContext) - log.debug { "Calling ${clientToServer.methodName}" } - val method = methodTable[clientToServer.methodName] ?: - throw RPCException("Received RPC for unknown method ${clientToServer.methodName} - possible client/server version skew?") - method.invoke(ops, *clientToServer.arguments.toTypedArray()) - } finally { - CURRENT_RPC_CONTEXT.remove() - } - } + val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) val resultWithExceptionUnwrapped = result.mapError { if (it is InvocationTargetException) { it.cause ?: RPCException("Caught InvocationTargetException without cause") @@ -239,21 +277,7 @@ class RPCServer( it } } - val reply = RPCApi.ServerToClient.RpcReply( - id = clientToServer.id, - result = resultWithExceptionUnwrapped - ) - val observableContext = ObservableContext( - clientToServer.id, - observableMap, - clientAddressToObservables, - clientToServer.clientAddress, - serverControl!!, - sessionAndProducerPool, - observationSendExecutor!!, - kryoPool - ) - observableContext.sendMessage(reply) + sendReply(clientToServer.id, clientToServer.clientAddress, resultWithExceptionUnwrapped) } } is RPCApi.ClientToServer.ObservablesClosed -> { @@ -263,6 +287,62 @@ class RPCServer( artemisMessage.acknowledge() } + private fun invokeRpc(rpcContext: RpcContext, methodName: String, arguments: List): ErrorOr { + return ErrorOr.catch { + try { + CURRENT_RPC_CONTEXT.set(rpcContext) + log.debug { "Calling $methodName" } + val method = methodTable[methodName] ?: + throw RPCException("Received RPC for unknown method $methodName - possible client/server version skew?") + method.invoke(ops, *arguments.toTypedArray()) + } finally { + CURRENT_RPC_CONTEXT.remove() + } + } + } + + private fun sendReply(requestId: RPCApi.RpcRequestId, clientAddress: SimpleString, resultWithExceptionUnwrapped: ErrorOr) { + val reply = RPCApi.ServerToClient.RpcReply( + id = requestId, + result = resultWithExceptionUnwrapped + ) + val observableContext = ObservableContext( + requestId, + observableMap, + clientAddressToObservables, + clientAddress, + serverControl!!, + sessionAndProducerPool, + observationSendExecutor!!, + kryoPool + ) + + val buffered = bufferIfQueueNotBound(clientAddress, reply, observableContext) + if (!buffered) observableContext.sendMessage(reply) + } + + /** + * Buffer the message if the queue at [clientAddress] is not yet bound. + * + * This can happen after server restart when the client consumer session initiates failover, + * but the client queue is not yet set up. We buffer the messages and flush the buffer only once + * we receive a notification that the client queue bindings were added. + */ + private fun bufferIfQueueNotBound(clientAddress: SimpleString, message: RPCApi.ServerToClient.RpcReply, context: ObservableContext): Boolean { + val clientBuffer = responseMessageBuffer.compute(clientAddress, { _, value -> + when (value) { + null -> BufferOrNone.Buffer(ArrayList()).apply { + container.add(MessageAndContext(message, context)) + } + is BufferOrNone.Buffer -> value.apply { + container.add(MessageAndContext(message, context)) + } + is BufferOrNone.None -> value + } + }) + return clientBuffer is BufferOrNone.Buffer + } + private fun reapSubscriptions() { observableMap.cleanUp() } diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index 925df7a4ad..326d01fb74 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -284,6 +284,12 @@ data class RPCDriverDSL( address = notificationAddress filterString = RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION isDurable = false + }, + CoreQueueConfiguration().apply { + name = RPCApi.RPC_CLIENT_BINDING_ADDITIONS + address = notificationAddress + filterString = RPCApi.RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION + isDurable = false } ) addressesSettings = mapOf( From 6b17a9a948a8fc9c64c7d9f4b5eb2fcc000f1bbc Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 22 Jun 2017 11:58:20 +0100 Subject: [PATCH 04/25] addDefaultFixes to IRS Demo to make it work (#885) --- .../kotlin/net/corda/irs/api/NodeInterestRates.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index f3791bbe64..e7c0b5ba8c 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -27,6 +27,7 @@ import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.irs.flows.RatesFixFlow +import org.apache.commons.io.IOUtils import java.math.BigDecimal import java.security.PublicKey import java.time.LocalDate @@ -90,7 +91,12 @@ object NodeInterestRates { services.myInfo.serviceIdentities(type).first(), services.myInfo.serviceIdentities(type).first().owningKey.keys.first { services.keyManagementService.keys.contains(it) }, services - ) + ) { + // Set some default fixes to the Oracle, so we can smoothly run the IRS Demo without uploading fixes. + // This is required to avoid a situation where the runnodes version of the demo isn't in a good state + // upon startup. + addDefaultFixes() + } // DOCEND 3 companion object { @@ -179,6 +185,10 @@ object NodeInterestRates { fun uploadFixes(s: String) { knownFixes = parseFile(s) } + + private fun addDefaultFixes() { + knownFixes = parseFile(IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name())) + } } // TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent) From a679a7ab0ea880ff26158029b158fb8697a79244 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 22 Jun 2017 15:09:07 +0100 Subject: [PATCH 05/25] Including test files in the IRS JAR to add the time change API to the cordapp. --- samples/irs-demo/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 33587b10da..f577151982 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -112,3 +112,7 @@ publishing { } } } + +jar { + from sourceSets.test.output +} \ No newline at end of file From 575acd2983db28b51ad36499868bb864df628c61 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 22 Jun 2017 15:57:40 +0100 Subject: [PATCH 06/25] Moves vault API information into the API section. --- docs/source/api-index.rst | 3 ++- docs/source/{vault-query.rst => api-vault.rst} | 4 ++-- docs/source/vault.rst | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) rename docs/source/{vault-query.rst => api-vault.rst} (99%) diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index b50e7be53a..bdec7b5a9e 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -6,8 +6,9 @@ This section describes the APIs that are available for the development of CorDap * :doc:`api-states` * :doc:`api-persistence` * :doc:`api-contracts` +* :doc:`api-vault` * :doc:`api-transactions` * :doc:`api-flows` * :doc:`api-core-types` -Before reading this page, you should be familiar with the key concepts of Corda: :doc:`key-concepts`. \ No newline at end of file +Before reading this page, you should be familiar with the :doc:`key concepts of Corda `. \ No newline at end of file diff --git a/docs/source/vault-query.rst b/docs/source/api-vault.rst similarity index 99% rename from docs/source/vault-query.rst rename to docs/source/api-vault.rst index 697fee22f4..d90683f8d3 100644 --- a/docs/source/vault-query.rst +++ b/docs/source/api-vault.rst @@ -1,5 +1,5 @@ -Vault Query -=========== +API: Vault +========== Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). diff --git a/docs/source/vault.rst b/docs/source/vault.rst index 33ef7f6bdc..c5a55638b6 100644 --- a/docs/source/vault.rst +++ b/docs/source/vault.rst @@ -46,7 +46,7 @@ Note the following: * the vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance) * vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. * customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing -* a :doc:`vault-query` API is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms +* a :doc:`vault query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms * a vault update API is internally used by transaction recording flows. * the vault database schemas are directly accessible via JDBC for customer joins and queries From e62a54b74d6cddee6e29a7b0822a12db7bedc47e Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 22 Jun 2017 16:13:54 +0100 Subject: [PATCH 07/25] Flow cookbook added and merged into flow API page. --- docs/source/api-flows.rst | 514 ++++++++++++++---- docs/source/building-a-cordapp-index.rst | 1 + .../java/net/corda/docs/FlowCookbookJava.java | 506 +++++++++++++++++ .../kotlin/net/corda/docs/FlowCookbook.kt | 470 ++++++++++++++++ docs/source/flow-cookbook.rst | 18 + 5 files changed, 1390 insertions(+), 119 deletions(-) create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt create mode 100644 docs/source/flow-cookbook.rst diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 7c57be9503..47676aefa3 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -9,9 +9,13 @@ API: Flows .. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-flows`. +.. contents:: + An example flow --------------- -Let's imagine a flow for agreeing a basic ledger update between Alice and Bob. This flow will have two sides: +Before we discuss the API offered by the flow, let's consider what a standard flow may look like. + +Imagine a flow for agreeing a basic ledger update between Alice and Bob. This flow will have two sides: * An ``Initiator`` side, that will initiate the request to update the ledger * A ``Responder`` side, that will respond to the request to update the ledger @@ -76,23 +80,42 @@ To respond to these actions, the responder takes the following steps: FlowLogic --------- -In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. Each ``FlowLogic`` subclass -must override ``FlowLogic.call()``, which describes the actions it will take as part of the flow. +In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. The ``FlowLogic`` +subclass's constructor can take any number of arguments of any type. The generic of ``FlowLogic`` (e.g. +``FlowLogic``) indicates the flow's return type. -So in the example above, we would have an ``Initiator`` ``FlowLogic`` subclass and a ``Responder`` ``FlowLogic`` -subclass. The actions of the initiator's side of the flow would be defined in ``Initiator.call``, and the actions -of the responder's side of the flow would be defined in ``Responder.call``. +.. container:: codeset + + .. sourcecode:: kotlin + + class Initiator(val arg1: Boolean, + val arg2: Int, + val counterparty: Party): FlowLogic() { } + + class Responder(val otherParty: Party) : FlowLogic() { } + + .. sourcecode:: java + + public static class Initiator extends FlowLogic { + private final boolean arg1; + private final int arg2; + private final Party counterparty; + + public Initiator(boolean arg1, int arg2, Party counterparty) { + this.arg1 = arg1; + this.arg2 = arg2; + this.counterparty = counterparty; + } + + } + + public static class Responder extends FlowLogic { } FlowLogic annotations -^^^^^^^^^^^^^^^^^^^^^ +--------------------- Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the ``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the -``@StartableByRPC`` annotation. - -Any flow that responds to a message from another flow must be annotated with the ``@InitiatedBy`` annotation. -``@InitiatedBy`` takes the class of the flow it is responding to as its single parameter. - -So in our example, we would have: +``@StartableByRPC`` annotation: .. container:: codeset @@ -100,74 +123,145 @@ So in our example, we would have: @InitiatingFlow @StartableByRPC - class Initiator(): FlowLogic() { - - ... - - @InitiatedBy(Initiator::class) - class Responder(val otherParty: Party) : FlowLogic() { + class Initiator(): FlowLogic() { } .. sourcecode:: java @InitiatingFlow @StartableByRPC - public static class Initiator extends FlowLogic { + public static class Initiator extends FlowLogic { } - ... +Meanwhile, any flow that responds to a message from another flow must be annotated with the ``@InitiatedBy`` annotation. +``@InitiatedBy`` takes the class of the flow it is responding to as its single parameter: + +.. container:: codeset + + .. sourcecode:: kotlin + + @InitiatedBy(Initiator::class) + class Responder(val otherParty: Party) : FlowLogic() { } + + .. sourcecode:: java @InitiatedBy(Initiator.class) - public static class Responder extends FlowLogic { + public static class Responder extends FlowLogic { } Additionally, any flow that is started by a ``SchedulableState`` must be annotated with the ``@SchedulableFlow`` annotation. -ServiceHub ----------- -Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the -various services the node provides. See :doc:`api-service-hub` for information about the services the ``ServiceHub`` -offers. +Call +---- +Each ``FlowLogic`` subclass must override ``FlowLogic.call()``, which describes the actions it will take as part of +the flow. For example, the actions of the initiator's side of the flow would be defined in ``Initiator.call``, and the +actions of the responder's side of the flow would be defined in ``Responder.call``. -Some common tasks performed using the ``ServiceHub`` are: - -* Looking up your own identity or the identity of a counterparty using the ``networkMapCache`` -* Identifying the providers of a given service (e.g. a notary service) using the ``networkMapCache`` -* Retrieving states to use in a transaction using the ``vaultService`` -* Retrieving attachments and past transactions to use in a transaction using the ``storageService`` -* Creating a timestamp using the ``clock`` -* Signing a transaction using the ``keyManagementService`` - -Common flow tasks ------------------ -There are a number of common tasks that you will need to perform within ``FlowLogic.call`` in order to agree ledger -updates. This section details the API for the most common tasks. - -Retrieving information about other nodes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We use the network map to retrieve information about other nodes on the network: +In order for nodes to be able to run multiple flows concurrently, and to allow flows to survive node upgrades and +restarts, flows need to be checkpointable and serializable to disk. This is achieved by marking ``FlowLogic.call()``, +as well as any function invoked from within ``FlowLogic.call()``, with an ``@Suspendable`` annotation. .. container:: codeset .. sourcecode:: kotlin - val networkMap = serviceHub.networkMapCache - - val allNodes = networkMap.partyNodes - val allNotaryNodes = networkMap.notaryNodes - val randomNotaryNode = networkMap.getAnyNotary() - - val alice = networkMap.getNodeByLegalName(X500Name("CN=Alice,O=Alice,L=London,C=GB")) - val bob = networkMap.getNodeByLegalIdentityKey(bobsKey) + class Initiator(val counterparty: Party): FlowLogic() { + @Suspendable + override fun call() { } + } .. sourcecode:: java - final NetworkMapCache networkMap = getServiceHub().getNetworkMapCache(); + public static class InitiatorFlow extends FlowLogic { + private final Party counterparty; - final List allNodes = networkMap.getPartyNodes(); - final List allNotaryNodes = networkMap.getNotaryNodes(); - final Party randomNotaryNode = networkMap.getAnyNotary(null); + public Initiator(Party counterparty) { + this.counterparty = counterparty; + } - final NodeInfo alice = networkMap.getNodeByLegalName(new X500Name("CN=Alice,O=Alice,L=London,C=GB")); - final NodeInfo bob = networkMap.getNodeByLegalIdentityKey(bobsKey); + @Suspendable + @Override + public Void call() throws FlowException { } + + } + +ServiceHub +---------- +Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the +various services the node provides. We will use the ``ServiceHub`` extensively in the examples that follow. You can +also see :doc:`api-service-hub` for information about the services the ``ServiceHub`` offers. + +Common flow tasks +----------------- +There are a number of common tasks that you will need to perform within ``FlowLogic.call`` in order to agree ledger +updates. This section details the API for common tasks. + +Transaction building +^^^^^^^^^^^^^^^^^^^^ +The majority of the work performed during a flow will be to build, verify and sign a transaction. We cover this in +:doc:`api-transactions`. + +Retrieving information about other nodes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We can retrieve information about other nodes on the network and the services they offer using +``ServiceHub.networkMapCache``. + +Notaries +~~~~~~~~ +Remember that a transaction generally needs a notary to: + +* Prevent double-spends if the transaction has inputs +* Serve as a timestamping authority if the transaction has a time-window + +There are several ways to retrieve a notary from the network map: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 12 + +Specific counterparties +~~~~~~~~~~~~~~~~~~~~~~~ +We can also use the network map to retrieve a specific counterparty: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 12 + +Specific services +~~~~~~~~~~~~~~~~~ +Finally, we can use the map to identify nodes providing a specific service (e.g. a regulator or an oracle): + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + :dedent: 12 Communication between parties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -180,50 +274,121 @@ Communication between parties * ``sendAndReceive(receiveType: Class, otherParty: Party, payload: Any)`` * Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back -Each ``FlowLogic`` subclass can be annotated to respond to messages from a given *counterparty* flow using the -``@InitiatedBy`` annotation. When a node first receives a message from a given ``FlowLogic.call()`` invocation, it -responds as follows: - -* The node checks whether they have a ``FlowLogic`` subclass that is registered to respond to the ``FlowLogic`` that - is sending the message: - - a. If yes, the node starts an instance of this ``FlowLogic`` by invoking ``FlowLogic.call()`` - b. Otherwise, the node ignores the message - -* The counterparty steps through their ``FlowLogic.call()`` method until they encounter a call to ``receive()``, at - which point they process the message from the initiator - -Upon calling ``receive()``/``sendAndReceive()``, the ``FlowLogic`` is suspended until it receives a response. - -UntrustworthyData -~~~~~~~~~~~~~~~~~ -``send()`` and ``sendAndReceive()`` return a payload wrapped in an ``UntrustworthyData`` instance. This is a -reminder that any data received off the wire is untrustworthy and must be verified. - -We verify the ``UntrustworthyData`` and retrieve its payload by calling ``unwrap``: +Send +~~~~ +We can send arbitrary data to a counterparty: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 12 - val partSignedTx = receive(otherParty).unwrap { partSignedTx -> - val wireTx = partSignedTx.verifySignatures(keyPair.public, notaryPubKey) - wireTx.toLedgerTransaction(serviceHub).verify() - partSignedTx - } + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + :dedent: 12 - .. sourcecode:: java +If this is the first ``send``, the counterparty will either: - final SignedTransaction partSignedTx = receive(SignedTransaction.class, otherParty) - .unwrap(tx -> { - try { - final WireTransaction wireTx = tx.verifySignatures(keyPair.getPublic(), notaryPubKey); - wireTx.toLedgerTransaction(getServiceHub()).verify(); - } catch (SignatureException ex) { - throw new FlowException(tx.getId() + " failed signature checks", ex); - } - return tx; - }); +1. Ignore the message if they are not registered to respond to messages from this flow. +2. Start the flow they have registered to respond to this flow, and run the flow until the first call to ``receive``, + at which point they process the message. In other words, we are assuming that the counterparty is registered to + respond to this flow, and has a corresponding ``receive`` call. + +Receive +~~~~~~~ +We can also wait to receive arbitrary data of a specific type from a counterparty. Again, this implies a corresponding +``send`` call in the counterparty's flow. A few scenarios: + +* We never receive a message back. In the current design, the flow is paused until the node's owner kills the flow. +* Instead of sending a message back, the counterparty throws a ``FlowException``. This exception is propagated back + to us, and we can use the error message to establish what happened. +* We receive a message back, but it's of the wrong type. In this case, a ``FlowException`` is thrown. +* We receive back a message of the correct type. All is good. + +Upon calling ``receive`` (or ``sendAndReceive``), the ``FlowLogic`` is suspended until it receives a response. + +We receive the data wrapped in an ``UntrustworthyData`` instance. This is a reminder that the data we receive may not +be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambda: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 12 + +We're not limited to sending to and receiving from a single counterparty. A flow can send messages to as many parties +as it likes, and each party can invoke a different response flow: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + :dedent: 12 + +SendAndReceive +~~~~~~~~~~~~~~ +We can also use a single call to send data to a counterparty and wait to receive data of a specific type back. The +type of data sent doesn't need to match the type of the data received back: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + :dedent: 12 + +Counterparty response +~~~~~~~~~~~~~~~~~~~~~ +Suppose we're now on the ``Responder`` side of the flow. We just received the following series of messages from the +``Initiator``: + +1. They sent us an ``Any`` instance +2. They waited to receive an ``Integer`` instance back +3. They sent a ``String`` instance and waited to receive a ``Boolean`` instance back + +Our side of the flow must mirror these calls. We could do this as follows: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + :dedent: 12 Subflows -------- @@ -236,27 +401,118 @@ Corda provides a number of built-in flows that should be used for handling commo * ``NotaryChangeFlow``, which should be used to change a state's notary These flows are designed to be used as building blocks in your own flows. You invoke them by calling -``FlowLogic.subFlow`` from within your flow's ``call`` method. Here is an example from ``TwoPartyDealFlow.kt``: +``FlowLogic.subFlow`` from within your flow's ``call`` method. Let's look at three very common examples. + +FinalityFlow +^^^^^^^^^^^^ +``FinalityFlow`` allows us to notarise the transaction and get it recorded in the vault of the participants of all +the transaction's states: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 + :start-after: DOCSTART 9 + :end-before: DOCEND 9 :dedent: 12 -In this example, we are starting a ``CollectSignaturesFlow``, passing in a partially signed transaction, and -receiving back a fully-signed version of the same transaction. + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + :dedent: 12 -Subflows in our example flow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In practice, many of the actions in our example flow would be automated using subflows: +We can also choose to send the transaction to additional parties who aren't one of the state's participants: -* Parts 2-4 of ``Initiator.call`` should be automated by invoking ``CollectSignaturesFlow`` -* Part 5 of ``Initiator.call`` should be automated by invoking ``FinalityFlow`` -* Part 1 of ``Responder.call`` should be automated by invoking ``SignTransactionFlow`` -* Part 2 of ``Responder.call`` will be handled automatically when the counterparty invokes ``FinalityFlow`` +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 10 + :end-before: DOCEND 10 + :dedent: 12 + +Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It does +**not** need to be called by each participant individually. + +CollectSignaturesFlow/SignTransactionFlow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The list of parties who need to sign a transaction is dictated by the transaction's commands. Once we've signed a +transaction ourselves, we can automatically gather the signatures of the other required signers using +``CollectSignaturesFlow``: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 15 + :end-before: DOCEND 15 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 15 + :end-before: DOCEND 15 + :dedent: 12 + +Each required signer will need to respond by invoking its own ``SignTransactionFlow`` subclass to check the +transaction and provide their signature if they are satisfied: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 16 + :end-before: DOCEND 16 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 16 + :end-before: DOCEND 16 + :dedent: 12 + +ResolveTransactionsFlow +^^^^^^^^^^^^^^^^^^^^^^^ +Verifying a transaction will also verify every transaction in the transaction's dependency chain. So if we receive a +transaction from a counterparty and it has any dependencies, we'd need to download all of these dependencies +using``ResolveTransactionsFlow`` before verifying it: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 13 + :end-before: DOCEND 13 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 13 + :end-before: DOCEND 13 + :dedent: 12 + +We can also resolve a `StateRef` dependency chain: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 14 + :end-before: DOCEND 14 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 14 + :end-before: DOCEND 14 + :dedent: 12 FlowException ------------- @@ -283,18 +539,38 @@ There are many scenarios in which throwing a ``FlowException`` would be appropri * The transaction does not match the parameters of the deal as discussed * You are reneging on a deal -Suspending flows ----------------- -In order for nodes to be able to run multiple flows concurrently, and to allow flows to survive node upgrades and -restarts, flows need to be checkpointable and serializable to disk. +ProgressTracker +--------------- +We can give our flow a progress tracker. This allows us to see the flow's progress visually in our node's CRaSH shell. -This is achieved by marking any function invoked from within ``FlowLogic.call()`` with an ``@Suspendable`` annotation. - -We can see an example in ``CollectSignaturesFlow``: +To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` in our flow: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 \ No newline at end of file + :start-after: DOCSTART 17 + :end-before: DOCEND 17 + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 17 + :end-before: DOCEND 17 + :dedent: 8 + +We then update the progress tracker's current step as we progress through the flow as follows: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 18 + :end-before: DOCEND 18 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 18 + :end-before: DOCEND 18 + :dedent: 12 \ No newline at end of file diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst index d5a3137e7d..1e6cb3114d 100644 --- a/docs/source/building-a-cordapp-index.rst +++ b/docs/source/building-a-cordapp-index.rst @@ -7,4 +7,5 @@ Building a CorDapp cordapp-overview writing-cordapps api-index + flow-cookbook cheat-sheet \ No newline at end of file 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 new file mode 100644 index 0000000000..5ac2bff1ce --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -0,0 +1,506 @@ +package net.corda.docs; + +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import net.corda.contracts.asset.Cash; +import net.corda.core.contracts.*; +import net.corda.core.contracts.TransactionType.General; +import net.corda.core.contracts.TransactionType.NotaryChange; +import net.corda.core.crypto.SecureHash; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.node.services.ServiceType; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.transactions.WireTransaction; +import net.corda.core.utilities.ProgressTracker; +import net.corda.core.utilities.ProgressTracker.Step; +import net.corda.core.utilities.UntrustworthyData; +import net.corda.flows.CollectSignaturesFlow; +import net.corda.flows.FinalityFlow; +import net.corda.flows.ResolveTransactionsFlow; +import net.corda.flows.SignTransactionFlow; +import org.bouncycastle.asn1.x500.X500Name; + +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; +import rx.Observable; + +import java.security.PublicKey; +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static net.corda.core.contracts.ContractsDSL.requireThat; +import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; + +// We group our two flows inside a singleton object to indicate that they work +// together. +public class FlowCookbookJava { + // ``InitiatorFlow`` is our first flow, and will communicate with + // ``ResponderFlow``, below. + // We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be + // started directly by the node. + @InitiatingFlow + // We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the + // node's owner to start the flow via RPC. + @StartableByRPC + // Every flow must subclass ``FlowLogic``. The generic indicates the + // flow's return type. + public static class InitiatorFlow extends FlowLogic { + + private final boolean arg1; + private final int arg2; + private final Party counterparty; + + public InitiatorFlow(boolean arg1, int arg2, Party counterparty) { + this.arg1 = arg1; + this.arg2 = arg2; + this.counterparty = counterparty; + } + + /*---------------------------------- + * WIRING UP THE PROGRESS TRACKER * + ----------------------------------*/ + // Giving our flow a progress tracker allows us to see the flow's + // progress visually in our node's CRaSH shell. + // DOCSTART 17 + private static final Step ID_OTHER_NODES = new Step("Identifying other nodes on the network."); + private static final Step SENDING_AND_RECEIVING_DATA = new Step("Sending data between parties."); + private static final Step EXTRACTING_VAULT_STATES = new Step("Extracting states from the vault."); + private static final Step OTHER_TX_COMPONENTS = new Step("Gathering a transaction's other components."); + private static final Step TX_BUILDING = new Step("Building a transaction."); + private static final Step TX_SIGNING = new Step("Signing a transaction."); + private static final Step TX_VERIFICATION = new Step("Verifying a transaction."); + private static final Step SIGS_GATHERING = new Step("Gathering a transaction's signatures.") { + // Wiring up a child progress tracker allows us to see the + // subflow's progress steps in our flow's progress tracker. + @Override public ProgressTracker childProgressTracker() { + return CollectSignaturesFlow.Companion.tracker(); + } + }; + private static final Step FINALISATION = new Step("Finalising a transaction.") { + @Override public ProgressTracker childProgressTracker() { + return FinalityFlow.Companion.tracker(); + } + }; + + private final ProgressTracker progressTracker = new ProgressTracker( + ID_OTHER_NODES, + SENDING_AND_RECEIVING_DATA, + EXTRACTING_VAULT_STATES, + OTHER_TX_COMPONENTS, + TX_BUILDING, + TX_SIGNING, + TX_VERIFICATION, + SIGS_GATHERING, + FINALISATION + ); + // DOCEND 17 + + @Suspendable + @Override + public Void call() throws FlowException { + // We'll be using a dummy public key for demonstration purposes. + // These are built in to Corda, and are generally used for writing + // tests. + PublicKey dummyPubKey = getDUMMY_PUBKEY_1(); + + /*--------------------------- + * IDENTIFYING OTHER NODES * + ---------------------------*/ + // DOCSTART 18 + progressTracker.setCurrentStep(ID_OTHER_NODES); + // DOCEND 18 + + // A transaction generally needs a notary: + // - To prevent double-spends if the transaction has inputs + // - To serve as a timestamping authority if the transaction has a time-window + // We retrieve a notary from the network map. + // DOCSTART 1 + Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(new X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK")); + Party anyNotary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + // Unlike the first two methods, ``getNotaryNodes`` returns a + // ``List``. We have to extract the notary identity of + // the node we want. + Party firstNotary = getServiceHub().getNetworkMapCache().getNotaryNodes().get(0).getNotaryIdentity(); + // DOCEND 1 + + // We may also need to identify a specific counterparty. + // Again, we do so using the network map. + // DOCSTART 2 + Party namedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalName(new X500Name("CN=NodeA,O=NodeA,L=London,C=UK")).getLegalIdentity(); + Party keyedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalIdentityKey(dummyPubKey).getLegalIdentity(); + Party firstCounterparty = getServiceHub().getNetworkMapCache().getPartyNodes().get(0).getLegalIdentity(); + // DOCEND 2 + + // Finally, we can use the map to identify nodes providing a + // specific service (e.g. a regulator or an oracle). + // DOCSTART 3 + Party regulator = getServiceHub().getNetworkMapCache().getNodesWithService(ServiceType.Companion.getRegulator()).get(0).getLegalIdentity(); + // DOCEND 3 + + /*------------------------------ + * SENDING AND RECEIVING DATA * + ------------------------------*/ + progressTracker.setCurrentStep(SENDING_AND_RECEIVING_DATA); + + // We can send arbitrary data to a counterparty. + // If this is the first ``send``, the counterparty will either: + // 1. Ignore the message if they are not registered to respond + // to messages from this flow. + // 2. Start the flow they have registered to respond to this flow, + // and run the flow until the first call to ``receive``, at + // which point they process the message. + // In other words, we are assuming that the counterparty is + // registered to respond to this flow, and has a corresponding + // ``receive`` call. + // DOCSTART 4 + send(counterparty, new Object()); + // DOCEND 4 + + // We can wait to receive arbitrary data of a specific type from a + // counterparty. Again, this implies a corresponding ``send`` call + // in the counterparty's flow. A few scenarios: + // - We never receive a message back. In the current design, the + // flow is paused until the node's owner kills the flow. + // - Instead of sending a message back, the counterparty throws a + // ``FlowException``. This exception is propagated back to us, + // and we can use the error message to establish what happened. + // - We receive a message back, but it's of the wrong type. In + // this case, a ``FlowException`` is thrown. + // - We receive back a message of the correct type. All is good. + // + // Upon calling ``receive()`` (or ``sendAndReceive()``), the + // ``FlowLogic`` is suspended until it receives a response. + // + // We receive the data wrapped in an ``UntrustworthyData`` + // instance. This is a reminder that the data we receive may not + // be what it appears to be! We must unwrap the + // ``UntrustworthyData`` using a lambda. + // DOCSTART 5 + UntrustworthyData packet1 = receive(Integer.class, counterparty); + Integer integer = packet1.unwrap(data -> { + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + return data; + }); + // DOCEND 5 + + // We can also use a single call to send data to a counterparty + // and wait to receive data of a specific type back. The type of + // data sent doesn't need to match the type of the data received + // back. + // DOCSTART 7 + UntrustworthyData packet2 = sendAndReceive(Boolean.class, counterparty, "You can send and receive any class!"); + Boolean bool = packet2.unwrap(data -> { + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + return data; + }); + // DOCEND 7 + + // We're not limited to sending to and receiving from a single + // counterparty. A flow can send messages to as many parties as it + // likes, and each party can invoke a different response flow. + // DOCSTART 6 + send(regulator, new Object()); + UntrustworthyData packet3 = receive(Object.class, regulator); + // DOCEND 6 + + /*------------------------------------ + * EXTRACTING STATES FROM THE VAULT * + ------------------------------------*/ + progressTracker.setCurrentStep(EXTRACTING_VAULT_STATES); + + // Let's assume there are already some ``DummyState``s in our + // node's vault, stored there as a result of running past flows, + // and we want to consume them in a transaction. There are many + // ways to extract these states from our vault. + + // For example, we would extract any unconsumed ``DummyState``s + // from our vault as follows: + Vault.StateStatus status = Vault.StateStatus.UNCONSUMED; + Set> dummyStateTypes = new HashSet<>(ImmutableList.of(DummyState.class)); + + VaultQueryCriteria criteria = new VaultQueryCriteria(status, null, dummyStateTypes); + Vault.Page results = getServiceHub().getVaultService().queryBy(criteria); + + List> dummyStates = results.getStates(); + + // For a full list of the available ways of extracting states from + // the vault, see the Vault Query docs page. + + // When building a transaction, input states are passed in as + // ``StateRef`` instances, which pair the hash of the transaction + // that generated the state with the state's index in the outputs + // of that transaction. + StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0); + // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef); + + /*------------------------------------------ + * GATHERING OTHER TRANSACTION COMPONENTS * + ------------------------------------------*/ + progressTracker.setCurrentStep(OTHER_TX_COMPONENTS); + + // Output states are constructed from scratch. + DummyState ourOutput = new DummyState(); + // Or as copies of other states with some properties changed. + DummyState ourOtherOutput = ourOutput.copy(77); + + // Commands pair a ``CommandData`` instance with a list of + // public keys. To be valid, the transaction requires a signature + // matching every public key in all of the transaction's commands. + CommandData commandData = new DummyContract.Commands.Create(); + PublicKey ourPubKey = getServiceHub().getLegalIdentityKey(); + PublicKey counterpartyPubKey = counterparty.getOwningKey(); + List requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey); + Command ourCommand = new Command(commandData, requiredSigners); + + // ``CommandData`` can either be: + // 1. Of type ``TypeOnlyCommandData``, in which case it only + // serves to attach signers to the transaction and possibly + // fork the contract's verification logic. + TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create(); + // 2. Include additional data which can be used by the contract + // during verification, alongside fulfilling the roles above + CommandData commandDataWithData = new Cash.Commands.Issue(12345678); + + // Attachments are identified by their hash. + // The attachment with the corresponding hash must have been + // uploaded ahead of time via the node's RPC interface. + SecureHash ourAttachment = SecureHash.sha256("DummyAttachment"); + + // Time windows can have a start and end time, or be open at either end. + TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX); + TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN); + TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX); + + /*------------------------ + * TRANSACTION BUILDING * + ------------------------*/ + progressTracker.setCurrentStep(TX_BUILDING); + + // There are two types of transaction (notary-change and general), + // and therefore two types of transaction builder: + TransactionBuilder notaryChangeTxBuilder = new TransactionBuilder(NotaryChange.INSTANCE, specificNotary); + TransactionBuilder regTxBuilder = new TransactionBuilder(General.INSTANCE, specificNotary); + + // We add items to the transaction builder using ``TransactionBuilder.withItems``: + regTxBuilder.withItems( + // Inputs, as ``StateRef``s that reference to the outputs of previous transactions + ourStateRef, + // Outputs, as ``ContractState``s + ourOutput, + // Commands, as ``Command``s + ourCommand + ); + + // We can also add items using methods for the individual components: + regTxBuilder.addInputState(ourStateAndRef); + regTxBuilder.addOutputState(ourOutput); + regTxBuilder.addCommand(ourCommand); + regTxBuilder.addAttachment(ourAttachment); + regTxBuilder.addTimeWindow(ourTimeWindow); + + /*----------------------- + * TRANSACTION SIGNING * + -----------------------*/ + progressTracker.setCurrentStep(TX_SIGNING); + + // We finalise the transaction by signing it, + // converting it into a ``SignedTransaction``. + SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(regTxBuilder); + + // If instead this was a ``SignedTransaction`` that we'd received + // from a counterparty and we needed to sign it, we would add our + // signature using: + SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx, dummyPubKey); + + /*---------------------------- + * TRANSACTION VERIFICATION * + ----------------------------*/ + progressTracker.setCurrentStep(TX_VERIFICATION); + + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain. So if this was a + // transaction we'd received from a counterparty and it had any + // dependencies, we'd need to download all of these dependencies + // using``ResolveTransactionsFlow`` before verifying it. + // DOCSTART 13 + subFlow(new ResolveTransactionsFlow(twiceSignedTx, counterparty)); + // DOCEND 13 + + // We can also resolve a `StateRef` dependency chain. + // DOCSTART 14 + subFlow(new ResolveTransactionsFlow(ImmutableSet.of(ourStateRef.getTxhash()), counterparty)); + // DOCEND 14 + + // We verify a transaction using the following one-liner: + twiceSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); + + // Let's break that down... + + // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` + // with signatures over this ``WireTransaction``. We don't verify + // a signed transaction per se, but rather the ``WireTransaction`` + // it contains. + WireTransaction wireTx = twiceSignedTx.getTx(); + // Before we can verify the transaction, we need the + // ``ServiceHub`` to use our node's local storage to resolve the + // transaction's inputs and attachments into actual objects, + // rather than just references. We do this by converting the + // ``WireTransaction`` into a ``LedgerTransaction``. + LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub()); + // We can now verify the transaction. + ledgerTx.verify(); + + // We'll often want to perform our own additional verification + // too. Just because a transaction is valid based on the contract + // rules and requires our signature doesn't mean we have to + // sign it! We need to make sure the transaction represents an + // agreement we actually want to enter into. + DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData(); + if (outputState.getMagicNumber() != 777) { + throw new FlowException("We expected a magic number of 777."); + } + + // Of course, if you are not a required signer on the transaction, + // you have no power to decide whether it is valid or not. If it + // requires signatures from all the required signers and is + // contractually valid, it's a valid ledger update. + + /*------------------------ + * GATHERING SIGNATURES * + ------------------------*/ + progressTracker.setCurrentStep(SIGS_GATHERING); + + // The list of parties who need to sign a transaction is dictated + // by the transaction's commands. Once we've signed a transaction + // ourselves, we can automatically gather the signatures of the + // other required signers using ``CollectSignaturesFlow``. + // The responder flow will need to call ``SignTransactionFlow``. + // DOCSTART 15 + SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())); + // DOCEND 15 + + /*------------------------------ + * FINALISING THE TRANSACTION * + ------------------------------*/ + progressTracker.setCurrentStep(FINALISATION); + + // We notarise the transaction and get it recorded in the vault of + // the participants of all the transaction's states. + // DOCSTART 9 + SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).get(0); + // DOCEND 9 + // We can also choose to send it to additional parties who aren't one + // of the state's participants. + // DOCSTART 10 + Set additionalParties = ImmutableSet.of(regulator, regulator); + SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(ImmutableList.of(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).get(0); + // DOCEND 10 + + return null; + } + } + + // ``ResponderFlow`` is our second flow, and will communicate with + // ``InitiatorFlow``. + // We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it + // can only be started in response to a message from its initiating flow. + // That's ``InitiatorFlow`` in this case. + // Each node also has several flow pairs registered by default - see + // ``AbstractNode.installCoreFlows``. + @InitiatedBy(InitiatorFlow.class) + public static class ResponderFlow extends FlowLogic { + + private final Party counterparty; + + public ResponderFlow(Party counterparty) { + this.counterparty = counterparty; + } + + private static final Step RECEIVING_AND_SENDING_DATA = new Step("Sending data between parties."); + private static final Step SIGNING = new Step("Responding to CollectSignaturesFlow."); + private static final Step FINALISATION = new Step("Finalising a transaction."); + + private final ProgressTracker progressTracker = new ProgressTracker( + RECEIVING_AND_SENDING_DATA, + SIGNING, + FINALISATION + ); + + @Suspendable + @Override + public Void call() throws FlowException { + // The ``ResponderFlow` has all the same APIs available. It looks + // up network information, sends and receives data, and constructs + // transactions in exactly the same way. + + /*------------------------------ + * SENDING AND RECEIVING DATA * + -----------------------------*/ + progressTracker.setCurrentStep(RECEIVING_AND_SENDING_DATA); + + // We need to respond to the messages sent by the initiator: + // 1. They sent us an ``Any`` instance + // 2. They waited to receive an ``Integer`` instance back + // 3. They sent a ``String`` instance and waited to receive a + // ``Boolean`` instance back + // Our side of the flow must mirror these calls. + // DOCSTART 8 + Object obj = receive(Object.class, counterparty).unwrap(data -> data); + String string = sendAndReceive(String.class, counterparty, 99).unwrap(data -> data); + send(counterparty, true); + // DOCEND 8 + + /*----------------------------------------- + * RESPONDING TO COLLECT_SIGNATURES_FLOW * + -----------------------------------------*/ + progressTracker.setCurrentStep(SIGNING); + + // The responder will often need to respond to a call to + // ``CollectSignaturesFlow``. It does so my invoking its own + // ``SignTransactionFlow`` subclass. + // DOCSTART 16 + class SignTxFlow extends SignTransactionFlow { + private SignTxFlow(Party otherParty, ProgressTracker progressTracker) { + super(otherParty, progressTracker); + } + + @Override + protected void checkTransaction(SignedTransaction stx) { + requireThat(require -> { + // Any additional checking we see fit... + DummyState outputState = (DummyState) stx.getTx().getOutputs().get(0).getData(); + assert (outputState.getMagicNumber() == 777); + return null; + }); + } + } + + subFlow(new SignTxFlow(counterparty, SignTransactionFlow.Companion.tracker())); + // DOCEND 16 + + /*------------------------------ + * FINALISING THE TRANSACTION * + ------------------------------*/ + progressTracker.setCurrentStep(FINALISATION); + + // Nothing to do here! As long as some other party calls + // ``FinalityFlow``, the recording of the transaction on our node + // we be handled automatically. + + return null; + } + } +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt new file mode 100644 index 0000000000..edc75fbb0e --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -0,0 +1,470 @@ +package net.corda.docs + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash +import net.corda.core.contracts.* +import net.corda.core.contracts.TransactionType.General +import net.corda.core.contracts.TransactionType.NotaryChange +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.node.services.ServiceType +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.* +import net.corda.core.utilities.ProgressTracker.Step +import net.corda.flows.CollectSignaturesFlow +import net.corda.flows.FinalityFlow +import net.corda.flows.ResolveTransactionsFlow +import net.corda.flows.SignTransactionFlow +import org.bouncycastle.asn1.x500.X500Name +import java.security.PublicKey +import java.time.Instant + +// We group our two flows inside a singleton object to indicate that they work +// together. +object FlowCookbook { + // ``InitiatorFlow`` is our first flow, and will communicate with + // ``ResponderFlow``, below. + // We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be + // started directly by the node. + @InitiatingFlow + // We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the + // node's owner to start the flow via RPC. + @StartableByRPC + // Every flow must subclass ``FlowLogic``. The generic indicates the + // flow's return type. + class InitiatorFlow(val arg1: Boolean, val arg2: Int, val counterparty: Party) : FlowLogic() { + + /**--------------------------------- + * WIRING UP THE PROGRESS TRACKER * + ---------------------------------**/ + // Giving our flow a progress tracker allows us to see the flow's + // progress visually in our node's CRaSH shell. + // DOCSTART 17 + companion object { + object ID_OTHER_NODES : Step("Identifying other nodes on the network.") + object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.") + object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.") + object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.") + object TX_BUILDING : Step("Building a transaction.") + object TX_SIGNING : Step("Signing a transaction.") + object TX_VERIFICATION : Step("Verifying a transaction.") + object SIGS_GATHERING : Step("Gathering a transaction's signatures.") { + // Wiring up a child progress tracker allows us to see the + // subflow's progress steps in our flow's progress tracker. + override fun childProgressTracker() = CollectSignaturesFlow.tracker() + } + object FINALISATION : Step("Finalising a transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker( + ID_OTHER_NODES, + SENDING_AND_RECEIVING_DATA, + EXTRACTING_VAULT_STATES, + OTHER_TX_COMPONENTS, + TX_BUILDING, + TX_SIGNING, + TX_VERIFICATION, + SIGS_GATHERING, + FINALISATION + ) + } + // DOCEND 17 + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call() { + // We'll be using a dummy public key for demonstration purposes. + // These are built in to Corda, and are generally used for writing + // tests. + val dummyPubKey: PublicKey = DUMMY_PUBKEY_1 + + /**-------------------------- + * IDENTIFYING OTHER NODES * + --------------------------**/ + // DOCSTART 18 + progressTracker.currentStep = ID_OTHER_NODES + // DOCEND 18 + + // A transaction generally needs a notary: + // - To prevent double-spends if the transaction has inputs + // - To serve as a timestamping authority if the transaction has a time-window + // We retrieve the notary from the network map. + // DOCSTART 1 + val specificNotary: Party? = serviceHub.networkMapCache.getNotary(X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK")) + val anyNotary: Party? = serviceHub.networkMapCache.getAnyNotary() + // Unlike the first two methods, ``getNotaryNodes`` returns a + // ``List``. We have to extract the notary identity of + // the node we want. + val firstNotary: Party = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity + // DOCEND 1 + + // We may also need to identify a specific counterparty. Again, we + // do so using the network map. + // DOCSTART 2 + val namedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalName(X500Name("CN=NodeA,O=NodeA,L=London,C=UK"))?.legalIdentity + val keyedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalIdentityKey(dummyPubKey)?.legalIdentity + val firstCounterparty: Party = serviceHub.networkMapCache.partyNodes[0].legalIdentity + // DOCEND 2 + + // Finally, we can use the map to identify nodes providing a + // specific service (e.g. a regulator or an oracle). + // DOCSTART 3 + val regulator: Party = serviceHub.networkMapCache.getNodesWithService(ServiceType.regulator)[0].legalIdentity + // DOCEND 3 + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = SENDING_AND_RECEIVING_DATA + + // We can send arbitrary data to a counterparty. + // If this is the first ``send``, the counterparty will either: + // 1. Ignore the message if they are not registered to respond + // to messages from this flow. + // 2. Start the flow they have registered to respond to this flow, + // and run the flow until the first call to ``receive``, at + // which point they process the message. + // In other words, we are assuming that the counterparty is + // registered to respond to this flow, and has a corresponding + // ``receive`` call. + // DOCSTART 4 + send(counterparty, Any()) + // DOCEND 4 + + // We can wait to receive arbitrary data of a specific type from a + // counterparty. Again, this implies a corresponding ``send`` call + // in the counterparty's flow. A few scenarios: + // - We never receive a message back. In the current design, the + // flow is paused until the node's owner kills the flow. + // - Instead of sending a message back, the counterparty throws a + // ``FlowException``. This exception is propagated back to us, + // and we can use the error message to establish what happened. + // - We receive a message back, but it's of the wrong type. In + // this case, a ``FlowException`` is thrown. + // - We receive back a message of the correct type. All is good. + // + // Upon calling ``receive()`` (or ``sendAndReceive()``), the + // ``FlowLogic`` is suspended until it receives a response. + // + // We receive the data wrapped in an ``UntrustworthyData`` + // instance. This is a reminder that the data we receive may not + // be what it appears to be! We must unwrap the + // ``UntrustworthyData`` using a lambda. + // DOCSTART 5 + val packet1: UntrustworthyData = receive(counterparty) + val int: Int = packet1.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data + } + // DOCEND 5 + + // We can also use a single call to send data to a counterparty + // and wait to receive data of a specific type back. The type of + // data sent doesn't need to match the type of the data received + // back. + // DOCSTART 7 + val packet2: UntrustworthyData = sendAndReceive(counterparty, "You can send and receive any class!") + val boolean: Boolean = packet2.unwrap { data -> + // Perform checking on the object received. + // T O D O: Check the received object. + // Return the object. + data + } + // DOCEND 7 + + // We're not limited to sending to and receiving from a single + // counterparty. A flow can send messages to as many parties as it + // likes, and each party can invoke a different response flow. + // DOCSTART 6 + send(regulator, Any()) + val packet3: UntrustworthyData = receive(regulator) + // DOCEND 6 + + /**----------------------------------- + * EXTRACTING STATES FROM THE VAULT * + -----------------------------------**/ + progressTracker.currentStep = EXTRACTING_VAULT_STATES + + // Let's assume there are already some ``DummyState``s in our + // node's vault, stored there as a result of running past flows, + // and we want to consume them in a transaction. There are many + // ways to extract these states from our vault. + + // For example, we would extract any unconsumed ``DummyState``s + // from our vault as follows: + val criteria = QueryCriteria.VaultQueryCriteria() // default is UNCONSUMED + val results: Vault.Page = serviceHub.vaultService.queryBy(criteria) + val dummyStates: List> = results.states + + // For a full list of the available ways of extracting states from + // the vault, see the Vault Query docs page. + + // When building a transaction, input states are passed in as + // ``StateRef`` instances, which pair the hash of the transaction + // that generated the state with the state's index in the outputs + // of that transaction. + val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) + // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) + + /**----------------------------------------- + * GATHERING OTHER TRANSACTION COMPONENTS * + -----------------------------------------**/ + progressTracker.currentStep = OTHER_TX_COMPONENTS + + // Output states are constructed from scratch. + val ourOutput: DummyState = DummyState() + // Or as copies of other states with some properties changed. + val ourOtherOutput: DummyState = ourOutput.copy(magicNumber = 77) + + // Commands pair a ``CommandData`` instance with a list of + // public keys. To be valid, the transaction requires a signature + // matching every public key in all of the transaction's commands. + val commandData: CommandData = DummyContract.Commands.Create() + val ourPubKey: PublicKey = serviceHub.legalIdentityKey + val counterpartyPubKey: PublicKey = counterparty.owningKey + val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) + val ourCommand: Command = Command(commandData, requiredSigners) + + // ``CommandData`` can either be: + // 1. Of type ``TypeOnlyCommandData``, in which case it only + // serves to attach signers to the transaction and possibly + // fork the contract's verification logic. + val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() + // 2. Include additional data which can be used by the contract + // during verification, alongside fulfilling the roles above + val commandDataWithData: CommandData = Cash.Commands.Issue(nonce = 12345678) + + // Attachments are identified by their hash. + // The attachment with the corresponding hash must have been + // uploaded ahead of time via the node's RPC interface. + val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") + + // Time windows can have a start and end time, or be open at either end. + val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) + val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) + val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) + + /**----------------------- + * TRANSACTION BUILDING * + -----------------------**/ + progressTracker.currentStep = TX_BUILDING + + // There are two types of transaction (notary-change and general), + // and therefore two types of transaction builder: + val notaryChangeTxBuilder: TransactionBuilder = TransactionBuilder(NotaryChange, specificNotary) + val regTxBuilder: TransactionBuilder = TransactionBuilder(General, specificNotary) + + // We add items to the transaction builder using ``TransactionBuilder.withItems``: + regTxBuilder.withItems( + // Inputs, as ``StateRef``s that reference to the outputs of previous transactions + ourStateRef, + // Outputs, as ``ContractState``s + ourOutput, + // Commands, as ``Command``s + ourCommand + ) + + // We can also add items using methods for the individual components: + regTxBuilder.addInputState(ourStateAndRef) + regTxBuilder.addOutputState(ourOutput) + regTxBuilder.addCommand(ourCommand) + regTxBuilder.addAttachment(ourAttachment) + regTxBuilder.addTimeWindow(ourTimeWindow) + + /**---------------------- + * TRANSACTION SIGNING * + ----------------------**/ + progressTracker.currentStep = TX_SIGNING + + // We finalise the transaction by signing it, converting it into a + // ``SignedTransaction``. + val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder) + + // If instead this was a ``SignedTransaction`` that we'd received + // from a counterparty and we needed to sign it, we would add our + // signature using: + val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx, dummyPubKey) + + // In practice, however, the process of gathering every signature + // but the first can be automated using ``CollectSignaturesFlow``. + // See the "Gathering Signatures" section below. + + /**--------------------------- + * TRANSACTION VERIFICATION * + ---------------------------**/ + progressTracker.currentStep = TX_VERIFICATION + + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain. So if this was a + // transaction we'd received from a counterparty and it had any + // dependencies, we'd need to download all of these dependencies + // using``ResolveTransactionsFlow`` before verifying it. + // DOCSTART 13 + subFlow(ResolveTransactionsFlow(twiceSignedTx, counterparty)) + // DOCEND 13 + + // We can also resolve a `StateRef` dependency chain. + // DOCSTART 14 + subFlow(ResolveTransactionsFlow(setOf(ourStateRef.txhash), counterparty)) + // DOCEND 14 + + // We verify a transaction using the following one-liner: + twiceSignedTx.tx.toLedgerTransaction(serviceHub).verify() + + // Let's break that down... + + // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` + // with signatures over this ``WireTransaction``. We don't verify + // a signed transaction per se, but rather the ``WireTransaction`` + // it contains. + val wireTx: WireTransaction = twiceSignedTx.tx + // Before we can verify the transaction, we need the + // ``ServiceHub`` to use our node's local storage to resolve the + // transaction's inputs and attachments into actual objects, + // rather than just references. We do this by converting the + // ``WireTransaction`` into a ``LedgerTransaction``. + val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub) + // We can now verify the transaction. + ledgerTx.verify() + + // We'll often want to perform our own additional verification + // too. Just because a transaction is valid based on the contract + // rules and requires our signature doesn't mean we have to + // sign it! We need to make sure the transaction represents an + // agreement we actually want to enter into. + val outputState: DummyState = wireTx.outputs.single().data as DummyState + if (outputState.magicNumber == 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. + throw FlowException("We expected a magic number of 777.") + } + + // Of course, if you are not a required signer on the transaction, + // you have no power to decide whether it is valid or not. If it + // requires signatures from all the required signers and is + // contractually valid, it's a valid ledger update. + + /**----------------------- + * GATHERING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = SIGS_GATHERING + + // The list of parties who need to sign a transaction is dictated + // by the transaction's commands. Once we've signed a transaction + // ourselves, we can automatically gather the signatures of the + // other required signers using ``CollectSignaturesFlow``. + // The responder flow will need to call ``SignTransactionFlow``. + // DOCSTART 15 + val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())) + // DOCEND 15 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // We notarise the transaction and get it recorded in the vault of + // the participants of all the transaction's states. + // DOCSTART 9 + val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).single() + // DOCEND 9 + // We can also choose to send it to additional parties who aren't one + // of the state's participants. + // DOCSTART 10 + val additionalParties: Set = setOf(regulator) + val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(listOf(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).single() + // DOCEND 10 + } + } + + // ``ResponderFlow`` is our second flow, and will communicate with + // ``InitiatorFlow``. + // We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it + // can only be started in response to a message from its initiating flow. + // That's ``InitiatorFlow`` in this case. + // Each node also has several flow pairs registered by default - see + // ``AbstractNode.installCoreFlows``. + @InitiatedBy(InitiatorFlow::class) + class ResponderFlow(val counterparty: Party) : FlowLogic() { + + companion object { + object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.") + object SIGNING : Step("Responding to CollectSignaturesFlow.") + object FINALISATION : Step("Finalising a transaction.") + + fun tracker() = ProgressTracker( + RECEIVING_AND_SENDING_DATA, + SIGNING, + FINALISATION + ) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call() { + // The ``ResponderFlow` has all the same APIs available. It looks + // up network information, sends and receives data, and constructs + // transactions in exactly the same way. + + /**----------------------------- + * SENDING AND RECEIVING DATA * + -----------------------------**/ + progressTracker.currentStep = RECEIVING_AND_SENDING_DATA + + // We need to respond to the messages sent by the initiator: + // 1. They sent us an ``Any`` instance + // 2. They waited to receive an ``Integer`` instance back + // 3. They sent a ``String`` instance and waited to receive a + // ``Boolean`` instance back + // Our side of the flow must mirror these calls. + // DOCSTART 8 + val any: Any = receive(counterparty).unwrap { data -> data } + val string: String = sendAndReceive(counterparty, 99).unwrap { data -> data } + send(counterparty, true) + // DOCEND 8 + + /**---------------------------------------- + * RESPONDING TO COLLECT_SIGNATURES_FLOW * + ----------------------------------------**/ + progressTracker.currentStep = SIGNING + + // The responder will often need to respond to a call to + // ``CollectSignaturesFlow``. It does so my invoking its own + // ``SignTransactionFlow`` subclass. + // DOCSTART 16 + val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterparty) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + // Any additional checking we see fit... + val outputState = stx.tx.outputs.single().data as DummyState + assert(outputState.magicNumber == 777) + } + } + + subFlow(signTransactionFlow) + // DOCEND 16 + + /**----------------------------- + * FINALISING THE TRANSACTION * + -----------------------------**/ + progressTracker.currentStep = FINALISATION + + // Nothing to do here! As long as some other party calls + // ``FinalityFlow``, the recording of the transaction on our node + // we be handled automatically. + } + } +} \ No newline at end of file diff --git a/docs/source/flow-cookbook.rst b/docs/source/flow-cookbook.rst new file mode 100644 index 0000000000..1402c2ea1c --- /dev/null +++ b/docs/source/flow-cookbook.rst @@ -0,0 +1,18 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Flow cookbook +============= + +This flow showcases how to use Corda's API, in both Java and Kotlin. + +.. container:: codeset + + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + + .. literalinclude:: example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java \ No newline at end of file From 48bb191ea25ceebd3fb8861d27182656fe4cb326 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 23 Jun 2017 10:17:28 +0100 Subject: [PATCH 08/25] Fix the corda logo, protect it from IDE formatter. (#882) --- .../main/kotlin/net/corda/node/internal/NodeStartup.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 3899c9bd85..095318a92f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -313,11 +313,11 @@ open class NodeStartup(val args: Array) { messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}" val (msg1, msg2) = messages.randomOrNull()!!.split('\n') - println(Ansi.ansi().fgBrightRed().a(""" -______ __ -/ ____/ _________/ /___ _ -/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a( - "/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a( + println(Ansi.ansi().newline().fgBrightRed().a( + """ ______ __""").newline().a( + """ / ____/ _________/ /___ _""").newline().a( + """ / / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a( + """/ /___ /_/ / / / /_/ / /_/ / """).fgBrightBlue().a(msg2).newline().fgBrightRed().a( """\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold(). a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------"). newline(). From 5233c50098625c28202f4b80bd284b81c9c1ea05 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 23 Jun 2017 10:17:49 +0100 Subject: [PATCH 09/25] More specific message when a corda service fails to instantiate. (#901) --- .../net/corda/node/internal/AbstractNode.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) 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 8c1a680dea..c0d7053e62 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -65,6 +65,7 @@ import org.jetbrains.exposed.sql.Database import org.slf4j.Logger import rx.Observable import java.io.IOException +import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier.* import java.net.JarURLConnection import java.net.URI @@ -273,6 +274,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return this } + private class ServiceInstantiationException(cause: Throwable?) : Exception(cause) + private fun installCordaServices(scanResult: ScanResult) { fun getServiceType(clazz: Class<*>): ServiceType? { return try { @@ -300,6 +303,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } catch (e: NoSuchMethodException) { log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " + "of type ${PluginServiceHub::class.java.name}") + } catch (e: ServiceInstantiationException) { + log.error("Corda service ${it.name} failed to instantiate", e.cause) } catch (e: Exception) { log.error("Unable to install Corda service ${it.name}", e) } @@ -310,13 +315,17 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Use this method to install your Corda services in your tests. This is automatically done by the node when it * starts up for all classes it finds which are annotated with [CordaService]. */ - fun installCordaService(clazz: Class): T { - clazz.requireAnnotation() - val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true } - val service = ctor.newInstance(services) - cordappServices.putInstance(clazz, service) + fun installCordaService(serviceClass: Class): T { + serviceClass.requireAnnotation() + val constructor = serviceClass.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true } + val service = try { + constructor.newInstance(services) + } catch (e: InvocationTargetException) { + throw ServiceInstantiationException(e.cause) + } + cordappServices.putInstance(serviceClass, service) smm.tokenizableServices += service - log.info("Installed ${clazz.name} Corda service") + log.info("Installed ${serviceClass.name} Corda service") return service } From 7e34d7b05a170aa6de289dbb192755fefdf40014 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 23 Jun 2017 10:44:26 +0100 Subject: [PATCH 10/25] SIMM demo Ack CordaSerializable (#911) --- .../src/main/kotlin/net/corda/vega/flows/SimmFlow.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index 9553a5347e..331f4b2b96 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -297,7 +297,7 @@ object SimmFlow { @Suspendable private fun updatePortfolio(portfolio: Portfolio) { logger.info("Handshake finished, awaiting Simm update") - send(replyToParty, Ack) // Hack to state that this party is ready + send(replyToParty, Ack) // Hack to state that this party is ready. subFlow(object : StateRevisionFlow.Receiver(replyToParty) { override fun verifyProposal(proposal: Proposal) { super.verifyProposal(proposal) @@ -320,5 +320,6 @@ object SimmFlow { } } + @CordaSerializable private object Ack } From cf5b089412ffa95bedc556b6b397276a15acfdb8 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 23 Jun 2017 11:25:49 +0100 Subject: [PATCH 11/25] Make 2 tests no longer depend on working directory (#893) --- .../kotlin/net/corda/node/BootTests.kt | 4 ++-- .../net/corda/testing/driver/DriverTests.kt | 4 ++-- .../kotlin/net/corda/testing/ProjectStructure.kt | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 86ed4bd3af..1bae088182 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -11,12 +11,12 @@ import net.corda.testing.driver.driver import net.corda.node.internal.NodeStartup import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User +import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import java.io.* import java.nio.file.Files -import java.nio.file.Paths import kotlin.test.assertEquals class BootTests { @@ -33,7 +33,7 @@ class BootTests { @Test fun `double node start doesn't write into log file`() { - val logConfigFile = Paths.get("..", "config", "dev", "log4j2.xml").toAbsolutePath() + val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { val alice = startNode(ALICE.name).get() diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 40079f3bdf..5ac11f2139 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -13,9 +13,9 @@ import net.corda.node.internal.NodeStartup import net.corda.node.services.api.RegulatorService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.ArtemisMessagingComponent +import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.nio.file.Paths import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -70,7 +70,7 @@ class DriverTests { @Test fun `debug mode enables debug logging level`() { // Make sure we're using the log4j2 config which writes to the log file - val logConfigFile = Paths.get("..", "config", "dev", "log4j2.xml").toAbsolutePath() + val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { val baseDirectory = startNode(DUMMY_BANK_A.name).getOrThrow().configuration.baseDirectory diff --git a/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt b/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt new file mode 100644 index 0000000000..3e866fdf4e --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt @@ -0,0 +1,16 @@ +package net.corda.testing + +import net.corda.core.div +import net.corda.core.isDirectory +import java.nio.file.Path +import java.nio.file.Paths + +object ProjectStructure { + val projectRootDir: Path = run { + var dir = Paths.get(javaClass.getResource("/").toURI()) + while (!(dir / ".git").isDirectory()) { + dir = dir.parent + } + dir + } +} From 47eee88f373d7189bf33466e8325e736c5c3e1cb Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 23 Jun 2017 12:40:06 +0100 Subject: [PATCH 12/25] Release notes for M13 (#910) --- docs/source/release-notes.rst | 45 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 0251c6907f..6ea371d138 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,9 +6,48 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- -Certificate checks have been enabled for much of the identity service, with additional checks coming targetted at M13. -These are part of the confidential identities work, and ensure that parties are actually who they claim to be by checking -their certificate path back to the network trust root (certificate authority). +Milestone 13 +------------ + +Following our first public beta in M12, this release continues the work on API stability and user friendliness. Apart +from bug fixes and code refactoring, there are also significant improvements in the Vault Query and the +Identity Service (for more detailed information about what has changed, see :doc:`changelog`). +More specifically: + +The long awaited new **Vault Query** service makes its debut in this release and provides advanced vault query +capabilities using criteria specifications (see ``QueryCriteria``), sorting, and pagination. Criteria specifications +enable selective filtering with and/or composition using multiple operator primitives on standard attributes stored in +Corda internal vault tables (eg. vault_states, vault_fungible_states, vault_linear_states), and also on custom contract +state schemas defined by CorDapp developers when modelling new contract types. Custom queries are specifiable using a +simple but sophisticated builder DSL (see ``QueryCriteriaUtils``). The new Vault Query service is usable by flows and by +RPC clients alike via two simple API functions: ``queryBy()`` and ``trackBy()``. The former provides point-in-time +snapshot queries whilst the later supplements the snapshot with dynamic streaming of updates. +See :doc:`vault-query` for full details. + +We have written a comprehensive Hello, World! tutorial, showing developers how to build a CorDapp from start +to finish. The tutorial shows how the core elements of a CorDapp - states, contracts and flows - fit together +to allow your node to handle new business processes. It also explains how you can use our contract and +flow testing frameworks to massively reduce CorDapp development time. + +Certificate checks have been enabled for much of the identity service. These are part of the confidential (anonymous) +identities work, and ensure that parties are actually who they claim to be by checking their certificate path back to +the network trust root (certificate authority). + +To deal with anonymized keys, we've also implemented a deterministic key derivation function that combines logic +from the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) protocol and the BIP32 hardened +parent-private-key -> child-private-key scheme. This function currently supports the following algorithms: +ECDSA secp256K1, ECDSA secpR1 (NIST P-256) and EdDSA ed25519. We are now very close to fully supporting anonymous +identities so as to increase privacy even against validating notaries. + +We have further tightened the set of objects which Corda will attempt to serialise from the stack during flow +checkpointing. As flows are arbitrary code in which it is convenient to do many things, we ended up pulling in a lot of +objects that didn't make sense to put in a checkpoint, such as ``Thread`` and ``Connection``. To minimize serialization +cost and increase security by not allowing certain classes to be serialized, we now support class blacklisting +that will return an ``IllegalStateException`` if such a class is encountered during a checkpoint. Blacklisting supports +superclass and superinterface inheritance and always precedes ``@CordaSerializable`` annotation checking. + +We've also started working on improving user experience when searching, by adding a new RPC to support fuzzy matching +of X.500 names. Milestone 12 - First Public Beta -------------------------------- From bf9e2c6f543cf076f811d2506088657059e9713a Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 23 Jun 2017 13:12:56 +0100 Subject: [PATCH 13/25] M13 changelog (#896) --- docs/source/changelog.rst | 93 ++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b9e9aba247..fc92b8316e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,25 +7,84 @@ from the previous milestone release. UNRELEASED ---------- -* A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to - an unambiguous identity by searching the network map. - -* The node driver has moved to net.corda.testing.driver in the test-utils module - -* Enable certificate validation in most scenarios (will be enforced in all cases in an upcoming milestone) - -* Added DER encoded format for CompositeKey so they can be used in X.509 certificates - -* Corrected several tests which made assumptions about counterparty keys, which are invalid when confidential identities - are used - Milestone 13 ------------- +---------- +Special thank you to `Frederic Dalibard `_, for his contribution which adds +support for more currencies to the DemoBench and Explorer tools. - * Web API related collections ``CordaPluginRegistry.webApis`` and ``CordaPluginRegistry.staticServeDirs`` moved to - ``net.corda.webserver.services.WebServerPluginRegistry`` in ``webserver`` module. - Classes serving Web API should now extend ``WebServerPluginRegistry`` instead of ``CordaPluginRegistry`` - and they should be registered in ``resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry``. +* A new Vault Query service: + + * Implemented using JPA and Hibernate, this new service provides the ability to specify advanced queries using + criteria specification sets for both vault attributes and custom contract specific attributes. In addition, new + queries provide sorting and pagination capabilities. + The new API provides two function variants which are exposed for usage within Flows and by RPC clients: + - ``queryBy()`` for point-in-time snapshot queries + (replaces several existing VaultService functions and a number of Kotlin-only extension functions) + - ``trackBy()`` for snapshot and streaming updates + (replaces the VaultService ``track()`` function and the RPC ``vaultAndUpdates()`` function) + Existing VaultService API methods will be maintained as deprecated until the following milestone release. + + * The NodeSchema service has been enhanced to automatically generate mapped objects for any ContractState objects + that extend FungibleAsset or LinearState, such that common attributes of those parent states are persisted to + two new vault tables: vault_fungible_states and vault_linear_states (and thus queryable using the new Vault Query + service API). + Similarly, two new common JPA superclass schemas (``CommonSchemaV1.FungibleState`` and + ``CommonSchemaV1.LinearState``) mirror the associated FungibleAsset and LinearState interface states to enable + CorDapp developers to create new custom schemas by extension (rather than duplication of common attribute mappings) + + * A new configurable field ``requiredSchemas`` has been added to the CordaPluginRegistry to enable CorDapps to + register custom contract state schemas they wish to query using the new Vault Query service API (using the + ``VaultCustomQueryCriteria``). + + * See :doc:`vault-query` for full details and code samples of using the new Vault Query service. + +* Identity and cryptography related changes: + + * Enable certificate validation in most scenarios (will be enforced in all cases in an upcoming milestone). + + * Added DER encoded format for CompositeKey so they can be used in X.509 certificates. + + * Corrected several tests which made assumptions about counterparty keys, which are invalid when confidential + identities are used. + + * A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to + an unambiguous identity by searching the network map. + + * A function for deterministic key derivation ``Crypto.deterministicKeyPair(privateKey: PrivateKey, seed: ByteArray)`` + has been implemented to support deterministic ``KeyPair`` derivation using an existing private key and a seed + as inputs. This operation is based on the HKDF scheme and it's a variant of the hardened parent-private -> + child-private key derivation function of the BIP32 protocol, but it doesn't utilize extension chain codes. + Currently, this function supports the following schemes: ECDSA secp256r1 (NIST P-256), ECDSA secp256k1 and + EdDSA ed25519. + +* A new ``ClassWhitelist`` implementation, ``AllButBlacklisted`` is used internally to blacklist classes/interfaces, + which are not expected to be serialised during checkpoints, such as ``Thread``, ``Connection`` and ``HashSet``. + This implementation supports inheritance and if a superclass or superinterface of a class is blacklisted, so is + the class itself. An ``IllegalStateException`` informs the user if a class is blacklisted and such an exception is + returned before checking for ``@CordaSerializable``; thus, blacklisting precedes annotation checking. + +* ``TimeWindow`` has a new 5th factory method ``TimeWindow.fromStartAndDuration(fromTime: Instant, duration: Duration)`` + which takes a start-time and a period-of-validity (after this start-time) as inputs. + +* The node driver has moved to net.corda.testing.driver in the test-utils module. + +* Web API related collections ``CordaPluginRegistry.webApis`` and ``CordaPluginRegistry.staticServeDirs`` moved to + ``net.corda.webserver.services.WebServerPluginRegistry`` in ``webserver`` module. + Classes serving Web API should now extend ``WebServerPluginRegistry`` instead of ``CordaPluginRegistry`` + and they should be registered in ``resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry``. + +* Added a flag to the driver that allows the running of started nodes in-process, allowing easier debugging. + To enable use `driver(startNodesInProcess = true) { .. }`, or `startNode(startInSameProcess = true, ..)` + to specify for individual nodes. + +* Dependencies changes: + * Upgraded Kotlin to v1.1.2. + * Upgraded Dokka to v0.9.14. + * Upgraded Gradle Plugins to 0.12.4. + * Upgraded Apache ActiveMQ Artemis to v2.1.0. + * Upgraded Netty to v4.1.9.Final. + * Upgraded BouncyCastle to v1.57. + * Upgraded Requery to v1.3.1. Milestone 12 ------------ From 7804c68401198c282daefea9834cc0a6b848b992 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 23 Jun 2017 11:42:55 +0100 Subject: [PATCH 14/25] Add a config option to toggle auto IP discovery --- docs/source/corda-configuration-file.rst | 4 ++++ node/src/main/kotlin/net/corda/node/internal/Node.kt | 7 +++++-- .../net/corda/node/services/config/NodeConfiguration.kt | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index daef3ac7df..8e04b7ac06 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -134,5 +134,9 @@ path to the node's base directory. does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false and keystore does not exist. +:detectPublicIp: This flag toggles the auto IP detection behaviour, it is enabled by default. On startup the node will + attempt to discover its externally visible IP address first by looking for any public addresses on its network + interfaces, and then by sending an IP discovery request to the network map service. Set to ``false`` to disable. + :certificateSigningService: Certificate Signing Server address. It is used by the certificate signing request utility to obtain SSL certificate. (See :doc:`permissioning` for more information.) 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 f25ed8e9bc..2ff859c56d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -169,8 +169,11 @@ open class Node(override val configuration: FullNodeConfiguration, private fun getAdvertisedAddress(): HostAndPort { return with(configuration) { - val publicHost = tryDetectIfNotPublicHost(p2pAddress.host) - val useHost = publicHost ?: p2pAddress.host + val useHost = if (detectPublicIp) { + tryDetectIfNotPublicHost(p2pAddress.host) ?: p2pAddress.host + } else { + p2pAddress.host + } HostAndPort.fromParts(useHost, p2pAddress.port) } } diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index f591f89c83..c670eca9b8 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -58,7 +58,8 @@ data class FullNodeConfiguration( val notaryClusterAddresses: List, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, - val useTestClock: Boolean = false + val useTestClock: Boolean = false, + val detectPublicIp: Boolean = true ) : NodeConfiguration { /** This is not retrieved from the config file but rather from a command line argument. */ @Suppress("DEPRECATION") From 849d5921d3527f298f4e637720b749eee8ad7359 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 23 Jun 2017 13:31:04 +0100 Subject: [PATCH 15/25] Updates the cookbook to reflect vault API changes. --- .../java/net/corda/docs/FlowCookbookJava.java | 16 ++++------------ .../main/kotlin/net/corda/docs/FlowCookbook.kt | 14 +++++++++----- 2 files changed, 13 insertions(+), 17 deletions(-) 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 5ac2bff1ce..b50156b057 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 @@ -12,7 +12,8 @@ import net.corda.core.flows.*; import net.corda.core.identity.Party; import net.corda.core.node.services.ServiceType; import net.corda.core.node.services.Vault; -import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.node.services.Vault.Page; +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.core.transactions.LedgerTransaction; import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.TransactionBuilder; @@ -26,13 +27,8 @@ import net.corda.flows.ResolveTransactionsFlow; import net.corda.flows.SignTransactionFlow; import org.bouncycastle.asn1.x500.X500Name; -import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import rx.Observable; - import java.security.PublicKey; import java.time.Instant; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -227,12 +223,8 @@ public class FlowCookbookJava { // For example, we would extract any unconsumed ``DummyState``s // from our vault as follows: - Vault.StateStatus status = Vault.StateStatus.UNCONSUMED; - Set> dummyStateTypes = new HashSet<>(ImmutableList.of(DummyState.class)); - - VaultQueryCriteria criteria = new VaultQueryCriteria(status, null, dummyStateTypes); - Vault.Page results = getServiceHub().getVaultService().queryBy(criteria); - + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED); + Page results = getServiceHub().getVaultQueryService().queryBy(DummyState.class, criteria); List> dummyStates = results.getStates(); // For a full list of the available ways of extracting states from diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index edc75fbb0e..e906651f32 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -9,14 +9,18 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType -import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.Vault.Page +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Step +import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.unwrap import net.corda.flows.CollectSignaturesFlow import net.corda.flows.FinalityFlow import net.corda.flows.ResolveTransactionsFlow @@ -202,8 +206,8 @@ object FlowCookbook { // For example, we would extract any unconsumed ``DummyState``s // from our vault as follows: - val criteria = QueryCriteria.VaultQueryCriteria() // default is UNCONSUMED - val results: Vault.Page = serviceHub.vaultService.queryBy(criteria) + val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED + val results: Page = serviceHub.vaultQueryService.queryBy(criteria) val dummyStates: List> = results.states // For a full list of the available ways of extracting states from From 3cabdf4430c9126f0618d7b155c68163d612612b Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 23 Jun 2017 13:35:50 +0100 Subject: [PATCH 16/25] Delete not used steps in IRS demo M13 rst (#912) --- docs/source/running-the-demos.rst | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 871037ead0..6440872320 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:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 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. @@ -41,7 +41,7 @@ To run from the command line in Windows: 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:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 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. @@ -57,16 +57,10 @@ on a simulated clock passes. To run from the command line in Unix: -1. Run ``./gradlew samples:irs-demo:deployNodes`` to install configs and a command line tool under ``samples/irs-demo/build``. +1. Run ``./gradlew samples:irs-demo:deployNodes`` to install configs and a command line tool under ``samples/irs-demo/build`` 2. Run ``./gradlew samples:irs-demo:installDist`` 3. Move to the ``samples/irs-demo/build`` directory 4. Run ``./nodes/runnodes`` to open up three new terminals with the three nodes (you may have to install xterm). -5. Run ``./install/irs-demo/bin/irs-demo --role UploadRates``. You should see a - message be printed to the first node (the notary/oracle/network map node) saying that it has accepted the new - interest rates -6. Now run ``./install/irs-demo/bin/irs-demo --role Trade 1``. The number is a trade ID. If you enter in node's terminal - ``flow watch`` you should see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on -7. Now run ``./install/irs-demo/bin/irs-demo --role Date 2017-12-12`` to roll the simulated clock forward and see some fixings take place To run from the command line in Windows: @@ -74,21 +68,15 @@ To run from the command line in Windows: 2. Run ``gradlew samples:irs-demo:installDist`` 3. Move to the ``samples\irs-demo\build`` directory 4. Run ``nodes\runnodes`` to open up three new terminals with the three nodes. -5. Run ``install\irs-demo\bin\irs-demo --role UploadRates``. You should see a - message be printed to the first node (the notary/oracle/network map node) saying that it has accepted the new - interest rates -6. Now run ``install\irs-demo\bin\irs-demo --role Trade 1``. The number is a trade ID. If you enter in node's terminal - ``flow watch`` you should see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on -7. Now run ``install\irs-demo\bin\irs-demo --role Date 2017-12-12`` to roll the simulated clock forward and see some fixings take place -This demo also has a web app. To use this, run nodes and upload rates, then navigate to +This demo also has a web app. To use this, run nodes and then navigate to http://localhost:10007/web/irsdemo and http://localhost:10010/web/irsdemo to see each node's view of the ledger. To use the web app, click the "Create Deal" button, fill in the form, then click the "Submit" button. You can then use the time controls at the top left of the home page to run the fixings. Click any individual trade in the blotter to view it. .. note:: The IRS web UI currently has a bug when changing the clock time where it may show no numbers or apply fixings inconsistently. - The issues will be addressed in M13 milestone release. Meanwhile, you can take a look at a simpler oracle example https://github.com/corda/oracle-example + The issues will be addressed in a future milestone release. Meanwhile, you can take a look at a simpler oracle example https://github.com/corda/oracle-example Attachment demo --------------- From 61a24897fe3411421f01b8214137baf318c38825 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 23 Jun 2017 13:48:21 +0100 Subject: [PATCH 17/25] deriveKeyPair renaming --- .../kotlin/net/corda/core/crypto/Crypto.kt | 6 ++--- .../net/corda/core/crypto/CryptoUtilsTest.kt | 24 +++++++++---------- docs/source/changelog.rst | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) 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 5bfd46466f..c508c9a176 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -587,7 +587,7 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ - fun deterministicKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { + fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } when (signatureScheme) { ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) @@ -605,8 +605,8 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ - fun deterministicKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { - return deterministicKeyPair(findSignatureScheme(privateKey), privateKey, seed) + fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { + return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed) } // Given the domain parameters, this routine deterministically generates an ECDSA key pair diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 62508c42df..7a45fe7ac3 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -662,7 +662,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256R1 deterministic key generation`() { val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) - val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. assertEquals(priv.algorithm, dpriv.algorithm) @@ -687,15 +687,15 @@ class CryptoUtilsTest { assertNotEquals(pub, dpub) // A new keyPair is always generated per different seed. - val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv2, dpub2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertNotEquals(dpriv, dpriv2) assertNotEquals(dpub, dpub2) // Check if the same input always produces the same output (i.e. deterministically generated). - val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv_1, dpub_1) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) assertEquals(dpriv, dpriv_1) assertEquals(dpub, dpub_1) - val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv_2, dpub_2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertEquals(dpriv2, dpriv_2) assertEquals(dpub2, dpub_2) } @@ -703,7 +703,7 @@ class CryptoUtilsTest { @Test fun `ECDSA secp256K1 deterministic key generation`() { val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) - val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. assertEquals(priv.algorithm, dpriv.algorithm) @@ -728,15 +728,15 @@ class CryptoUtilsTest { assertNotEquals(pub, dpub) // A new keyPair is always generated per different seed. - val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv2, dpub2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertNotEquals(dpriv, dpriv2) assertNotEquals(dpub, dpub2) // Check if the same input always produces the same output (i.e. deterministically generated). - val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv_1, dpub_1) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) assertEquals(dpriv, dpriv_1) assertEquals(dpub, dpub_1) - val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv_2, dpub_2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertEquals(dpriv2, dpriv_2) assertEquals(dpub2, dpub_2) } @@ -744,7 +744,7 @@ class CryptoUtilsTest { @Test fun `EdDSA ed25519 deterministic key generation`() { val (priv, pub) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) - val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv, dpub) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) // Check scheme. assertEquals(priv.algorithm, dpriv.algorithm) @@ -769,15 +769,15 @@ class CryptoUtilsTest { assertNotEquals(pub, dpub) // A new keyPair is always generated per different seed. - val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv2, dpub2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertNotEquals(dpriv, dpriv2) assertNotEquals(dpub, dpub2) // Check if the same input always produces the same output (i.e. deterministically generated). - val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + val (dpriv_1, dpub_1) = Crypto.deriveKeyPair(priv, "seed-1".toByteArray()) assertEquals(dpriv, dpriv_1) assertEquals(dpub, dpub_1) - val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + val (dpriv_2, dpub_2) = Crypto.deriveKeyPair(priv, "seed-2".toByteArray()) assertEquals(dpriv2, dpriv_2) assertEquals(dpub2, dpub_2) } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index fc92b8316e..ec669aea03 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -50,7 +50,7 @@ support for more currencies to the DemoBench and Explorer tools. * A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to an unambiguous identity by searching the network map. - * A function for deterministic key derivation ``Crypto.deterministicKeyPair(privateKey: PrivateKey, seed: ByteArray)`` + * A function for deterministic key derivation ``Crypto.deriveKeyPair(privateKey: PrivateKey, seed: ByteArray)`` has been implemented to support deterministic ``KeyPair`` derivation using an existing private key and a seed as inputs. This operation is based on the HKDF scheme and it's a variant of the hardened parent-private -> child-private key derivation function of the BIP32 protocol, but it doesn't utilize extension chain codes. From 1866f6ff7fbcae2868af792800ee521685e9a38f Mon Sep 17 00:00:00 2001 From: josecoll Date: Fri, 23 Jun 2017 14:35:55 +0100 Subject: [PATCH 18/25] Align CordaRPCOps Vault Query API's for Java/Kotlin with VaultQuery service equivalents. (#914) Added Kotlin extension functions for ease of use in simplest case. Added missing Java contractType class (previously was forced cast of ContractState::class) Fixed generic typing. Addressed items raised in SA PR review. Removed 2 blank lines. Fixed generics typing. Updates RST doc and grouped API helpers accordingly. --- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 5 +- .../net/corda/core/messaging/CordaRPCOps.kt | 68 +++++++++++++++---- .../net/corda/core/node/services/Services.kt | 56 ++++++++++----- docs/source/api-vault.rst | 11 ++- .../corda/node/internal/CordaRPCOpsImpl.kt | 11 +-- .../services/vault/HibernateVaultQueryImpl.kt | 4 +- .../net/corda/node/CordaRPCOpsImplTest.kt | 9 ++- 7 files changed, 115 insertions(+), 49 deletions(-) diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 8863b6c559..3c0c055655 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -11,10 +11,7 @@ import net.corda.core.contracts.SWISS_FRANCS import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.startFlow -import net.corda.core.messaging.startTrackedFlow +import net.corda.core.messaging.* import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index e58efb36f0..bfe9910de8 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -83,11 +83,31 @@ interface CordaRPCOps : RPCOps { * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. */ // DOCSTART VaultQueryByAPI - fun vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.Page + @RPCReturnsObservables + fun vaultQueryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractType: Class): Vault.Page // DOCEND VaultQueryByAPI + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + + // DOCSTART VaultQueryAPIHelpers + fun vaultQuery(contractType: Class): Vault.Page { + return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + } + fun vaultQueryByCriteria(criteria: QueryCriteria, contractType: Class): Vault.Page { + return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + } + fun vaultQueryByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return vaultQueryBy(criteria, paging, Sort(emptySet()), contractType) + } + fun vaultQueryByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { + return vaultQueryBy(criteria, PageSpecification(), sorting, contractType) + } + // DOCEND VaultQueryAPIHelpers + /** * Returns a snapshot (as per queryBy) and an observable of future updates to the vault for the given query criteria. * @@ -102,23 +122,29 @@ interface CordaRPCOps : RPCOps { */ // DOCSTART VaultTrackByAPI @RPCReturnsObservables - fun vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates + fun vaultTrackBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractType: Class): Vault.PageAndUpdates // DOCEND VaultTrackByAPI // Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Java Helpers - // DOCSTART VaultQueryAPIJavaHelpers - fun vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page = vaultQueryBy(criteria) - fun vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = vaultQueryBy(criteria, paging) - fun vaultQueryByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.Page = vaultQueryBy(criteria, sorting = sorting) - - fun vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates = vaultTrackBy(criteria) - fun vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = vaultTrackBy(criteria, paging) - fun vaultTrackByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = vaultTrackBy(criteria, sorting = sorting) - // DOCEND VaultQueryAPIJavaHelpers + // DOCSTART VaultTrackAPIHelpers + fun vaultTrack(contractType: Class): Vault.PageAndUpdates { + return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + } + fun vaultTrackByCriteria(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + } + fun vaultTrackByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType) + } + fun vaultTrackByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + return vaultTrackBy(criteria, PageSpecification(), sorting, contractType) + } + // DOCEND VaultTrackAPIHelpers /** * Returns a pair of head states in the vault and an observable of future updates to the vault. @@ -258,6 +284,18 @@ interface CordaRPCOps : RPCOps { fun registeredFlows(): List } +inline fun CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.Page { + return vaultQueryBy(criteria, paging, sorting, T::class.java) +} + +inline fun CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates { + return vaultTrackBy(criteria, paging, sorting, T::class.java) +} + /** * These allow type safe invocations of flows from Kotlin, e.g.: * diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index daf2c2cfbe..dfb7c4c5e1 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -361,10 +361,10 @@ interface VaultQueryService { * Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting */ @Throws(VaultQueryException::class) - fun _queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet()), - contractType: Class): Vault.Page + fun _queryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractType: Class): Vault.Page /** * Generic vault query function which takes a [QueryCriteria] object to define filters, * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), @@ -378,25 +378,45 @@ interface VaultQueryService { * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). */ @Throws(VaultQueryException::class) - fun _trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), - paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet()), - contractType: Class): Vault.PageAndUpdates + fun _trackBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractType: Class): Vault.PageAndUpdates // DOCEND VaultQueryAPI // Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Java Helpers - fun queryBy(contractType: Class): Vault.Page = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) - fun queryBy(contractType: Class, criteria: QueryCriteria): Vault.Page = _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType) - fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page = _queryBy(criteria, paging, Sort(emptySet()), contractType) - fun queryBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page = _queryBy(criteria, PageSpecification(), sorting, contractType) - fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page = _queryBy(criteria, paging, sorting, contractType) + fun queryBy(contractType: Class): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + } + fun queryBy(contractType: Class, criteria: QueryCriteria): Vault.Page { + return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + } + fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page { + return _queryBy(criteria, paging, Sort(emptySet()), contractType) + } + fun queryBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page { + return _queryBy(criteria, PageSpecification(), sorting, contractType) + } + fun queryBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + return _queryBy(criteria, paging, sorting, contractType) + } - fun trackBy(contractType: Class): Vault.Page = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) - fun trackBy(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates = _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = _trackBy(criteria, paging, Sort(emptySet()), contractType) - fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = _trackBy(criteria, PageSpecification(), sorting, contractType) - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates = _trackBy(criteria, paging, sorting, contractType) + fun trackBy(contractType: Class): Vault.Page { + return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + } + fun trackBy(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) + } + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + return _trackBy(criteria, paging, Sort(emptySet()), contractType) + } + fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + return _trackBy(criteria, PageSpecification(), sorting, contractType) + } + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { + return _trackBy(criteria, paging, sorting, contractType) + } } inline fun VaultQueryService.queryBy(): Vault.Page { diff --git a/docs/source/api-vault.rst b/docs/source/api-vault.rst index d90683f8d3..44706e1c15 100644 --- a/docs/source/api-vault.rst +++ b/docs/source/api-vault.rst @@ -28,12 +28,17 @@ and via ``CordaRPCOps`` for use by RPC client applications: :start-after: DOCSTART VaultTrackByAPI :end-before: DOCEND VaultTrackByAPI -Java helper methods are also provided with default values for arguments: +Helper methods are also provided with default values for arguments: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin - :start-after: DOCSTART VaultQueryAPIJavaHelpers - :end-before: DOCEND VaultQueryAPIJavaHelpers + :start-after: DOCSTART VaultQueryAPIHelpers + :end-before: DOCEND VaultQueryAPIHelpers + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt + :language: kotlin + :start-after: DOCSTART VaultTrackAPIHelpers + :end-before: DOCEND VaultTrackAPIHelpers The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria. diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 0cb8ed074c..479b58c8db 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -58,18 +58,20 @@ class CordaRPCOpsImpl( override fun vaultQueryBy(criteria: QueryCriteria, paging: PageSpecification, - sorting: Sort): Vault.Page { + sorting: Sort, + contractType: Class): Vault.Page { return database.transaction { - services.vaultQueryService._queryBy(criteria, paging, sorting, ContractState::class.java as Class) + services.vaultQueryService._queryBy(criteria, paging, sorting, contractType) } } @RPCReturnsObservables override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, - sorting: Sort): Vault.PageAndUpdates { + sorting: Sort, + contractType: Class): Vault.PageAndUpdates { return database.transaction { - services.vaultQueryService._trackBy(criteria, paging, sorting, ContractState::class.java as Class) + services.vaultQueryService._trackBy(criteria, paging, sorting, contractType) } } @@ -195,3 +197,4 @@ class CordaRPCOpsImpl( } } + diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index b5a34ff5f0..a014a3b545 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -39,7 +39,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, private val criteriaBuilder = sessionFactory.criteriaBuilder @Throws(VaultQueryException::class) - override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { + override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") val session = sessionFactory.withOptions(). @@ -102,7 +102,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, private val mutex = ThreadBox ({ updatesPublisher }) @Throws(VaultQueryException::class) - override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.PageAndUpdates { + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.PageAndUpdates { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractType) Vault.PageAndUpdates(snapshotResults, diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index f47a4355d6..831f0b9d16 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -7,8 +7,7 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.startFlow +import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.unconsumedStates @@ -50,7 +49,7 @@ class CordaRPCOpsImplTest { lateinit var mockNet: MockNetwork lateinit var aliceNode: MockNode lateinit var notaryNode: MockNode - lateinit var rpc: CordaRPCOpsImpl + lateinit var rpc: CordaRPCOps lateinit var stateMachineUpdates: Observable lateinit var transactions: Observable lateinit var vaultUpdates: Observable // TODO: deprecated @@ -95,6 +94,10 @@ class CordaRPCOpsImplTest { Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), recipient) + // Query vault via RPC + val cash = rpc.vaultQueryBy() + assertEquals(expectedState, cash.states.first().state.data) + var issueSmId: StateMachineRunId? = null stateMachineUpdates.expectEvents { sequence( From 14068f1b963c89d3075fc3e3dc6d3d0ec25749a2 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 23 Jun 2017 15:18:13 +0100 Subject: [PATCH 19/25] Enforce certificate constraints on all identities * Enforce that the identity service must always have a root CA specified, which all identities have certificates signed by (or intermediaries of). Also adds a certificate store to the identity service for help building/verifying certificate paths. * Add a certificate store for the CA certificate and intermediaries * Use the certificate factory directly to build paths rather than assembling them via an interim API call. After reducing the complexity of the utility API, it's replacing two lines of code, at which point it seems better to make the behaviour clearer rather than having a function hide what's actually going on. --- .../core/node/services/IdentityService.kt | 10 +++++---- .../serialization/DefaultKryoCustomizer.kt | 1 + .../services/messaging/P2PSecurityTest.kt | 12 +++++------ .../net/corda/node/internal/AbstractNode.kt | 21 ++++++++++++------- .../identity/InMemoryIdentityService.kt | 20 ++++++++++++------ .../net/corda/irs/simulation/trade.json | 4 ++-- .../kotlin/net/corda/testing/CoreTestUtils.kt | 9 ++------ .../kotlin/net/corda/testing/node/MockNode.kt | 15 +++++++++---- .../net/corda/testing/node/SimpleNode.kt | 4 ++-- 9 files changed, 57 insertions(+), 39 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index ce5d5f9262..3e35feb6f2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -2,13 +2,11 @@ package net.corda.core.node.services import net.corda.core.contracts.PartyAndReference import net.corda.core.identity.* -import net.corda.core.node.NodeInfo import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException import java.security.PublicKey -import java.security.cert.CertPath -import java.security.cert.CertificateExpiredException -import java.security.cert.CertificateNotYetValidException +import java.security.cert.* /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus @@ -16,6 +14,10 @@ import java.security.cert.CertificateNotYetValidException * identities back to the well known identity (i.e. the identity in the network map) of a party. */ interface IdentityService { + val trustRoot: X509Certificate + val trustRootHolder: X509CertificateHolder + val caCertStore: CertStore + /** * Verify and then store a well known identity. * diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index a88e2ec073..7963fb549e 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -73,6 +73,7 @@ object DefaultKryoCustomizer { noReferencesWithin() + register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index a0eaea5923..fa9c503c3f 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -2,14 +2,12 @@ package net.corda.services.messaging import com.google.common.util.concurrent.ListenableFuture import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.cert import net.corda.core.getOrThrow import net.corda.core.node.NodeInfo import net.corda.core.random63BitValue import net.corda.core.seconds -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.core.utilities.* import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest @@ -24,8 +22,8 @@ import net.corda.testing.node.SimpleNode import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder import org.junit.Test +import java.security.cert.X509Certificate import java.time.Instant import java.util.concurrent.TimeoutException @@ -46,7 +44,7 @@ class P2PSecurityTest : NodeBasedTest() { @Test fun `register with the network map service using a legal name different from the TLS CN`() { - startSimpleNode(DUMMY_BANK_A.name).use { + startSimpleNode(DUMMY_BANK_A.name, DUMMY_CA.certificate.cert).use { // Register with the network map using a different legal name val response = it.registerWithNetworkMap(DUMMY_BANK_B.name) // We don't expect a response because the network map's host verification will prevent a connection back @@ -58,7 +56,7 @@ class P2PSecurityTest : NodeBasedTest() { } private fun startSimpleNode(legalName: X500Name, - trustRoot: X509CertificateHolder? = null): SimpleNode { + trustRoot: X509Certificate): SimpleNode { val config = TestNodeConfiguration( baseDirectory = baseDirectory(legalName), myLegalName = legalName, 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 c0d7053e62..74c6a7ef81 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -221,7 +221,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Do all of this in a database transaction so anything that might need a connection has one. initialiseDatabasePersistence { - val tokenizableServices = makeServices() + val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) + val tokenizableServices = makeServices(keyStoreWrapper) smm = StateMachineManager(services, checkpointStorage, @@ -452,7 +453,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(): MutableList { + private fun makeServices(keyStoreWrapper: KeyStoreWrapper): MutableList { + val keyStore = keyStoreWrapper.keyStore val storageServices = initialiseStorageService(configuration.baseDirectory) storage = storageServices.first checkpointStorage = storageServices.second @@ -465,7 +467,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, auditService = DummyAuditService() info = makeInfo() - identity = makeIdentityService() + identity = makeIdentityService(keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, + keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), + info.legalIdentityAndCert) // 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 // the identity key. But the infrastructure to make that easy isn't here yet. @@ -701,10 +705,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun makeUniquenessProvider(type: ServiceType): UniquenessProvider - protected open fun makeIdentityService(): IdentityService { - val keyStore = KeyStoreUtilities.loadKeyStore(configuration.trustStoreFile, configuration.trustStorePassword) - val trustRoot = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as? X509Certificate - val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot) + protected open fun makeIdentityService(trustRoot: X509Certificate, + clientCa: CertificateAndKeyPair?, + legalIdentity: PartyAndCertificate): IdentityService { + val caCertificates: Array = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert) + .filterNotNull() + .toTypedArray() + val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentityAndCert) } netMapCache.changed.subscribe { mapChange -> // TODO how should we handle network map removal diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index ff8553bc84..4438f2e9c5 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -31,22 +31,30 @@ import kotlin.collections.ArrayList * @param certPaths initial set of certificate paths for the service, typically only used for unit tests. */ @ThreadSafe -class InMemoryIdentityService(identities: Iterable, +class InMemoryIdentityService(identities: Iterable = emptySet(), certPaths: Map = emptyMap(), - val trustRoot: X509Certificate?) : SingletonSerializeAsToken(), IdentityService { + override val trustRoot: X509Certificate, + vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService { constructor(identities: Iterable = emptySet(), certPaths: Map = emptyMap(), - trustRoot: X509CertificateHolder?) : this(identities, certPaths, trustRoot?.cert) + trustRoot: X509CertificateHolder) : this(identities, certPaths, trustRoot.cert) companion object { private val log = loggerFor() } - private val trustAnchor: TrustAnchor? = trustRoot?.let { cert -> TrustAnchor(cert, null) } + /** + * Certificate store for certificate authority and intermediary certificates. + */ + override val caCertStore: CertStore + override val trustRootHolder = X509CertificateHolder(trustRoot.encoded) + private val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null) private val keyToParties = ConcurrentHashMap() private val principalToParties = ConcurrentHashMap() private val partyToPath = ConcurrentHashMap() init { + val caCertificatesWithRoot: Set = caCertificates.toSet() + trustRoot + caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificatesWithRoot)) keyToParties.putAll(identities.associateBy { it.owningKey } ) principalToParties.putAll(identities.associateBy { it.name }) partyToPath.putAll(certPaths) @@ -57,7 +65,7 @@ class InMemoryIdentityService(identities: Iterable, override fun registerIdentity(party: PartyAndCertificate) { require(party.certPath.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } // Validate the chain first, before we do anything clever with it - if (trustRoot != null) validateCertificatePath(party.party, party.certPath) + validateCertificatePath(party.party, party.certPath) log.trace { "Registering identity $party" } require(Arrays.equals(party.certificate.subjectPublicKeyInfo.encoded, party.owningKey.encoded)) { "Party certificate must end with party's public key" } @@ -122,7 +130,7 @@ class InMemoryIdentityService(identities: Iterable, val fullParty = certificateFromParty(party) ?: throw IllegalArgumentException("Unknown identity ${party.name}") require(path.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } // Validate the chain first, before we do anything clever with it - if (trustRoot != null) validateCertificatePath(anonymousParty, path) + validateCertificatePath(anonymousParty, path) val subjectCertificate = path.certificates.first() require(subjectCertificate is X509Certificate && subjectCertificate.subject == fullParty.name) { "Subject of the transaction certificate must match the well known identity" } diff --git a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json index ba8168b1a9..c08790043a 100644 --- a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json +++ b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "8Kqd4oWdx4KQAVcA8RDJXNvzFMvBkqWTZPhRtg9dSpNs6T6eZ4cGJWA7FWK", + "fixedRatePayer": "8Kqd4oWdx4KQGHGR7xcgpFf9JmP6HiXqTf85NpSgdSu431EGEhujA6ePaFD", "notional": "$25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -22,7 +22,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "8Kqd4oWdx4KQAVc3Si48msuQrMJPGpA3TnGGuWTqXyoshjL25wzzdxtGyjq", + "floatingRatePayer": "8Kqd4oWdx4KQGHGJSFTX4kdZukmHohBRN3gvPekticL4eHTdmbJTVZNZJUj", "notional": { "quantity": 2500000000, "token": "USD" 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 950c680422..1f7fb0d0db 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -5,11 +5,7 @@ package net.corda.testing import com.google.common.net.HostAndPort import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.commonName -import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.AnonymousParty +import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.ServiceHub @@ -33,7 +29,6 @@ import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey -import java.security.cert.CertPath import java.util.* import java.util.concurrent.atomic.AtomicInteger @@ -89,7 +84,7 @@ val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY) val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) -val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate) +val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert) val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 55ddd92b92..5980df134f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -5,8 +5,9 @@ import com.google.common.jimfs.Jimfs import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* +import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.cert import net.corda.core.crypto.entropyToKeyPair -import net.corda.flows.TxKeyFlow import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps @@ -18,6 +19,7 @@ import net.corda.core.node.services.* import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.loggerFor +import net.corda.flows.TxKeyFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.InMemoryIdentityService @@ -166,9 +168,14 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, .getOrThrow() } - // TODO: Specify a CA to validate registration against - override fun makeIdentityService(): IdentityService { - return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(), trustRoot = null as X509Certificate?) + override fun makeIdentityService(trustRoot: X509Certificate, + clientCa: CertificateAndKeyPair?, + legalIdentity: PartyAndCertificate): IdentityService { + val caCertificates: Array = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert) + .filterNotNull() + .toTypedArray() + return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(), + trustRoot = trustRoot, caCertificates = *caCertificates) } override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 0104fd981c..70f66961ca 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -21,10 +21,10 @@ import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.freeLocalHostAndPort -import org.bouncycastle.cert.X509CertificateHolder import org.jetbrains.exposed.sql.Database import java.io.Closeable import java.security.KeyPair +import java.security.cert.X509Certificate import kotlin.concurrent.thread /** @@ -33,7 +33,7 @@ import kotlin.concurrent.thread */ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), rpcAddress: HostAndPort = freeLocalHostAndPort(), - trustRoot: X509CertificateHolder? = null) : AutoCloseable { + trustRoot: X509Certificate) : AutoCloseable { private val databaseWithCloseable: Pair = configureDatabase(config.dataSourceProperties) val database: Database get() = databaseWithCloseable.second From a69178ca7310eef4a3f35ed7da6fe9bdf907ae5c Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 23 Jun 2017 17:49:07 +0100 Subject: [PATCH 20/25] Fix logging that always says zero (#895) --- test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8dca081231..593b59af7b 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 @@ -351,7 +351,7 @@ fun poll( executorService.schedule(task@ { counter++ if (counter == warnCount) { - log.warn("Been polling $pollName for ${pollInterval.seconds * warnCount} seconds...") + log.warn("Been polling $pollName for ${pollInterval.multipliedBy(warnCount.toLong()).seconds} seconds...") } val result = try { check() From 27a8f6fc47280c555e113dfe885c65cf7ad123af Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 23 Jun 2017 17:49:17 +0100 Subject: [PATCH 21/25] Unrwap ExecutionException, use Duration. (#900) --- test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 593b59af7b..5e77d52cbb 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 @@ -400,7 +400,7 @@ class ShutdownManager(private val executorService: ExecutorService) { registeredShutdowns } } - val shutdowns = shutdownFutures.map { ErrorOr.catch { it.get(1, SECONDS) } } + val shutdowns = shutdownFutures.map { ErrorOr.catch { it.getOrThrow(1.seconds) } } shutdowns.reversed().forEach { errorOrShutdown -> errorOrShutdown.match( onValue = { shutdown -> From 433a9ab5c3d62472fb875a9f436c17dc62f80a62 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 26 Jun 2017 10:12:48 +0100 Subject: [PATCH 22/25] DemoBench operates exclusively on localhost, so no need for public IP. (#919) --- .../src/main/kotlin/net/corda/demobench/model/NodeConfig.kt | 3 ++- .../test/kotlin/net/corda/demobench/model/NodeConfigTest.kt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 14ce84779f..64406da824 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -56,11 +56,12 @@ class NodeConfig( .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) .withValue("h2port", valueFor(h2Port)) .withValue("useTestClock", valueFor(true)) + .withValue("detectPublicIp", valueFor(false)) fun toText(): String = toFileConfig().root().render(renderOptions) fun moveTo(baseDir: Path) = NodeConfig( - baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap + baseDir, legalName, p2pPort, rpcPort, webPort, h2Port, extraServices, users, networkMap ) fun install(plugins: Collection) { diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index e5db3e1233..6d9b04d3e5 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -142,6 +142,7 @@ class NodeConfigTest { users = listOf(user("jenny")) ) assertEquals(prettyPrint("{" + + "\"detectPublicIp\":false," + "\"extraAdvertisedServiceIds\":[\"my.service\"]," + "\"h2port\":30001," + "\"myLegalName\":\"CN=My Name,OU=Corda QA Department,O=R3 CEV,L=New York,C=US\"," @@ -169,6 +170,7 @@ class NodeConfigTest { config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) assertEquals(prettyPrint("{" + + "\"detectPublicIp\":false," + "\"extraAdvertisedServiceIds\":[\"my.service\"]," + "\"h2port\":30001," + "\"myLegalName\":\"CN=My Name,OU=Corda QA Department,O=R3 CEV,L=New York,C=US\"," @@ -210,6 +212,7 @@ class NodeConfigTest { assertEquals(NetworkMapInfo(localPort(12345), DUMMY_NOTARY.name), fullConfig.networkMapService) assertTrue((fullConfig.dataSourceProperties["dataSource.url"] as String).contains("AUTO_SERVER_PORT=30001")) assertTrue(fullConfig.useTestClock) + assertFalse(fullConfig.detectPublicIp) } @Test From 92c238b2aaec600c837edb9a730e2ed9b0d85fcc Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 26 Jun 2017 13:24:24 +0100 Subject: [PATCH 23/25] Alter logging patterns to a more explicit form. (#918) --- config/dev/log4j2.xml | 6 +++--- config/test/log4j2.xml | 2 +- samples/simm-valuation-demo/src/main/resources/log4j2.xml | 8 ++++---- tools/demobench/src/main/resources/log4j2.xml | 4 ++-- tools/demobench/src/test/resources/log4j2-test.xml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index f85a22fbee..fa6ff74c5b 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -13,7 +13,7 @@ - + @@ -25,9 +25,9 @@ those that are older than 60 days, but keep the most recent 10 GB --> + filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/config/test/log4j2.xml b/config/test/log4j2.xml index eca12924ef..adcab4aeb5 100644 --- a/config/test/log4j2.xml +++ b/config/test/log4j2.xml @@ -5,7 +5,7 @@ - + diff --git a/samples/simm-valuation-demo/src/main/resources/log4j2.xml b/samples/simm-valuation-demo/src/main/resources/log4j2.xml index b929e776d9..0205e4fbfe 100644 --- a/samples/simm-valuation-demo/src/main/resources/log4j2.xml +++ b/samples/simm-valuation-demo/src/main/resources/log4j2.xml @@ -13,7 +13,7 @@ - [%-5level] %d{HH:mm:ss.SSS} [%t] %c{2}.%M - %msg%n + [%-5level] %date{HH:mm:ss.SSS} [%t] %c{2}.%method - %msg%n > @@ -27,9 +27,9 @@ those that are older than 60 days, but keep the most recent 10 GB --> + filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + @@ -61,4 +61,4 @@ - \ No newline at end of file + diff --git a/tools/demobench/src/main/resources/log4j2.xml b/tools/demobench/src/main/resources/log4j2.xml index c1a85aa61d..526b1bdb46 100644 --- a/tools/demobench/src/main/resources/log4j2.xml +++ b/tools/demobench/src/main/resources/log4j2.xml @@ -16,9 +16,9 @@ those that are older than 60 days, but keep the most recent 10 GB --> + filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/tools/demobench/src/test/resources/log4j2-test.xml b/tools/demobench/src/test/resources/log4j2-test.xml index 5850a9619c..b0be744c42 100644 --- a/tools/demobench/src/test/resources/log4j2-test.xml +++ b/tools/demobench/src/test/resources/log4j2-test.xml @@ -3,7 +3,7 @@ - + From 3e124b0b48ab6a0d745fc97975e32bb3f99d47c8 Mon Sep 17 00:00:00 2001 From: josecoll Date: Mon, 26 Jun 2017 18:01:07 +0100 Subject: [PATCH 24/25] =?UTF-8?q?Automatically=20persist=20Linear=20and/or?= =?UTF-8?q?=20FungibleState=20attributes=20for=20quer=E2=80=A6=20(#920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Automatically persist Linear and/or FungibleState attributes for querying (even when Contract does not implement QueryableState). * Return single() state. --- .../WorkflowTransactionBuildTutorialTest.kt | 24 +++++++++++-------- .../node/services/schema/HibernateObserver.kt | 9 +++---- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index b456cb8e02..17da495c0e 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -2,11 +2,14 @@ package net.corda.docs import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.getOrThrow import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.linearHeadsOfType +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.and import net.corda.core.toFuture import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY @@ -26,10 +29,10 @@ class WorkflowTransactionBuildTutorialTest { lateinit var nodeB: MockNetwork.MockNode // Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef - private inline fun ServiceHub.latest(ref: StateRef): StateAndRef { - val linearHeads = vaultService.linearHeadsOfType() - val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef(ref.index) - return linearHeads[original.state.data.linearId]!! + private inline fun ServiceHub.latest(ref: UniqueIdentifier): StateAndRef { + val linearHeads = vaultQueryService.queryBy(QueryCriteria.LinearStateQueryCriteria(linearId = listOf(ref)) + .and(QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED))) + return linearHeads.states.single() } @Before @@ -59,14 +62,15 @@ class WorkflowTransactionBuildTutorialTest { val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.legalIdentity)) // Wait for the flow to finish val proposalRef = flow1.resultFuture.getOrThrow() + val proposalLinearId = proposalRef.state.data.linearId // Wait for NodeB to include it's copy in the vault nodeBVaultUpdate.get() // Fetch the latest copy of the state from both nodes val latestFromA = nodeA.database.transaction { - nodeA.services.latest(proposalRef.ref) + nodeA.services.latest(proposalLinearId) } val latestFromB = nodeB.database.transaction { - nodeB.services.latest(proposalRef.ref) + nodeB.services.latest(proposalLinearId) } // Confirm the state as as expected assertEquals(WorkflowState.NEW, proposalRef.state.data.state) @@ -87,10 +91,10 @@ class WorkflowTransactionBuildTutorialTest { secondNodeBVaultUpdate.get() // Fetch the latest copies from the vault val finalFromA = nodeA.database.transaction { - nodeA.services.latest(proposalRef.ref) + nodeA.services.latest(proposalLinearId) } val finalFromB = nodeB.database.transaction { - nodeB.services.latest(proposalRef.ref) + nodeB.services.latest(proposalLinearId) } // Confirm the state is as expected assertEquals(WorkflowState.APPROVED, completedRef.state.data.state) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index d53b29d5b1..b152f4c04b 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.StateRef import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef -import net.corda.core.schemas.QueryableState import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration @@ -34,13 +33,11 @@ class HibernateObserver(vaultUpdates: Observable, val config: Hibe private fun persistState(stateAndRef: StateAndRef) { val state = stateAndRef.state.data - if (state is QueryableState) { - logger.debug { "Asked to persist state ${stateAndRef.ref}" } - config.schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } - } + logger.debug { "Asked to persist state ${stateAndRef.ref}" } + config.schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) } } - fun persistStateWithSchema(state: QueryableState, stateRef: StateRef, schema: MappedSchema) { + fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { val sessionFactory = config.sessionFactoryForSchema(schema) val session = sessionFactory.withOptions(). connection(TransactionManager.current().connection). From 37c918a8f5b97ffa3ce4cef416abf14232c317f8 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 27 Jun 2017 11:49:19 +0100 Subject: [PATCH 25/25] BFT tests no longer use NodeBasedTest (#917) * Move Raft config to the interface * Inline method only used in 1 test --- .../node/services/BFTNotaryServiceTests.kt | 52 ++++++++------ .../services/messaging/P2PSecurityTest.kt | 10 +-- .../net/corda/node/internal/AbstractNode.kt | 3 +- .../node/services/config/NodeConfiguration.kt | 9 ++- .../config/FullNodeConfigurationTest.kt | 34 +++++++-- .../messaging/ArtemisMessagingTests.kt | 7 +- .../NetworkisRegistrationHelperTest.kt | 7 +- .../net/corda/netmap/simulation/Simulation.kt | 27 +++---- .../kotlin/net/corda/testing/CoreTestUtils.kt | 70 +++++++------------ .../kotlin/net/corda/testing/node/MockNode.kt | 28 +++++--- 10 files changed, 132 insertions(+), 115 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 15b258d796..5e8c9516c6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -1,55 +1,63 @@ package net.corda.node.services -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture +import com.google.common.net.HostAndPort +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.* import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.appendToCommonName import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode +import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction -import net.corda.testing.node.NodeBasedTest +import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name import org.junit.Ignore +import org.junit.After import org.junit.Test import java.nio.file.Files import kotlin.test.* -class BFTNotaryServiceTests : NodeBasedTest() { +class BFTNotaryServiceTests { companion object { private val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") private val serviceType = BFTNonValidatingNotaryService.type } - private fun bftNotaryCluster(clusterSize: Int): ListenableFuture { + private val mockNet = MockNetwork() + private val node = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + @After + fun stopNodes() { + mockNet.stopNodes() + } + + private fun bftNotaryCluster(clusterSize: Int): Party { Files.deleteIfExists("config" / "currentView") // XXX: Make config object warn if this exists? val replicaIds = (0 until clusterSize) - val replicaNames = replicaIds.map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val party = ServiceIdentityGenerator.generateToDisk( - replicaNames.map { baseDirectory(it) }, + replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) }, serviceType.id, clusterName) - val advertisedServices = setOf(ServiceInfo(serviceType, clusterName)) - val config = mapOf("notaryClusterAddresses" to replicaIds.map { "localhost:${11000 + it * 10}" }) - return Futures.allAsList(replicaIds.map { - startNode( - replicaNames[it], - advertisedServices = advertisedServices, - configOverrides = mapOf("bftReplicaId" to it) + config - ) - }).map { party } + val bftNotaryService = ServiceInfo(serviceType, clusterName) + val notaryClusterAddresses = replicaIds.map { HostAndPort.fromParts("localhost", 11000 + it * 10) } + replicaIds.forEach { replicaId -> + mockNet.createNode( + node.info.address, + advertisedServices = bftNotaryService, + configOverrides = { + whenever(it.bftReplicaId).thenReturn(replicaId) + whenever(it.notaryClusterAddresses).thenReturn(notaryClusterAddresses) + }) + } + return party } @Test @@ -66,9 +74,8 @@ class BFTNotaryServiceTests : NodeBasedTest() { private fun detectDoubleSpend(faultyReplicas: Int) { val clusterSize = minClusterSize(faultyReplicas) - val aliceFuture = startNode(ALICE.name) - val notary = bftNotaryCluster(clusterSize).getOrThrow() - aliceFuture.getOrThrow().run { + val notary = bftNotaryCluster(clusterSize) + node.run { val issueTx = signInitialTransaction(notary) { addOutputState(DummyContract.SingleOwnerState(owner = info.legalIdentity)) } @@ -83,6 +90,7 @@ class BFTNotaryServiceTests : NodeBasedTest() { assertEquals(spendTxs.size, spendTxs.map { it.id }.distinct().size) val flows = spendTxs.map { NotaryFlow.Client(it) } val stateMachines = flows.map { services.startFlow(it) } + mockNet.runNetwork() val results = stateMachines.map { ErrorOr.catch { it.resultFuture.getOrThrow() } } val successfulIndex = results.mapIndexedNotNull { index, result -> if (result.error == null) { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index fa9c503c3f..bca09c4a0f 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -1,6 +1,7 @@ package net.corda.services.messaging import com.google.common.util.concurrent.ListenableFuture +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.cert import net.corda.core.getOrThrow @@ -16,9 +17,9 @@ import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NodeRegistration import net.corda.node.utilities.AddOrRemove import net.corda.testing.MOCK_VERSION_INFO -import net.corda.testing.TestNodeConfiguration import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.SimpleNode +import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name @@ -57,10 +58,11 @@ class P2PSecurityTest : NodeBasedTest() { private fun startSimpleNode(legalName: X500Name, trustRoot: X509Certificate): SimpleNode { - val config = TestNodeConfiguration( + val config = testNodeConfiguration( baseDirectory = baseDirectory(legalName), - myLegalName = legalName, - networkMapService = NetworkMapInfo(networkMapNode.configuration.p2pAddress, networkMapNode.info.legalIdentity.name)) + myLegalName = legalName).also { + whenever(it.networkMapService).thenReturn(NetworkMapInfo(networkMapNode.configuration.p2pAddress, networkMapNode.info.legalIdentity.name)) + } config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name return SimpleNode(config, trustRoot = trustRoot).apply { start() } } 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 74c6a7ef81..18b02e4e53 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -29,7 +29,6 @@ import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.* import net.corda.node.services.* import net.corda.node.services.api.* -import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.database.HibernateConfiguration @@ -686,7 +685,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, ValidatingNotaryService.type -> ValidatingNotaryService(timeWindowChecker, uniquenessProvider) RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) - BFTNonValidatingNotaryService.type -> with(configuration as FullNodeConfiguration) { + BFTNonValidatingNotaryService.type -> with(configuration) { val replicaId = bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") BFTSMaRtConfig(notaryClusterAddresses).use { config -> BFTNonValidatingNotaryService(config, services, timeWindowChecker, replicaId, database).also { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index c670eca9b8..a052e20968 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -26,6 +26,9 @@ interface NodeConfiguration : NodeSSLConfiguration { val certificateChainCheckPolicies: List val verifierType: VerifierType val messageRedeliveryDelaySeconds: Int + val bftReplicaId: Int? + val notaryNodeAddress: HostAndPort? + val notaryClusterAddresses: List } data class FullNodeConfiguration( @@ -53,9 +56,9 @@ data class FullNodeConfiguration( // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: HostAndPort?, val extraAdvertisedServiceIds: List, - val bftReplicaId: Int?, - val notaryNodeAddress: HostAndPort?, - val notaryClusterAddresses: List, + override val bftReplicaId: Int?, + override val notaryNodeAddress: HostAndPort?, + override val notaryClusterAddresses: List, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 96904b7020..01cfc1a8f0 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -1,21 +1,43 @@ package net.corda.node.services.config +import com.google.common.net.HostAndPort +import net.corda.core.crypto.commonName import net.corda.core.utilities.ALICE import net.corda.nodeapi.User -import net.corda.testing.testConfiguration +import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThatThrownBy -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test +import java.net.URL import java.nio.file.Paths class FullNodeConfigurationTest { @Test fun `Artemis special characters not permitted in RPC usernames`() { - fun configWithRPCUsername(username: String): FullNodeConfiguration { - return testConfiguration(Paths.get("."), ALICE.name, 0).copy( - rpcUsers = listOf(User(username, "pass", emptySet()))) - } + val testConfiguration = FullNodeConfiguration( + basedir = Paths.get("."), + myLegalName = ALICE.name, + networkMapService = null, + emailAddress = "", + keyStorePassword = "cordacadevpass", + trustStorePassword = "trustpass", + dataSourceProperties = makeTestDataSourceProperties(ALICE.name.commonName), + certificateSigningService = URL("http://localhost"), + rpcUsers = emptyList(), + verifierType = VerifierType.InMemory, + useHTTPS = false, + p2pAddress = HostAndPort.fromParts("localhost", 0), + rpcAddress = HostAndPort.fromParts("localhost", 1), + messagingServerAddress = null, + extraAdvertisedServiceIds = emptyList(), + bftReplicaId = null, + notaryNodeAddress = null, + notaryClusterAddresses = emptyList(), + certificateChainCheckPolicies = emptyList(), + devMode = true) + fun configWithRPCUsername(username: String) { + testConfiguration.copy(rpcUsers = listOf(User(username, "pass", emptySet()))) + } assertThatThrownBy { configWithRPCUsername("user.1") }.hasMessageContaining(".") assertThatThrownBy { configWithRPCUsername("user*1") }.hasMessageContaining("*") assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#") diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 41c4386930..174dab96f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -22,10 +22,10 @@ import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MOCK_VERSION_INFO -import net.corda.testing.TestNodeConfiguration import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freePort import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.jetbrains.exposed.sql.Database @@ -70,10 +70,9 @@ class ArtemisMessagingTests { fun setUp() { val baseDirectory = temporaryFolder.root.toPath() userService = RPCUserServiceImpl(emptyList()) - config = TestNodeConfiguration( + config = testNodeConfiguration( baseDirectory = baseDirectory, - myLegalName = ALICE.name, - networkMapService = null) + myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) dataSource = dataSourceAndDatabase.first 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 1a2dfa6569..6e2ea2f3d3 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 @@ -7,8 +7,8 @@ import net.corda.core.crypto.* import net.corda.core.exists import net.corda.core.toTypedArray import net.corda.core.utilities.ALICE -import net.corda.testing.TestNodeConfiguration import net.corda.testing.getTestX509Name +import net.corda.testing.testNodeConfiguration import org.bouncycastle.cert.X509CertificateHolder import org.junit.Rule import org.junit.Test @@ -38,10 +38,9 @@ class NetworkRegistrationHelperTest { on { retrieveCertificates(eq(id)) }.then { certs } } - val config = TestNodeConfiguration( + val config = testNodeConfiguration( baseDirectory = tempFolder.root.toPath(), - myLegalName = ALICE.name, - networkMapService = null) + myLegalName = ALICE.name) assertFalse(config.nodeKeystore.exists()) assertFalse(config.sslKeystore.exists()) diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index d3576d0cda..89f4b7dfa9 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -20,11 +20,11 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.transaction -import net.corda.testing.TestNodeConfiguration import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import net.corda.testing.node.TestClock import net.corda.testing.node.setTo +import net.corda.testing.testNodeConfiguration import org.bouncycastle.asn1.x500.X500Name import rx.Observable import rx.subjects.PublishSubject @@ -71,10 +71,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val letter = 'A' + counter val (city, country) = bankLocations[counter++ % bankLocations.size] - val cfg = TestNodeConfiguration( + val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = X500Name("CN=Bank $letter,O=Bank $letter,L=$city,C=$country"), - networkMapService = null) + myLegalName = X500Name("CN=Bank $letter,O=Bank $letter,L=$city,C=$country")) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) } @@ -93,10 +92,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { require(advertisedServices.containsType(NetworkMapService.type)) - val cfg = TestNodeConfiguration( + val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = DUMMY_MAP.name, - networkMapService = null) + myLegalName = DUMMY_MAP.name) return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {} } } @@ -106,10 +104,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { require(advertisedServices.containsType(SimpleNotaryService.type)) - val cfg = TestNodeConfiguration( + val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = DUMMY_NOTARY.name, - networkMapService = null) + myLegalName = DUMMY_NOTARY.name) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) } } @@ -122,10 +119,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { require(advertisedServices.containsType(NodeInterestRates.Oracle.type)) - val cfg = TestNodeConfiguration( + val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = RATES_SERVICE_NAME, - networkMapService = null) + myLegalName = RATES_SERVICE_NAME) return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun start(): MockNetwork.MockNode { super.start() @@ -146,10 +142,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { - val cfg = TestNodeConfiguration( + val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = DUMMY_REGULATOR.name, - networkMapService = null) + myLegalName = DUMMY_REGULATOR.name) return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. 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 1f7fb0d0db..febd95d3dd 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -4,7 +4,13 @@ package net.corda.testing import com.google.common.net.HostAndPort +import com.nhaarman.mockito_kotlin.spy +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.commonName +import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -14,10 +20,8 @@ import net.corda.core.node.services.IdentityService import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* -import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.* import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties @@ -29,7 +33,6 @@ import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey -import java.util.* import java.util.concurrent.atomic.AtomicInteger /** @@ -142,47 +145,26 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } -// TODO Replace this with testConfiguration -data class TestNodeConfiguration( - override val baseDirectory: Path, - override val myLegalName: X500Name, - override val networkMapService: NetworkMapInfo?, - override val minimumPlatformVersion: Int = 1, - override val keyStorePassword: String = "cordacadevpass", - override val trustStorePassword: String = "trustpass", - override val rpcUsers: List = emptyList(), - override val dataSourceProperties: Properties = makeTestDataSourceProperties(myLegalName.commonName), - override val emailAddress: String = "", - override val exportJMXto: String = "", - override val devMode: Boolean = true, - override val certificateSigningService: URL = URL("http://localhost"), - override val certificateChainCheckPolicies: List = emptyList(), - override val verifierType: VerifierType = VerifierType.InMemory, - override val messageRedeliveryDelaySeconds: Int = 5) : NodeConfiguration { -} - -fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int): FullNodeConfiguration { - return FullNodeConfiguration( - basedir = baseDirectory, - myLegalName = legalName, - networkMapService = null, - emailAddress = "", - keyStorePassword = "cordacadevpass", - trustStorePassword = "trustpass", - dataSourceProperties = makeTestDataSourceProperties(legalName.commonName), - certificateSigningService = URL("http://localhost"), - rpcUsers = emptyList(), - verifierType = VerifierType.InMemory, - useHTTPS = false, - p2pAddress = HostAndPort.fromParts("localhost", basePort), - rpcAddress = HostAndPort.fromParts("localhost", basePort + 1), - messagingServerAddress = null, - extraAdvertisedServiceIds = emptyList(), - bftReplicaId = null, - notaryNodeAddress = null, - notaryClusterAddresses = emptyList(), - certificateChainCheckPolicies = emptyList(), - devMode = true) +fun testNodeConfiguration( + baseDirectory: Path, + myLegalName: X500Name): NodeConfiguration { + abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. + val nc = spy() + whenever(nc.baseDirectory).thenReturn(baseDirectory) + whenever(nc.myLegalName).thenReturn(myLegalName) + whenever(nc.minimumPlatformVersion).thenReturn(1) + whenever(nc.keyStorePassword).thenReturn("cordacadevpass") + whenever(nc.trustStorePassword).thenReturn("trustpass") + whenever(nc.rpcUsers).thenReturn(emptyList()) + whenever(nc.dataSourceProperties).thenReturn(makeTestDataSourceProperties(myLegalName.commonName)) + whenever(nc.emailAddress).thenReturn("") + whenever(nc.exportJMXto).thenReturn("") + whenever(nc.devMode).thenReturn(true) + whenever(nc.certificateSigningService).thenReturn(URL("http://localhost")) + whenever(nc.certificateChainCheckPolicies).thenReturn(emptyList()) + whenever(nc.verifierType).thenReturn(VerifierType.InMemory) + whenever(nc.messageRedeliveryDelaySeconds).thenReturn(5) + return nc } @JvmOverloads diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 5980df134f..6a068d60db 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -4,6 +4,7 @@ import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.* import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.cert @@ -35,8 +36,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.testing.MOCK_VERSION_INFO -import net.corda.testing.TestNodeConfiguration import net.corda.testing.getTestX509Name +import net.corda.testing.testNodeConfiguration import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger @@ -66,7 +67,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val defaultFactory: Factory = MockNetwork.DefaultFactory) { - private var nextNodeId = 0 + val nextNodeId + get() = _nextNodeId + private var _nextNodeId = 0 val filesystem: FileSystem = Jimfs.newFileSystem(unix()) private val busyLatch: ReusableLatch = ReusableLatch() val messagingNetwork = InMemoryMessagingNetwork(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) @@ -275,24 +278,27 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, * @param overrideServices a set of service entries to use in place of the node's default service entries, * for example where a node's service is part of a cluster. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, - * but can be overriden to cause nodes to have stable or colliding identity/service keys. + * but can be overridden to cause nodes to have stable or colliding identity/service keys. + * @param configOverrides add/override behaviour of the [NodeConfiguration] mock object. */ fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory, start: Boolean = true, legalName: X500Name? = null, overrideServices: Map? = null, entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), - vararg advertisedServices: ServiceInfo): MockNode { + vararg advertisedServices: ServiceInfo, + configOverrides: (NodeConfiguration) -> Any? = {}): MockNode { val newNode = forcedID == -1 - val id = if (newNode) nextNodeId++ else forcedID + val id = if (newNode) _nextNodeId++ else forcedID - val path = filesystem.getPath("/nodes/$id") + val path = baseDirectory(id) if (newNode) (path / "attachments").createDirectories() - val config = TestNodeConfiguration( + val config = testNodeConfiguration( baseDirectory = path, - myLegalName = legalName ?: getTestX509Name("Mock Company $id"), - networkMapService = null, - dataSourceProperties = makeTestDataSourceProperties("node_${id}_net_$networkId")) + myLegalName = legalName ?: getTestX509Name("Mock Company $id")).also { + whenever(it.dataSourceProperties).thenReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")) + configOverrides(it) + } val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot) if (start) { node.setup().start() @@ -305,6 +311,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return node } + fun baseDirectory(nodeId: Int) = filesystem.getPath("/nodes/$nodeId") + /** * Asks every node in order to process any queued up inbound messages. This may in turn result in nodes * sending more messages to each other, thus, a typical usage is to call runNetwork with the [rounds]