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
13 changed files with 316 additions and 21 deletions

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 {