diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt index 583207edcd..38140b59c6 100644 --- a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt @@ -23,6 +23,7 @@ import com.r3.corda.enterprise.perftestcordapp.utils.sumCash import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrNull import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection +import net.corda.core.crypto.toStringShort import java.math.BigInteger import java.security.PublicKey import java.util.* @@ -81,7 +82,7 @@ class Cash : OnLedgerAsset() { owner = this.owner, pennies = this.amount.quantity, currency = this.amount.token.product.currencyCode, - issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), + issuerPartyHash = this.amount.token.issuer.party.owningKey.toStringShort(), issuerRef = this.amount.token.issuer.reference.bytes ) /** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ @@ -341,10 +342,12 @@ class Cash : OnLedgerAsset() { // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. +/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ +val DUMMY_CASH_ISSUER_NAME = CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB") /** A randomly generated key. */ val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_CASH_ISSUER by lazy { Party(CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB"), DUMMY_CASH_ISSUER_KEY.public).ref(1) } +val DUMMY_CASH_ISSUER by lazy { Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public).ref(1) } /** An extension property that lets you write 100.DOLLARS.CASH */ val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY) /** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */ diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt index 4cd869572b..b49ef42297 100644 --- a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt @@ -27,7 +27,9 @@ import kotlin.concurrent.withLock * Custom implementations must implement this interface and declare their implementation in * META-INF/services/net.corda.contracts.asset.CashSelection */ -abstract class AbstractCashSelection { +// TODO: make parameters configurable when we get CorDapp configuration. +abstract class AbstractCashSelection(private val maxRetries: Int = 8, private val retrySleep: Int = 100, + private val retryCap: Int = 2000) { companion object { val instance = AtomicReference() @@ -44,14 +46,10 @@ abstract class AbstractCashSelection { }.invoke() } - val log = loggerFor() + private val log = contextLogger() } // coin selection retry loop counter, sleep (msecs) and lock for selecting states - // TODO: make parameters configurable when we get CorDapp configuration. - private val MAX_RETRIES = 8 - private val RETRY_SLEEP = 100 - private val RETRY_CAP = 2000 private val spendLock: ReentrantLock = ReentrantLock() /** @@ -64,7 +62,7 @@ abstract class AbstractCashSelection { /** * A vendor specific query(ies) to gather Cash states that are available. - * @param statement The service hub to allow access to the database session + * @param connection The service hub to allow access to the database session * @param amount The amount of currency desired (ignoring issues, but specifying the currency) * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. @@ -72,13 +70,14 @@ abstract class AbstractCashSelection { * with this notary are included. * @param onlyFromIssuerParties Optional issuer parties to match against. * @param withIssuerRefs Optional issuer references to match against. - * @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked, + * @param withResultSet Function that contains the business logic. The JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked, * otherwise what is available is returned unlocked for informational purposes. + * @return The result of the withResultSet function */ abstract fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, - onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet + onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean - override abstract fun toString() : String + override abstract fun toString(): String /** * Query to gather Cash states that are available and retry if they are temporarily unavailable. @@ -103,13 +102,13 @@ abstract class AbstractCashSelection { withIssuerRefs: Set = emptySet()): List> { val stateAndRefs = mutableListOf>() - for (retryCount in 1..MAX_RETRIES) { + for (retryCount in 1..maxRetries) { if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { log.warn("Coin selection failed on attempt $retryCount") // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { + if (retryCount != maxRetries) { stateAndRefs.clear() - val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt() + val durationMillis = (minOf(retrySleep.shl(retryCount), retryCap / 2) * (1.0 + Math.random())).toInt() FlowLogic.sleep(durationMillis.millis) } else { log.warn("Insufficient spendable states identified for $amount") @@ -127,34 +126,40 @@ abstract class AbstractCashSelection { try { // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) // the softLockReserve update will detect whether we try to lock states locked by others - val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) - stateAndRefs.clear() + return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs -> + stateAndRefs.clear() - var totalPennies = 0L - val stateRefs = mutableSetOf() - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val pennies = rs.getLong(3) - totalPennies = rs.getLong(4) - val rowLockId = rs.getString(5) - stateRefs.add(StateRef(txHash, index)) - log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" } + var totalPennies = 0L + val stateRefs = mutableSetOf() + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val pennies = rs.getLong(3) + totalPennies = rs.getLong(4) + val rowLockId = rs.getString(5) + stateRefs.add(StateRef(txHash, index)) + log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" } + } + + if (stateRefs.isNotEmpty()) { + // TODO: future implementation to retrieve contract states from a Vault BLOB store + stateAndRefs.addAll(services.loadStates(stateRefs) as Collection>) + } + + val success = stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity + if (success) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + } else { + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + } + success } - if (stateRefs.isNotEmpty()) - // TODO: future implementation to retrieve contract states from a Vault BLOB store - stateAndRefs.addAll(services.loadStates(stateRefs) as Collection>) - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return true - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") // retry as more states may become available } catch (e: SQLException) { log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt index cfb5163b30..f94ae307eb 100644 --- a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -1,6 +1,7 @@ package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection import net.corda.core.contracts.Amount +import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.utilities.* @@ -13,7 +14,7 @@ class CashSelectionH2Impl : AbstractCashSelection() { companion object { const val JDBC_DRIVER_NAME = "H2 JDBC Driver" - val log = loggerFor() + private val log = contextLogger() } override fun isCompatible(metadata: DatabaseMetaData): Boolean { @@ -22,16 +23,14 @@ class CashSelectionH2Impl : AbstractCashSelection() { override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the // running total of such an accumulator // 2) H2 uses session variables to perform this accumulator function: // http://www.h2database.com/html/functions.html#set // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, - onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet { - connection.createStatement().execute("CALL SET(@t, 0);") + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { + connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") } val selectJoin = """ SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id @@ -44,24 +43,27 @@ class CashSelectionH2Impl : AbstractCashSelection() { (if (notary != null) " AND vs.notary_name = ?" else "") + (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN (?)" else "") + + " AND ccs.issuer_key_hash IN (?)" else "") + (if (withIssuerRefs.isNotEmpty()) " AND ccs.issuer_ref IN (?)" else "") // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) - val psSelectJoin = connection.prepareStatement(selectJoin) - var pIndex = 0 - psSelectJoin.setString(++pIndex, amount.token.currencyCode) - psSelectJoin.setLong(++pIndex, amount.quantity) - psSelectJoin.setString(++pIndex, lockId.toString()) - if (notary != null) - psSelectJoin.setString(++pIndex, notary.name.toString()) - if (onlyFromIssuerParties.isNotEmpty()) - psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) - if (withIssuerRefs.isNotEmpty()) - psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) - log.debug { psSelectJoin.toString() } + connection.prepareStatement(selectJoin).use { psSelectJoin -> + var pIndex = 0 + psSelectJoin.setString(++pIndex, amount.token.currencyCode) + psSelectJoin.setLong(++pIndex, amount.quantity) + psSelectJoin.setString(++pIndex, lockId.toString()) + if (notary != null) + psSelectJoin.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray()) + if (withIssuerRefs.isNotEmpty()) + psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray()) + log.debug { psSelectJoin.toString() } - return psSelectJoin.executeQuery() + psSelectJoin.executeQuery().use { rs -> + return withResultSet(rs) + } + } } } \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt new file mode 100644 index 0000000000..dfe0ba7681 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -0,0 +1,27 @@ +package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection + +import net.corda.core.contracts.Amount +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.ResultSet +import java.util.* + +class CashSelectionMySQLImpl : AbstractCashSelection() { + + companion object { + const val JDBC_DRIVER_NAME = "MySQL JDBC Driver" + } + + override fun isCompatible(metadata: DatabaseMetaData): Boolean { + return metadata.driverName == JDBC_DRIVER_NAME + } + + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { + TODO("MySQL cash selection not implemented") + } + + override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}" +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt new file mode 100644 index 0000000000..e94ab56c0d --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt @@ -0,0 +1,81 @@ +package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection + +import net.corda.core.contracts.Amount +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.utilities.* +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.ResultSet +import java.util.* + +class CashSelectionPostgreSQLImpl : AbstractCashSelection() { + + companion object { + val JDBC_DRIVER_NAME = "PostgreSQL JDBC Driver" + private val log = contextLogger() + } + + override fun isCompatible(metadata: DatabaseMetaData): Boolean { + return metadata.driverName == JDBC_DRIVER_NAME + } + + override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" + + // This is using PostgreSQL window functions for selecting a minimum set of rows that match a request amount of coins: + // 1) This may also be possible with user-defined functions (e.g. using PL/pgSQL) + // 2) The window function accumulated column (`total`) does not include the current row (starts from 0) and cannot + // appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query. + // 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { + val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies, + nested.total+nested.pennies as total_pennies, nested.lock_id + FROM + (SELECT vs.transaction_id, vs.output_index, ccs.pennies, + coalesce((SUM(ccs.pennies) OVER (PARTITION BY 1 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)), 0) + AS total, vs.lock_id + FROM vault_states AS vs, contract_cash_states AS ccs + WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index + AND vs.state_status = 0 + AND ccs.ccy_code = ? + AND (vs.lock_id = ? OR vs.lock_id is null) + """ + + (if (notary != null) + " AND vs.notary_name = ?" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key = ANY (?)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref = ANY (?)" else "") + + """) + nested WHERE nested.total < ? + """ + + connection.prepareStatement(selectJoin).use { statement -> + statement.setString(1, amount.token.toString()) + statement.setString(2, lockId.toString()) + var paramOffset = 0 + if (notary != null) { + statement.setString(3, notary.name.toString()) + paramOffset += 1 + } + if (onlyFromIssuerParties.isNotEmpty()) { + val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map + { it.owningKey.toBase58String() }.toTypedArray()) + statement.setArray(3 + paramOffset, issuerKeys) + paramOffset += 1 + } + if (withIssuerRefs.isNotEmpty()) { + val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map + { it.bytes }.toTypedArray()) + statement.setArray(3 + paramOffset, issuerRefs) + paramOffset += 1 + } + statement.setLong(3 + paramOffset, amount.quantity) + log.debug { statement.toString() } + + statement.executeQuery().use { rs -> + return withResultSet(rs) + } + } + } +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt new file mode 100644 index 0000000000..927637c306 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt @@ -0,0 +1,72 @@ +package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection + +import net.corda.core.contracts.Amount +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.toBase58String +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.ResultSet +import java.util.* + +/** + * SQL Server / SQL Azure + */ +class CashSelectionSQLServerImpl : AbstractCashSelection(maxRetries = 16, retrySleep = 1000, retryCap = 5000) { + + companion object { + val JDBC_DRIVER_NAME = "Microsoft JDBC Driver 6.2 for SQL Server" + private val log = contextLogger() + } + + override fun isCompatible(metadata: DatabaseMetaData): Boolean { + return metadata.driverName == JDBC_DRIVER_NAME + } + + override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" + + override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { + + val selectJoin = """ + WITH row(transaction_id, output_index, pennies, total, lock_id) AS + ( + SELECT vs.transaction_id, vs.output_index, ccs.pennies, + SUM(ccs.pennies) OVER (ORDER BY ccs.transaction_id RANGE UNBOUNDED PRECEDING), vs.lock_id + FROM contract_pt_cash_states AS ccs, vault_states AS vs + WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index + AND vs.state_status = 0 + AND ccs.ccy_code = ? + AND (vs.lock_id = ? OR vs.lock_id is null)""" + + (if (notary != null) + " AND vs.notary_name = ?" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN (?)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN (?)" else "") + + """) + SELECT row.transaction_id, row.output_index, row.pennies, row.total, row.lock_id + FROM row where row.total <= ? + row.pennies""" + + // Use prepared statement for protection against SQL Injection + connection.prepareStatement(selectJoin).use { statement -> + var pIndex = 0 + statement.setString(++pIndex, amount.token.currencyCode) + statement.setString(++pIndex, lockId.toString()) + if (notary != null) + statement.setString(++pIndex, notary.name.toString()) + if (onlyFromIssuerParties.isNotEmpty()) + statement.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any }.toTypedArray()) + if (withIssuerRefs.isNotEmpty()) + statement.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray()) + statement.setLong(++pIndex, amount.quantity) + log.debug(selectJoin) + + statement.executeQuery().use { rs -> + return withResultSet(rs) + } + } + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt index 9ec6f97ddf..2aabf08c27 100644 --- a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt @@ -5,10 +5,10 @@ import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Index -import javax.persistence.Table +import net.corda.core.utilities.MAX_HASH_HEX_SIZE +import net.corda.core.contracts.MAX_ISSUER_REF_SIZE +import org.hibernate.annotations.Type +import javax.persistence.* /** * An object used to fully qualify the [CashSchema] family name (i.e. independent of version). @@ -36,10 +36,11 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version @Column(name = "ccy_code", length = 3) var currency: String, - @Column(name = "issuer_key") - var issuerParty: String, + @Column(name = "issuer_key_hash", length = MAX_HASH_HEX_SIZE) + var issuerPartyHash: String, - @Column(name = "issuer_ref") + @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE) + @Type(type = "corda-wrapper-binary") var issuerRef: ByteArray ) : PersistentState() } diff --git a/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection index c9ce8a6ee9..1108a0b2fe 100644 --- a/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection +++ b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection @@ -1,2 +1,4 @@ com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl - +com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionMySQLImpl +com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionPostgreSQLImpl +com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionSQLServerImpl diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt index 9ff77dfe6d..27898fefe7 100644 --- a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt @@ -226,7 +226,7 @@ class CommercialPaperTestsGeneric { aliceVaultService = aliceServices.vaultService databaseAlice.transaction { - alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER) + alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, issuedBy = DUMMY_CASH_ISSUER) aliceVaultService = aliceServices.vaultService } @@ -239,7 +239,7 @@ class CommercialPaperTestsGeneric { bigCorpVaultService = bigCorpServices.vaultService databaseBigCorp.transaction { - bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER) + bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, issuedBy = DUMMY_CASH_ISSUER) bigCorpVaultService = bigCorpServices.vaultService } diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt index 3d40f70dad..cf4c86d5d2 100644 --- a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt @@ -1,6 +1,8 @@ package com.r3.corda.enterprise.perftestcordapp.contracts.asset +import com.nhaarman.mockito_kotlin.argThat +import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever import com.r3.corda.enterprise.perftestcordapp.* import com.r3.corda.enterprise.perftestcordapp.utils.sumCash @@ -18,7 +20,6 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.queryBy -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes @@ -27,15 +28,14 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.DummyState -import net.corda.testing.contracts.VaultFiller.Companion.calculateRandomlySizedAmounts import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestIdentityService import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mockito.doReturn -import java.security.KeyPair import java.util.* import kotlin.test.* @@ -53,37 +53,29 @@ import kotlin.test.* fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, issuerServices: ServiceHub = this, outputNotary: Party = DUMMY_NOTARY, - atLeastThisManyStates: Int = 3, - atMostThisManyStates: Int = 10, - rng: Random = Random(), ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), ownedBy: AbstractParty? = null, issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault { - val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) - val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey val anonParty = AnonymousParty(myKey) // We will allocate one state to one transaction, for simplicities sake. val cash = Cash() - val transactions: List = amounts.map { pennies -> - val issuance = TransactionBuilder(null as Party?) - cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary) + val issuance = TransactionBuilder(null as Party?) + cash.generateIssue(issuance, Amount(howMuch.quantity, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary) - return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey) - } - - recordTransactions(transactions) + val transaction = issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey) + recordTransactions(listOf(transaction)) // Get all the StateRefs of all the generated transactions. - val states = transactions.flatMap { stx -> - stx.tx.outputs.indices.map { i -> stx.tx.outRef(i) } - } - + val states = transaction.tx.outputs.indices.map { i -> transaction.tx.outRef(i) } return Vault(states) } class CashTests { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() private val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) private val defaultIssuer = MEGA_CORP.ref(defaultRef) private val inState = Cash.State( @@ -98,38 +90,59 @@ class CashTests { amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) + private lateinit var ourServices: MockServices private lateinit var miniCorpServices: MockServices private lateinit var megaCorpServices: MockServices val vault: VaultService get() = miniCorpServices.vaultService lateinit var database: CordaPersistence private lateinit var vaultStatesUnconsumed: List> + private lateinit var ourIdentity: AbstractParty + private lateinit var miniCorpAnonymised: AnonymousParty + private val CHARLIE_ANONYMISED = CHARLIE_IDENTITY.party.anonymise() + + private lateinit var WALLET: List> + @Before - fun setUp() = withTestSerialization { + fun setUp() { LogHelper.setLevel(NodeVaultService::class) - megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset","com.r3.corda.enterprise.perftestcordapp.schemas"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + miniCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock().also { + doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name }) + }, MINI_CORP.name, MINI_CORP_KEY) + val notaryServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val databaseAndServices = makeTestDatabaseAndMockServices( - listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY), - makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"), - CordaX500Name("Me", "London", "GB")) + cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), + initialIdentityName = CordaX500Name(organisation = "Me", locality = "London", country = "GB"), + keys = listOf(generateKeyPair()), + identityService = makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY))) database = databaseAndServices.first - miniCorpServices = databaseAndServices.second + ourServices = databaseAndServices.second + + // Set up and register identities + ourIdentity = ourServices.myInfo.singleIdentity() + miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise() + (miniCorpServices.myInfo.legalIdentitiesAndCerts + megaCorpServices.myInfo.legalIdentitiesAndCerts + notaryServices.myInfo.legalIdentitiesAndCerts).forEach { identity -> + ourServices.identityService.verifyAndRegisterIdentity(identity) + } // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved. database.transaction { - miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices) - miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices) - miniCorpServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - 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) + ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 100.DOLLARS) + ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 400.DOLLARS) + ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.DOLLARS) + ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.SWISS_FRANCS) } + database.transaction { - vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy().states + vaultStatesUnconsumed = ourServices.vaultService.queryBy().states } + WALLET = listOf( + makeCash(100.DOLLARS, MEGA_CORP), + makeCash(400.DOLLARS, MEGA_CORP), + makeCash(80.DOLLARS, MINI_CORP), + makeCash(80.SWISS_FRANCS, MINI_CORP, 2) + ) } @After @@ -138,11 +151,10 @@ class CashTests { } @Test - fun trivial() = withTestSerialization { + fun trivial() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) - tweak { output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer)) command(ALICE_PUBKEY, Cash.Commands.Move()) @@ -172,25 +184,22 @@ class CashTests { this.verifies() } } - Unit } @Test - fun `issue by move`() = withTestSerialization { + fun `issue by move`() { // Check we can't "move" money into existence. transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, DummyState()) output(Cash.PROGRAM_ID, outState) command(MINI_CORP_PUBKEY, Cash.Commands.Move()) - this `fails with` "there is at least one cash input for this group" } - Unit } @Test - fun issue() = withTestSerialization { + fun issue() { // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised // institution is allowed to issue as much cash as they want. transaction { @@ -204,17 +213,14 @@ class CashTests { output(Cash.PROGRAM_ID, Cash.State( amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), - owner = AnonymousParty(ALICE_PUBKEY) - ) - ) + owner = AnonymousParty(ALICE_PUBKEY))) command(MINI_CORP_PUBKEY, Cash.Commands.Issue()) this.verifies() } - Unit } @Test - fun generateIssueRaw() = withTestSerialization { + fun generateIssueRaw() { // Test generation works. val tx: WireTransaction = TransactionBuilder(notary = null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY) @@ -229,7 +235,7 @@ class CashTests { } @Test - fun generateIssueFromAmount() = withTestSerialization { + fun generateIssueFromAmount() { // Test issuance from an issued amount val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val tx: WireTransaction = TransactionBuilder(notary = null).apply { @@ -240,13 +246,12 @@ class CashTests { } @Test - fun `extended issue examples`() = withTestSerialization { + fun `extended issue examples`() { // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer. transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, issuerInState) output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2)) - // Move fails: not allowed to summon money. tweak { command(ALICE_PUBKEY, Cash.Commands.Move()) @@ -290,7 +295,6 @@ class CashTests { } this.verifies() } - Unit } /** @@ -298,7 +302,7 @@ class CashTests { * cash inputs. */ @Test(expected = IllegalStateException::class) - fun `reject issuance with inputs`() = withTestSerialization { + fun `reject issuance with inputs`() { // Issue some cash var ptx = TransactionBuilder(DUMMY_NOTARY) @@ -309,11 +313,10 @@ class CashTests { ptx = TransactionBuilder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef(0)) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) - Unit } @Test - fun testMergeSplit() = withTestSerialization { + fun testMergeSplit() { // Splitting value works. transaction { attachment(Cash.PROGRAM_ID) @@ -340,11 +343,10 @@ class CashTests { this.verifies() } } - Unit } @Test - fun zeroSizedValues() = withTestSerialization { + fun zeroSizedValues() { transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) @@ -360,11 +362,10 @@ class CashTests { command(ALICE_PUBKEY, Cash.Commands.Move()) this `fails with` "zero sized outputs" } - Unit } @Test - fun trivialMismatches() = withTestSerialization { + fun trivialMismatches() { // Can't change issuer. transaction { attachment(Cash.PROGRAM_ID) @@ -387,7 +388,7 @@ class CashTests { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, inState) output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer)) - output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer)) + output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer)) command(ALICE_PUBKEY, Cash.Commands.Move()) this `fails with` "the amounts balance" } @@ -397,9 +398,7 @@ class CashTests { input(Cash.PROGRAM_ID, inState.copy( amount = 150.POUNDS `issued by` defaultIssuer, - owner = AnonymousParty(BOB_PUBKEY) - ) - ) + owner = AnonymousParty(BOB_PUBKEY))) output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer)) command(ALICE_PUBKEY, Cash.Commands.Move()) this `fails with` "the amounts balance" @@ -422,17 +421,15 @@ class CashTests { command(ALICE_PUBKEY, Cash.Commands.Move()) this `fails with` "for reference [01]" } - Unit } @Test - fun exitLedger() = withTestSerialization { + fun exitLedger() { // Single input/output straightforward case. transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, issuerInState) output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) - tweak { command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer)) command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) @@ -449,35 +446,28 @@ class CashTests { } } } - Unit } @Test - fun `exit ledger with multiple issuers`() = withTestSerialization { + fun `exit ledger with multiple issuers`() { // Multi-issuer case. transaction { attachment(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, issuerInState) input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP) - output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP) output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) - command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move()) - this `fails with` "the amounts balance" - command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) this `fails with` "the amounts balance" - command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef))) this.verifies() } - Unit } @Test - fun `exit cash not held by its issuer`() = withTestSerialization { + fun `exit cash not held by its issuer`() { // Single input/output straightforward case. transaction { attachment(Cash.PROGRAM_ID) @@ -487,18 +477,16 @@ class CashTests { command(ALICE_PUBKEY, Cash.Commands.Move()) this `fails with` "the amounts balance" } - Unit } @Test - fun multiIssuer() = withTestSerialization { + fun multiIssuer() { transaction { attachment(Cash.PROGRAM_ID) // Gather 2000 dollars from two different issuers. input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP) command(ALICE_PUBKEY, Cash.Commands.Move()) - // Can't merge them together. tweak { output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer)) @@ -516,11 +504,10 @@ class CashTests { output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP) this.verifies() } - Unit } @Test - fun multiCurrency() = withTestSerialization { + fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { attachment(Cash.PROGRAM_ID) @@ -530,36 +517,20 @@ class CashTests { output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY)) output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY)) command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move()) - this.verifies() } - Unit } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Spend tx generation - private val OUR_KEY: KeyPair by lazy { generateKeyPair() } - private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) - private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public) - - private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) - private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) - private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) - private val WALLET = listOf( - makeCash(100.DOLLARS, MEGA_CORP), - makeCash(400.DOLLARS, MEGA_CORP), - makeCash(80.DOLLARS, MINI_CORP), - makeCash(80.SWISS_FRANCS, MINI_CORP, 2) - ) - /** * Generate an exit transaction, removing some amount of cash from the ledger. */ @@ -570,19 +541,21 @@ class CashTests { return tx.toWireTransaction(serviceHub) } - private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { + private fun makeSpend(services: ServiceHub, amount: Amount, dest: AbstractParty): WireTransaction { + val ourIdentity = services.myInfo.singleIdentityAndCert() + val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false) val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest) + Cash.generateSpend(services, tx, amount, changeIdentity, dest) } - return tx.toWireTransaction(miniCorpServices) + return tx.toWireTransaction(services) } /** * Try exiting an amount which matches a single state. */ @Test - fun generateSimpleExit() = withTestSerialization { + fun generateSimpleExit() { val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(0, wtx.outputs.size) @@ -597,7 +570,7 @@ class CashTests { * Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly. */ @Test - fun generatePartialExit() = withTestSerialization { + fun generatePartialExit() { val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1) val actualInput = wtx.inputs.single() // Filter the available inputs and confirm exactly one has been used @@ -614,80 +587,76 @@ class CashTests { * Try exiting a currency we don't have. */ @Test - fun generateAbsentExit() = withTestSerialization { + fun generateAbsentExit() { assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } - Unit } /** * Try exiting with a reference mis-match. */ @Test - fun generateInvalidReferenceExit() = withTestSerialization { + fun generateInvalidReferenceExit() { assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } - Unit } /** * Try exiting an amount greater than the maximum available. */ @Test - fun generateInsufficientExit() = withTestSerialization { + fun generateInsufficientExit() { assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } - Unit } /** * Try exiting for an owner with no states */ @Test - fun generateOwnerWithNoStatesExit() = withTestSerialization { + fun generateOwnerWithNoStatesExit() { assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } - Unit } /** * Try exiting when vault is empty */ @Test - fun generateExitWithEmptyVault() = withTestSerialization { + fun generateExitWithEmptyVault() { assertFailsWith { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1) + Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), ourIdentity) } - Unit } @Test - fun generateSimpleDirectSpend() = withTestSerialization { + fun generateSimpleDirectSpend() { val wtx = database.transaction { - makeSpend(100.DOLLARS, THEIR_IDENTITY_1) + makeSpend(ourServices, 100.DOLLARS, miniCorpAnonymised) } database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) - assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0)) - assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised), wtx.getOutput(0)) + assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @Test - fun generateSimpleSpendWithParties() = withTestSerialization { + fun generateSimpleSpendWithParties() { + val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false) database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(ourServices, tx, 80.DOLLARS, changeIdentity, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } } @Test - fun generateSimpleSpendWithChange() = withTestSerialization { + fun generateSimpleSpendWithChange() { val wtx = database.transaction { - makeSpend(10.DOLLARS, THEIR_IDENTITY_1) + makeSpend(ourServices, 10.DOLLARS, miniCorpAnonymised) } database.transaction { val vaultState = vaultStatesUnconsumed.elementAt(0) @@ -700,35 +669,35 @@ class CashTests { } } val changeOwner = (likelyChangeState as Cash.State).owner - assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size) + assertEquals(1, ourServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size) assertEquals(vaultState.ref, wtx.inputs[0]) - assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) assertEquals(vaultState.state.data.copy(amount = changeAmount, owner = changeOwner), wtx.outputs[1].data) - assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @Test - fun generateSpendWithTwoInputs() = withTestSerialization { + fun generateSpendWithTwoInputs() { val wtx = database.transaction { - makeSpend(500.DOLLARS, THEIR_IDENTITY_1) + makeSpend(ourServices, 500.DOLLARS, miniCorpAnonymised) } database.transaction { val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) - assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0)) - assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0)) + assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @Test - fun generateSpendMixedDeposits() = withTestSerialization { + fun generateSpendMixedDeposits() { val wtx = database.transaction { - val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) + val wtx = makeSpend(ourServices, 580.DOLLARS, miniCorpAnonymised) assertEquals(3, wtx.inputs.size) wtx } @@ -739,26 +708,25 @@ class CashTests { assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState2.ref, wtx.inputs[2]) - assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) - assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data) - assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) + assertEquals(vaultState2.state.data.copy(owner = miniCorpAnonymised), wtx.outputs[0].data) + assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @Test - fun generateSpendInsufficientBalance() = withTestSerialization { + fun generateSpendInsufficientBalance() { database.transaction { val e: InsufficientBalanceException = assertFailsWith("balance") { - makeSpend(1000.DOLLARS, THEIR_IDENTITY_1) + makeSpend(ourServices, 1000.DOLLARS, miniCorpAnonymised) } assertEquals((1000 - 580).DOLLARS, e.amountMissing) assertFailsWith(InsufficientBalanceException::class) { - makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1) + makeSpend(ourServices, 81.SWISS_FRANCS, miniCorpAnonymised) } } - Unit } /** @@ -846,7 +814,7 @@ class CashTests { // Double spend. @Test - fun chainCashDoubleSpendFailsWith() = withTestSerialization { + fun chainCashDoubleSpendFailsWith() { val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) }, MEGA_CORP.name, MEGA_CORP_KEY) @@ -856,9 +824,7 @@ class CashTests { output(Cash.PROGRAM_ID, "MEGA_CORP cash", Cash.State( amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP - ) - ) + owner = MEGA_CORP)) } transaction { @@ -883,20 +849,20 @@ class CashTests { this.verifies() } - Unit } @Test - fun multiSpend() = withTestSerialization { + fun multiSpend() { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { + val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false) val payments = listOf( - PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS), - PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS) + PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), + PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS) ) - Cash.generateSpend(miniCorpServices, tx, payments) + Cash.generateSpend(ourServices, tx, payments, changeIdentity) } - val wtx = tx.toWireTransaction(miniCorpServices) + val wtx = tx.toWireTransaction(ourServices) fun out(i: Int) = wtx.getOutput(i) as Cash.State assertEquals(4, wtx.outputs.size) assertEquals(80.DOLLARS, out(0).amount.withoutIssuer()) diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt index c5005a29b3..6e6861320b 100644 --- a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt @@ -174,8 +174,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { bobNode.internals.disableDBCloseOnStop() val cashStates = bobNode.database.transaction { - bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3, - issuedBy = issuer) + bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction {