From 7edc18f85deb0b18171dc63f37d1dfa8ea87e4c3 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 3 Oct 2018 13:41:25 +0100 Subject: [PATCH 1/2] CORDA-1997 Added constraint type information to vault states table. (#3975) * Added constraint type information to vault states table. * Added Vault Query criteria support for constraint data. * Added documentation and changelog entry. * Added missing @CordaSerializable. * Fix minor bug in test setup and parsing code. * Use binary encoding data types instead of serialize/deserialize. * Optimized storage of constraints data. Additional assertions on Vault Query constraint data contents (to validate encoding/decoding). Tested with CompositeKey containing 10 keys. * Addressing PR review feedback. * Query by constraints type and data. * Revert back accidentally removed code for contractStateType filtering. * Incorporating final PR review feedback. Use @JvmOverloads on constructor. * Make sure constraintInfo is class evolution friendly. --- .../corda/core/node/services/VaultService.kt | 74 +++++++-- .../core/node/services/vault/QueryCriteria.kt | 6 +- .../node/services/vault/QueryCriteriaUtils.kt | 3 +- docs/source/api-contract-constraints.rst | 2 + docs/source/api-vault-query.rst | 19 ++- docs/source/changelog.rst | 2 + .../vault/HibernateQueryCriteriaParser.kt | 35 ++++- .../node/services/vault/NodeVaultService.kt | 13 +- .../corda/node/services/vault/VaultSchema.kt | 12 +- .../vault-schema.changelog-master.xml | 1 + .../migration/vault-schema.changelog-v6.xml | 17 ++ .../node/services/vault/VaultQueryTests.kt | 147 ++++++++++++++++++ .../testing/internal/vault/VaultFiller.kt | 6 +- 13 files changed, 316 insertions(+), 21 deletions(-) create mode 100644 node/src/main/resources/migration/vault-schema.changelog-v6.xml diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 20d1984d8d..43b71bf966 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -5,6 +5,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* +import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -18,6 +19,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.toHexString import rx.Observable import java.time.Instant import java.util.* @@ -125,6 +127,44 @@ class Vault(val states: Iterable>) { RELEVANT, NOT_RELEVANT, ALL } + /** + * Contract constraint information associated with a [ContractState]. + * See [AttachmentConstraint] + */ + @CordaSerializable + data class ConstraintInfo(val constraint: AttachmentConstraint) { + @CordaSerializable + enum class Type { + ALWAYS_ACCEPT, HASH, CZ_WHITELISTED, SIGNATURE + } + fun type(): Type { + return when (constraint::class.java) { + AlwaysAcceptAttachmentConstraint::class.java -> Type.ALWAYS_ACCEPT + HashAttachmentConstraint::class.java -> Type.HASH + WhitelistedByZoneAttachmentConstraint::class.java -> Type.CZ_WHITELISTED + SignatureAttachmentConstraint::class.java -> Type.SIGNATURE + else -> throw IllegalArgumentException("Invalid constraint type: $constraint") + } + } + fun data(): ByteArray? { + return when (type()) { + Type.HASH -> (constraint as HashAttachmentConstraint).attachmentId.bytes + Type.SIGNATURE -> (constraint as SignatureAttachmentConstraint).key.encoded + else -> null + } + } + companion object { + fun constraintInfo(type: Type, data: ByteArray?): ConstraintInfo { + return when (type) { + Type.ALWAYS_ACCEPT -> ConstraintInfo(AlwaysAcceptAttachmentConstraint) + Type.HASH -> ConstraintInfo(HashAttachmentConstraint(SecureHash.parse(data!!.toHexString()))) + Type.CZ_WHITELISTED -> ConstraintInfo(WhitelistedByZoneAttachmentConstraint) + Type.SIGNATURE -> ConstraintInfo(SignatureAttachmentConstraint(Crypto.decodePublicKey(data!!))) + } + } + } + } + @CordaSerializable enum class UpdateType { GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE @@ -151,7 +191,7 @@ class Vault(val states: Iterable>) { val otherResults: List) @CordaSerializable - data class StateMetadata constructor( + data class StateMetadata @JvmOverloads constructor( val ref: StateRef, val contractStateClassName: String, val recordedTime: Instant, @@ -160,18 +200,9 @@ class Vault(val states: Iterable>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val relevancyStatus: Vault.RelevancyStatus? + val relevancyStatus: Vault.RelevancyStatus? = null, + val constraintInfo: ConstraintInfo? = null ) { - constructor(ref: StateRef, - contractStateClassName: String, - recordedTime: Instant, - consumedTime: Instant?, - status: Vault.StateStatus, - notary: AbstractParty?, - lockId: String?, - lockUpdateTime: Instant? - ) : this(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) - fun copy( ref: StateRef = this.ref, contractStateClassName: String = this.contractStateClassName, @@ -184,6 +215,19 @@ class Vault(val states: Iterable>) { ): StateMetadata { return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) } + fun copy( + ref: StateRef = this.ref, + contractStateClassName: String = this.contractStateClassName, + recordedTime: Instant = this.recordedTime, + consumedTime: Instant? = this.consumedTime, + status: Vault.StateStatus = this.status, + notary: AbstractParty? = this.notary, + lockId: String? = this.lockId, + lockUpdateTime: Instant? = this.lockUpdateTime, + relevancyStatus: Vault.RelevancyStatus? + ): StateMetadata { + return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, relevancyStatus, ConstraintInfo(AlwaysAcceptAttachmentConstraint)) + } } companion object { @@ -194,6 +238,12 @@ class Vault(val states: Iterable>) { } } +/** + * The maximum permissible size of contract constraint type data (for storage in vault states database table). + * Maximum value equates to a CompositeKey with 10 EDDSA_ED25519_SHA512 keys stored in. + */ +const val MAX_CONSTRAINT_DATA_SIZE = 563 + /** * A [VaultService] is responsible for securely and safely persisting the current state of a vault to storage. The * vault service vends immutable snapshots of the current vault for working with: if you build a transaction based 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 609434a60b..2ee555dee1 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 @@ -74,6 +74,8 @@ sealed class QueryCriteria : GenericQueryCriteria = emptySet() + open val constraints: Set = emptySet() abstract val contractStateTypes: Set>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -90,7 +92,9 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null, - override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL, + override val constraintTypes: Set = emptySet(), + override val constraints: Set = emptySet() ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) 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 faac1f7fef..2492313d0b 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 @@ -184,7 +184,8 @@ data class Sort(val columns: Collection) : BaseSort() { STATE_STATUS("stateStatus"), RECORDED_TIME("recordedTime"), CONSUMED_TIME("consumedTime"), - LOCK_ID("lockId") + LOCK_ID("lockId"), + CONSTRAINT_TYPE("constraintType") } enum class LinearStateAttribute(val attributeName: String) : Attribute { diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 3863926981..df09bb5f26 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -51,6 +51,8 @@ upgrade approach is that you can upgrade states regardless of their constraint, anticipate a need to do so. But it requires everyone to sign, requires everyone to manually authorise the upgrade, consumes notary and ledger resources, and is just in general more complex. +.. _implicit_constraint_types: + How constraints work -------------------- diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index a012b84ea7..2243a6bfea 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -78,7 +78,8 @@ and/or composition and a rich set of operators to include: 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). + CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED), + state constraints (see :ref:`Constraint Types `). .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft locked states). @@ -260,6 +261,22 @@ Query for unconsumed states for several contract state types: :end-before: DOCEND VaultQueryExample3 :dedent: 12 +Query for unconsumed states for specified contract state constraint types and sorted in ascending alphabetical order: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample30 + :end-before: DOCEND VaultQueryExample30 + :dedent: 12 + +Query for unconsumed states for specified contract state constraints (type and data): + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample31 + :end-before: DOCEND VaultQueryExample31 + :dedent: 12 + Query for unconsumed states for a given notary: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 78e75bd607..3017257931 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,8 @@ Unreleased * Introduce minimum and target platform version for CorDapps. +* Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. + * New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. * Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index a3fa9aee01..f4d912ffb1 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -225,6 +225,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate + private val constraintPredicates = mutableSetOf() var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED @@ -508,7 +509,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class).values.map { (it as LiteralExpression).literal }.toSet() + if (existingTypes != criteria.constraintTypes) { + log.warn("Enriching previous attribute [${VaultSchemaV1.VaultStates::constraintType.name}] values [$existingTypes] with [${criteria.constraintTypes}]") + commonPredicates.replace(predicateID, criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes.plus(existingTypes)))) + } + } else { + commonPredicates[predicateID] = criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes)) + } + } + + // contract constraint information (type and data) + if (criteria.constraints.isNotEmpty()) { + criteria.constraints.forEach { constraint -> + val predicateConstraintType = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::constraintType.name), constraint.type()) + if (constraint.data() != null) { + val predicateConstraintData = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::constraintData.name), constraint.data()) + val compositePredicate = criteriaBuilder.and(predicateConstraintType, predicateConstraintData) + if (constraintPredicates.isNotEmpty()) { + val previousPredicate = constraintPredicates.last() + constraintPredicates.clear() + constraintPredicates.add(criteriaBuilder.or(previousPredicate, compositePredicate)) + } + else constraintPredicates.add(compositePredicate) + } + else constraintPredicates.add(criteriaBuilder.or(predicateConstraintType)) + } + } + return emptySet() } 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 e3d11f0cab..1d20d8311f 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 @@ -11,8 +11,12 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.node.services.* import net.corda.core.node.services.vault.* +import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo import net.corda.core.schemas.PersistentStateRef +import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.transactions.* import net.corda.core.utilities.* import net.corda.node.services.api.SchemaService @@ -132,12 +136,15 @@ class NodeVaultService( // Adding a new column in the "VaultStates" table was considered the best approach. val keys = stateOnly.participants.map { it.owningKey } val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) + val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint) val stateToAdd = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, recordedTime = clock.instant(), - relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT + relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT, + constraintType = constraintInfo.type(), + constraintData = constraintInfo.data() ) stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) session.save(stateToAdd) @@ -513,7 +520,9 @@ class NodeVaultService( vaultState.notary, vaultState.lockId, vaultState.lockUpdateTime, - vaultState.relevancyStatus)) + vaultState.relevancyStatus, + constraintInfo(vaultState.constraintType, vaultState.constraintData) + )) } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index e12f8fc7ac..8755eccd94 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.node.services.MAX_CONSTRAINT_DATA_SIZE import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState @@ -66,7 +67,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) - var lockUpdateTime: Instant? = null + var lockUpdateTime: Instant? = null, + + /** refers to constraint type (none, hash, whitelisted, signature) associated with a contract state */ + @Column(name = "constraint_type", nullable = false) + var constraintType: Vault.ConstraintInfo.Type, + + /** associated constraint type data (if any) */ + @Column(name = "constraint_data", length = MAX_CONSTRAINT_DATA_SIZE, nullable = true) + @Type(type = "corda-wrapper-binary") + var constraintData: ByteArray? = null ) : PersistentState() @Entity diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index a9fbc181c5..0c3d274098 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -9,4 +9,5 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v6.xml b/node/src/main/resources/migration/vault-schema.changelog-v6.xml new file mode 100644 index 0000000000..71214f8050 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v6.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + 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 0d9cbe9f68..99010acca7 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 @@ -9,6 +9,7 @@ import net.corda.core.internal.packageName 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.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* @@ -472,6 +473,125 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `query by contract states constraint type`() { + database.transaction { + // insert states with different constraint types + vaultFiller.fillWithSomeTestLinearStates(1).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint + // hash constraint + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint + // signature constraint (single key) + val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + val constraintSignature = linearStateSignature.states.first().state.constraint as SignatureAttachmentConstraint + // signature constraint (composite key) + val compositeKey = CompositeKey.Builder().addKeys(alice.publicKey, bob.publicKey, charlie.publicKey, bankOfCorda.publicKey, bigCorp.publicKey, megaCorp.publicKey, miniCorp.publicKey, cashNotary.publicKey, dummyNotary.publicKey, dummyCashIssuer.publicKey).build() + val linearStateSignatureCompositeKey = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(compositeKey)) + val constraintSignatureCompositeKey = linearStateSignatureCompositeKey.states.first().state.constraint as SignatureAttachmentConstraint + + // default Constraint Type is ALL + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + + // search for states with Vault.ConstraintInfo.Type = ALWAYS_ACCEPT + val constraintTypeCriteria1 = VaultQueryCriteria(constraintTypes = setOf(ALWAYS_ACCEPT)) + val constraintResults1 = vaultService.queryBy(constraintTypeCriteria1) + assertThat(constraintResults1.states).hasSize(1) + + // search for states with [Vault.ConstraintInfo.Type] = HASH + val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH)) + val constraintResults2 = vaultService.queryBy(constraintTypeCriteria2) + assertThat(constraintResults2.states).hasSize(2) + assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash) + + // search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED + // DOCSTART VaultQueryExample30 + val constraintTypeCriteria = VaultQueryCriteria(constraintTypes = setOf(HASH, CZ_WHITELISTED)) + val sortAttribute = SortAttribute.Standard(Sort.VaultStateAttribute.CONSTRAINT_TYPE) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + val constraintResults = vaultService.queryBy(constraintTypeCriteria, sorter) + // DOCEND VaultQueryExample30 + assertThat(constraintResults.states).hasSize(3) + + // search for states with [Vault.ConstraintInfo.Type] = SIGNATURE + val constraintTypeCriteria4 = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE)) + val constraintResults4 = vaultService.queryBy(constraintTypeCriteria4) + assertThat(constraintResults4.states).hasSize(2) + assertThat(constraintResults4.states.map { it.state.constraint }).containsAll(listOf(constraintSignature, constraintSignatureCompositeKey)) + + // search for states with [Vault.ConstraintInfo.Type] = SIGNATURE or CZ_WHITELISED + val constraintTypeCriteria5 = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE, CZ_WHITELISTED)) + val constraintResults5 = vaultService.queryBy(constraintTypeCriteria5) + assertThat(constraintResults5.states).hasSize(3) + } + } + + @Test + fun `query by contract states constraint type and data`() { + database.transaction { + // insert states with different constraint types + vaultFiller.fillWithSomeTestLinearStates(1).states.first().state.constraint + val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) + // hash constraint + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint + // signature constraint (single key) + val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + val constraintSignature = linearStateSignature.states.first().state.constraint as SignatureAttachmentConstraint + // signature constraint (composite key) + val compositeKey = CompositeKey.Builder().addKeys(alice.publicKey, bob.publicKey, charlie.publicKey, bankOfCorda.publicKey, bigCorp.publicKey, megaCorp.publicKey, miniCorp.publicKey, cashNotary.publicKey, dummyNotary.publicKey, dummyCashIssuer.publicKey).build() + val linearStateSignatureCompositeKey = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(compositeKey)) + val constraintSignatureCompositeKey = linearStateSignatureCompositeKey.states.first().state.constraint as SignatureAttachmentConstraint + + // default Constraint Type is ALL + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + + // search for states with AlwaysAcceptAttachmentConstraint + val constraintCriteria1 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(AlwaysAcceptAttachmentConstraint))) + val constraintResults1 = vaultService.queryBy(constraintCriteria1) + assertThat(constraintResults1.states).hasSize(1) + assertThat(constraintResults1.states.first().state.constraint).isEqualTo(alwaysAcceptConstraint) + + // search for states for a specific HashAttachmentConstraint + val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash))) + val constraintResults2 = vaultService.queryBy(constraintsCriteria2) + assertThat(constraintResults2.states).hasSize(1) + assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash) + + // search for states with a specific SignatureAttachmentConstraint constraint + val constraintCriteria3 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignatureCompositeKey))) + val constraintResults3 = vaultService.queryBy(constraintCriteria3) + assertThat(constraintResults3.states).hasSize(1) + assertThat(constraintResults3.states.first().state.constraint).isEqualTo(constraintSignatureCompositeKey) + + // search for states for given set of mixed constraint types + // DOCSTART VaultQueryExample31 + val constraintCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignature), + Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash))) + val constraintResults = vaultService.queryBy(constraintCriteria) + // DOCEND VaultQueryExample31 + assertThat(constraintResults.states).hasSize(3) + assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey)) + + // exercise enriched query + + // Base criteria + val baseCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(AlwaysAcceptAttachmentConstraint))) + + // Enrich and override QueryCriteria with additional default attributes + val enrichedCriteria = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintSignature))) + + // Execute query + val enrichedResults = services.vaultService.queryBy(baseCriteria and enrichedCriteria).states + assertThat(enrichedResults).hasSize(2) + assertThat(enrichedResults.map { it.state.constraint }).containsAll(listOf(constraintSignature, alwaysAcceptConstraint)) + } + } + @Test fun `consumed states`() { database.transaction { @@ -2211,6 +2331,33 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `sorted, enriched and overridden composite query with constraints handles defaults correctly`() { + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256())) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint) + + // Base criteria + val baseCriteria = VaultQueryCriteria(constraintTypes = setOf(ALWAYS_ACCEPT)) + + // Enrich and override QueryCriteria with additional default attributes (contract constraints) + val enrichedCriteria = VaultQueryCriteria(constraintTypes = setOf(SIGNATURE, HASH, ALWAYS_ACCEPT)) // enrich + + // Sorting + val sortAttribute = SortAttribute.Standard(Sort.VaultStateAttribute.CONSTRAINT_TYPE) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + + // Execute query + val results = services.vaultService.queryBy(baseCriteria and enrichedCriteria, sorter).states + assertThat(results).hasSize(3) + assertThat(results[0].state.constraint is AlwaysAcceptAttachmentConstraint) + assertThat(results[1].state.constraint is HashAttachmentConstraint) + assertThat(results[2].state.constraint is SignatureAttachmentConstraint) + } + } + @Test fun unconsumedCashStatesForSpending_single_issuer_reference() { database.transaction { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index f5ae1f852a..eb46690305 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -102,7 +102,8 @@ class VaultFiller @JvmOverloads constructor( linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, - linearTimestamp: Instant = now()): Vault { + linearTimestamp: Instant = now(), + constraint: AttachmentConstraint = AutomaticHashConstraint): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair @@ -116,7 +117,8 @@ class VaultFiller @JvmOverloads constructor( linearString = linearString, linearNumber = linearNumber, linearBoolean = linearBoolean, - linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) + linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID, + constraint = constraint) addCommand(dummyCommand()) } return@map services.signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata) From 3110c758474e26ddb30e2c03b957680ace2cb6db Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 3 Oct 2018 13:41:52 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Network=20bootstrapper=20tool:=20optional?= =?UTF-8?q?=20configuration=20setting=20to=20specify=20the=20minimum=20pla?= =?UTF-8?q?t=E2=80=A6=20(#4005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Provide an optional configuration setting to specify the minimum platform version to use in the network params file. * Leave Cordform signature intact. * Leave previous Gradle Plugin called signature intact. * Incorporating feedback from PR review. * Added minimum platform version validation check. * Removed final 2 references to "default" * Added changelog entry. --- docs/source/changelog.rst | 2 ++ .../internal/network/NetworkBootstrapper.kt | 28 +++++++++++++------ .../network/NetworkBootstrapperTest.kt | 3 +- .../kotlin/net/corda/bootstrapper/Main.kt | 6 +++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3017257931..9b3e3bb571 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter + * Introduce minimum and target platform version for CorDapps. * Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index 0a80988f27..49d0704c84 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -162,16 +162,23 @@ internal constructor(private val initSerEnv: Boolean, } /** Entry point for the tool */ - fun bootstrap(directory: Path, copyCordapps: Boolean) { + fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int) { + require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" } // Don't accidently include the bootstrapper jar as a CorDapp! val bootstrapperJar = javaClass.location.toPath() val cordappJars = directory.list { paths -> paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList() } - bootstrap(directory, cordappJars, copyCordapps, fromCordform = false) + bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion) } - private fun bootstrap(directory: Path, cordappJars: List, copyCordapps: Boolean, fromCordform: Boolean) { + private fun bootstrap( + directory: Path, + cordappJars: List, + copyCordapps: Boolean, + fromCordform: Boolean, + minimumPlatformVersion: Int = PLATFORM_VERSION + ) { directory.createDirectories() println("Bootstrapping local test network in $directory") if (!fromCordform) { @@ -210,7 +217,7 @@ internal constructor(private val initSerEnv: Boolean, val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs) println("Generating contract implementations whitelist") val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter)) - val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs) + val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion) if (newNetParams != existingNetParams) { println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams") } else { @@ -337,10 +344,13 @@ internal constructor(private val initSerEnv: Boolean, throw IllegalStateException(msg.toString()) } - private fun installNetworkParameters(notaryInfos: List, - whitelist: Map>, - existingNetParams: NetworkParameters?, - nodeDirs: List): NetworkParameters { + private fun installNetworkParameters( + notaryInfos: List, + whitelist: Map>, + existingNetParams: NetworkParameters?, + nodeDirs: List, + minimumPlatformVersion: Int + ): NetworkParameters { // TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize val netParams = if (existingNetParams != null) { if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) { @@ -355,7 +365,7 @@ internal constructor(private val initSerEnv: Boolean, } } else { NetworkParameters( - minimumPlatformVersion = 4, + minimumPlatformVersion = minimumPlatformVersion, notaries = notaryInfos, modifiedTime = Instant.now(), maxMessageSize = 10485760, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt index 0b265171e6..1871ea553f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapperTest.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.toConfig @@ -217,7 +218,7 @@ class NetworkBootstrapperTest { private fun bootstrap(copyCordapps: Boolean = true) { providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null } - bootstrapper.bootstrap(rootDir, copyCordapps) + bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION) } private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) { diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 09e6ca9972..9d382516fa 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -2,6 +2,7 @@ package net.corda.bootstrapper import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.start +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.nodeapi.internal.network.NetworkBootstrapper import picocli.CommandLine.Option import java.nio.file.Path @@ -24,8 +25,11 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) private var noCopy: Boolean = false + @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"]) + private var minimumPlatformVersion = PLATFORM_VERSION + override fun runProgram(): Int { - NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy) + NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy, minimumPlatformVersion = minimumPlatformVersion) return 0 //exit code } } \ No newline at end of file