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.DoNotImplement
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic 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.toFuture
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.toHexString
import rx.Observable import rx.Observable
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -125,6 +127,44 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
RELEVANT, NOT_RELEVANT, ALL 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 @CordaSerializable
enum class UpdateType { enum class UpdateType {
GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE
@ -151,7 +191,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val otherResults: List<Any>) val otherResults: List<Any>)
@CordaSerializable @CordaSerializable
data class StateMetadata constructor( data class StateMetadata @JvmOverloads constructor(
val ref: StateRef, val ref: StateRef,
val contractStateClassName: String, val contractStateClassName: String,
val recordedTime: Instant, val recordedTime: Instant,
@ -160,18 +200,9 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val notary: AbstractParty?, val notary: AbstractParty?,
val lockId: String?, val lockId: String?,
val lockUpdateTime: Instant?, 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( fun copy(
ref: StateRef = this.ref, ref: StateRef = this.ref,
contractStateClassName: String = this.contractStateClassName, contractStateClassName: String = this.contractStateClassName,
@ -184,6 +215,19 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
): StateMetadata { ): StateMetadata {
return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) 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 { 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 * 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 * 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 class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus abstract val status: Vault.StateStatus
open val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL 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>>? abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this)
@ -90,7 +92,9 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
val notary: List<AbstractParty>? = null, val notary: List<AbstractParty>? = null,
val softLockingCondition: SoftLockingCondition? = null, val softLockingCondition: SoftLockingCondition? = null,
val timeCondition: TimeCondition? = 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() { ) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser) super.visit(parser)

View File

@ -184,7 +184,8 @@ data class Sort(val columns: Collection<SortColumn>) : BaseSort() {
STATE_STATUS("stateStatus"), STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"), RECORDED_TIME("recordedTime"),
CONSUMED_TIME("consumedTime"), CONSUMED_TIME("consumedTime"),
LOCK_ID("lockId") LOCK_ID("lockId"),
CONSTRAINT_TYPE("constraintType")
} }
enum class LinearStateAttribute(val attributeName: String) : Attribute { 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, 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. consumes notary and ledger resources, and is just in general more complex.
.. _implicit_constraint_types:
How constraints work 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. 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, 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 .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, always include soft
locked states). locked states).
@ -260,6 +261,22 @@ Query for unconsumed states for several contract state types:
:end-before: DOCEND VaultQueryExample3 :end-before: DOCEND VaultQueryExample3
:dedent: 12 :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: Query for unconsumed states for a given notary:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt .. 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. * 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. * 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. * 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 rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
private val aggregateExpressions = mutableListOf<Expression<*>>() private val aggregateExpressions = mutableListOf<Expression<*>>()
private val commonPredicates = mutableMapOf<Pair<String, Operator>, Predicate>() // schema attribute Name, operator -> predicate 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 var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
@ -508,7 +509,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
else else
aggregateExpressions aggregateExpressions
criteriaQuery.multiselect(selections) criteriaQuery.multiselect(selections)
val combinedPredicates = commonPredicates.values.plus(predicateSet) val combinedPredicates = commonPredicates.values.plus(predicateSet).plus(constraintPredicates)
criteriaQuery.where(*combinedPredicates.toTypedArray()) criteriaQuery.where(*combinedPredicates.toTypedArray())
return predicateSet 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() 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.StatesToRecord
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.node.services.vault.* 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.schemas.PersistentStateRef
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
import net.corda.core.serialization.SingletonSerializeAsToken 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.transactions.*
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.services.api.SchemaService 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. // Adding a new column in the "VaultStates" table was considered the best approach.
val keys = stateOnly.participants.map { it.owningKey } val keys = stateOnly.participants.map { it.owningKey }
val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet())
val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint)
val stateToAdd = VaultSchemaV1.VaultStates( val stateToAdd = VaultSchemaV1.VaultStates(
notary = stateAndRef.value.state.notary, notary = stateAndRef.value.state.notary,
contractStateClassName = stateAndRef.value.state.data.javaClass.name, contractStateClassName = stateAndRef.value.state.data.javaClass.name,
stateStatus = Vault.StateStatus.UNCONSUMED, stateStatus = Vault.StateStatus.UNCONSUMED,
recordedTime = clock.instant(), 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) stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
session.save(stateToAdd) session.save(stateToAdd)
@ -513,7 +520,9 @@ class NodeVaultService(
vaultState.notary, vaultState.notary,
vaultState.lockId, vaultState.lockId,
vaultState.lockUpdateTime, vaultState.lockUpdateTime,
vaultState.relevancyStatus)) vaultState.relevancyStatus,
constraintInfo(vaultState.constraintType, vaultState.constraintData)
))
} else { } else {
// TODO: improve typing of returned other results // TODO: improve typing of returned other results
log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } 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.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party 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.node.services.Vault
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState 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) */ /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
@Column(name = "lock_timestamp", nullable = true) @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() ) : PersistentState()
@Entity @Entity

View File

@ -9,4 +9,5 @@
<include file="migration/vault-schema.changelog-v4.xml"/> <include file="migration/vault-schema.changelog-v4.xml"/>
<include file="migration/vault-schema.changelog-pkey.xml"/> <include file="migration/vault-schema.changelog-pkey.xml"/>
<include file="migration/vault-schema.changelog-v5.xml"/> <include file="migration/vault-schema.changelog-v5.xml"/>
<include file="migration/vault-schema.changelog-v6.xml"/>
</databaseChangeLog> </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.*
import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.* 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.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.* 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 @Test
fun `consumed states`() { fun `consumed states`() {
database.transaction { 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 @Test
fun unconsumedCashStatesForSpending_single_issuer_reference() { fun unconsumedCashStatesForSpending_single_issuer_reference() {
database.transaction { database.transaction {

View File

@ -102,7 +102,8 @@ class VaultFiller @JvmOverloads constructor(
linearString: String = "", linearString: String = "",
linearNumber: Long = 0L, linearNumber: Long = 0L,
linearBoolean: Boolean = false, linearBoolean: Boolean = false,
linearTimestamp: Instant = now()): Vault<LinearState> { linearTimestamp: Instant = now(),
constraint: AttachmentConstraint = AutomaticHashConstraint): Vault<LinearState> {
val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey
val me = AnonymousParty(myKey) val me = AnonymousParty(myKey)
val issuerKey = defaultNotary.keyPair val issuerKey = defaultNotary.keyPair
@ -116,7 +117,8 @@ class VaultFiller @JvmOverloads constructor(
linearString = linearString, linearString = linearString,
linearNumber = linearNumber, linearNumber = linearNumber,
linearBoolean = linearBoolean, linearBoolean = linearBoolean,
linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID) linearTimestamp = linearTimestamp), DUMMY_LINEAR_CONTRACT_PROGRAM_ID,
constraint = constraint)
addCommand(dummyCommand()) addCommand(dummyCommand())
} }
return@map services.signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata) return@map services.signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata)