SQL coin selection (#186)

* Copy SQL server cash selection and all other required changes from finance to perftestcordapp

* Copy SQL server cash selection and all other required changes from finance to perftestcordapp

* Add cash selection to manifest
This commit is contained in:
Christian Sailer 2017-12-14 11:56:48 +00:00 committed by GitHub
parent 2512b29b26
commit 205663d37f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 375 additions and 217 deletions

View File

@ -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.sumCashOrNull
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
import net.corda.core.crypto.toStringShort
import java.math.BigInteger import java.math.BigInteger
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -81,7 +82,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
owner = this.owner, owner = this.owner,
pennies = this.amount.quantity, pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode, 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 issuerRef = this.amount.token.issuer.reference.bytes
) )
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ /** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
@ -341,10 +342,12 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
// Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. // 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. */ /** A randomly generated key. */
val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) }
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ /** 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 */ /** An extension property that lets you write 100.DOLLARS.CASH */
val Amount<Currency>.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY) val Amount<Currency>.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] */ /** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */

View File

@ -27,7 +27,9 @@ import kotlin.concurrent.withLock
* Custom implementations must implement this interface and declare their implementation in * Custom implementations must implement this interface and declare their implementation in
* META-INF/services/net.corda.contracts.asset.CashSelection * 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 { companion object {
val instance = AtomicReference<AbstractCashSelection>() val instance = AtomicReference<AbstractCashSelection>()
@ -44,14 +46,10 @@ abstract class AbstractCashSelection {
}.invoke() }.invoke()
} }
val log = loggerFor<AbstractCashSelection>() private val log = contextLogger()
} }
// coin selection retry loop counter, sleep (msecs) and lock for selecting states // 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() private val spendLock: ReentrantLock = ReentrantLock()
/** /**
@ -64,7 +62,7 @@ abstract class AbstractCashSelection {
/** /**
* A vendor specific query(ies) to gather Cash states that are available. * 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 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. * @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. * 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. * with this notary are included.
* @param onlyFromIssuerParties Optional issuer parties to match against. * @param onlyFromIssuerParties Optional issuer parties to match against.
* @param withIssuerRefs Optional issuer references 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. * otherwise what is available is returned unlocked for informational purposes.
* @return The result of the withResultSet function
*/ */
abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, 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. * Query to gather Cash states that are available and retry if they are temporarily unavailable.
@ -103,13 +102,13 @@ abstract class AbstractCashSelection {
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> { withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>() val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
for (retryCount in 1..MAX_RETRIES) { for (retryCount in 1..maxRetries) {
if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) { if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) {
log.warn("Coin selection failed on attempt $retryCount") log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending. // TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) { if (retryCount != maxRetries) {
stateAndRefs.clear() 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) FlowLogic.sleep(durationMillis.millis)
} else { } else {
log.warn("Insufficient spendable states identified for $amount") log.warn("Insufficient spendable states identified for $amount")
@ -127,34 +126,40 @@ abstract class AbstractCashSelection {
try { try {
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) // 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 // the softLockReserve update will detect whether we try to lock states locked by others
val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs ->
stateAndRefs.clear() stateAndRefs.clear()
var totalPennies = 0L var totalPennies = 0L
val stateRefs = mutableSetOf<StateRef>() val stateRefs = mutableSetOf<StateRef>()
while (rs.next()) { while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1)) val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2) val index = rs.getInt(2)
val pennies = rs.getLong(3) val pennies = rs.getLong(3)
totalPennies = rs.getLong(4) totalPennies = rs.getLong(4)
val rowLockId = rs.getString(5) val rowLockId = rs.getString(5)
stateRefs.add(StateRef(txHash, index)) stateRefs.add(StateRef(txHash, index))
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" } 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<StateAndRef<Cash.State>>)
}
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<StateAndRef<Cash.State>>)
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 // retry as more states may become available
} catch (e: SQLException) { } catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]

View File

