* Added "isRelevant" functionality to the vault. (#3789)

* * Added "isRelevant" functionality to the vault.

* * Changed "isRelevant" to "isParticipant" as this makes more sense in the context of Corda.
* Minor tweak to "isParticipant" method in NodeVaultService.
* Fixed API break and broken JAva tests.

* * Addressed PR comments from Jose.
* Changed all mentions of "relevant" and "participant" to "modifiable".
* Made the default behaviour of vault queries to return ALL states instead of just MODIFIABLE states as this is what always previously happened.
* Updated cash balance queries to only return MODIFIABLE states.

* * Updated cash selection and tryLockFungfibleStatesForSpending.
This commit is contained in:
Roger Willis 2018-08-17 13:39:05 +01:00 committed by GitHub
parent f3392e31d2
commit b7867c3bcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 55 deletions

View File

@ -11,6 +11,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.DataFeed
import net.corda.core.node.services.Vault.StateModificationStatus.*
import net.corda.core.node.services.Vault.StateStatus
import net.corda.core.node.services.vault.*
import net.corda.core.serialization.CordaSerializable
@ -105,6 +106,26 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
UNCONSUMED, CONSUMED, ALL
}
/**
* If the querying node is a participant in a state then it is classed as [MODIFIABLE], although technically the
* state is only _potentially_ modifiable as the contract code may forbid them from performing any actions.
*
* If the querying node is not a participant in a state then it is classed as [NOT_MODIFIABLE]. These types of
* states can still be recorded in the vault if the transaction containing them was recorded with the
* [StatesToRecord.ALL_VISIBLE] flag. This will typically happen for things like reference data which can be
* referenced in transactions as a [ReferencedStateAndRef] but cannot be modified by any party but the maintainer.
*
* If both [MODIFIABLE] and [NOT_MODIFIABLE] states are required to be returned from a query, then the [ALL] flag
* can be used.
*
* NOTE: Default behaviour is for ALL STATES to be returned as this is how Corda behaved before the introduction of
* this query criterion.
*/
@CordaSerializable
enum class StateModificationStatus {
MODIFIABLE, NOT_MODIFIABLE, ALL
}
@CordaSerializable
enum class UpdateType {
GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE
@ -131,14 +152,40 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val otherResults: List<Any>)
@CordaSerializable
data class StateMetadata(val ref: StateRef,
data class StateMetadata constructor(
val ref: StateRef,
val contractStateClassName: String,
val recordedTime: Instant,
val consumedTime: Instant?,
val status: Vault.StateStatus,
val notary: AbstractParty?,
val lockId: String?,
val lockUpdateTime: Instant?)
val lockUpdateTime: Instant?,
val isModifiable: Vault.StateModificationStatus?
) {
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,
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
): StateMetadata {
return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null)
}
}
companion object {
@Deprecated("No longer used. The vault does not emit empty updates")
@ -181,7 +228,10 @@ interface VaultService {
*/
@DeleteForDJVM
fun whenConsumed(ref: StateRef): CordaFuture<Vault.Update<ContractState>> {
val query = QueryCriteria.VaultQueryCriteria(stateRefs = listOf(ref), status = Vault.StateStatus.CONSUMED)
val query = QueryCriteria.VaultQueryCriteria(
stateRefs = listOf(ref),
status = Vault.StateStatus.CONSUMED
)
val result = trackBy<ContractState>(query)
val snapshot = result.snapshot.states
return if (snapshot.isNotEmpty()) {

View File

@ -73,6 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
abstract class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus
open val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
abstract val contractStateTypes: Set<Class<out ContractState>>?
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this)
@ -82,51 +83,124 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
/**
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
*/
data class VaultQueryCriteria @JvmOverloads constructor(override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
data class VaultQueryCriteria @JvmOverloads constructor(
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
val stateRefs: List<StateRef>? = null,
val notary: List<AbstractParty>? = null,
val softLockingCondition: SoftLockingCondition? = null,
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
val timeCondition: TimeCondition? = null,
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
return parser.parseCriteria(this)
}
fun copy(
status: Vault.StateStatus = this.status,
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes,
stateRefs: List<StateRef>? = this.stateRefs,
notary: List<AbstractParty>? = this.notary,
softLockingCondition: SoftLockingCondition? = this.softLockingCondition,
timeCondition: TimeCondition? = this.timeCondition
): VaultQueryCriteria {
return VaultQueryCriteria(
status,
contractStateTypes,
stateRefs,
notary,
softLockingCondition,
timeCondition
)
}
}
/**
* LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState]
*/
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
data class LinearStateQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
val uuid: List<UUID>? = null,
val externalId: List<String>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
constructor(participants: List<AbstractParty>? = null,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
) : CommonQueryCriteria() {
constructor(
participants: List<AbstractParty>? = null,
linearId: List<UniqueIdentifier>? = null,
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
contractStateTypes: Set<Class<out ContractState>>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
contractStateTypes: Set<Class<out ContractState>>? = null,
isRelevant: Vault.StateModificationStatus
) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant)
constructor(
participants: List<AbstractParty>? = null,
linearId: List<UniqueIdentifier>? = null,
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
contractStateTypes: Set<Class<out ContractState>>? = null
) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
return parser.parseCriteria(this)
}
fun copy(
participants: List<AbstractParty>? = this.participants,
uuid: List<UUID>? = this.uuid,
externalId: List<String>? = this.externalId,
status: Vault.StateStatus = this.status,
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
): LinearStateQueryCriteria {
return LinearStateQueryCriteria(
participants,
uuid,
externalId,
status,
contractStateTypes
)
}
}
/**
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*/
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
data class FungibleAssetQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
val issuer: List<AbstractParty>? = null,
val issuerRef: List<OpaqueBytes>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
override val contractStateTypes: Set<Class<out ContractState>>? = null,
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
return parser.parseCriteria(this)
}
fun copy(
participants: List<AbstractParty>? = this.participants,
owner: List<AbstractParty>? = this.owner,
quantity: ColumnPredicate<Long>? = this.quantity,
issuer: List<AbstractParty>? = this.issuer,
issuerRef: List<OpaqueBytes>? = this.issuerRef,
status: Vault.StateStatus = this.status,
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
): FungibleAssetQueryCriteria {
return FungibleAssetQueryCriteria(
participants,
owner,
quantity,
issuer,
issuerRef,
status,
contractStateTypes
)
}
}
/**
@ -137,14 +211,28 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
* Params
* [expression] refers to a (composable) type safe [CriteriaExpression]
*/
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
(val expression: CriteriaExpression<L, Boolean>,
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor(
val expression: CriteriaExpression<L, Boolean>,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
override val contractStateTypes: Set<Class<out ContractState>>? = null,
override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
return parser.parseCriteria(this)
}
fun copy(
expression: CriteriaExpression<L, Boolean> = this.expression,
status: Vault.StateStatus = this.status,
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
): VaultCustomQueryCriteria<L> {
return VaultCustomQueryCriteria(
expression,
status,
contractStateTypes
)
}
}
// timestamps stored in the vault states table [VaultSchema.VaultStates]

