mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
* 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:
parent
f3392e31d2
commit
b7867c3bcb
@ -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,
|
||||
val contractStateClassName: String,
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?)
|
||||
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 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()) {
|
||||
|
@ -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,
|
||||
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() {
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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)
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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() {
|
||||
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,
|
||||
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>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
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,
|
||||
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]
|
||||
|
@ -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 {
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
""" +
|
||||
|
@ -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)
|
||||
""" +
|
||||
|
@ -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)
|
||||
"""
|
||||
|
@ -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()) {
|
||||
|
@ -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())}" }
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user