Skip to content

Commit

Permalink
CORE-17431 utxo transaction metadata (#4845)
Browse files Browse the repository at this point in the history
CORE-17431 Store metadata in utxo_transaction_metadata since it is relatively large but only changes occasionally.
  • Loading branch information
vlajos authored and lankydan committed Oct 13, 2023
1 parent e1827d4 commit bb04f5f
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,6 @@ class UtxoPersistenceServiceImplTest {
// Verify persisted data
entityManagerFactory.transaction { em ->
val dbTransaction = em.find(entityFactory.utxoTransaction, signedTransaction.id.toString())

assertThat(dbTransaction).isNotNull
val txPrivacySalt = dbTransaction.field<ByteArray>("privacySalt")
val txAccountId = dbTransaction.field<String>("accountId")
Expand All @@ -416,19 +415,19 @@ class UtxoPersistenceServiceImplTest {
assertThat(txAccountId).isEqualTo(account)
assertThat(txCreatedTs).isNotNull

val componentGroupLists = signedTransaction.wireTransaction.componentGroupLists
val componentGroupListsWithoutMetadata = signedTransaction.wireTransaction.componentGroupLists.drop(1)
val txComponents = dbTransaction.field<Collection<Any>?>("components")
assertThat(txComponents).isNotNull
.hasSameSizeAs(componentGroupLists.flatten().filter { it.isNotEmpty() })
.hasSameSizeAs(componentGroupListsWithoutMetadata.flatten().filter { it.isNotEmpty() })
txComponents!!
.sortedWith(compareBy<Any> { it.field<Int>("groupIndex") }.thenBy { it.field<Int>("leafIndex") })
.groupBy { it.field<Int>("groupIndex") }.values
.zip(componentGroupLists)
.zip(componentGroupListsWithoutMetadata)
.forEachIndexed { groupIndex, (dbComponentGroup, componentGroup) ->
assertThat(dbComponentGroup).hasSameSizeAs(componentGroup)
dbComponentGroup.zip(componentGroup)
.forEachIndexed { leafIndex, (dbComponent, component) ->
assertThat(dbComponent.field<Int>("groupIndex")).isEqualTo(groupIndex)
assertThat(dbComponent.field<Int>("groupIndex")).isEqualTo(groupIndex +1 )
assertThat(dbComponent.field<Int>("leafIndex")).isEqualTo(leafIndex)
assertThat(dbComponent.field<ByteArray>("data")).isEqualTo(component)
assertThat(dbComponent.field<String>("hash")).isEqualTo(
Expand All @@ -437,14 +436,21 @@ class UtxoPersistenceServiceImplTest {
}
}

val dbMetadata = dbTransaction.field<Any>("metadata")
assertThat(dbMetadata).isNotNull
assertThat(dbMetadata.field<ByteArray>("canonicalData"))
.isEqualTo(signedTransaction.wireTransaction.componentGroupLists[0][0])
assertThat(dbMetadata.field<String>("groupParametersHash")).isNotNull
assertThat(dbMetadata.field<String>("cpiFileChecksum")).isNotNull

val dbTransactionOutputs = em.createNamedQuery(
"UtxoVisibleTransactionOutputEntity.findByTransactionId",
entityFactory.utxoVisibleTransactionOutput
)
.setParameter("transactionId", signedTransaction.id.toString())
.resultList
assertThat(dbTransactionOutputs).isNotNull
.hasSameSizeAs(componentGroupLists[UtxoComponentGroup.OUTPUTS.ordinal])
.hasSameSizeAs(componentGroupListsWithoutMetadata[UtxoComponentGroup.OUTPUTS.ordinal-1])
dbTransactionOutputs
.sortedWith(compareBy<Any> { it.field<Int>("groupIndex") }.thenBy { it.field<Int>("leafIndex") })
.zip(defaultVisibleTransactionOutputs)
Expand Down Expand Up @@ -556,25 +562,38 @@ class UtxoPersistenceServiceImplTest {
createdTs: Instant = testClock.instant(),
status: TransactionStatus = UNVERIFIED
): Any {
val metadataBytes = signedTransaction.wireTransaction.componentGroupLists[0][0]
val metadata = entityFactory.createOrFindUtxoTransactionMetadataEntity(
digest("SHA-256", metadataBytes).toString(),
metadataBytes,
"fakeGroupParametersHash",
"fakeCpiFileChecksum"
)

return entityFactory.createUtxoTransactionEntity(
signedTransaction.id.toString(),
signedTransaction.wireTransaction.privacySalt.bytes,
account,
createdTs,
status.value,
createdTs
createdTs,
metadata
).also { transaction ->
transaction.field<MutableCollection<Any>>("components").addAll(
signedTransaction.wireTransaction.componentGroupLists.flatMapIndexed { groupIndex, componentGroup ->
componentGroup.mapIndexed { leafIndex: Int, component ->
entityFactory.createUtxoTransactionComponentEntity(
transaction,
groupIndex,
leafIndex,
component,
digest("SHA-256", component).toString()
)
}
if (groupIndex != 0 || leafIndex != 0) {
entityFactory.createUtxoTransactionComponentEntity(
transaction,
groupIndex,
leafIndex,
component,
digest("SHA-256", component).toString()
)
} else {
null
}
}.filterNotNull()
}
)
transaction.field<MutableCollection<Any>>("signatures").addAll(
Expand Down Expand Up @@ -658,6 +677,8 @@ class UtxoPersistenceServiceImplTest {
get() = transactionContainer.id
override val privacySalt: PrivacySalt
get() = transactionContainer.wireTransaction.privacySalt
override val metadata: TransactionMetadataInternal
get() = transactionContainer.wireTransaction.metadata as TransactionMetadataInternal
override val rawGroupLists: List<List<ByteArray>>
get() = transactionContainer.wireTransaction.componentGroupLists
override val signatures: List<DigitalSignatureAndMetadata>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package net.corda.ledger.persistence.utxo.tests.datamodel

import net.corda.orm.utils.transaction
import java.time.Instant
import javax.persistence.EntityManagerFactory

class UtxoEntityFactory(entityManagerFactory: EntityManagerFactory) {
class UtxoEntityFactory(private val entityManagerFactory: EntityManagerFactory) {
private val entityMap = entityManagerFactory.metamodel.entities.associate { it.name to it.bindableJavaType }

val utxoTransaction: Class<*> get() = classFor("UtxoTransactionEntity")
val utxoTransactionMetadata: Class<*> get() = classFor("UtxoTransactionMetadataEntity")
val utxoTransactionComponent: Class<*> get() = classFor("UtxoTransactionComponentEntity")
val utxoVisibleTransactionOutput: Class<*> get() = classFor("UtxoVisibleTransactionOutputEntity")
val utxoTransactionSignature: Class<*> get() = classFor("UtxoTransactionSignatureEntity")
Expand All @@ -17,10 +19,40 @@ class UtxoEntityFactory(entityManagerFactory: EntityManagerFactory) {
accountId: String,
created: Instant,
status: String,
updated: Instant
updated: Instant,
utxoTransactionMetadata: Any
): Any {
return utxoTransaction.constructors.single { it.parameterCount == 6 }.newInstance(
transactionId, privacySalt, accountId, created, status, updated
return utxoTransaction.constructors.single { it.parameterCount == 7 }.newInstance(
transactionId, privacySalt, accountId, created, status, updated, utxoTransactionMetadata
)
}

fun createOrFindUtxoTransactionMetadataEntity(
hash: String,
canonicalData: ByteArray,
groupParametersHash: String,
cpiFileChecksum: String,
): Any {
return entityManagerFactory.transaction { em ->
em.find(utxoTransactionMetadata, hash) ?: createUtxoTransactionMetadataEntity(
hash,
canonicalData,
groupParametersHash,
cpiFileChecksum,
).also {
em.persist(it)
}
}
}

fun createUtxoTransactionMetadataEntity(
hash: String,
canonicalData: ByteArray,
groupParametersHash: String,
cpiFileChecksum: String,
): Any {
return utxoTransactionMetadata.constructors.single { it.parameterCount == 4 }.newInstance(
hash, canonicalData, groupParametersHash, cpiFileChecksum
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface UtxoRepository {
id: String
): SignedTransactionContainer?

/** Retrieves transaction component leafs */
/** Retrieves transaction component leafs except metadata which is stored separately */
fun findTransactionComponentLeafs(
entityManager: EntityManager,
transactionId: String
Expand Down Expand Up @@ -76,7 +76,17 @@ interface UtxoRepository {
privacySalt: ByteArray,
account: String,
timestamp: Instant,
status: TransactionStatus
status: TransactionStatus,
metadataHash: String
)

/** Persists transaction metadata (operation is idempotent) */
fun persistTransactionMetadata(
entityManager: EntityManager,
hash: String,
metadataBytes: ByteArray,
groupParametersHash: String,
cpiFileChecksum: String
)

/** Persists transaction component leaf [data] (operation is idempotent) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import net.corda.v5.application.crypto.DigitalSignatureAndMetadata
import net.corda.v5.crypto.SecureHash
import net.corda.v5.ledger.common.transaction.CordaPackageSummary
import net.corda.ledger.common.data.transaction.PrivacySalt
import net.corda.ledger.common.data.transaction.TransactionMetadataInternal
import net.corda.v5.ledger.utxo.ContractState
import net.corda.v5.ledger.utxo.StateAndRef
import net.corda.v5.ledger.utxo.StateRef
Expand All @@ -13,6 +14,8 @@ interface UtxoTransactionReader {

val id: SecureHash

val metadata: TransactionMetadataInternal

val account: String

val status: TransactionStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ abstract class AbstractUtxoQueryProvider : UtxoQueryProvider {
val UNVERIFIED = TransactionStatus.UNVERIFIED.value
}

override val findTransactionPrivacySalt: String
override val findTransactionPrivacySaltAndMetadata: String
get() = """
SELECT privacy_salt
FROM {h-schema}utxo_transaction
SELECT privacy_salt,
utm.canonical_data
FROM {h-schema}utxo_transaction AS ut
JOIN {h-schema}utxo_transaction_metadata AS utm
ON ut.metadata_hash = utm.hash
WHERE id = :transactionId"""
.trimIndent()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ class PostgresUtxoQueryProvider @Activate constructor(

override val persistTransaction: String
get() = """
INSERT INTO {h-schema}utxo_transaction(id, privacy_salt, account_id, created, status, updated)
VALUES (:id, :privacySalt, :accountId, :createdAt, :status, :updatedAt)
INSERT INTO {h-schema}utxo_transaction(id, privacy_salt, account_id, created, status, updated, metadata_hash)
VALUES (:id, :privacySalt, :accountId, :createdAt, :status, :updatedAt, :metadataHash)
ON CONFLICT(id) DO
UPDATE SET status = EXCLUDED.status, updated = EXCLUDED.updated
WHERE utxo_transaction.status = EXCLUDED.status OR utxo_transaction.status = '$UNVERIFIED'"""
.trimIndent()

override val persistTransactionMetadata: String
get() = """
INSERT INTO {h-schema}utxo_transaction_metadata(hash, canonical_data, group_parameters_hash, cpi_file_checksum)
VALUES (:hash, :canonicalData, :groupParametersHash, :cpiFileChecksum)
ON CONFLICT DO NOTHING"""
.trimIndent()

override val persistTransactionComponentLeaf: String
get() = """
INSERT INTO {h-schema}utxo_transaction_component(transaction_id, group_idx, leaf_idx, data, hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,27 @@ class UtxoPersistenceServiceImpl(
val nowUtc = utcClock.instant()
val transactionIdString = transaction.id.toString()

val metadataBytes = transaction.rawGroupLists[0][0]
val metadataHash = sandboxDigestService.hash(metadataBytes, DigestAlgorithmName.SHA2_256).toString()

val metadata = transaction.metadata
repository.persistTransactionMetadata(
em,
metadataHash,
metadataBytes,
requireNotNull(metadata.getMembershipGroupParametersHash()) { "Metadata without membership group parameters hash" },
requireNotNull(metadata.getCpiMetadata()) { "Metadata without CPI metadata" }.fileChecksum
)

// Insert the Transaction
repository.persistTransaction(
em,
transactionIdString,
transaction.privacySalt.bytes,
transaction.account,
nowUtc,
transaction.status
transaction.status,
metadataHash
)

// Insert the Transactions components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ package net.corda.ledger.persistence.utxo.impl
*/
interface UtxoQueryProvider {
/**
* @property findTransactionPrivacySalt SQL text for [UtxoRepositoryImpl.findTransactionPrivacySalt].
* @property findTransactionPrivacySaltAndMetadata SQL text for [UtxoRepositoryImpl.findTransactionPrivacySaltAndMetadata].
*/
val findTransactionPrivacySalt: String
val findTransactionPrivacySaltAndMetadata: String

/**
* @property findTransactionComponentLeafs SQL text for [UtxoRepositoryImpl.findTransactionComponentLeafs].
Expand Down Expand Up @@ -61,6 +61,11 @@ interface UtxoQueryProvider {
*/
val persistTransaction: String

/**
* @property persistTransactionMetadata SQL text for [UtxoRepositoryImpl.persistTransactionMetadata].
*/
val persistTransactionMetadata: String

/**
* @property persistTransactionComponentLeaf SQL text for [UtxoRepositoryImpl.persistTransactionComponentLeaf].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class UtxoRepositoryImpl @Activate constructor(
entityManager: EntityManager,
id: String
): SignedTransactionContainer? {
val privacySalt = findTransactionPrivacySalt(entityManager, id) ?: return null
val (privacySalt, metadataBytes) = findTransactionPrivacySaltAndMetadata(entityManager, id) ?: return null
val wireTransaction = wireTransactionFactory.create(
findTransactionComponentLeafs(entityManager, id),
mapOf(0 to listOf(metadataBytes)) + findTransactionComponentLeafs(entityManager, id),
privacySalt
)
return SignedTransactionContainer(
Expand All @@ -87,14 +87,14 @@ class UtxoRepositoryImpl @Activate constructor(
.associate { r -> parseSecureHash(r.get(0) as String) to r.get(1) as String }
}

private fun findTransactionPrivacySalt(
private fun findTransactionPrivacySaltAndMetadata(
entityManager: EntityManager,
transactionId: String
): PrivacySaltImpl? {
return entityManager.createNativeQuery(queryProvider.findTransactionPrivacySalt, Tuple::class.java)
): Pair<PrivacySaltImpl, ByteArray>? {
return entityManager.createNativeQuery(queryProvider.findTransactionPrivacySaltAndMetadata, Tuple::class.java)
.setParameter("transactionId", transactionId)
.resultListAsTuples()
.map { r -> PrivacySaltImpl(r.get(0) as ByteArray) }
.map { r -> Pair(PrivacySaltImpl(r.get(0) as ByteArray), r.get(1) as ByteArray) }
.firstOrNull()
}

Expand Down Expand Up @@ -183,7 +183,8 @@ class UtxoRepositoryImpl @Activate constructor(
privacySalt: ByteArray,
account: String,
timestamp: Instant,
status: TransactionStatus
status: TransactionStatus,
metadataHash: String
) {
entityManager.createNativeQuery(queryProvider.persistTransaction)
.setParameter("id", id)
Expand All @@ -192,10 +193,27 @@ class UtxoRepositoryImpl @Activate constructor(
.setParameter("createdAt", timestamp)
.setParameter("status", status.value)
.setParameter("updatedAt", timestamp)
.setParameter("metadataHash", metadataHash)
.executeUpdate()
.logResult("transaction [$id]")
}

override fun persistTransactionMetadata(
entityManager: EntityManager,
hash: String,
metadataBytes: ByteArray,
groupParametersHash: String,
cpiFileChecksum: String
){
entityManager.createNativeQuery(queryProvider.persistTransactionMetadata)
.setParameter("hash", hash)
.setParameter("canonicalData", metadataBytes)
.setParameter("groupParametersHash", groupParametersHash)
.setParameter("cpiFileChecksum", cpiFileChecksum)
.executeUpdate()
.logResult("transaction metadata [$hash]")
}

override fun persistTransactionComponentLeaf(
entityManager: EntityManager,
transactionId: String,
Expand All @@ -204,6 +222,10 @@ class UtxoRepositoryImpl @Activate constructor(
data: ByteArray,
hash: String
) {
// Metadata is not stored with the other components. See persistTransactionMetadata().
if (groupIndex == 0 && leafIndex == 0) {
return
}
entityManager.createNativeQuery(queryProvider.persistTransactionComponentLeaf)
.setParameter("transactionId", transactionId)
.setParameter("groupIndex", groupIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class UtxoTransactionReaderImpl(
override val privacySalt: PrivacySalt
get() = signedTransaction.wireTransaction.privacySalt

override val metadata: TransactionMetadataInternal
get() = signedTransaction.wireTransaction.metadata as TransactionMetadataInternal

override val rawGroupLists: List<List<ByteArray>>
get() = signedTransaction.wireTransaction.componentGroupLists

Expand Down
Loading

0 comments on commit bb04f5f

Please sign in to comment.