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.
This commit is contained in:
josecoll 2018-10-03 13:41:25 +01:00 committed by GitHub
parent beb4dc008f
commit 7edc18f85d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 316 additions and 21 deletions

View File

@ -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<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
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<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val otherResults: List<Any>)
@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<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
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<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
): 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<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
}
}
/**
* 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

View File

@ -74,6 +74,8 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
abstract class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus
open val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
open val constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet()
open val constraints: Set<Vault.ConstraintInfo> = emptySet()
abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this)
@ -90,7 +92,9 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
val notary: List<AbstractParty>? = 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<Vault.ConstraintInfo.Type> = emptySet(),
override val constraints: Set<Vault.ConstraintInfo> = emptySet()
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)

View File

@ -184,7 +184,8 @@ data class Sort(val columns: Collection<SortColumn>) : 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 {

View File

@ -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
--------------------

View File

@ -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 <implicit_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

View File

@ -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.

View File

@ -225,6 +225,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
private val aggregateExpressions = mutableListOf<Expression<*>>()
private val commonPredicates = mutableMapOf<Pair<String, Operator>, Predicate>() // schema attribute Name, operator -> predicate
private val constraintPredicates = mutableSetOf<Predicate>()
var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
@ -508,7 +509,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
else
aggregateExpressions
criteriaQuery.multiselect(selections)
val combinedPredicates = commonPredicates.values.plus(predicateSet)
val combinedPredicates = commonPredicates.values.plus(predicateSet).plus(constraintPredicates)
criteriaQuery.where(*combinedPredicates.toTypedArray())
return predicateSet
@ -561,6 +562,38 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
}
}
// contract constraint types
if (criteria.constraintTypes.isNotEmpty()) {
val predicateID = Pair(VaultSchemaV1.VaultStates::constraintType.name, IN)
if (commonPredicates.containsKey(predicateID)) {
val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).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<Vault.ConstraintInfo.Type>(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes.plus(existingTypes))))
}
} else {
commonPredicates[predicateID] = criteriaBuilder.and(vaultStates.get<Vault.ConstraintInfo.Type>(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<Vault.ConstraintInfo>(VaultSchemaV1.VaultStates::constraintType.name), constraint.type())
if (constraint.data() != null) {
val predicateConstraintData = criteriaBuilder.equal(vaultStates.get<Vault.ConstraintInfo>(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()
}

View File

@ -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())}" }

View File

@ -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

View File

@ -9,4 +9,5 @@
<include file="migration/vault-schema.changelog-v4.xml"/>
<include file="migration/vault-schema.changelog-pkey.xml"/>
<include file="migration/vault-schema.changelog-v5.xml"/>
<include file="migration/vault-schema.changelog-v6.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,17 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="add_is_constraint_information_columns">
<addColumn tableName="vault_states">
<column name="constraint_type" type="INT" defaultValue="0">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="vault_states">
<column name="constraint_data" type="varbinary(563)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@ -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<LinearState>()
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<LinearState>(constraintTypeCriteria1)
assertThat(constraintResults1.states).hasSize(1)
// search for states with [Vault.ConstraintInfo.Type] = HASH
val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH))
val constraintResults2 = vaultService.queryBy<LinearState>(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<LinearState>(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<LinearState>(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<LinearState>(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<LinearState>()
assertThat(results.states).hasSize(6)
// search for states with AlwaysAcceptAttachmentConstraint
val constraintCriteria1 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(AlwaysAcceptAttachmentConstraint)))
val constraintResults1 = vaultService.queryBy<LinearState>(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<LinearState>(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<LinearState>(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<LinearState>(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<LinearState>(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<LinearState>(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 {

View File

@ -102,7 +102,8 @@ class VaultFiller @JvmOverloads constructor(
linearString: String = "",
linearNumber: Long = 0L,
linearBoolean: Boolean = false,
linearTimestamp: Instant = now()): Vault<LinearState> {
linearTimestamp: Instant = now(),
constraint: AttachmentConstraint = AutomaticHashConstraint): Vault<LinearState> {
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)