Requery removal (#1276)

* Removed Requery object relational mapping usage (and associated schemas including node-schemas module)

* Fixed issues with NodeAttachmentService tests.
Cannot use JPA custom converters with Primary Key fields.
Hibernate entities require explicit call to flush() to persist to disk.

* Removed redundant requery converters (equivalents not even required in Hibernate).

* Removed remaining gradle requery dependency definitions.

* Fixed broken tests.

* Fixes for failing NodeVaultService tests:
- Dynamic SQL updates (in soft locking code)
- Explicit request by session to participate in transaction (causing "TransactionRequiredException" Executing an update/delete query)
- Explicit flush() required to persist to disk

* Updated changelog.
Fixed compiler warning.

* Fixed WHERE clause AND/OR condition.
Enforced immediate data visibility through transaction commit.

* Final fixes to address failing tests.

* Deferred all hibernate session/txn management to DatabaseTransactionManager.

* Fixed transaction boundaries in failing Cash tests.

* Fixes to address failing tests (transaction boundaries, merge detached object, config clean-up).

* Final adjustment to transaction boundaries in JUnit tests.

* Refactored AttachmentSchemaV1 into NodeAttachmentService itself and referenced from NodeServicesV1.

* Refactored HSQL UPDATE statements to use CriteriaUpdate API.

* Updated all criteria API getters to reference attribute names by type.

* Remove redundant VaultSchema entity name (required when previously using HSQL UPDATE syntax)

* Fix compiler warnings.

* Minor changes following rebase from master.

* Fixed suppress warning type.
This commit is contained in:
josecoll 2017-08-21 10:42:59 +01:00 committed by GitHub
parent 56a84882a7
commit a2ede0fc73
44 changed files with 764 additions and 2023 deletions

View File

@ -42,7 +42,6 @@ buildscript {
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4'
ext.requery_version = '1.3.1'
ext.dokka_version = '0.9.14'
ext.eddsa_version = '0.2.0'
@ -249,7 +248,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'

View File

@ -63,9 +63,6 @@ dependencies {
// JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
// Requery: SQL based query & persistence for Kotlin
compile "io.requery:requery-kotlin:$requery_version"
}
configurations {

View File

@ -1,6 +1,5 @@
package net.corda.core.schemas
import io.requery.Persistable
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.serialization.CordaSerializable
@ -69,4 +68,4 @@ data class PersistentStateRef(
/**
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
*/
interface StatePersistable : Persistable
interface StatePersistable

View File

@ -1,27 +0,0 @@
package net.corda.core.schemas.requery
import io.requery.Key
import io.requery.Persistable
import io.requery.Superclass
import net.corda.core.contracts.StateRef
import net.corda.core.schemas.StatePersistable
import javax.persistence.Column
object Requery {
/**
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
*/
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
// once we cut-over all existing Hibernate ContractState persistence to Requery
@Superclass interface PersistentState : StatePersistable {
@get:Key
@get:Column(name = "transaction_id", length = 64)
var txId: String
@get:Key
@get:Column(name = "output_index")
var index: Int
}
}

View File

@ -1,28 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import java.sql.Blob
import javax.sql.rowset.serial.SerialBlob
/**
* Converts from a [ByteArray] to a [Blob].
*/
class BlobConverter : Converter<ByteArray, Blob> {
override fun getMappedType(): Class<ByteArray> = ByteArray::class.java
override fun getPersistedType(): Class<Blob> = Blob::class.java
/**
* creates BLOB(INT.MAX) = 2 GB
*/
override fun getPersistedSize(): Int? = null
override fun convertToPersisted(value: ByteArray?): Blob? {
return value?.let { SerialBlob(value) }
}
override fun convertToMapped(type: Class<out ByteArray>?, value: Blob?): ByteArray? {
return value?.getBytes(1, value.length().toInt())
}
}

View File

@ -1,22 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import java.sql.Timestamp
import java.time.Instant
/**
* Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that
* when converting between the time type and the database type all times will be converted to the
* UTC zone offset.
*/
class InstantConverter : Converter<Instant, Timestamp> {
override fun getMappedType() = Instant::class.java
override fun getPersistedType() = Timestamp::class.java
override fun getPersistedSize() = null
override fun convertToPersisted(value: Instant?) = value?.let { Timestamp.from(it) }
override fun convertToMapped(type: Class<out Instant>, value: Timestamp?) = value?.toInstant()
}

View File

@ -1,28 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import net.corda.core.crypto.SecureHash
/**
* Convert from a [SecureHash] to a [String]
*/
class SecureHashConverter : Converter<SecureHash, String> {
override fun getMappedType(): Class<SecureHash> = SecureHash::class.java
override fun getPersistedType(): Class<String> = String::class.java
/**
* SecureHash consists of 32 bytes which need VARCHAR(64) in hex
* TODO: think about other hash widths
*/
override fun getPersistedSize(): Int? = 64
override fun convertToPersisted(value: SecureHash?): String? {
return value?.toString()
}
override fun convertToMapped(type: Class<out SecureHash>, value: String?): SecureHash? {
return value?.let { SecureHash.parse(value) }
}
}

View File

@ -1,21 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.Converter
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
/**
* Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index
*/
class StateRefConverter : Converter<StateRef, Pair<String, Int>> {
override fun getMappedType() = StateRef::class.java
@Suppress("UNCHECKED_CAST")
override fun getPersistedType() = Pair::class.java as Class<Pair<String, Int>>
override fun getPersistedSize() = null
override fun convertToPersisted(value: StateRef?) = value?.let { Pair(it.txhash.toString(), it.index) }
override fun convertToMapped(type: Class<out StateRef>, value: Pair<String, Int>?) = value?.let { StateRef(SecureHash.parse(it.first), it.second) }
}

View File

@ -1,9 +0,0 @@
package net.corda.core.schemas.requery.converters
import io.requery.converter.EnumOrdinalConverter
import net.corda.core.node.services.Vault
/**
* Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation
*/
class VaultStateStatusConverter : EnumOrdinalConverter<Vault.StateStatus>(Vault.StateStatus::class.java)

View File

@ -4,20 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith
class AttachmentTests {
lateinit var mockNet: MockNetwork
lateinit var configuration: RequeryConfiguration
@Before
fun setUp() {
mockNet = MockNetwork()
val dataSourceProperties = makeTestDataSourceProperties()
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
}
@After
@ -137,11 +132,9 @@ class AttachmentTests {
val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = AttachmentEntity()
corruptAttachment.attId = id
corruptAttachment.content = attachment
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
n0.database.transaction {
n0.attachments.session.update(corruptAttachment)
DatabaseTransactionManager.current().session.update(corruptAttachment)
}
// Get n1 to fetch the attachment. Should receive corrupted bytes.

View File

@ -49,8 +49,8 @@ class ContractUpgradeFlowTest {
notary = nodes.notaryNode.info.notaryIdentity
val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity }
a.services.identityService.registerIdentity(nodeIdentity)
b.services.identityService.registerIdentity(nodeIdentity)
a.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
b.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
}
@After

View File

@ -18,7 +18,7 @@ import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Before
@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten
* @see NodeAttachmentService.importAttachment
*/
private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) {
with(session) {
withTransaction {
update(AttachmentEntity().apply {
attId = attachmentId
content = data
})
}
val session = DatabaseTransactionManager.current().session
val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
attachment?.let {
attachment.content = data
session.save(attachment)
}
}

View File

@ -7,6 +7,11 @@ from the previous milestone release.
UNRELEASED
----------
* Removed usage of Requery ORM library (repalced with JPA/Hibernate)
* Vault Query performance improvement (replaced expensive per query SQL statement to obtain concrete state types
with single query on start-up followed by dynamic updates using vault state observable))
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
* Following deprecated methods have been removed:

View File

@ -20,7 +20,6 @@ The Corda repository comprises the following folders:
* **lib** contains some dependencies
* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence)
* **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC
* **node-schemas** contains entity classes used to represent relational database tables
* **samples** contains all our Corda demos and code samples
* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the
mock network) implementation

View File

@ -132,7 +132,6 @@ object TopupIssuerFlow {
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val issuer = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, notaryParty)
val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)

View File

@ -14,7 +14,6 @@ dependencies {
// Note the :finance module is a CorDapp in its own right
// and CorDapps using :finance features should use 'cordapp' not 'compile' linkage.
cordaCompile project(':core')
cordaCompile project(':node-schemas')
testCompile project(':test-utils')
testCompile project(path: ':core', configuration: 'testArtifacts')

View File

@ -68,7 +68,8 @@ class CashTests : TestDependencyInjectionBase() {
ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
}
database.transaction {
vaultStatesUnconsumed = miniCorpServices.vaultQueryService.queryBy<Cash.State>().states
}
resetTestSerialization()
@ -560,9 +561,11 @@ class CashTests : TestDependencyInjectionBase() {
@Test
fun generateSimpleDirectSpend() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
val wtx = makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
@Suppress("UNCHECKED_CAST")
val vaultState = vaultStatesUnconsumed.elementAt(0)
assertEquals(vaultState.ref, wtx.inputs[0])
@ -586,10 +589,11 @@ class CashTests : TestDependencyInjectionBase() {
@Test
fun generateSimpleSpendWithChange() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
val wtx = makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
@Suppress("UNCHECKED_CAST")
val vaultState = vaultStatesUnconsumed.elementAt(0)
assertEquals(vaultState.ref, wtx.inputs[0])
@ -602,9 +606,11 @@ class CashTests : TestDependencyInjectionBase() {
@Test
fun generateSpendWithTwoInputs() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
val wtx = makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
@Suppress("UNCHECKED_CAST")
val vaultState0 = vaultStatesUnconsumed.elementAt(0)
val vaultState1 = vaultStatesUnconsumed.elementAt(1)
@ -618,10 +624,13 @@ class CashTests : TestDependencyInjectionBase() {
@Test
fun generateSpendMixedDeposits() {
initialiseTestSerialization()
val wtx =
database.transaction {
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
assertEquals(3, wtx.inputs.size)
wtx
}
database.transaction {
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
assertEquals(3, wtx.inputs.size)
@Suppress("UNCHECKED_CAST")
val vaultState0 = vaultStatesUnconsumed.elementAt(0)
val vaultState1 = vaultStatesUnconsumed.elementAt(1)

View File

@ -8,7 +8,6 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import io.requery.util.CloseableIterator
import net.corda.core.internal.LazyPool
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
@ -103,14 +102,9 @@ open class SerializationFactoryImpl : SerializationFactory {
private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>() {
override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) {
val message = if (closeable is CloseableIterator<*>) {
"A live Iterator pointing to the database has been detected during flow checkpointing. This may be due " +
"to a Vault query - move it into a private method."
} else {
"${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " +
val message = "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " +
"Restoring such resources across node restarts is not supported. Make sure code accessing it is " +
"confined to a private method or the reference is nulled out."
}
throw UnsupportedOperationException(message)
}

View File

@ -1,37 +0,0 @@
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
apply plugin: 'idea'
apply plugin: 'com.jfrog.artifactory'
description 'Corda node database schemas'
dependencies {
compile project(':core')
// Requery: SQL based query & persistence for Kotlin
kapt "io.requery:requery-processor:$requery_version"
testCompile project(':test-utils')
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testCompile "junit:junit:$junit_version"
// For H2 database support in persistence
testCompile "com.h2database:h2:$h2_version"
}
sourceSets {
main {
kotlin {
srcDir "$buildDir/generated/source/kapt/main"
}
}
}
jar {
baseName 'corda-node-schemas'
}
publish {
name jar.baseName
}

View File

@ -1,18 +0,0 @@
package net.corda.node.services.persistence.schemas.requery
import io.requery.*
import net.corda.core.crypto.SecureHash
import net.corda.core.schemas.requery.converters.BlobConverter
@Table(name = "attachments")
@Entity(model = "persistence")
interface Attachment : Persistable {
@get:Key
@get:Column(name = "att_id", index = true)
var attId: SecureHash
@get:Column(name = "content")
@get:Convert(BlobConverter::class)
var content: ByteArray
}

View File

@ -1,74 +0,0 @@
package net.corda.node.services.vault.schemas.requery
import io.requery.*
import net.corda.core.node.services.Vault
import net.corda.core.schemas.requery.Requery
import java.time.Instant
import java.util.*
object VaultSchema {
@Table(name = "vault_transaction_notes")
@Entity(model = "vault")
interface VaultTxnNote : Persistable {
@get:Key
@get:Generated
@get:Column(name = "seq_no", index = true)
var seqNo: Int
@get:Column(name = "transaction_id", length = 64, index = true)
var txId: String
@get:Column(name = "note")
var note: String
}
@Table(name = "vault_cash_balances")
@Entity(model = "vault")
interface VaultCashBalances : Persistable {
@get:Key
@get:Column(name = "currency_code", length = 3)
var currency: String
@get:Column(name = "amount", value = "0")
var amount: Long
}
@Table(name = "vault_states")
@Entity(model = "vault")
interface VaultStates : Requery.PersistentState {
/** refers to the notary a state is attached to */
@get:Column(name = "notary_name")
var notaryName: String
/** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */
@get:Column(name = "contract_state_class_name")
var contractStateClassName: String
/** refers to serialized transaction Contract State */
// TODO: define contract state size maximum size and adjust length accordingly
@get:Column(name = "contract_state", length = 100000)
var contractState: ByteArray
/** state lifecycle: unconsumed, consumed */
@get:Column(name = "state_status")
var stateStatus: Vault.StateStatus
/** refers to timestamp recorded upon entering UNCONSUMED state */
@get:Column(name = "recorded_timestamp")
var recordedTime: Instant
/** refers to timestamp recorded upon entering CONSUMED state */
@get:Column(name = "consumed_timestamp", nullable = true)
var consumedTime: Instant?
/** used to denote a state has been soft locked (to prevent double spend)
* will contain a temporary unique [UUID] obtained from a flow session */
@get:Column(name = "lock_id", nullable = true)
var lockId: String?
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
@get:Column(name = "lock_timestamp", nullable = true)
var lockUpdateTime: Instant?
}
}

View File

@ -1,666 +0,0 @@
package net.corda.node.services.vault.schemas
import io.requery.Persistable
import io.requery.TransactionIsolation
import io.requery.kotlin.*
import io.requery.query.RowExpression
import io.requery.rx.KotlinRxEntityStore
import io.requery.sql.*
import io.requery.sql.platform.Generic
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.schemas.requery.converters.InstantConverter
import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.LedgerTransaction
import net.corda.node.services.vault.schemas.requery.*
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.contracts.DummyContract
import org.h2.jdbcx.JdbcDataSource
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import rx.Observable
import java.time.Instant
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class VaultSchemaTest : TestDependencyInjectionBase() {
var instance: KotlinEntityDataStore<Persistable>? = null
val data: KotlinEntityDataStore<Persistable> get() = instance!!
var oinstance: KotlinRxEntityStore<Persistable>? = null
val odata: KotlinRxEntityStore<Persistable> get() = oinstance!!
var transaction: LedgerTransaction? = null
@Before
fun setup() {
val dataSource = JdbcDataSource()
dataSource.setURL("jdbc:h2:mem:vault_persistence;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1")
val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping(), useDefaultLogging = true)
instance = KotlinEntityDataStore<Persistable>(configuration)
oinstance = KotlinRxEntityStore(KotlinEntityDataStore<Persistable>(configuration))
val tables = SchemaModifier(configuration)
val mode = TableCreationMode.DROP_CREATE
tables.createTables(mode)
// create dummy test data
setupDummyData()
}
private fun setupCustomMapping(): Mapping? {
val mapping = GenericMapping(Generic())
val instantConverter = InstantConverter()
mapping.addConverter(instantConverter, instantConverter.mappedType)
val vaultStateStatusConverter = VaultStateStatusConverter()
mapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType)
return mapping
}
@After
fun tearDown() {
data.close()
}
private class VaultNoopContract : Contract {
data class VaultNoopState(override val owner: AbstractParty) : OwnableState {
override val contract = VaultNoopContract()
override val participants: List<AbstractParty>
get() = listOf(owner)
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Create(), copy(owner = newOwner))
}
interface Commands : CommandData {
class Create : TypeOnlyCommandData(), Commands
}
override fun verify(tx: LedgerTransaction) {
// Always accepts.
}
}
private fun setupDummyData() {
// dummy Transaction comprised of 3 different Contract State types
// 1. SingleOwnerState
// 2. MultiOwnerState
// 3. VaultNoopState
val notary: Party = DUMMY_NOTARY
val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary)
val inState2 = TransactionState(DummyContract.MultiOwnerState(0,
listOf(ALICE, BOB)), notary)
val inState3 = TransactionState(VaultNoopContract.VaultNoopState(ALICE), notary)
val outState1 = inState1.copy()
val outState2 = inState2.copy()
val outState3 = inState3.copy()
val inputs = listOf(StateAndRef(inState1, StateRef(SecureHash.randomSHA256(), 0)),
StateAndRef(inState2, StateRef(SecureHash.randomSHA256(), 0)),
StateAndRef(inState3, StateRef(SecureHash.randomSHA256(), 0)))
val outputs = listOf(outState1, outState2, outState3)
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt: PrivacySalt = PrivacySalt()
transaction = LedgerTransaction(
inputs,
outputs,
commands,
attachments,
id,
notary,
timeWindow,
privacySalt
)
}
private fun createTxnWithTwoStateTypes(): LedgerTransaction {
val notary: Party = DUMMY_NOTARY
val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary)
val inState2 = TransactionState(DummyContract.MultiOwnerState(0,
listOf(ALICE, BOB)), notary)
val outState1 = inState1.copy()
val outState2 = inState2.copy()
val state1TxHash = SecureHash.randomSHA256()
val state2TxHash = SecureHash.randomSHA256()
val inputs = listOf(StateAndRef(inState1, StateRef(state1TxHash, 0)),
StateAndRef(inState1, StateRef(state1TxHash, 1)),
StateAndRef(inState2, StateRef(state2TxHash, 0)),
StateAndRef(inState1, StateRef(state1TxHash, 2))) // bogus state not in db
val outputs = listOf(outState1, outState2)
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt: PrivacySalt = PrivacySalt()
return LedgerTransaction(
inputs,
outputs,
commands,
attachments,
id,
notary,
timeWindow,
privacySalt
)
}
private fun dummyStatesInsert(txn: LedgerTransaction) {
data.invoke {
// skip inserting the last txn state (to mimic spend attempt of non existent unconsumed state)
txn.inputs.subList(0, txn.inputs.lastIndex).forEach {
insert(createStateEntity(it))
// create additional state entities with idx >0
for (i in 3..4) {
try {
createStateEntity(it, idx = i).apply {
insert(this)
}
} catch(e: Exception) {
}
}
// create additional state entities with different txn id
for (i in 1..3) {
createStateEntity(it, txHash = SecureHash.randomSHA256().toString()).apply {
insert(this)
}
}
}
// insert an additional MultiOwnerState with idx 1
insert(createStateEntity(txn.inputs[2], idx = 1))
// insert entities with other state type
for (i in 1..5) {
VaultStatesEntity().apply {
txId = SecureHash.randomSHA256().toString()
index = 0
contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name
stateStatus = Vault.StateStatus.UNCONSUMED
insert(this)
}
}
}
// check total numner of inserted states
assertEquals(3 + 4 + 9 + 1 + 5, data.select(VaultSchema.VaultStates::class).get().count())
}
/**
* Vault Schema: VaultStates
*/
@Test
fun testInsertState() {
val state = VaultStatesEntity()
state.txId = "12345"
state.index = 0
data.invoke {
insert(state)
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId)
Assert.assertSame(state, result().first())
}
}
@Test
fun testUpsertUnconsumedState() {
val stateEntity = createStateEntity(transaction!!.inputs[0])
data.invoke {
upsert(stateEntity)
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId)
Assert.assertSame(stateEntity, result().first())
}
}
@Test
fun testUpsertConsumedState() {
val stateEntity = createStateEntity(transaction!!.inputs[0])
data.invoke {
upsert(stateEntity)
}
val keys = mapOf(VaultStatesEntity.TX_ID to stateEntity.txId,
VaultStatesEntity.INDEX to stateEntity.index)
val key = io.requery.proxy.CompositeKey(keys)
data.invoke {
val state = findByKey(VaultStatesEntity::class, key)
state?.run {
stateStatus = Vault.StateStatus.CONSUMED
consumedTime = Instant.now()
update(state)
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId)
assertEquals(Vault.StateStatus.CONSUMED, result().first().stateStatus)
}
}
}
@Test
fun testCashBalanceUpdate() {
val cashBalanceEntity = VaultCashBalancesEntity()
cashBalanceEntity.currency = "USD"
cashBalanceEntity.amount = 100
data.invoke {
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
assertNull(state)
upsert(cashBalanceEntity)
}
data.invoke {
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
state?.let {
state.amount -= 80
upsert(state)
}
assertEquals(20, state!!.amount)
}
}
@Test
fun testTransactionalUpsertState() {
data.withTransaction(TransactionIsolation.REPEATABLE_READ) {
transaction!!.inputs.forEach {
val stateEntity = createStateEntity(it)
insert(stateEntity)
}
val result = select(VaultSchema.VaultStates::class)
Assert.assertSame(3, result().toList().size)
}
data.invoke {
val result = select(VaultSchema.VaultStates::class)
Assert.assertSame(3, result().toList().size)
}
}
@Test
fun testDistinctContractStateTypes() {
val txn = createTxnWithTwoStateTypes()
dummyStatesInsert(txn)
data.invoke {
transaction!!.inputs.forEach {
val stateEntity = createStateEntity(it)
insert(stateEntity)
}
val query = select(VaultSchema.VaultStates::contractStateClassName).distinct()
val results = query.get()
Assert.assertSame(3, results.count())
}
}
private fun createStateEntity(stateAndRef: StateAndRef<*>, idx: Int? = null, txHash: String? = null): VaultStatesEntity {
val stateRef = stateAndRef.ref
val state = stateAndRef.state
return VaultStatesEntity().apply {
txId = txHash ?: stateRef.txhash.toString()
index = idx ?: stateRef.index
stateStatus = Vault.StateStatus.UNCONSUMED
contractStateClassName = state.data.javaClass.name
contractState = state.serialize().bytes
notaryName = state.notary.name.toString()
recordedTime = Instant.now()
}
}
/**
* Vault Schema: Transaction Notes
*/
@Test
fun testInsertTxnNote() {
val txnNoteEntity = VaultTxnNoteEntity()
txnNoteEntity.txId = "12345"
txnNoteEntity.note = "Sample transaction note"
data.invoke {
insert(txnNoteEntity)
val result = select(VaultSchema.VaultTxnNote::class)
Assert.assertSame(txnNoteEntity, result().first())
}
}
@Test
fun testFindTxnNote() {
val txnNoteEntity = VaultTxnNoteEntity()
txnNoteEntity.txId = "12345"
txnNoteEntity.note = "Sample transaction note #1"
val txnNoteEntity2 = VaultTxnNoteEntity()
txnNoteEntity2.txId = "23456"
txnNoteEntity2.note = "Sample transaction note #2"
data.invoke {
insert(txnNoteEntity)
insert(txnNoteEntity2)
}
data.invoke {
val result = select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnNoteEntity2.txId)
assertEquals(result().count(), 1)
Assert.assertSame(txnNoteEntity2, result().first())
}
}
/**
* Vault Schema: Cash Balances
*/
@Test
fun testInsertCashBalance() {
val cashBalanceEntity = VaultCashBalancesEntity()
cashBalanceEntity.currency = "GPB"
cashBalanceEntity.amount = 12345
data.invoke {
insert(cashBalanceEntity)
val result = select(VaultSchema.VaultCashBalances::class)
Assert.assertSame(cashBalanceEntity, result().first())
}
}
@Test
fun testUpdateCashBalance() {
val cashBalanceEntity = VaultCashBalancesEntity()
cashBalanceEntity.currency = "GPB"
cashBalanceEntity.amount = 12345
data.invoke {
insert(cashBalanceEntity)
}
data.invoke {
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
assertNotNull(state)
state?.let {
state.amount += 10000
update(state)
val result = select(VaultCashBalancesEntity::class)
assertEquals(22345, result().first().amount)
}
}
}
@Test
fun testUpsertCashBalance() {
val cashBalanceEntity = VaultCashBalancesEntity()
cashBalanceEntity.currency = "GPB"
cashBalanceEntity.amount = 12345
data.invoke {
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
state?.let {
state.amount += 10000
}
val result = upsert(state ?: cashBalanceEntity)
assertEquals(12345, result.amount)
}
}
@Test
fun testAllUnconsumedStates() {
data.invoke {
transaction!!.inputs.forEach {
insert(createStateEntity(it))
}
}
val stateAndRefs = unconsumedStates<ContractState>()
assertNotNull(stateAndRefs)
assertTrue { stateAndRefs.size == 3 }
}
@Test
fun tesUnconsumedDummyStates() {
data.invoke {
transaction!!.inputs.forEach {
insert(createStateEntity(it))
}
}
val stateAndRefs = unconsumedStates<DummyContract.State>()
assertNotNull(stateAndRefs)
assertTrue { stateAndRefs.size == 2 }
}
@Test
fun tesUnconsumedDummySingleOwnerStates() {
data.invoke {
transaction!!.inputs.forEach {
insert(createStateEntity(it))
}
}
val stateAndRefs = unconsumedStates<DummyContract.SingleOwnerState>()
assertNotNull(stateAndRefs)
assertTrue { stateAndRefs.size == 1 }
}
inline fun <reified T : ContractState> unconsumedStates(): List<StateAndRef<T>> {
val stateAndRefs =
data.invoke {
val result = select(VaultSchema.VaultStates::class)
.where(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
result.get()
.map { it ->
val stateRef = StateRef(SecureHash.parse(it.txId), it.index)
val state = it.contractState.deserialize<TransactionState<T>>()
StateAndRef(state, stateRef)
}.filter {
T::class.java.isAssignableFrom(it.state.data.javaClass)
}.toList()
}
return stateAndRefs
}
/**
* Observables testing
*/
@Test
@Throws(Exception::class)
fun testInsert() {
val stateEntity = createStateEntity(transaction!!.inputs[0])
val latch = CountDownLatch(1)
odata.insert(stateEntity).subscribe {
Assert.assertNotNull(it.txId)
Assert.assertTrue(it.txId.isNotEmpty())
val cached = data.select(VaultSchema.VaultStates::class)
.where(VaultSchema.VaultStates::txId.eq(it.txId)).get().first()
Assert.assertSame(cached, it)
latch.countDown()
}
latch.await()
}
@Test
@Throws(Exception::class)
fun testInsertCount() {
val stateEntity = createStateEntity(transaction!!.inputs[0])
Observable.just(stateEntity)
.concatMap { person -> odata.insert(person).toObservable() }
odata.insert(stateEntity).toBlocking().value()
Assert.assertNotNull(stateEntity.txId)
Assert.assertTrue(stateEntity.txId.isNotEmpty())
val count = data.count(VaultSchema.VaultStates::class).get().value()
Assert.assertEquals(1, count.toLong())
}
@Test
@Throws(Exception::class)
fun testQueryEmpty() {
val latch = CountDownLatch(1)
odata.select(VaultSchema.VaultStates::class).get().toObservable()
.subscribe({ Assert.fail() }, { Assert.fail() }) { latch.countDown() }
if (!latch.await(1, TimeUnit.SECONDS)) {
Assert.fail()
}
}
@Test
@Throws(Exception::class)
fun testQueryObservable() {
transaction!!.inputs.forEach {
val stateEntity = createStateEntity(it)
odata.insert(stateEntity).toBlocking().value()
}
val states = ArrayList<VaultStatesEntity>()
odata.select(VaultSchema.VaultStates::class).get()
.toObservable()
.subscribe { it -> states.add(it as VaultStatesEntity) }
Assert.assertEquals(3, states.size)
}
/**
* Requery composite key tests (using RowExpression introduced in 1.2.1)
*/
@Test
fun testQueryWithCompositeKey() {
// txn entity with 4 input states (SingleOwnerState x 3, MultiOwnerState x 1)
val txn = createTxnWithTwoStateTypes()
dummyStatesInsert(txn)
data.invoke {
val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX)
val expression = RowExpression.of(primaryCompositeKey)
val stateRefs = txn.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) }
val result = select(VaultStatesEntity::class) where (expression.`in`(stateRefs))
assertEquals(3, result.get().count())
}
}
@Test
fun testUpdateWithCompositeKey() {
// txn entity with 4 input states (SingleOwnerState x 3, MultiOwnerState x 1)
val txn = createTxnWithTwoStateTypes()
dummyStatesInsert(txn)
data.invoke {
val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX)
val expression = RowExpression.of(primaryCompositeKey)
val stateRefs = txn.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) }
val update = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, "")
.set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now())
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and(expression.`in`(stateRefs)).get()
assertEquals(3, update.value())
}
}
/**
* Soft locking tests
*/
@Test
fun testSingleSoftLockUpdate() {
// insert unconsumed state
val stateEntity = createStateEntity(transaction!!.inputs[0])
data.invoke {
upsert(stateEntity)
}
// reserve soft lock on state
stateEntity.apply {
this.lockId = "LOCK#1"
this.lockUpdateTime = Instant.now()
data.invoke {
upsert(stateEntity)
}
}
// select unlocked states
data.invoke {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId)
.and(VaultSchema.VaultStates::lockId.isNull())
assertEquals(0, result.get().count())
}
// release soft lock on state
data.invoke {
val update = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, null)
.set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now())
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and(VaultStatesEntity.LOCK_ID eq "LOCK#1").get()
assertEquals(1, update.value())
}
// select unlocked states
data.invoke {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId)
.and(VaultSchema.VaultStates::lockId.isNull())
assertEquals(1, result.get().count())
}
}
@Test
fun testMultipleSoftLocksUpdate() {
// insert unconsumed state
data.withTransaction(TransactionIsolation.REPEATABLE_READ) {
transaction!!.inputs.forEach {
val stateEntity = createStateEntity(it)
insert(stateEntity)
}
val result = select(VaultSchema.VaultStates::class)
Assert.assertSame(3, result().toList().size)
}
// reserve soft locks on states
transaction!!.inputs.forEach {
val stateEntity = createStateEntity(it)
stateEntity.apply {
this.lockId = "LOCK#1"
this.lockUpdateTime = Instant.now()
data.invoke {
upsert(stateEntity)
}
}
}
// select unlocked states
val txnIds = transaction!!.inputs.map { it.ref.txhash.toString() }.toSet()
data.invoke {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId `in` txnIds)
.and(VaultSchema.VaultStates::lockId eq "")
assertEquals(0, result.get().count())
}
// release soft lock on states
data.invoke {
val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX)
val expression = RowExpression.of(primaryCompositeKey)
val stateRefs = transaction!!.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) }
val update = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, "")
.set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now())
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and(expression.`in`(stateRefs)).get()
assertEquals(3, update.value())
}
// select unlocked states
data.invoke {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId `in` txnIds)
.and(VaultSchema.VaultStates::lockId eq "")
assertEquals(3, result.get().count())
}
}
@Test
fun insertWithBigCompositeKey() {
val vaultStEntity = VaultStatesEntity().apply {
txId = SecureHash.randomSHA256().toString()
index = 314
stateStatus = Vault.StateStatus.UNCONSUMED
contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name
notaryName = "Huge distributed notary"
recordedTime = Instant.now()
}
data.insert(vaultStEntity)
assertEquals(1, data.select(VaultSchema.VaultStates::class).get().count())
}
}

View File

@ -76,7 +76,6 @@ processSmokeTestResources {
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
dependencies {
compile project(':node-schemas')
compile project(':node-api')
compile project(':client:rpc')
compile project(':cordform-common')
@ -180,9 +179,6 @@ dependencies {
compile 'commons-codec:commons-codec:1.10'
compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
// Requery: object mapper for Kotlin
compile "io.requery:requery-kotlin:$requery_version"
// FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'

View File

@ -412,7 +412,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private fun makeServices(): MutableList<Any> {
checkpointStorage = DBCheckpointStorage()
_services = ServiceHubInternalImpl()
attachments = NodeAttachmentService(configuration.dataSourceProperties, services.monitoringService.metrics, configuration.database)
attachments = NodeAttachmentService(services.monitoringService.metrics)
val legalIdentity = obtainIdentity("identity", configuration.myLegalName)
network = makeMessagingService(legalIdentity)
info = makeInfo(legalIdentity)
@ -749,6 +749,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
protected open fun generateKeyPair() = cryptoGenerateKeyPair()
private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() {
private val hibernateConfig by lazy { HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }) }
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
override val auditService = DummyAuditService()
@ -756,9 +759,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val validatedTransactions = makeTransactionStorage()
override val transactionVerifierService by lazy { makeTransactionVerifierService() }
override val networkMapCache by lazy { InMemoryNetworkMapCache(this) }
override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) }
override val vaultService by lazy { NodeVaultService(this) }
override val vaultQueryService by lazy {
HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }), vaultService)
HibernateVaultQueryImpl(hibernateConfig, vaultService)
}
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with

View File

@ -7,6 +7,7 @@ import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverte
import net.corda.core.utilities.loggerFor
import net.corda.node.services.api.SchemaService
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.node.utilities.parserTransactionIsolationLevel
import org.hibernate.SessionFactory
import org.hibernate.boot.MetadataSources
import org.hibernate.boot.model.naming.Identifier
@ -28,6 +29,8 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"")
init {
schemaService.schemaOptions.map { it.key }.forEach { mappedSchema ->
sessionFactories.computeIfAbsent(mappedSchema, { makeSessionFactoryForSchema(mappedSchema) })
@ -60,6 +63,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate")
.setProperty("hibernate.format_sql", "true")
.setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString())
schemas.forEach { schema ->
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session

View File

@ -1,146 +0,0 @@
package net.corda.node.services.database
import io.requery.EntityCache
import io.requery.TransactionIsolation
import io.requery.TransactionListener
import io.requery.cache.WeakEntityCache
import io.requery.meta.EntityModel
import io.requery.sql.*
import io.requery.sql.platform.H2
import io.requery.util.function.Function
import io.requery.util.function.Supplier
import net.corda.core.schemas.requery.converters.InstantConverter
import net.corda.core.schemas.requery.converters.SecureHashConverter
import net.corda.core.schemas.requery.converters.StateRefConverter
import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
import net.corda.node.utilities.DatabaseTransactionManager
import java.sql.Connection
import java.util.*
import java.util.concurrent.Executor
import javax.sql.DataSource
/**
* Requery KotlinConfiguration wrapper class to enable us to pass in an existing database connection and
* associated transaction context.
*/
class KotlinConfigurationTransactionWrapper(private val model: EntityModel,
dataSource: DataSource,
private val mapping: Mapping? = null,
private val platform: Platform? = null,
private val cache: EntityCache = WeakEntityCache(),
private val useDefaultLogging: Boolean = false,
private val statementCacheSize: Int = 0,
private val batchUpdateSize: Int = 64,
private val quoteTableNames: Boolean = false,
private val quoteColumnNames: Boolean = false,
private val tableTransformer: Function<String, String>? = null,
private val columnTransformer: Function<String, String>? = null,
private val transactionMode: TransactionMode = TransactionMode.NONE,
private val transactionIsolation: TransactionIsolation? = null,
private val statementListeners: Set<StatementListener> = LinkedHashSet(),
private val entityStateListeners: Set<EntityStateListener<Any>> = LinkedHashSet(),
private val transactionListeners: Set<Supplier<TransactionListener>> = LinkedHashSet(),
private val writeExecutor: Executor? = null) : Configuration {
private val connectionProvider = CordaDataSourceConnectionProvider(dataSource)
override fun getBatchUpdateSize(): Int {
return batchUpdateSize
}
override fun getConnectionProvider(): ConnectionProvider? {
return connectionProvider
}
override fun getCache(): EntityCache? {
return cache
}
override fun getEntityStateListeners(): Set<EntityStateListener<Any>> {
return entityStateListeners
}
override fun getMapping(): Mapping? {
// TODO: database platform provider to become configurable and parameterised into this configuration
val customMapping = GenericMapping(H2())
// register our custom converters
val instantConverter = InstantConverter()
customMapping.addConverter(instantConverter, instantConverter.mappedType)
val vaultStateStatusConverter = VaultStateStatusConverter()
customMapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType)
customMapping.addConverter(StateRefConverter(), StateRefConverter::getMappedType.javaClass)
customMapping.addConverter(SecureHashConverter(), SecureHashConverter::getMappedType.javaClass)
return customMapping
}
override fun getModel(): EntityModel {
return model
}
override fun getPlatform(): Platform? {
return platform
}
override fun getQuoteTableNames(): Boolean {
return quoteTableNames
}
override fun getQuoteColumnNames(): Boolean {
return quoteColumnNames
}
override fun getTableTransformer(): Function<String, String>? {
return tableTransformer
}
override fun getColumnTransformer(): Function<String, String>? {
return columnTransformer
}
override fun getStatementCacheSize(): Int {
return statementCacheSize
}
override fun getStatementListeners(): Set<StatementListener>? {
return statementListeners
}
override fun getTransactionMode(): TransactionMode? {
return transactionMode
}
override fun getTransactionIsolation(): TransactionIsolation? {
return transactionIsolation
}
override fun getTransactionListenerFactories(): Set<Supplier<TransactionListener>>? {
return transactionListeners
}
override fun getUseDefaultLogging(): Boolean {
return useDefaultLogging
}
override fun getWriteExecutor(): Executor? {
return writeExecutor
}
class CordaDataSourceConnectionProvider(val dataSource: DataSource) : ConnectionProvider {
override fun getConnection(): Connection = CordaConnection(DatabaseTransactionManager.current().connection)
}
class CordaConnection(val connection: Connection) : Connection by connection {
override fun close() {
// TODO: address requery auto-closing the connection in SchemaModifier upon table creation
// https://github.com/requery/requery/issues/424
}
override fun setAutoCommit(autoCommit: Boolean) {
// TODO: address requery bug in ConnectionTransaction commit()
// https://github.com/requery/requery/issues/423
connection.autoCommit = false
}
}
}

View File

@ -1,65 +0,0 @@
package net.corda.node.services.database
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.requery.Persistable
import io.requery.TransactionIsolation
import io.requery.meta.EntityModel
import io.requery.sql.KotlinEntityDataStore
import io.requery.sql.SchemaModifier
import io.requery.sql.TableCreationMode
import net.corda.core.utilities.loggerFor
import net.corda.node.utilities.DatabaseTransactionManager
import java.sql.Connection
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class RequeryConfiguration(val properties: Properties, val useDefaultLogging: Boolean = false, val databaseProperties: Properties) {
companion object {
val logger = loggerFor<RequeryConfiguration>()
}
// TODO:
// 1. schemaService schemaOptions needs to be applied: eg. default schema, table prefix
// 2. set other generic database configuration options: show_sql, format_sql
// 3. Configure Requery Database platform specific features (see http://requery.github.io/javadoc/io/requery/sql/Platform.html)
// 4. Configure Cache Manager and Cache Provider and set in Requery Configuration (see http://requery.github.io/javadoc/io/requery/EntityCache.html)
// 5. Consider database schema deployment/upgrade strategies to replace dynamic table creation.
// Note: Annotations are pre-processed using (kapt) so no need to register dynamically
val config = HikariConfig(properties)
val dataSource = HikariDataSource(config)
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
private val sessionFactories = ConcurrentHashMap<EntityModel, KotlinEntityDataStore<Persistable>>()
fun sessionForModel(model: EntityModel): KotlinEntityDataStore<Persistable> {
return sessionFactories.computeIfAbsent(model, { makeSessionFactoryForModel(it) })
}
fun makeSessionFactoryForModel(model: EntityModel): KotlinEntityDataStore<Persistable> {
val configuration = KotlinConfigurationTransactionWrapper(model, dataSource, useDefaultLogging = this.useDefaultLogging)
val tables = SchemaModifier(configuration)
if (databaseProperties.getProperty("initDatabase","true") == "true" ) {
val mode = TableCreationMode.CREATE_NOT_EXISTS
tables.createTables(mode)
}
return KotlinEntityDataStore(configuration)
}
// TODO: remove once Requery supports QUERY WITH COMPOSITE_KEY IN
fun jdbcSession(): Connection = DatabaseTransactionManager.current().connection
}
fun parserTransactionIsolationLevel(property: String?) : TransactionIsolation =
when (property) {
"none" -> TransactionIsolation.NONE
"readUncommitted" -> TransactionIsolation.READ_UNCOMMITTED
"readCommitted" -> TransactionIsolation.READ_COMMITTED
"repeatableRead" -> TransactionIsolation.REPEATABLE_READ
"serializable" -> TransactionIsolation.SERIALIZABLE
else -> {
TransactionIsolation.REPEATABLE_READ
}
}

View File

@ -12,41 +12,50 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.*
import net.corda.core.utilities.loggerFor
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.services.persistence.schemas.requery.Models
import java.io.ByteArrayInputStream
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.node.utilities.NODE_DATABASE_PREFIX
import java.io.*
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Paths
import java.util.*
import java.util.jar.JarInputStream
import javax.annotation.concurrent.ThreadSafe
import javax.persistence.*
/**
* Stores attachments in H2 database.
* Stores attachments using Hibernate to database.
*/
@ThreadSafe
class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricRegistry, databaseProperties: Properties?)
: AttachmentStorage, SingletonSerializeAsToken() {
class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, SingletonSerializeAsToken() {
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}attachments",
indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id")))
class DBAttachment(
@Id
@Column(name = "att_id", length = 65535)
var attId: String,
@Column(name = "content")
@Lob
var content: ByteArray
) : Serializable
companion object {
private val log = loggerFor<NodeAttachmentService>()
}
val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties())
val session = configuration.sessionForModel(Models.PERSISTENCE)
@VisibleForTesting
var checkAttachmentsOnLoad = true
private val attachmentCount = metrics.counter("Attachments")
init {
session.withTransaction {
attachmentCount.inc(session.count(AttachmentEntity::class).get().value().toLong())
}
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
val count = session.createQuery(criteriaQuery).singleResult
attachmentCount.inc(count)
}
@CordaSerializable
@ -128,16 +137,13 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg
}
override fun openAttachment(id: SecureHash): Attachment? = session.withTransaction {
try {
session.select(AttachmentEntity::class)
.where(AttachmentEntity.ATT_ID.eq(id))
.get()
.single()
} catch (e: NoSuchElementException) {
null
override fun openAttachment(id: SecureHash): Attachment? {
val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
attachment?.let {
return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad)
}
}?.run { AttachmentImpl(id, { content }, checkAttachmentsOnLoad) }
return null
}
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
override fun importAttachment(jar: InputStream): SecureHash {
@ -153,25 +159,21 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg
checkIsAValidJAR(ByteArrayInputStream(bytes))
val id = SecureHash.SHA256(hs.hash().asBytes())
val count = session.withTransaction {
session.count(AttachmentEntity::class)
.where(AttachmentEntity.ATT_ID.eq(id))
.get().value()
}
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
criteriaQuery.where(criteriaBuilder.equal(attachments.get<String>(DBAttachment::attId.name), id.toString()))
val count = session.createQuery(criteriaQuery).singleResult
if (count > 0) {
throw FileAlreadyExistsException(id.toString())
}
session.withTransaction {
val attachment = AttachmentEntity()
attachment.attId = id
attachment.content = bytes
session.insert(attachment)
}
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
return id

View File

@ -14,6 +14,7 @@ import net.corda.node.services.keys.PersistentKeyManagementService
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.persistence.DBTransactionMappingStorage
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.services.vault.VaultSchemaV1
@ -36,7 +37,9 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
DBTransactionMappingStorage.DBTransactionMapping::class.java,
PersistentKeyManagementService.PersistentKey::class.java,
PersistentUniquenessProvider.PersistentUniqueness::class.java,
NodeSchedulerService.PersistentScheduledState::class.java
PersistentUniquenessProvider.PersistentUniqueness::class.java,
NodeSchedulerService.PersistentScheduledState::class.java,
NodeAttachmentService.DBAttachment::class.java
))
// Required schemas are those used by internal Corda services
@ -46,7 +49,6 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
Pair(VaultSchemaV1, SchemaService.SchemaOptions()),
Pair(NodeServicesV1, SchemaService.SchemaOptions()))
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas.plus(customSchemas.map {
mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions())
})

View File

@ -191,7 +191,7 @@ object BFTSMaRt {
}
override fun getStateManager() = stateManagerOverride
// TODO: Use Requery with proper DB schema instead of JDBCHashMap.
// TODO: Use proper DB schema instead of JDBCHashMap.
// Must be initialised before ServiceReplica is started
private val commitLog = services.database.transaction { JDBCHashMap<StateRef, UniquenessProvider.ConsumingTx>(tableName) }
private val replica = run {

View File

@ -2,15 +2,11 @@ package net.corda.node.services.vault
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand
import net.corda.core.internal.VisibleForTesting
import io.requery.PersistenceException
import io.requery.kotlin.eq
import io.requery.query.RowExpression
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.containsAny
import net.corda.core.crypto.toBase58String
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.tee
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
@ -21,6 +17,7 @@ import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.SortAttribute
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
@ -28,22 +25,15 @@ import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.database.parserTransactionIsolationLevel
import net.corda.core.utilities.*
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.vault.schemas.requery.Models
import net.corda.node.services.vault.schemas.requery.VaultSchema
import net.corda.node.services.vault.schemas.requery.VaultStatesEntity
import net.corda.node.services.vault.schemas.requery.VaultTxnNoteEntity
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.node.utilities.bufferUntilDatabaseCommit
import net.corda.node.utilities.wrapWithDatabaseTransaction
import rx.Observable
import rx.subjects.PublishSubject
import java.security.PublicKey
import java.time.Instant
import java.util.*
import javax.persistence.criteria.Predicate
@ -54,23 +44,15 @@ import javax.persistence.criteria.Predicate
* This class needs database transactions to be in-flight during method calls and init, and will throw exceptions if
* this is not the case.
*
* TODO: move query / filter criteria into the database query.
* TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time.
* TODO: have transaction storage do some caching.
*/
class NodeVaultService(private val services: ServiceHub, dataSourceProperties: Properties, databaseProperties: Properties?) : SingletonSerializeAsToken(), VaultService {
class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsToken(), VaultService {
private companion object {
val log = loggerFor<NodeVaultService>()
// Define composite primary key used in Requery Expression
val stateRefCompositeColumn: RowExpression = RowExpression.of(listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX))
}
val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties())
val session = configuration.sessionForModel(Models.VAULT)
private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties?.getProperty("transactionIsolationLevel") ?:"")
private class InnerState {
val _updatesPublisher = PublishSubject.create<Vault.Update<ContractState>>()!!
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update<ContractState>>()!!
@ -89,35 +71,29 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
val consumedStateRefs = update.consumed.map { it.ref }
log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." }
session.withTransaction(transactionIsolationLevel) {
producedStateRefsMap.forEach { it ->
val state = VaultStatesEntity().apply {
txId = it.key.txhash.toString()
index = it.key.index
stateStatus = Vault.StateStatus.UNCONSUMED
contractStateClassName = it.value.state.data.javaClass.name
contractState = it.value.state.serialize(context = STORAGE_CONTEXT).bytes
notaryName = it.value.state.notary.name.toString()
recordedTime = services.clock.instant()
}
insert(state)
}
// TODO: awaiting support of UPDATE WHERE <Composite key> IN in Requery DSL
consumedStateRefs.forEach { stateRef ->
val queryKey = io.requery.proxy.CompositeKey(mapOf(VaultStatesEntity.TX_ID to stateRef.txhash.toString(),
VaultStatesEntity.INDEX to stateRef.index))
val state = findByKey(VaultStatesEntity::class, queryKey)
state?.run {
stateStatus = Vault.StateStatus.CONSUMED
consumedTime = services.clock.instant()
// remove lock (if held)
if (lockId != null) {
lockId = null
lockUpdateTime = services.clock.instant()
log.trace("Releasing soft lock on consumed state: $stateRef")
}
update(state)
val session = DatabaseTransactionManager.current().session
producedStateRefsMap.forEach { stateAndRef ->
val state = VaultSchemaV1.VaultStates(
notary = stateAndRef.value.state.notary,
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
contractState = stateAndRef.value.state.serialize(context = STORAGE_CONTEXT).bytes,
stateStatus = Vault.StateStatus.UNCONSUMED,
recordedTime = services.clock.instant())
state.stateRef = PersistentStateRef(stateAndRef.key)
session.save(state)
}
consumedStateRefs.forEach { stateRef ->
val state = session.get<VaultSchemaV1.VaultStates>(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef))
state?.run {
stateStatus = Vault.StateStatus.CONSUMED
consumedTime = services.clock.instant()
// remove lock (if held)
if (lockId != null) {
lockId = null
lockUpdateTime = services.clock.instant()
log.trace("Releasing soft lock on consumed state: $stateRef")
}
session.save(state)
}
}
}
@ -220,19 +196,25 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
processAndNotify(netDelta)
}
// TODO: replace this method in favour of a VaultQuery query
private fun loadStates(refs: Collection<StateRef>): HashSet<StateAndRef<ContractState>> {
val states = HashSet<StateAndRef<ContractState>>()
if (refs.isNotEmpty()) {
session.withTransaction(transactionIsolationLevel) {
val result = select(VaultStatesEntity::class).
where(stateRefCompositeColumn.`in`(stateRefArgs(refs))).
and(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
result.get().forEach {
val txHash = SecureHash.parse(it.txId)
val index = it.index
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
states.add(StateAndRef(state, StateRef(txHash, index)))
}
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
val statusPredicate = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
val persistentStateRefs = refs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
criteriaQuery.where(statusPredicate, stateRefsPredicate)
val results = session.createQuery(criteriaQuery).resultList
results.asSequence().forEach {
val txHash = SecureHash.parse(it.stateRef?.txId!!)
val index = it.stateRef?.index!!
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
states.add(StateAndRef(state, StateRef(txHash, index)))
}
}
return states
@ -251,85 +233,105 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
}
override fun addNoteToTransaction(txnId: SecureHash, noteText: String) {
session.withTransaction(transactionIsolationLevel) {
val txnNoteEntity = VaultTxnNoteEntity()
txnNoteEntity.txId = txnId.toString()
txnNoteEntity.note = noteText
insert(txnNoteEntity)
}
val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText)
DatabaseTransactionManager.current().session.save(txnNoteEntity)
}
override fun getTransactionNotes(txnId: SecureHash): Iterable<String> {
return session.withTransaction(transactionIsolationLevel) {
(select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnId.toString())).get().asIterable().map { it.note }
}
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java)
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java)
val txIdPredicate = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultTxnNote::txId.name), txnId.toString())
criteriaQuery.where(txIdPredicate)
val results = session.createQuery(criteriaQuery).resultList
return results.asIterable().map { it.note }
}
@Throws(StatesNotAvailableException::class)
override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet<StateRef>) {
val softLockTimestamp = services.clock.instant()
val stateRefArgs = stateRefArgs(stateRefs)
try {
session.withTransaction(transactionIsolationLevel) {
val updatedRows = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, lockId.toString())
.set(VaultStatesEntity.LOCK_UPDATE_TIME, softLockTimestamp)
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and((VaultStatesEntity.LOCK_ID eq lockId.toString()) or (VaultStatesEntity.LOCK_ID.isNull()))
.and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value()
if (updatedRows > 0 && updatedRows == stateRefs.size) {
log.trace("Reserving soft lock states for $lockId: $stateRefs")
FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true
} else {
// revert partial soft locks
val revertUpdatedRows = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, null)
.where(VaultStatesEntity.LOCK_UPDATE_TIME eq softLockTimestamp)
.and(VaultStatesEntity.LOCK_ID eq lockId.toString())
.and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value()
if (revertUpdatedRows > 0) {
log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId")
}
throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available")
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
val lockIdPredicate = criteriaBuilder.or(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name).isNull,
criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()))
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
criteriaUpdate.set(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate)
val updatedRows = session.createQuery(criteriaUpdate).executeUpdate()
if (updatedRows > 0 && updatedRows == stateRefs.size) {
log.trace("Reserving soft lock states for $lockId: $stateRefs")
FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true
} else {
// revert partial soft locks
val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java)
val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
val compositeKeyRevert = vaultStatesRevert.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert))
criteriaRevertUpdate.set(vaultStatesRevert.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert)
val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate()
if (revertUpdatedRows > 0) {
log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId")
}
throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available")
}
} catch (e: PersistenceException) {
} catch (e: Exception) {
log.error("""soft lock update error attempting to reserve states for $lockId and $stateRefs")
$e.
""")
if (e.cause is StatesNotAvailableException) throw (e.cause as StatesNotAvailableException)
throw e
}
}
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
val softLockTimestamp = services.clock.instant()
val session = DatabaseTransactionManager.current().session
val criteriaBuilder = session.criteriaBuilder
if (stateRefs == null) {
session.withTransaction(transactionIsolationLevel) {
val update = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, null)
.set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant())
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and(VaultStatesEntity.LOCK_ID eq lockId.toString()).get()
if (update.value() > 0) {
log.trace("Releasing ${update.value()} soft locked states for $lockId")
}
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
val lockIdPredicate = criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
criteriaUpdate.set<String>(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
criteriaUpdate.where(stateStatusPredication, lockIdPredicate)
val update = session.createQuery(criteriaUpdate).executeUpdate()
if (update > 0) {
log.trace("Releasing $update soft locked states for $lockId")
}
} else {
try {
session.withTransaction(transactionIsolationLevel) {
val updatedRows = update(VaultStatesEntity::class)
.set(VaultStatesEntity.LOCK_ID, null)
.set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant())
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
.and(VaultStatesEntity.LOCK_ID eq lockId.toString())
.and(stateRefCompositeColumn.`in`(stateRefArgs(stateRefs))).get().value()
if (updatedRows > 0) {
log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs")
}
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
val lockIdPredicate = criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
criteriaUpdate.set<String>(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate)
val updatedRows = session.createQuery(criteriaUpdate).executeUpdate()
if (updatedRows > 0) {
log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs")
}
} catch (e: PersistenceException) {
} catch (e: Exception) {
log.error("""soft lock update error attempting to release states for $lockId and $stateRefs")
$e.
""")
throw e
}
}
}
@ -462,11 +464,4 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
is LinearState -> state.isRelevant(ourKeys)
else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty()
}
/**
* Helper method to generate a string formatted list of Composite Keys for Requery Expression clause
*/
private fun stateRefArgs(stateRefs: Iterable<StateRef>): List<List<Any>> {
return stateRefs.map { listOf("'${it.txhash}'", it.index) }
}
}

View File

@ -8,6 +8,9 @@ import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import org.hibernate.annotations.Generated
import org.hibernate.annotations.GenerationTime
import java.io.Serializable
import java.time.Instant
import java.util.*
import javax.persistence.*
@ -22,7 +25,7 @@ object VaultSchema
*/
@CordaSerializable
object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1,
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java)) {
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) {
@Entity
@Table(name = "vault_states",
indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status")))
@ -50,16 +53,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
/** refers to timestamp recorded upon entering CONSUMED state */
@Column(name = "consumed_timestamp", nullable = true)
var consumedTime: Instant?,
var consumedTime: Instant? = null,
/** used to denote a state has been soft locked (to prevent double spend)
* will contain a temporary unique [UUID] obtained from a flow session */
@Column(name = "lock_id", nullable = true)
var lockId: String,
var lockId: String? = null,
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
@Column(name = "lock_timestamp", nullable = true)
var lockUpdateTime: Instant?
var lockUpdateTime: Instant? = null
) : PersistentState()
@Entity
@ -133,4 +136,23 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
issuerRef = _issuerRef.bytes,
participants = _participants.toMutableSet())
}
@Entity
@Table(name = "vault_transaction_notes",
indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"),
Index(name = "transaction_id_index", columnList = "transaction_id")))
class VaultTxnNote(
@Id
@GeneratedValue
@Column(name = "seq_no")
var seqNo: Int,
@Column(name = "transaction_id", length = 64)
var txId: String,
@Column(name = "note")
var note: String
) : Serializable {
constructor(txId: String, note: String) : this(0, txId, note)
}
}

View File

@ -1,7 +1,7 @@
package net.corda.node.services.vault;
import com.google.common.collect.ImmutableSet;
import kotlin.Pair;
import kotlin.*;
import net.corda.contracts.DealState;
import net.corda.contracts.asset.Cash;
import net.corda.contracts.asset.CashUtilities;
@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
import net.corda.core.schemas.MappedSchema;
import net.corda.core.utilities.OpaqueBytes;
import net.corda.node.utilities.CordaPersistence;
import net.corda.node.utilities.*;
import net.corda.schemas.CashSchemaV1;
import net.corda.testing.TestConstants;
import net.corda.testing.TestDependencyInjectionBase;
@ -66,6 +66,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
Set<MappedSchema> requiredSchemas = new HashSet<>();
requiredSchemas.add(CashSchemaV1.INSTANCE);
IdentityService identitySvc = makeTestIdentityService();
@SuppressWarnings("unchecked")
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc);
issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY());
database = databaseAndServices.getFirst();
@ -89,9 +90,10 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Test
public void unconsumedLinearStates() throws VaultQueryException {
database.transaction(tx -> {
VaultFiller.fillWithSomeTestLinearStates(services, 3);
return tx;
});
database.transaction(tx -> {
// DOCSTART VaultJavaQueryExample0
Vault.Page<LinearState> results = vaultQuerySvc.queryBy(LinearState.class);
// DOCEND VaultJavaQueryExample0
@ -104,11 +106,12 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Test
public void unconsumedStatesForStateRefsSortedByTxnId() {
Vault<LinearState> issuedStates =
database.transaction(tx -> {
VaultFiller.fillWithSomeTestLinearStates(services, 8);
return VaultFiller.fillWithSomeTestLinearStates(services, 2);
});
database.transaction(tx -> {
VaultFiller.fillWithSomeTestLinearStates(services, 8);
Vault<LinearState> issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2);
Stream<StateRef> stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef);
List<StateRef> stateRefs = stateRefsStream.collect(Collectors.toList());
@ -129,13 +132,11 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Test
public void consumedCashStates() {
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
database.transaction(tx -> {
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
VaultFiller.fillWithSomeTestCash(services,
new Amount<Currency>(100, Currency.getInstance("USD")),
issuerServices,
issuerServices,
TestConstants.getDUMMY_NOTARY(),
3,
3,
@ -143,9 +144,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
new OpaqueBytes("1".getBytes()),
null,
CashUtilities.getDUMMY_CASH_ISSUER());
return tx;
});
database.transaction(tx -> {
VaultFiller.consumeCash(services, amount, getDUMMY_NOTARY());
return tx;
});
database.transaction(tx -> {
// DOCSTART VaultJavaQueryExample1
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED);
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
@ -159,19 +164,24 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Test
public void consumedDealStatesPagedSorted() throws VaultQueryException {
List<String> dealIds = Arrays.asList("123", "456", "789");
@SuppressWarnings("unchecked")
Triple<StateAndRef<LinearState>, UniqueIdentifier, Vault<DealState>> ids =
database.transaction((DatabaseTransaction tx) -> {
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
StateAndRef<LinearState> linearState = states.getStates().iterator().next();
UniqueIdentifier uid = linearState.component1().getData().getLinearId();
Vault<DealState> dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds);
return new Triple(linearState,uid,dealStates);
});
database.transaction(tx -> {
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
StateAndRef<LinearState> linearState = states.getStates().iterator().next();
UniqueIdentifier uid = linearState.component1().getData().getLinearId();
List<String> dealIds = Arrays.asList("123", "456", "789");
Vault<DealState> dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds);
// consume states
VaultFiller.consumeDeals(services, (List<? extends StateAndRef<? extends DealState>>) dealStates.getStates(), getDUMMY_NOTARY());
VaultFiller.consumeLinearStates(services, Collections.singletonList(linearState), getDUMMY_NOTARY());
VaultFiller.consumeDeals(services, (List<? extends StateAndRef<? extends DealState>>) ids.getThird().getStates(), getDUMMY_NOTARY());
VaultFiller.consumeLinearStates(services, Collections.singletonList(ids.getFirst()), getDUMMY_NOTARY());
return tx;
});
database.transaction(tx -> {
// DOCSTART VaultJavaQueryExample2
Vault.StateStatus status = Vault.StateStatus.CONSUMED;
@SuppressWarnings("unchecked")
@ -179,7 +189,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes);
List<UUID> linearIds = Collections.singletonList(uid.getId());
List<UUID> linearIds = Collections.singletonList(ids.getSecond().getId());
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds);
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds);
@ -212,7 +222,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
VaultFiller.fillWithSomeTestCash(services, dollars100, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
VaultFiller.fillWithSomeTestCash(services, dollars10, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
VaultFiller.fillWithSomeTestCash(services, dollars1, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
return tx;
});
database.transaction(tx -> {
try {
// DOCSTART VaultJavaQueryExample3
QueryCriteria generalCriteria = new VaultQueryCriteria(Vault.StateStatus.ALL);
@ -256,7 +268,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
new OpaqueBytes("1".getBytes()),
null,
getDUMMY_CASH_ISSUER());
return tx;
});
database.transaction(tx -> {
// DOCSTART VaultJavaQueryExample4
@SuppressWarnings("unchecked")
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
@ -276,14 +290,16 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Test
public void trackDealStatesPagedSorted() {
List<String> dealIds = Arrays.asList("123", "456", "789");
UniqueIdentifier uid =
database.transaction(tx -> {
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId();
VaultFiller.fillWithSomeTestDeals(services, dealIds);
return _uid;
});
database.transaction(tx -> {
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId();
List<String> dealIds = Arrays.asList("123", "456", "789");
VaultFiller.fillWithSomeTestDeals(services, dealIds);
// DOCSTART VaultJavaQueryExample5
@SuppressWarnings("unchecked")
Set<Class<ContractState>> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class));
@ -331,6 +347,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
return tx;
});
database.transaction(tx -> {
try {
// DOCSTART VaultJavaQueryExample21
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
@ -376,6 +395,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
return tx;
});
database.transaction(tx -> {
try {
// DOCSTART VaultJavaQueryExample22
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
@ -434,7 +456,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@SuppressWarnings("unchecked")
public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() {
database.transaction(tx -> {
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
Amount<Currency> dollars200 = new Amount<>(200, Currency.getInstance("USD"));
Amount<Currency> pounds300 = new Amount<>(300, Currency.getInstance("GBP"));
@ -445,6 +466,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
VaultFiller.fillWithSomeTestCash(services, pounds300, issuerServices, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
VaultFiller.fillWithSomeTestCash(services, pounds400, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())));
return tx;
});
database.transaction(tx -> {
try {
// DOCSTART VaultJavaQueryExample23
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");

View File

@ -74,7 +74,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
database.transaction {
hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), ::makeTestIdentityService)
services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override val vaultService: VaultService = makeVaultService(hibernateConfig)
override fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>) {
for (stx in txs) {

View File

@ -1,213 +0,0 @@
package net.corda.node.services.database
import io.requery.Persistable
import io.requery.kotlin.eq
import io.requery.sql.KotlinEntityDataStore
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
import net.corda.core.identity.AnonymousParty
import net.corda.core.node.services.Vault
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.vault.schemas.requery.Models
import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity
import net.corda.node.services.vault.schemas.requery.VaultSchema
import net.corda.node.services.vault.schemas.requery.VaultStatesEntity
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.dummyCommand
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.time.Instant
import java.util.*
class RequeryConfigurationTest : TestDependencyInjectionBase() {
lateinit var database: CordaPersistence
lateinit var transactionStorage: DBTransactionStorage
lateinit var requerySession: KotlinEntityDataStore<Persistable>
@Before
fun setUp() {
val dataSourceProperties = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
newTransactionStorage()
newRequeryStorage(dataSourceProperties)
}
@After
fun cleanUp() {
database.close()
}
@Test
fun `transaction inserts in same DB transaction scope across two persistence engines`() {
val txn = newTransaction()
database.transaction {
transactionStorage.addTransaction(txn)
requerySession.withTransaction {
insert(createVaultStateEntity(txn))
}
}
database.transaction {
Assertions.assertThat(transactionStorage.transactions).containsOnly(txn)
requerySession.withTransaction {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
Assertions.assertThat(result.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString())
}
}
}
@Test
fun `transaction operations in same DB transaction scope across two persistence engines`() {
val txn = newTransaction()
database.transaction {
transactionStorage.addTransaction(txn)
requerySession.withTransaction {
upsert(createCashBalance())
select(VaultSchema.VaultCashBalances::class).get().first()
insert(createVaultStateEntity(txn))
}
}
database.transaction {
Assertions.assertThat(transactionStorage.transactions).containsOnly(txn)
requerySession.withTransaction {
val cashQuery = select(VaultSchema.VaultCashBalances::class) where (VaultSchema.VaultCashBalances::currency eq "GBP")
assertEquals(12345, cashQuery.get().first().amount)
val stateQuery = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
Assertions.assertThat(stateQuery.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString())
}
}
}
@Test
fun `transaction rollback in same DB transaction scope across two persistence engines`() {
val txn = newTransaction()
database.transaction {
transactionStorage.addTransaction(txn)
requerySession.withTransaction {
insert(createVaultStateEntity(txn))
}
rollback()
}
database.transaction {
Assertions.assertThat(transactionStorage.transactions).isEmpty()
requerySession.withTransaction {
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
Assertions.assertThat(result.get().count() == 0)
}
}
}
@Test
fun `bounded iteration`() {
// insert 100 entities
database.transaction {
requerySession.withTransaction {
(1..100)
.map { newTransaction(it) }
.forEach { insert(createVaultStateEntity(it)) }
}
}
// query entities 41..45
database.transaction {
requerySession.withTransaction {
// Note: cannot specify a limit explicitly when using iterator skip & take
val query = select(VaultSchema.VaultStates::class)
val count = query.get().count()
Assertions.assertThat(count).isEqualTo(100)
val result = query.get().iterator(40, 5)
Assertions.assertThat(result.asSequence().count()).isEqualTo(5)
}
}
}
@Test
fun `test calling an arbitrary JDBC native query`() {
val txn = newTransaction()
database.transaction {
transactionStorage.addTransaction(txn)
requerySession.withTransaction {
insert(createVaultStateEntity(txn))
}
}
val dataSourceProperties = makeTestDataSourceProperties()
val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0"
database.transaction {
val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties())
val jdbcSession = configuration.jdbcSession()
val prepStatement = jdbcSession.prepareStatement(nativeQuery)
val rs = prepStatement.executeQuery()
assertTrue(rs.next())
assertEquals(rs.getString(1), txn.tx.inputs[0].txhash.toString())
assertEquals(rs.getInt(2), txn.tx.inputs[0].index)
}
}
private fun createVaultStateEntity(txn: SignedTransaction): VaultStatesEntity {
val txnState = txn.tx.inputs[0]
val state = VaultStatesEntity().apply {
txId = txnState.txhash.toString()
index = txnState.index
stateStatus = Vault.StateStatus.UNCONSUMED
contractStateClassName = DummyContract.SingleOwnerState::class.java.name
contractState = DummyContract.SingleOwnerState(owner = AnonymousParty(MEGA_CORP_PUBKEY)).serialize().bytes
notaryName = txn.tx.notary!!.name.toString()
recordedTime = Instant.now()
}
return state
}
private fun createCashBalance(): VaultCashBalancesEntity {
val cashBalanceEntity = VaultCashBalancesEntity()
cashBalanceEntity.currency = "GBP"
cashBalanceEntity.amount = 12345
return cashBalanceEntity
}
private fun newTransactionStorage() {
database.transaction {
transactionStorage = DBTransactionStorage()
}
}
private fun newRequeryStorage(dataSourceProperties: Properties) {
database.transaction {
val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties())
requerySession = configuration.sessionForModel(Models.VAULT)
}
}
private fun newTransaction(index: Int = 0): SignedTransaction {
val wtx = WireTransaction(
inputs = listOf(StateRef(SecureHash.randomSHA256(), index)),
attachments = emptyList(),
outputs = emptyList(),
commands = listOf(dummyCommand()),
notary = DUMMY_NOTARY,
timeWindow = null
)
return SignedTransaction(wtx, listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID))))
}
}

View File

@ -11,8 +11,10 @@ import net.corda.core.node.services.VaultService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.services.MockServiceHubInternal
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.vault.NodeVaultService
@ -71,7 +73,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
smmHasRemovedAllFlows = CountDownLatch(1)
calls = 0
val dataSourceProps = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
val databaseProperties = makeTestDatabaseProperties()
database = configureDatabase(dataSourceProps, databaseProperties, identitySvc = ::makeTestIdentityService)
val identityService = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate)
val kms = MockKeyManagementService(identityService, ALICE_KEY)
@ -88,7 +91,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
overrideClock = testClock,
keyManagement = kms,
network = mockMessagingService), TestReference {
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
override val vaultService: VaultService = NodeVaultService(this)
override val testReference = this@NodeSchedulerServiceTest
}
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)

View File

@ -59,7 +59,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() {
services = object : MockServices(BOB_KEY) {
override val vaultService: VaultService get() {
val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
val vaultService = NodeVaultService(this)
hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig)
return vaultService
}

View File

@ -7,14 +7,13 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.read
import net.corda.core.internal.readAll
import net.corda.testing.LogHelper
import net.corda.core.internal.write
import net.corda.core.internal.writeLines
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.node.utilities.configureDatabase
import net.corda.testing.LogHelper
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.node.makeTestIdentityService
@ -25,7 +24,6 @@ import java.nio.charset.Charset
import java.nio.file.FileAlreadyExistsException
import java.nio.file.FileSystem
import java.nio.file.Path
import java.util.*
import java.util.jar.JarEntry
import java.util.jar.JarOutputStream
import kotlin.test.assertEquals
@ -36,17 +34,13 @@ class NodeAttachmentStorageTest {
// Use an in memory file system for testing attachment storage.
lateinit var fs: FileSystem
lateinit var database: CordaPersistence
lateinit var dataSourceProperties: Properties
lateinit var configuration: RequeryConfiguration
@Before
fun setUp() {
LogHelper.setLevel(PersistentUniquenessProvider::class)
dataSourceProperties = makeTestDataSourceProperties()
val dataSourceProperties = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
fs = Jimfs.newFileSystem(Configuration.unix())
}
@ -61,7 +55,7 @@ class NodeAttachmentStorageTest {
val expectedHash = testJar.readAll().sha256()
database.transaction {
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
val storage = NodeAttachmentService(MetricRegistry())
val id = testJar.read { storage.importAttachment(it) }
assertEquals(expectedHash, id)
@ -87,7 +81,7 @@ class NodeAttachmentStorageTest {
fun `duplicates not allowed`() {
val testJar = makeTestJar()
database.transaction {
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
val storage = NodeAttachmentService(MetricRegistry())
testJar.read {
storage.importAttachment(it)
}
@ -102,19 +96,21 @@ class NodeAttachmentStorageTest {
@Test
fun `corrupt entry throws exception`() {
val testJar = makeTestJar()
val id =
database.transaction {
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
val storage = NodeAttachmentService(MetricRegistry())
val id = testJar.read { storage.importAttachment(it) }
// Corrupt the file in the store.
val bytes = testJar.readAll()
val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size)
val corruptAttachment = AttachmentEntity()
corruptAttachment.attId = id
corruptAttachment.content = bytes
storage.session.update(corruptAttachment)
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes)
DatabaseTransactionManager.current().session.merge(corruptAttachment)
id
}
database.transaction {
val storage = NodeAttachmentService(MetricRegistry())
val e = assertFailsWith<NodeAttachmentService.HashMismatchException> {
storage.openAttachment(id)!!.open().use { it.readBytes() }
}
@ -131,7 +127,7 @@ class NodeAttachmentStorageTest {
@Test
fun `non jar rejected`() {
database.transaction {
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
val storage = NodeAttachmentService(MetricRegistry())
val path = fs.getPath("notajar")
path.writeLines(listOf("Hey", "there!"))
path.read {

View File

@ -84,9 +84,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `states not local to instance`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
}
database.transaction {
val w1 = vaultQuery.queryBy<Cash.State>().states
assertThat(w1).hasSize(3)
@ -112,9 +112,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `states for refs`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
}
database.transaction {
val w1 = vaultQuery.queryBy<Cash.State>().states
assertThat(w1).hasSize(3)
@ -126,8 +126,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `states soft locking reserve and release`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
}
database.transaction {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(3)
@ -309,8 +310,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `unconsumedStatesForSpending exact amount`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
}
database.transaction {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1)
@ -327,10 +329,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `unconsumedStatesForSpending from two issuer parties`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)))
}
database.transaction {
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
spendableStatesUSD.forEach(::println)
@ -344,12 +346,12 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `unconsumedStatesForSpending from specific issuer party and refs`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), ref = OpaqueBytes.of(1))
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), ref = OpaqueBytes.of(2))
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3))
}
database.transaction {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(4)
@ -366,9 +368,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `unconsumedStatesForSpending insufficient amount`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
}
database.transaction {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1)
@ -383,9 +385,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `unconsumedStatesForSpending small amount`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L))
}
database.transaction {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(2)
@ -401,11 +403,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Test
fun `states soft locking query granularity`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
}
database.transaction {
var unlockedStates = 30
val allStates = vaultQuery.queryBy<Cash.State>().states
assertThat(allStates).hasSize(unlockedStates)

View File

@ -61,7 +61,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction {
// Fix the PRNG so that we get the same splits every time.
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER)
}
database.transaction {
val w = vaultQuery.queryBy<Cash.State>().states
assertEquals(3, w.size)
@ -77,25 +78,31 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
@Test
fun `issue and spend total correctly and irrelevant ignored`() {
val megaCorpServices = MockServices(MEGA_CORP_KEY)
val freshKey = services.keyManagementService.freshKey()
val usefulTX =
database.transaction {
// A tx that sends us money.
val usefulBuilder = TransactionBuilder(null)
Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY)
megaCorpServices.signInitialTransaction(usefulBuilder)
}
database.transaction {
// A tx that sends us money.
val freshKey = services.keyManagementService.freshKey()
val usefulBuilder = TransactionBuilder(null)
Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY)
val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder)
assertEquals(0.DOLLARS, services.getCashBalance(USD))
services.recordTransactions(usefulTX)
// A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
val spendTX = notaryServices.addSignature(spendPTX)
}
val spendTX =
database.transaction {
// A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
notaryServices.addSignature(spendPTX)
}
database.transaction {
assertEquals(100.DOLLARS, services.getCashBalance(USD))
}
database.transaction {
// A tx that doesn't send us anything.
val irrelevantBuilder = TransactionBuilder(DUMMY_NOTARY)
Cash().generateIssue(irrelevantBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY)
@ -104,12 +111,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
val irrelevantTX = notaryServices.addSignature(irrelevantPTX)
services.recordTransactions(irrelevantTX)
}
database.transaction {
assertEquals(100.DOLLARS, services.getCashBalance(USD))
}
database.transaction {
services.recordTransactions(spendTX)
}
database.transaction {
assertEquals(20.DOLLARS, services.getCashBalance(USD))
// TODO: Flesh out these tests as needed.
}
}
@ -123,7 +133,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L), ownedBy = AnonymousParty(freshKey),
issuedBy = MEGA_CORP.ref(1))
println("Cash balance: ${services.getCashBalance(USD)}")
}
database.transaction {
assertThat(vaultQuery.queryBy<Cash.State>().states).hasSize(10)
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
}
@ -234,22 +245,23 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
@Test
fun `sequencing LinearStates works`() {
database.transaction {
val freshKey = services.keyManagementService.freshKey()
val freshIdentity = AnonymousParty(freshKey)
val freshKey = services.keyManagementService.freshKey()
val freshIdentity = AnonymousParty(freshKey)
val linearId = UniqueIdentifier()
val linearId = UniqueIdentifier()
// Issue a linear state
val dummyIssue =
database.transaction { // Issue a linear state
val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY)
.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)))
.addCommand(dummyCommand(notaryServices.legalIdentityKey))
.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))).addCommand(dummyCommand(notaryServices.legalIdentityKey))
val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder)
val dummyIssue = services.addSignature(dummyIssuePtx)
dummyIssue.toLedgerTransaction(services).verify()
dummyIssue.toLedgerTransaction(services).verify()
services.recordTransactions(dummyIssue)
services.recordTransactions(dummyIssue)
dummyIssue
}
database.transaction {
assertThat(vaultQuery.queryBy<DummyLinearContract.State>().states).hasSize(1)
// Move the same state
@ -263,6 +275,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
dummyIssue.toLedgerTransaction(services).verify()
services.recordTransactions(dummyMove)
}
database.transaction {
assertThat(vaultQuery.queryBy<DummyLinearContract.State>().states).hasSize(1)
}
}
@ -275,10 +289,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = AnonymousParty(freshKey))
services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L))
services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
}
database.transaction {
val cash = vaultQuery.queryBy<Cash.State>().states
cash.forEach { println(it.state.data.amount) }
}
database.transaction {
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
}
database.transaction {
val deals = vaultQuery.queryBy<DummyDealContract.State>().states
deals.forEach { println(it.state.data.linearId.externalId!!) }
}
@ -290,7 +309,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
val spendTX = services.addSignature(spendPTX, freshKey)
services.recordTransactions(spendTX)
}
database.transaction {
val consumedStates = vaultQuery.queryBy<ContractState>(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states
assertEquals(3, consumedStates.count())
@ -300,17 +320,21 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
}
@Test
fun `consuming multiple contract state types in same transaction`() {
fun `consuming multiple contract state types`() {
val freshKey = services.keyManagementService.freshKey()
val freshIdentity = AnonymousParty(freshKey)
database.transaction {
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
val deals = vaultQuery.queryBy<DummyDealContract.State>().states
deals.forEach { println(it.state.data.linearId.externalId!!) }
}
val deals =
database.transaction {
vaultQuery.queryBy<DummyDealContract.State>().states
}
database.transaction {
services.fillWithSomeTestLinearStates(3)
}
database.transaction {
val linearStates = vaultQuery.queryBy<DummyLinearContract.State>().states
linearStates.forEach { println(it.state.data.linearId) }
@ -324,10 +348,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
}
val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder)
dummyMove.toLedgerTransaction(services).verify()
services.recordTransactions(dummyMove)
}
database.transaction {
val consumedStates = vaultQuery.queryBy<ContractState>(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states
assertEquals(2, consumedStates.count())

View File

@ -6,7 +6,6 @@ include 'finance:isolated'
include 'core'
include 'docs'
include 'node-api'
include 'node-schemas'
include 'node'
include 'node:capsule'
include 'client:jackson'

View File

@ -87,8 +87,8 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
lateinit var hibernatePersister: HibernateObserver
fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultService {
val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
fun makeVaultService(hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), { identityService })): VaultService {
val vaultService = NodeVaultService(this)
hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig)
return vaultService
}
@ -227,7 +227,7 @@ fun makeTestDatabaseAndMockServices(customSchemas: Set<MappedSchema> = setOf(Com
val mockService = database.transaction {
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identitySvc = identitySvc)
object : MockServices(*(keys.toTypedArray())) {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override val vaultService: VaultService = makeVaultService(hibernateConfig)
override fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>) {
for (stx in txs) {