@ -1,6 +1,7 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.utilities.* import net.corda.core.utilities.*
@ -13,7 +14,7 @@ class CashSelectionH2Impl : AbstractCashSelection() {
companion object { companion object {
const val JDBC_DRIVER_NAME = "H2 JDBC Driver" const val JDBC_DRIVER_NAME = "H2 JDBC Driver"
val log = loggerFor<CashSelectionH2Impl>() private val log = contextLogger()
} }
override fun isCompatible(metadata: DatabaseMetaData): Boolean { override fun isCompatible(metadata: DatabaseMetaData): Boolean {
@ -22,16 +23,14 @@ class CashSelectionH2Impl : AbstractCashSelection() {
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME" 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: // 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 // 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 // running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function: // 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set // 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) // 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<Currency>, lockId: UUID, notary: Party?, override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet { connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
connection.createStatement().execute("CALL SET(@t, 0);")
val selectJoin = """ val selectJoin = """
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id 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) (if (notary != null)
" AND vs.notary_name = ?" else "") + " AND vs.notary_name = ?" else "") +
(if (onlyFromIssuerParties.isNotEmpty()) (if (onlyFromIssuerParties.isNotEmpty())
" AND ccs.issuer_key IN (?)" else "") + " AND ccs.issuer_key_hash IN (?)" else "") +
(if (withIssuerRefs.isNotEmpty()) (if (withIssuerRefs.isNotEmpty())
" AND ccs.issuer_ref IN (?)" else "") " AND ccs.issuer_ref IN (?)" else "")
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection) // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
val psSelectJoin = connection.prepareStatement(selectJoin) connection.prepareStatement(selectJoin).use { psSelectJoin ->
var pIndex = 0 var pIndex = 0
psSelectJoin.setString(++pIndex, amount.token.currencyCode) psSelectJoin.setString(++pIndex, amount.token.currencyCode)
psSelectJoin.setLong(++pIndex, amount.quantity) psSelectJoin.setLong(++pIndex, amount.quantity)
psSelectJoin.setString(++pIndex, lockId.toString()) psSelectJoin.setString(++pIndex, lockId.toString())
if (notary != null) if (notary != null)
psSelectJoin.setString(++pIndex, notary.name.toString()) psSelectJoin.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty()) if (onlyFromIssuerParties.isNotEmpty())
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() ) psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray())
if (withIssuerRefs.isNotEmpty()) if (withIssuerRefs.isNotEmpty())
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray()) psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
log.debug { psSelectJoin.toString() } log.debug { psSelectJoin.toString() }
return psSelectJoin.executeQuery() psSelectJoin.executeQuery().use { rs ->
return withResultSet(rs)
}
}
} }
} }

View File

@ -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<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
TODO("MySQL cash selection not implemented")
}
override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}"
}

View File

@ -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<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, 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)
}
}
}
}

View File

@ -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<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, 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)
}
}
}
}

View File

@ -5,10 +5,10 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import javax.persistence.Column import net.corda.core.utilities.MAX_HASH_HEX_SIZE
import javax.persistence.Entity import net.corda.core.contracts.MAX_ISSUER_REF_SIZE
import javax.persistence.Index import org.hibernate.annotations.Type
import javax.persistence.Table import javax.persistence.*
/** /**
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version). * 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) @Column(name = "ccy_code", length = 3)
var currency: String, var currency: String,
@Column(name = "issuer_key") @Column(name = "issuer_key_hash", length = MAX_HASH_HEX_SIZE)
var issuerParty: String, var issuerPartyHash: String,
@Column(name = "issuer_ref") @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE)
@Type(type = "corda-wrapper-binary")
var issuerRef: ByteArray var issuerRef: ByteArray
) : PersistentState() ) : PersistentState()
} }

View File

@ -1,2 +1,4 @@
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl 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

View File

@ -226,7 +226,7 @@ class CommercialPaperTestsGeneric {
aliceVaultService = aliceServices.vaultService aliceVaultService = aliceServices.vaultService
databaseAlice.transaction { 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 aliceVaultService = aliceServices.vaultService
} }
@ -239,7 +239,7 @@ class CommercialPaperTestsGeneric {
bigCorpVaultService = bigCorpServices.vaultService bigCorpVaultService = bigCorpServices.vaultService
databaseBigCorp.transaction { 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 bigCorpVaultService = bigCorpServices.vaultService
} }

View File

@ -1,6 +1,8 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset 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.nhaarman.mockito_kotlin.whenever
import com.r3.corda.enterprise.perftestcordapp.* import com.r3.corda.enterprise.perftestcordapp.*
import com.r3.corda.enterprise.perftestcordapp.utils.sumCash 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.Vault
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes 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.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.DummyState 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
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.makeTestIdentityService
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
import java.security.KeyPair
import java.util.* import java.util.*
import kotlin.test.* import kotlin.test.*
@ -53,37 +53,29 @@ import kotlin.test.*
fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>, fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
issuerServices: ServiceHub = this, issuerServices: ServiceHub = this,
outputNotary: Party = DUMMY_NOTARY, outputNotary: Party = DUMMY_NOTARY,
atLeastThisManyStates: Int = 3,
atMostThisManyStates: Int = 10,
rng: Random = Random(),
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
ownedBy: AbstractParty? = null, ownedBy: AbstractParty? = null,
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault<Cash.State> { issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault<Cash.State> {
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey
val anonParty = AnonymousParty(myKey) val anonParty = AnonymousParty(myKey)
// We will allocate one state to one transaction, for simplicities sake. // We will allocate one state to one transaction, for simplicities sake.
val cash = Cash() val cash = Cash()
val transactions: List<SignedTransaction> = amounts.map { pennies -> val issuance = TransactionBuilder(null as Party?)
val issuance = TransactionBuilder(null as Party?) cash.generateIssue(issuance, Amount(howMuch.quantity, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey) val transaction = issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
} recordTransactions(listOf(transaction))
recordTransactions(transactions)
// Get all the StateRefs of all the generated transactions. // Get all the StateRefs of all the generated transactions.
val states = transactions.flatMap { stx -> val states = transaction.tx.outputs.indices.map { i -> transaction.tx.outRef<Cash.State>(i) }
stx.tx.outputs.indices.map { i -> stx.tx.outRef<Cash.State>(i) }
}
return Vault(states) return Vault(states)
} }
class CashTests { class CashTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) private val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
private val defaultIssuer = MEGA_CORP.ref(defaultRef) private val defaultIssuer = MEGA_CORP.ref(defaultRef)
private val inState = Cash.State( 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)))) 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 miniCorpServices: MockServices
private lateinit var megaCorpServices: MockServices private lateinit var megaCorpServices: MockServices
val vault: VaultService get() = miniCorpServices.vaultService val vault: VaultService get() = miniCorpServices.vaultService
lateinit var database: CordaPersistence lateinit var database: CordaPersistence
private lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>> private lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>
private lateinit var ourIdentity: AbstractParty
private lateinit var miniCorpAnonymised: AnonymousParty
private val CHARLIE_ANONYMISED = CHARLIE_IDENTITY.party.anonymise()
private lateinit var WALLET: List<StateAndRef<Cash.State>>
@Before @Before
fun setUp() = withTestSerialization { fun setUp() {
LogHelper.setLevel(NodeVaultService::class) 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<IdentityServiceInternal>().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( val databaseAndServices = makeTestDatabaseAndMockServices(
listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), initialIdentityName = CordaX500Name(organisation = "Me", locality = "London", country = "GB"),
listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"), keys = listOf(generateKeyPair()),
CordaX500Name("Me", "London", "GB")) identityService = makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)))
database = databaseAndServices.first 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. // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
database.transaction { database.transaction {
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 100.DOLLARS)
ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices) ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 400.DOLLARS)
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.DOLLARS)
ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices) ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.SWISS_FRANCS)
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)
} }
database.transaction { database.transaction {
vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy<Cash.State>().states vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().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 @After
@ -138,11 +151,10 @@ class CashTests {
} }
@Test @Test
fun trivial() = withTestSerialization { fun trivial() {
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState)
tweak { tweak {
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer)) output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
@ -172,25 +184,22 @@ class CashTests {
this.verifies() this.verifies()
} }
} }
Unit
} }
@Test @Test
fun `issue by move`() = withTestSerialization { fun `issue by move`() {
// Check we can't "move" money into existence. // Check we can't "move" money into existence.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, DummyState()) input(Cash.PROGRAM_ID, DummyState())
output(Cash.PROGRAM_ID, outState) output(Cash.PROGRAM_ID, outState)
command(MINI_CORP_PUBKEY, Cash.Commands.Move()) command(MINI_CORP_PUBKEY, Cash.Commands.Move())
this `fails with` "there is at least one cash input for this group" this `fails with` "there is at least one cash input for this group"
} }
Unit
} }
@Test @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 // 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. // institution is allowed to issue as much cash as they want.
transaction { transaction {
@ -204,17 +213,14 @@ class CashTests {
output(Cash.PROGRAM_ID, output(Cash.PROGRAM_ID,
Cash.State( Cash.State(
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), 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()) command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
this.verifies() this.verifies()
} }
Unit
} }
@Test @Test
fun generateIssueRaw() = withTestSerialization { fun generateIssueRaw() {
// Test generation works. // Test generation works.
val tx: WireTransaction = TransactionBuilder(notary = null).apply { 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) 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 @Test
fun generateIssueFromAmount() = withTestSerialization { fun generateIssueFromAmount() {
// Test issuance from an issued amount // Test issuance from an issued amount
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
val tx: WireTransaction = TransactionBuilder(notary = null).apply { val tx: WireTransaction = TransactionBuilder(notary = null).apply {
@ -240,13 +246,12 @@ class CashTests {
} }
@Test @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. // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, issuerInState) input(Cash.PROGRAM_ID, issuerInState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2)) output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
// Move fails: not allowed to summon money. // Move fails: not allowed to summon money.
tweak { tweak {
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
@ -290,7 +295,6 @@ class CashTests {
} }
this.verifies() this.verifies()
} }
Unit
} }
/** /**
@ -298,7 +302,7 @@ class CashTests {
* cash inputs. * cash inputs.
*/ */
@Test(expected = IllegalStateException::class) @Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() = withTestSerialization { fun `reject issuance with inputs`() {
// Issue some cash // Issue some cash
var ptx = TransactionBuilder(DUMMY_NOTARY) var ptx = TransactionBuilder(DUMMY_NOTARY)
@ -309,11 +313,10 @@ class CashTests {
ptx = TransactionBuilder(DUMMY_NOTARY) ptx = TransactionBuilder(DUMMY_NOTARY)
ptx.addInputState(tx.tx.outRef<Cash.State>(0)) ptx.addInputState(tx.tx.outRef<Cash.State>(0))
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
Unit
} }
@Test @Test
fun testMergeSplit() = withTestSerialization { fun testMergeSplit() {
// Splitting value works. // Splitting value works.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
@ -340,11 +343,10 @@ class CashTests {
this.verifies() this.verifies()
} }
} }
Unit
} }
@Test @Test
fun zeroSizedValues() = withTestSerialization { fun zeroSizedValues() {
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState)
@ -360,11 +362,10 @@ class CashTests {
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "zero sized outputs" this `fails with` "zero sized outputs"
} }
Unit
} }
@Test @Test
fun trivialMismatches() = withTestSerialization { fun trivialMismatches() {
// Can't change issuer. // Can't change issuer.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
@ -387,7 +388,7 @@ class CashTests {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer)) 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()) command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
} }
@ -397,9 +398,7 @@ class CashTests {
input(Cash.PROGRAM_ID, input(Cash.PROGRAM_ID,
inState.copy( inState.copy(
amount = 150.POUNDS `issued by` defaultIssuer, 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)) output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
@ -422,17 +421,15 @@ class CashTests {
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "for reference [01]" this `fails with` "for reference [01]"
} }
Unit
} }
@Test @Test
fun exitLedger() = withTestSerialization { fun exitLedger() {
// Single input/output straightforward case. // Single input/output straightforward case.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, issuerInState) input(Cash.PROGRAM_ID, issuerInState)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer))) output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
tweak { tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer)) command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
@ -449,35 +446,28 @@ class CashTests {
} }
} }
} }
Unit
} }
@Test @Test
fun `exit ledger with multiple issuers`() = withTestSerialization { fun `exit ledger with multiple issuers`() {
// Multi-issuer case. // Multi-issuer case.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, issuerInState) input(Cash.PROGRAM_ID, issuerInState)
input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP) 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(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))) 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()) command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move())
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer)) command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef))) command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)))
this.verifies() this.verifies()
} }
Unit
} }
@Test @Test
fun `exit cash not held by its issuer`() = withTestSerialization { fun `exit cash not held by its issuer`() {
// Single input/output straightforward case. // Single input/output straightforward case.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
@ -487,18 +477,16 @@ class CashTests {
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
} }
Unit
} }
@Test @Test
fun multiIssuer() = withTestSerialization { fun multiIssuer() {
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
// Gather 2000 dollars from two different issuers. // Gather 2000 dollars from two different issuers.
input(Cash.PROGRAM_ID, inState) input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP) input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move()) command(ALICE_PUBKEY, Cash.Commands.Move())
// Can't merge them together. // Can't merge them together.
tweak { tweak {
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer)) 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) output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
this.verifies() this.verifies()
} }
Unit
} }
@Test @Test
fun multiCurrency() = withTestSerialization { fun multiCurrency() {
// Check we can do an atomic currency trade tx. // Check we can do an atomic currency trade tx.
transaction { transaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
@ -530,36 +517,20 @@ class CashTests {
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY)) output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY)) output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move()) command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
this.verifies() this.verifies()
} }
Unit
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// Spend tx generation // 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<Currency>, issuer: AbstractParty, depositRef: Byte = 1) = private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
StateAndRef( 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)) 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. * Generate an exit transaction, removing some amount of cash from the ledger.
*/ */
@ -570,19 +541,21 @@ class CashTests {
return tx.toWireTransaction(serviceHub) return tx.toWireTransaction(serviceHub)
} }
private fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction { private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val ourIdentity = services.myInfo.singleIdentityAndCert()
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false)
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction { 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. * Try exiting an amount which matches a single state.
*/ */
@Test @Test
fun generateSimpleExit() = withTestSerialization { fun generateSimpleExit() {
val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1) val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size) 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. * Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
*/ */
@Test @Test
fun generatePartialExit() = withTestSerialization { fun generatePartialExit() {
val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1) val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1)
val actualInput = wtx.inputs.single() val actualInput = wtx.inputs.single()
// Filter the available inputs and confirm exactly one has been used // 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. * Try exiting a currency we don't have.
*/ */
@Test @Test
fun generateAbsentExit() = withTestSerialization { fun generateAbsentExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
Unit
} }
/** /**
* Try exiting with a reference mis-match. * Try exiting with a reference mis-match.
*/ */
@Test @Test
fun generateInvalidReferenceExit() = withTestSerialization { fun generateInvalidReferenceExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
Unit
} }
/** /**
* Try exiting an amount greater than the maximum available. * Try exiting an amount greater than the maximum available.
*/ */
@Test @Test
fun generateInsufficientExit() = withTestSerialization { fun generateInsufficientExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
Unit
} }
/** /**
* Try exiting for an owner with no states * Try exiting for an owner with no states
*/ */
@Test @Test
fun generateOwnerWithNoStatesExit() = withTestSerialization { fun generateOwnerWithNoStatesExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
Unit
} }
/** /**
* Try exiting when vault is empty * Try exiting when vault is empty
*/ */
@Test @Test
fun generateExitWithEmptyVault() = withTestSerialization { fun generateExitWithEmptyVault() {
assertFailsWith<IllegalArgumentException> { assertFailsWith<IllegalArgumentException> {
val tx = TransactionBuilder(DUMMY_NOTARY) 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 @Test
fun generateSimpleDirectSpend() = withTestSerialization { fun generateSimpleDirectSpend() {
val wtx = val wtx =
database.transaction { database.transaction {
makeSpend(100.DOLLARS, THEIR_IDENTITY_1) makeSpend(ourServices, 100.DOLLARS, miniCorpAnonymised)
} }
database.transaction { database.transaction {
val vaultState = vaultStatesUnconsumed.elementAt(0) val vaultState = vaultStatesUnconsumed.elementAt(0)
assertEquals(vaultState.ref, wtx.inputs[0]) assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0)) assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised), wtx.getOutput(0))
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 @Test
fun generateSimpleSpendWithParties() = withTestSerialization { fun generateSimpleSpendWithParties() {
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
database.transaction { database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY) 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]) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
} }
} }
@Test @Test
fun generateSimpleSpendWithChange() = withTestSerialization { fun generateSimpleSpendWithChange() {
val wtx = val wtx =
database.transaction { database.transaction {
makeSpend(10.DOLLARS, THEIR_IDENTITY_1) makeSpend(ourServices, 10.DOLLARS, miniCorpAnonymised)
} }
database.transaction { database.transaction {
val vaultState = vaultStatesUnconsumed.elementAt(0) val vaultState = vaultStatesUnconsumed.elementAt(0)
@ -700,35 +669,35 @@ class CashTests {
} }
} }
val changeOwner = (likelyChangeState as Cash.State).owner 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.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(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 @Test
fun generateSpendWithTwoInputs() = withTestSerialization { fun generateSpendWithTwoInputs() {
val wtx = val wtx =
database.transaction { database.transaction {
makeSpend(500.DOLLARS, THEIR_IDENTITY_1) makeSpend(ourServices, 500.DOLLARS, miniCorpAnonymised)
} }
database.transaction { database.transaction {
val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState0 = vaultStatesUnconsumed.elementAt(0)
val vaultState1 = vaultStatesUnconsumed.elementAt(1) val vaultState1 = vaultStatesUnconsumed.elementAt(1)
assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1]) 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(vaultState0.state.data.copy(owner = miniCorpAnonymised, 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(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
} }
} }
@Test @Test
fun generateSpendMixedDeposits() = withTestSerialization { fun generateSpendMixedDeposits() {
val wtx = val wtx =
database.transaction { database.transaction {
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) val wtx = makeSpend(ourServices, 580.DOLLARS, miniCorpAnonymised)
assertEquals(3, wtx.inputs.size) assertEquals(3, wtx.inputs.size)
wtx wtx
} }
@ -739,26 +708,25 @@ class CashTests {
assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState2.ref, wtx.inputs[2]) 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(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data) assertEquals(vaultState2.state.data.copy(owner = miniCorpAnonymised), wtx.outputs[0].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 @Test
fun generateSpendInsufficientBalance() = withTestSerialization { fun generateSpendInsufficientBalance() {
database.transaction { database.transaction {
val e: InsufficientBalanceException = assertFailsWith("balance") { val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_IDENTITY_1) makeSpend(ourServices, 1000.DOLLARS, miniCorpAnonymised)
} }
assertEquals((1000 - 580).DOLLARS, e.amountMissing) assertEquals((1000 - 580).DOLLARS, e.amountMissing)
assertFailsWith(InsufficientBalanceException::class) { 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. // Double spend.
@Test @Test
fun chainCashDoubleSpendFailsWith() = withTestSerialization { fun chainCashDoubleSpendFailsWith() {
val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock<IdentityServiceInternal>().also { val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
}, MEGA_CORP.name, MEGA_CORP_KEY) }, MEGA_CORP.name, MEGA_CORP_KEY)
@ -856,9 +824,7 @@ class CashTests {
output(Cash.PROGRAM_ID, "MEGA_CORP cash", output(Cash.PROGRAM_ID, "MEGA_CORP cash",
Cash.State( Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP owner = MEGA_CORP))
)
)
} }
transaction { transaction {
@ -883,20 +849,20 @@ class CashTests {
this.verifies() this.verifies()
} }
Unit
} }
@Test @Test
fun multiSpend() = withTestSerialization { fun multiSpend() {
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction { database.transaction {
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
val payments = listOf( val payments = listOf(
PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS), PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
PartyAndAmount(THEIR_IDENTITY_2, 150.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 fun out(i: Int) = wtx.getOutput(i) as Cash.State
assertEquals(4, wtx.outputs.size) assertEquals(4, wtx.outputs.size)
assertEquals(80.DOLLARS, out(0).amount.withoutIssuer()) assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())

View File

@ -174,8 +174,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
bobNode.internals.disableDBCloseOnStop() bobNode.internals.disableDBCloseOnStop()
val cashStates = bobNode.database.transaction { val cashStates = bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3, bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, issuedBy = issuer)
issuedBy = issuer)
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {