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)