View File

@ -5,6 +5,7 @@ import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.transactions.LedgerTransaction
@ -102,7 +103,10 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTra
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId))
val query = QueryCriteria.LinearStateQueryCriteria(
linearId = listOf(linearId),
isRelevant = Vault.StateModificationStatus.ALL
)
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
return subFlow(FinalityFlow(
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {

View File

@ -21,7 +21,8 @@ private fun generateCashSumCriteria(currency: Currency): QueryCriteria {
val sumCriteria = QueryCriteria.VaultCustomQueryCriteria(sum)
val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) }
val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex)
// This query should only return cash states the calling node is a participant of (meaning they can be modified/spent).
val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isModifiable = Vault.StateModificationStatus.MODIFIABLE)
return sumCriteria.and(ccyCriteria)
}
@ -30,7 +31,8 @@ private fun generateCashSumsCriteria(): QueryCriteria {
CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency),
orderBy = Sort.Direction.DESC)
}
return QueryCriteria.VaultCustomQueryCriteria(sum)
// This query should only return cash states the calling node is a participant of (meaning they can be modified/spent).
return QueryCriteria.VaultCustomQueryCriteria(sum, isModifiable = Vault.StateModificationStatus.MODIFIABLE)
}
private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>): Amount<Currency> {

View File

@ -33,11 +33,14 @@ class CashSelectionH2Impl : AbstractCashSelection() {
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
// state_status = 0 -> UNCONSUMED.
// is_modifiable = 0 -> MODIFIABLE.
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND vs.is_modifiable = 0
AND ccs.ccy_code = ? and @t < ?
AND (vs.lock_id = ? OR vs.lock_id is null)
""" +

View File

@ -4,7 +4,9 @@ import net.corda.core.contracts.Amount
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.utilities.*
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import java.sql.Connection
import java.sql.DatabaseMetaData
import java.sql.ResultSet
@ -29,6 +31,8 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
// state_status = 0 -> UNCONSUMED.
// is_modifiable = 0 -> MODIFIABLE.
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
nested.total+nested.pennies as total_pennies, nested.lock_id
FROM
@ -38,6 +42,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND vs.is_modifiable = 0
AND ccs.ccy_code = ?
AND (vs.lock_id = ? OR vs.lock_id is null)
""" +

View File

@ -42,6 +42,8 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
// Query plan does index scan on pennies_idx, which may be unavoidable due to the nature of the query.
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
val sb = StringBuilder()
// state_status = 0 -> UNCONSUMED.
// is_modifiable = 0 -> MODIFIABLE.
sb.append( """
;WITH CTE AS
(
@ -56,6 +58,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
ON vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
WHERE
vs.state_status = 0
vs.is_modifiable = 0
AND ccs.ccy_code = ?
AND (vs.lock_id = ? OR vs.lock_id IS NULL)
"""

View File

@ -486,6 +486,20 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
}
}
// state relevance.
if (criteria.isModifiable != Vault.StateModificationStatus.ALL) {
val predicateID = Pair(VaultSchemaV1.VaultStates::isModifiable.name, EqualityComparisonOperator.EQUAL)
if (commonPredicates.containsKey(predicateID)) {
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
if (existingStatus != criteria.isModifiable) {
log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::isModifiable.name}] value $existingStatus with ${criteria.status}")
commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.StateModificationStatus>(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable))
}
} else {
commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get<Vault.StateModificationStatus>(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable)
}
}
// contract state types
val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes)
if (contractStateTypes.isNotEmpty()) {

View File

@ -18,7 +18,10 @@ import net.corda.node.services.api.SchemaService
import net.corda.node.services.api.VaultServiceInternal
import net.corda.node.services.schema.PersistentStateService
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.*
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
import org.hibernate.Session
import rx.Observable
import rx.subjects.PublishSubject
@ -104,13 +107,37 @@ class NodeVaultService(
val session = currentDBSession()
producedStateRefsMap.forEach { stateAndRef ->
val state = VaultSchemaV1.VaultStates(
val stateOnly = stateAndRef.value.state.data
// TODO: Optimise this.
//
// For EVERY state to be committed to the vault, this checks whether it is spendable by the recording
// node. The behaviour is as follows:
//
// 1) All vault updates marked as MODIFIABLE will, of, course all have isModifiable = true.
// 2) For ALL_VISIBLE updates, those which are not modifiable will have isModifiable = false.
//
// This is useful when it comes to querying for fungible states, when we do not want non-modifiable states
// included in the result.
//
// The same functionality could be obtained by passing in a list of participants to the vault query,
// however this:
//
// * requires a join on the participants table which results in slow queries
// * states may flip from being non-modifiable to modifiable
// * it's more complicated for CorDapp developers
//
// Adding a new column in the "VaultStates" table was considered the best approach.
val keys = stateOnly.participants.map { it.owningKey }
val isModifiable = isModifiable(stateOnly, keyManagementService.filterMyKeys(keys).toSet())
val stateToAdd = VaultSchemaV1.VaultStates(
notary = stateAndRef.value.state.notary,
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
stateStatus = Vault.StateStatus.UNCONSUMED,
recordedTime = clock.instant())
state.stateRef = PersistentStateRef(stateAndRef.key)
session.save(state)
recordedTime = clock.instant(),
isModifiable = if (isModifiable) Vault.StateModificationStatus.MODIFIABLE else Vault.StateModificationStatus.NOT_MODIFIABLE
)
stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
session.save(stateToAdd)
}
consumedStateRefs.forEach { stateRef ->
val state = session.get<VaultSchemaV1.VaultStates>(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef))
@ -161,7 +188,7 @@ class NodeVaultService(
val ourNewStates = when (statesToRecord) {
StatesToRecord.NONE -> throw AssertionError("Should not reach here")
StatesToRecord.ONLY_RELEVANT -> tx.outputs.withIndex().filter {
isRelevant(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet())
isModifiable(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet())
}
StatesToRecord.ALL_VISIBLE -> tx.outputs.withIndex()
}.map { tx.outRef<ContractState>(it.index) }
@ -190,7 +217,7 @@ class NodeVaultService(
val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) }
val (consumedStateAndRefs, producedStates) = ltx.inputs.zip(ltx.outputs).filter { (_, output) ->
if (statesToRecord == StatesToRecord.ONLY_RELEVANT) {
isRelevant(output.data, myKeys.toSet())
isModifiable(output.data, myKeys.toSet())
} else {
true
}
@ -368,12 +395,15 @@ class NodeVaultService(
return emptyList()
}
// Enrich QueryCriteria with additional default attributes (such as soft locks)
// Enrich QueryCriteria with additional default attributes (such as soft locks).
// We only want to return MODIFIABLE states here.
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
val enrichedCriteria = QueryCriteria.VaultQueryCriteria(
contractStateTypes = setOf(contractStateType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)))
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
isModifiable = Vault.StateModificationStatus.MODIFIABLE
)
val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter)
var claimedAmount = 0L
@ -396,9 +426,11 @@ class NodeVaultService(
}
@VisibleForTesting
internal fun isRelevant(state: ContractState, myKeys: Set<PublicKey>): Boolean {
internal fun isModifiable(state: ContractState, myKeys: Set<PublicKey>): Boolean {
val keysToCheck = when (state) {
is OwnableState -> listOf(state.owner.owningKey)
// Sometimes developers forget to add the owning key to participants for OwnableStates.
// TODO: This logic should probably be moved to OwnableState so we can just do a simple intersection here.
is OwnableState -> (state.participants.map { it.owningKey } + state.owner.owningKey).toSet()
else -> state.participants.map { it.owningKey }
}
return keysToCheck.any { it in myKeys }
@ -477,7 +509,8 @@ class NodeVaultService(
vaultState.stateStatus,
vaultState.notary,
vaultState.lockId,
vaultState.lockUpdateTime))
vaultState.lockUpdateTime,
vaultState.isModifiable))
} else {
// TODO: improve typing of returned other results
log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" }

View File

@ -57,6 +57,10 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
@Column(name = "lock_id", nullable = true)
var lockId: String? = null,
/** Used to determine whether a state is modifiable by the recording node */
@Column(name = "is_modifiable", nullable = false)
var isModifiable: Vault.StateModificationStatus,
/** 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

View File

@ -28,6 +28,8 @@ import net.corda.finance.utils.sumCash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.api.WritableTransactionStorage
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
@ -51,7 +53,7 @@ import kotlin.test.assertTrue
class NodeVaultServiceTest {
private companion object {
val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName)
val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts")
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1)
val bankOfCorda = TestIdentity(BOC_NAME)
@ -526,17 +528,17 @@ class NodeVaultServiceTest {
val amount = Amount(1000, Issued(BOC.ref(1), GBP))
val wellKnownCash = Cash.State(amount, identity.party)
val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey))
assertTrue { service.isRelevant(wellKnownCash, myKeys.toSet()) }
assertTrue { service.isModifiable(wellKnownCash, myKeys.toSet()) }
val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false)
val anonymousCash = Cash.State(amount, anonymousIdentity.party)
val anonymousKeys = services.keyManagementService.filterMyKeys(listOf(anonymousCash.owner.owningKey))
assertTrue { service.isRelevant(anonymousCash, anonymousKeys.toSet()) }
assertTrue { service.isModifiable(anonymousCash, anonymousKeys.toSet()) }
val thirdPartyIdentity = AnonymousParty(generateKeyPair().public)
val thirdPartyCash = Cash.State(amount, thirdPartyIdentity)
val thirdPartyKeys = services.keyManagementService.filterMyKeys(listOf(thirdPartyCash.owner.owningKey))
assertFalse { service.isRelevant(thirdPartyCash, thirdPartyKeys.toSet()) }
assertFalse { service.isModifiable(thirdPartyCash, thirdPartyKeys.toSet()) }
}
// TODO: Unit test linear state relevancy checks
@ -727,4 +729,43 @@ class NodeVaultServiceTest {
}
assertThat(recordedStates).isEqualTo(coins.size)
}
@Test
fun `test state relevance criteria`() {
fun createTx(number: Int, vararg participants: Party): SignedTransaction {
return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply {
addOutputState(DummyState(number, participants.toList()), DummyContract.PROGRAM_ID)
addCommand(DummyCommandData, listOf(megaCorp.publicKey))
})
}
fun List<StateAndRef<DummyState>>.getNumbers() = map { it.state.data.magicNumber }.toSet()
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(1, megaCorp.party)))
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(2, miniCorp.party)))
services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(3, miniCorp.party, megaCorp.party)))
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(4, miniCorp.party)))
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(5, bankOfCorda.party)))
services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(6, megaCorp.party, bankOfCorda.party)))
services.recordTransactions(StatesToRecord.NONE, listOf(createTx(7, bankOfCorda.party)))
// Test one.
// StateModificationStatus is MODIFIABLE by default. This should return two states.
val resultOne = vaultService.queryBy<DummyState>().states.getNumbers()
assertEquals(setOf(1, 3, 4, 5, 6), resultOne)
// Test two.
// StateModificationStatus set to NOT_MODIFIABLE.
val criteriaTwo = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.NOT_MODIFIABLE)
val resultTwo = vaultService.queryBy<DummyState>(criteriaTwo).states.getNumbers()
assertEquals(setOf(4, 5), resultTwo)
// Test three.
// StateModificationStatus set to ALL.
val criteriaThree = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.MODIFIABLE)
val resultThree = vaultService.queryBy<DummyState>(criteriaThree).states.getNumbers()
assertEquals(setOf(1, 3, 6), resultThree)
// We should never see 2 or 7.
}
}