diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 0028c8444c..58d8d92ba5 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4041,7 +4041,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Fungi @Nullable public final java.util.List getOwner() @Nullable - public final java.util.List getParticipants() + public java.util.List getParticipants() @Nullable public final net.corda.core.node.services.vault.ColumnPredicate getQuantity() @NotNull @@ -4078,7 +4078,7 @@ public static final class net.corda.core.node.services.vault.QueryCriteria$Linea @Nullable public final java.util.List getExternalId() @Nullable - public final java.util.List getParticipants() + public java.util.List getParticipants() @NotNull public net.corda.core.node.services.Vault$StateStatus getStatus() @Nullable @@ -4305,16 +4305,16 @@ public abstract class net.corda.core.node.services.vault.SortAttribute extends j ## @CordaSerializable public static final class net.corda.core.node.services.vault.SortAttribute$Custom extends net.corda.core.node.services.vault.SortAttribute - public (Class, String) + public (Class, String) @NotNull - public final Class component1() + public final Class component1() @NotNull public final String component2() @NotNull - public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) + public final net.corda.core.node.services.vault.SortAttribute$Custom copy(Class, String) public boolean equals(Object) @NotNull - public final Class getEntityStateClass() + public final Class getEntityStateClass() @NotNull public final String getEntityStateColumnName() public int hashCode() diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 553320c7fa..d85bc7b3f4 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -8,7 +8,7 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.StatePersistable import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import java.time.Instant @@ -76,6 +76,7 @@ sealed class QueryCriteria : GenericQueryCriteria = emptySet() open val constraints: Set = emptySet() + open val participants: List? = null abstract val contractStateTypes: Set>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -94,7 +95,8 @@ sealed class QueryCriteria : GenericQueryCriteria = emptySet(), - override val constraints: Set = emptySet() + override val constraints: Set = emptySet(), + override val participants: List? = null ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -124,7 +126,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val uuid: List? = null, val externalId: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, @@ -172,7 +174,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val quantity: ColumnPredicate? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, @@ -188,7 +190,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + override val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, val issuer: List? = null, @@ -231,7 +233,7 @@ sealed class QueryCriteria : GenericQueryCriteria @JvmOverloads constructor( + data class VaultCustomQueryCriteria @JvmOverloads constructor( val expression: CriteriaExpression, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, @@ -299,7 +301,7 @@ interface IQueryCriteriaParser : BaseQueryCriteriaParser fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection - fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection + fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 2492313d0b..7bf52cffe8 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -10,6 +10,7 @@ import net.corda.core.node.services.vault.ColumnPredicate.* import net.corda.core.node.services.vault.EqualityComparisonOperator.* import net.corda.core.node.services.vault.LikenessOperator.* import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.StatePersistable import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field import kotlin.jvm.internal.CallableReference @@ -234,7 +235,7 @@ sealed class SortAttribute { * [entityStateColumnName] should reference an entity attribute name as defined by the associated mapped schema * (for example, [CashSchemaV1.PersistentCashState::currency.name]) */ - data class Custom(val entityStateClass: Class, + data class Custom(val entityStateClass: Class, val entityStateColumnName: String) : SortAttribute() } diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index f1ca9ef1df..70008c75e4 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -164,7 +164,7 @@ useful if off-ledger data must be maintained in conjunction with on-ledger state as a custom schema. See Samples below. The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to -a list of mapped types which is passed to ``MappedSChema``. This is exactly how state schemas are defined, except that +a list of mapped types which is passed to ``MappedSchema``. This is exactly how state schemas are defined, except that the entity in this case should not subclass ``PersistentState`` (as it is not a state object). See examples: .. container:: codeset @@ -173,7 +173,6 @@ the entity in this case should not subclass ``PersistentState`` (as it is not a public class FooSchema {} - @CordaSerializable public class FooSchemaV1 extends MappedSchema { FooSchemaV1() { super(FooSchema.class, 1, ImmutableList.of(PersistentFoo.class)); @@ -208,9 +207,8 @@ Instances of ``PersistentFoo`` can be persisted inside a flow as follows: .. sourcecode:: java PersistentFoo foo = new PersistentFoo(new UniqueIdentifier().getId().toString(), "Bar"); - node.getServices().withEntityManager(entityManager -> { + serviceHub.withEntityManager(entityManager -> { entityManager.persist(foo); - entityManager.flush(); return null; }); diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 2243a6bfea..7b5c63e2ff 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + API: Vault Query ================ @@ -569,4 +575,78 @@ The Corda Tutorials provide examples satisfying these additional Use Cases: .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql .. _JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html +Mapping owning keys to external IDs +----------------------------------- +When creating new public keys via the ``KeyManagementService``, it is possible to create an association between the newly created public +key and an external ID. This, in effect, allows CorDapp developers to group state ownership/participation keys by an account ID. + +.. note:: This only works with freshly generated public keys and *not* the node's legal identity key. If you require that the freshly + generated keys be for the node's identity then use ``PersistentKeyManagementService.freshKeyAndCert`` instead of ``freshKey``. + Currently, the generation of keys for other identities is not supported. + +The code snippet below show how keys can be associated with an external ID by using the exposed JPA functionality: + +.. container:: codeset + + .. sourcecode:: java + + public AnonymousParty freshKeyForExternalId(UUID externalId, ServiceHub services) { + // Create a fresh key pair and return the public key. + AnonymousParty anonymousParty = freshKey(); + // Associate the fresh key to an external ID. + services.withEntityManager(entityManager -> { + PersistentKeyManagementService.PublicKeyHashToExternalId mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey); + entityManager.persist(mapping); + return null; + }); + return anonymousParty; + } + + .. sourcecode:: kotlin + + fun freshKeyForExternalId(externalId: UUID, services: ServiceHub): AnonymousParty { + // Create a fresh key pair and return the public key. + val anonymousParty = freshKey() + // Associate the fresh key to an external ID. + services.withEntityManager { + val mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey) + persist(mapping) + } + return anonymousParty + } + +As can be seen in the code snippet above, the ``PublicKeyHashToExternalId`` entity has been added to ``PersistentKeyManagementService``, +which allows you to associate your public keys with external IDs. So far, so good. + +.. note:: Here, it is worth noting that we must map **owning keys** to external IDs, as opposed to **state objects**. This is because it + might be the case that a ``LinearState`` is owned by two public keys generated by the same node. + +The intuition here is that when these public keys are used to own or participate in a state object, it is trivial to then associate those +states with a particular external ID. Behind the scenes, when states are persisted to the vault, the owning keys for each state are +persisted to a ``PersistentParty`` table. The ``PersistentParty`` table can be joined with the ``PublicKeyHashToExternalId`` table to create +a view which maps each state to one or more external IDs. The entity relationship diagram below helps to explain how this works. + +.. image:: resources/state-to-external-id.png + +When performing a vault query, it is now possible to query for states by external ID using a custom query criteria. + +.. container:: codeset + + .. sourcecode:: java + + UUID id = someExternalId; + FieldInfo externalIdField = getField("externalId", VaultSchemaV1.StateToExternalId.class); + CriteriaExpression externalId = Builder.equal(externalIdField, id); + QueryCriteria query = new VaultCustomQueryCriteria(externalId); + Vault.Page results = vaultService.queryBy(StateType.class, query); + + .. sourcecode:: kotlin + + val id: UUID = someExternalId + val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.equal(id) } + val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) + val results = vaultService.queryBy(queryCriteria).states + +The ``VaultCustomQueryCriteria`` can also be combined with other query criteria, like custom schemas, for instance. See the vault query API +examples above for how to combine ``QueryCriteria``. \ No newline at end of file diff --git a/docs/source/resources/state-to-external-id.png b/docs/source/resources/state-to-external-id.png new file mode 100644 index 0000000000..78cda151e4 Binary files /dev/null and b/docs/source/resources/state-to-external-id.png differ diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index 574ca82aca..0cbaa360de 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -14,10 +14,8 @@ import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.Lob +import java.util.* +import javax.persistence.* /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. @@ -29,7 +27,7 @@ import javax.persistence.Lob class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService, private val database: CordaPersistence) : SingletonSerializeAsToken(), KeyManagementServiceInternal { @Entity - @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") + @Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") class PersistentKey( @Id @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false) @@ -46,6 +44,24 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi : this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded) } + @Entity + @Table(name = "pk_hash_to_ext_id_map", indexes = [Index(name = "pk_hash_to_xid_idx", columnList = "public_key_hash")]) + class PublicKeyHashToExternalId( + @Id + @GeneratedValue + @Column(name = "id", unique = true, nullable = false) + var key: Long? = null, + + @Column(name = "external_id", nullable = false) + var externalId: UUID, + + @Column(name = "public_key_hash", nullable = false) + var publicKeyHash: String + ) { + constructor(accountId: UUID, publicKey: PublicKey) + : this(null, accountId, publicKey.toStringShort()) + } + private companion object { fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { return AppendOnlyPersistentMap( diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 1259adf2d8..5c9b99ef19 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -44,8 +44,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java, - RunOnceService.MutualExclusion::class.java - )){ + RunOnceService.MutualExclusion::class.java, + PersistentKeyManagementService.PublicKeyHashToExternalId::class.java) override val migrationResource = "node-core.changelog-master" } @@ -82,17 +82,11 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() // Because schema is always one supported by the state, just delegate. override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState { if ((schema === VaultSchemaV1) && (state is LinearState)) - return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants) + return VaultSchemaV1.VaultLinearStates(state.linearId) if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>)) - return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) + return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference) if ((schema === VaultSchemaV1) && (state is FungibleState<*>)) - return VaultSchemaV1.VaultFungibleStates( - participants = state.participants.toMutableSet(), - owner = null, - quantity = state.amount.quantity, - issuer = null, - issuerRef = null - ) + return VaultSchemaV1.VaultFungibleStates(owner = null, quantity = state.amount.quantity, issuer = null, issuerRef = null) return (state as QueryableState).generateMappedObject(schema) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index f4d912ffb1..e4aec418df 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -17,6 +17,7 @@ import net.corda.core.node.services.vault.NullOperator.NOT_NULL import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef +import net.corda.core.schemas.StatePersistable import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace @@ -222,7 +223,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) + private val rootEntities = mutableMapOf, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate private val constraintPredicates = mutableSetOf() @@ -412,13 +413,25 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("issuerRef").`in`(issuerRefs))) } - // participants + // Participants. criteria.participants?.let { - val participants = criteria.participants as List - val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") - predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) - criteriaQuery.distinct(true) + val participants = criteria.participants!! + + // Get the persistent party entity. + val persistentPartyEntity = VaultSchemaV1.PersistentParty::class.java + val entityRoot = rootEntities.getOrElse(persistentPartyEntity) { + val entityRoot = criteriaQuery.from(persistentPartyEntity) + rootEntities[persistentPartyEntity] = entityRoot + entityRoot + } + + // Add the join and participants predicates. + val statePartyJoin = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) + val participantsPredicate = criteriaBuilder.and(entityRoot.get("x500Name").`in`(participants)) + predicateSet.add(statePartyJoin) + predicateSet.add(participantsPredicate) } + return predicateSet } @@ -452,17 +465,29 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("externalId").`in`(externalIds))) } - // deal participants + // Participants. criteria.participants?.let { - val participants = criteria.participants as List - val joinLinearStateToParty = vaultLinearStates.joinSet("participants") - predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) - criteriaQuery.distinct(true) + val participants = criteria.participants!! + + // Get the persistent party entity. + val persistentPartyEntity = VaultSchemaV1.PersistentParty::class.java + val entityRoot = rootEntities.getOrElse(persistentPartyEntity) { + val entityRoot = criteriaQuery.from(persistentPartyEntity) + rootEntities[persistentPartyEntity] = entityRoot + entityRoot + } + + // Add the join and participants predicates. + val statePartyJoin = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) + val participantsPredicate = criteriaBuilder.and(entityRoot.get("x500Name").`in`(participants)) + predicateSet.add(statePartyJoin) + predicateSet.add(participantsPredicate) } + return predicateSet } - override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { + override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { log.trace { "Parsing VaultCustomQueryCriteria: $criteria" } val predicateSet = mutableSetOf() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 0bfefa0f0f..f83bdfecac 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -149,8 +149,15 @@ class NodeVaultService( // // Adding a new column in the "VaultStates" table was considered the best approach. val keys = stateOnly.participants.map { it.owningKey } + val persistentStateRef = PersistentStateRef(stateAndRef.key) val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint) + // Save a row for each party in the state_party table. + // TODO: Perhaps these can be stored in a batch? + stateOnly.participants.forEach { participant -> + val persistentParty = VaultSchemaV1.PersistentParty(persistentStateRef, participant) + session.save(persistentParty) + } val stateToAdd = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, @@ -162,7 +169,7 @@ class NodeVaultService( constraintType = constraintInfo.type(), constraintData = constraintInfo.data() ) - stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) + stateToAdd.stateRef = persistentStateRef session.save(stateToAdd) } if (consumedStateRefs.isNotEmpty()) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index db23815db2..b2cf297557 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -3,14 +3,18 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.MAX_ISSUER_REF_SIZE import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.crypto.toStringShort 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 +import net.corda.core.schemas.PersistentStateRef +import net.corda.core.schemas.StatePersistable import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes +import org.hibernate.annotations.Immutable import org.hibernate.annotations.Type import java.time.Instant import java.util.* @@ -25,8 +29,18 @@ object VaultSchema * First version of the Vault ORM schema */ @CordaSerializable -object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, - mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { +object VaultSchemaV1 : MappedSchema( + schemaFamily = VaultSchema.javaClass, + version = 1, + mappedTypes = listOf( + VaultStates::class.java, + VaultLinearStates::class.java, + VaultFungibleStates::class.java, + VaultTxnNote::class.java, + PersistentParty::class.java, + StateToExternalId::class.java + ) +) { override val migrationResource = "vault-schema.changelog-master" @@ -84,16 +98,6 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio class VaultLinearStates( /** [ContractState] attributes */ - /** X500Name of participant parties **/ - @ElementCollection - @CollectionTable(name = "vault_linear_states_parts", - joinColumns = [(JoinColumn(name = "output_index", referencedColumnName = "output_index")), (JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))], - foreignKey = ForeignKey(name = "FK__lin_stat_parts__lin_stat")) - @Column(name = "participants") - var participants: MutableSet? = null, - // Reason for not using Set is described here: - // https://stackoverflow.com/questions/44213074/kotlin-collection-has-neither-generic-type-or-onetomany-targetentity - /** * Represents a [LinearState] [UniqueIdentifier] */ @@ -104,25 +108,12 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Type(type = "uuid-char") var uuid: UUID ) : PersistentState() { - constructor(uid: UniqueIdentifier, _participants: List) : - this(externalId = uid.externalId, - uuid = uid.id, - participants = _participants.toMutableSet()) + constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id) } @Entity @Table(name = "vault_fungible_states") class VaultFungibleStates( - /** [ContractState] attributes */ - - /** X500Name of participant parties **/ - @ElementCollection - @CollectionTable(name = "vault_fungible_states_parts", - joinColumns = [(JoinColumn(name = "output_index", referencedColumnName = "output_index")), (JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))], - foreignKey = ForeignKey(name = "FK__fung_st_parts__fung_st")) - @Column(name = "participants", nullable = true) - var participants: MutableSet? = null, - /** [OwnableState] attributes */ /** X500Name of owner party **/ @@ -149,12 +140,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Type(type = "corda-wrapper-binary") var issuerRef: ByteArray? ) : PersistentState() { - constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : - this(owner = _owner, - quantity = _quantity, - issuer = _issuerParty, - issuerRef = _issuerRef.bytes, - participants = _participants.toMutableSet()) + constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes) : + this(owner = _owner, quantity = _quantity, issuer = _issuerParty, issuerRef = _issuerRef.bytes) } @Entity @@ -173,4 +160,47 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio ) { constructor(txId: String, note: String) : this(0, txId, note) } -} \ No newline at end of file + + @Entity + @Table(name = "state_party", indexes = [Index(name = "state_party_idx", columnList = "public_key_hash")]) + class PersistentParty( + @Id + @GeneratedValue + @Column(name = "id", unique = true, nullable = false) + var id: Long? = null, + + // Foreign key. + @Column(name = "state_ref") + var stateRef: PersistentStateRef, + + @Column(name = "public_key_hash", nullable = false) + var publicKeyHash: String, + + @Column(name = "x500_name", nullable = true) + var x500Name: AbstractParty? = null + ) : StatePersistable { + constructor(stateRef: PersistentStateRef, abstractParty: AbstractParty) + : this(null, stateRef, abstractParty.owningKey.toStringShort(), abstractParty) + } + + @Entity + @Immutable + @Table(name = "v_pkey_hash_ex_id_map") + class StateToExternalId( + @Id + @GeneratedValue + @Column(name = "id", unique = true, nullable = false) + var id: Long? = null, + + // Foreign key. + @Column(name = "state_ref") + var stateRef: PersistentStateRef, + + @Column(name = "public_key_hash") + var publicKeyHash: String, + + @Column(name = "external_id") + var externalId: UUID + ) : StatePersistable +} + diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index d1b1cd6f3d..9a049e261c 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -11,5 +11,6 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v8.xml b/node/src/main/resources/migration/vault-schema.changelog-v8.xml new file mode 100644 index 0000000000..3f7adda2c2 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v8.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + select + state_party.id, + state_party.public_key_hash, + state_party.transaction_id, + state_party.output_index, + pk_hash_to_ext_id_map.external_id + from state_party + join pk_hash_to_ext_id_map + on state_party.public_key_hash = pk_hash_to_ext_id_map.public_key_hash + + + diff --git a/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt new file mode 100644 index 0000000000..8e5b966e47 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/ExternalIdMappingTest.kt @@ -0,0 +1,131 @@ +package net.corda.node.services.vault + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.builder +import net.corda.core.transactions.TransactionBuilder +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.node.services.keys.PersistentKeyManagementService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import org.junit.Rule +import org.junit.Test +import java.util.* +import kotlin.test.assertEquals + +class ExternalIdMappingTest { + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val cordapps = listOf( + "net.corda.node.services.persistence", + "net.corda.testing.contracts" + ) + + private val myself = TestIdentity(CordaX500Name("Me", "London", "GB")) + private val notary = TestIdentity(CordaX500Name("NotaryService", "London", "GB"), 1337L) + private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices( + cordappPackages = cordapps, + identityService = rigorousMock().also { + doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey) + doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party) + doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name) + }, + initialIdentity = myself, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + + private val services: MockServices = databaseAndServices.second + private val database: CordaPersistence = databaseAndServices.first + + private fun freshKeyForExternalId(externalId: UUID): AnonymousParty { + val anonymousParty = freshKey() + database.transaction { + services.withEntityManager { + val mapping = PersistentKeyManagementService.PublicKeyHashToExternalId(externalId, anonymousParty.owningKey) + persist(mapping) + flush() + } + } + return anonymousParty + } + + private fun freshKey(): AnonymousParty { + val key = services.keyManagementService.freshKey() + val anonymousParty = AnonymousParty(key) + // Add behaviour to the mock identity management service for dealing with the new key. + // It won't be able to resolve it as it's just an anonymous key that is not linked to an identity. + services.identityService.also { doReturn(null).whenever(it).wellKnownPartyFromAnonymous(anonymousParty) } + return anonymousParty + } + + private fun createDummyState(participants: List): DummyState { + val tx = TransactionBuilder(notary = notary.party).apply { + addOutputState(DummyState(1, participants), DummyContract.PROGRAM_ID) + addCommand(DummyContract.Commands.Create(), participants.map { it.owningKey }) + } + val stx = services.signInitialTransaction(tx) + database.transaction { services.recordTransactions(stx) } + return stx.tx.outputsOfType().single() + } + + @Test + fun `Two states can be mapped to a single externalId`() { + val vaultService = services.vaultService + // Create new external ID and two keys mapped to it. + val id = UUID.randomUUID() + val keyOne = freshKeyForExternalId(id) + val keyTwo = freshKeyForExternalId(id) + // Create states with a public key assigned to the new external ID. + val dummyStateOne = createDummyState(listOf(keyOne)) + val dummyStateTwo = createDummyState(listOf(keyTwo)) + // This query should return two states! + val result = database.transaction { + val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(id)) } + val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) + vaultService.queryBy(queryCriteria).states + } + assertEquals(setOf(dummyStateOne, dummyStateTwo), result.map { it.state.data }.toSet()) + + // This query should return two states! + val resultTwo = database.transaction { + val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.equal(id) } + val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) + vaultService.queryBy(queryCriteria).states + } + assertEquals(setOf(dummyStateOne, dummyStateTwo), resultTwo.map { it.state.data }.toSet()) + } + + @Test + fun `One state can be mapped to multiple externalIds`() { + val vaultService = services.vaultService + // Create new external ID. + val idOne = UUID.randomUUID() + val keyOne = freshKeyForExternalId(idOne) + val idTwo = UUID.randomUUID() + val keyTwo = freshKeyForExternalId(idTwo) + // Create state with a public key assigned to the new external ID. + val dummyState = createDummyState(listOf(keyOne, keyTwo)) + // This query should return one state! + val result = database.transaction { + val externalId = builder { VaultSchemaV1.StateToExternalId::externalId.`in`(listOf(idOne, idTwo)) } + val queryCriteria = QueryCriteria.VaultCustomQueryCriteria(externalId) + vaultService.queryBy(queryCriteria).states + } + assertEquals(dummyState, result.single().state.data) + } + +} \ No newline at end of file