Merge pull request #56 from corda/christians_perftestflows

Isolated cash and commercial paper for performance testing
This commit is contained in:
Christian Sailer 2017-10-19 12:03:54 +01:00 committed by GitHub
commit 8428f78821
27 changed files with 4278 additions and 0 deletions

2
.idea/compiler.xml generated
View File

@ -86,6 +86,8 @@
<module name="node_test" target="1.8" />
<module name="notary-demo_main" target="1.8" />
<module name="notary-demo_test" target="1.8" />
<module name="perftestcordapp_main" target="1.8" />
<module name="perftestcordapp_test" target="1.8" />
<module name="publish-utils_main" target="1.8" />
<module name="publish-utils_test" target="1.8" />
<module name="quasar-hook_main" target="1.8" />

View File

@ -0,0 +1,49 @@
apply plugin: 'kotlin'
// Java Persistence API support: create no-arg constructor
// see: http://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell
apply plugin: 'kotlin-jpa'
apply plugin: CanonicalizerPlugin
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordapp'
//apply plugin: 'com.jfrog.artifactory'
description 'Corda performance test modules'
dependencies {
// Note the :finance module is a CorDapp in its own right
// and CorDapps using :finance features should use 'cordapp' not 'compile' linkage.
cordaCompile project(':core')
cordaCompile project(':confidential-identities')
// TODO Remove this once we have app configs
compile "com.typesafe:config:$typesafe_config_version"
testCompile project(':test-utils')
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "junit:junit:$junit_version"
// AssertJ: for fluent assertions for testing
testCompile "org.assertj:assertj-core:$assertj_version"
}
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
classifier "tests"
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
jar {
baseName 'corda-ptflows'
}
publish {
name jar.baseName
}

View File

@ -0,0 +1,34 @@
@file:JvmName("Currencies")
package com.r3.corda.enterprise.perftestcordapp
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued
import net.corda.core.contracts.PartyAndReference
import java.math.BigDecimal
import java.util.*
@JvmField val USD: Currency = Currency.getInstance("USD")
@JvmField val GBP: Currency = Currency.getInstance("GBP")
@JvmField val EUR: Currency = Currency.getInstance("EUR")
@JvmField val CHF: Currency = Currency.getInstance("CHF")
@JvmField val JPY: Currency = Currency.getInstance("JPY")
@JvmField val RUB: Currency = Currency.getInstance("RUB")
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))

View File

@ -0,0 +1,197 @@
package com.r3.corda.enterprise.perftestcordapp.contracts
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.crypto.NullKeys.NULL_PARTY
import net.corda.core.utilities.toBase58String
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.Emoji
import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import com.r3.corda.enterprise.perftestcordapp.schemas.CommercialPaperSchemaV1
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashBy
import java.time.Instant
import java.util.*
/**
* This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate
* bond. It can be seen as a company-specific currency. A company issues CP with a particular face value, say $100,
* but sells it for less, say $90. The paper can be redeemed for cash at a given date in the future. Thus this example
* would have a 10% interest rate with a single repayment. Commercial paper is often rolled over (the maturity date
* is adjusted as if the paper was redeemed and immediately repurchased, but without having to front the cash).
*
* This contract is not intended to realistically model CP. It is here only to act as a next step up above cash in
* the prototyping phase. It is thus very incomplete.
*
* Open issues:
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
* of cash, or reuse some of the cash code? Waiting on response from Ayoub and Rajar about whether CP can always
* be split/merged or only in secondary markets. Even if current systems can't do this, would it be a desirable
* feature to have anyway?
* - The funding steps of CP is totally ignored in this model.
* - No attention is paid to the existing roles of custodians, funding banks, etc.
* - There are regional variations on the CP concept, for instance, American CP requires a special "CUSIP number"
* which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how
* to do this in the Apache BVal project).
*/
val CP_PROGRAM_ID = "com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper"
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
class CommercialPaper : Contract {
companion object {
const val CP_PROGRAM_ID: ContractClassName = "com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper"
}
data class State(
val issuance: PartyAndReference,
override val owner: AbstractParty,
val faceValue: Amount<Issued<Currency>>,
val maturityDate: Instant
) : OwnableState, QueryableState{
override val participants = listOf(owner)
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
fun withoutOwner() = copy(owner = NULL_PARTY)
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)"
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
fun withOwner(newOwner: AbstractParty): State = copy(owner = newOwner)
fun withFaceValue(newFaceValue: Amount<Issued<Currency>>): State = copy(faceValue = newFaceValue)
fun withMaturityDate(newMaturityDate: Instant): State = copy(maturityDate = newMaturityDate)
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CommercialPaperSchemaV1)
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is CommercialPaperSchemaV1 -> CommercialPaperSchemaV1.PersistentCommercialPaperState(
issuanceParty = this.issuance.party.owningKey.toBase58String(),
issuanceRef = this.issuance.reference.bytes,
owner = this.owner.owningKey.toBase58String(),
maturity = this.maturityDate,
faceValue = this.faceValue.quantity,
currency = this.faceValue.token.product.currencyCode,
faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(),
faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes
)
/** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner)
}
interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands
class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
class Issue : TypeOnlyCommandData(), Commands
}
override fun verify(tx: LedgerTransaction) {
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
val groups = tx.groupStates(State::withoutOwner)
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
val timeWindow: TimeWindow? = tx.timeWindow
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
@Suppress("UNUSED_VARIABLE")
for ((inputs, outputs, key) in groups) {
when (command.value) {
is Commands.Move -> {
val input = inputs.single()
requireThat {
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
"the state is propagated" using (outputs.size == 1)
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
// the input ignoring the owner field due to the grouping.
}
}
is Commands.Redeem -> {
// Redemption of the paper requires movement of on-ledger cash.
val input = inputs.single()
val received = tx.outputStates.sumCashBy(input.owner)
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
requireThat {
"the paper must have matured" using (time >= input.maturityDate)
"the received amount equals the face value" using (received == input.faceValue)
"the paper must be destroyed" using outputs.isEmpty()
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
}
}
is Commands.Issue -> {
val output = outputs.single()
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window")
requireThat {
// Don't allow people to issue commercial paper under other entities identities.
"output states are issued by a command signer" using
(output.issuance.party.owningKey in command.signers)
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
"the maturity date is not in the past" using (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance.
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
"output values sum to more than the inputs" using inputs.isEmpty()
}
}
// TODO: Think about how to evolve contracts over time with new commands.
else -> throw IllegalArgumentException("Unrecognised command")
}
}
}
/**
* Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
* at the moment: this restriction is not fundamental and may be lifted later.
*/
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
notary: Party): TransactionBuilder {
val state = State(issuance, issuance.party, faceValue, maturityDate)
return TransactionBuilder(notary = notary).withItems(StateAndContract(state, CP_PROGRAM_ID), Command(Commands.Issue(), issuance.party.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
*/
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
tx.addInputState(paper)
tx.addOutputState(paper.state.data.withOwner(newOwner), CP_PROGRAM_ID)
tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey)
}
/**
* Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish
* to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face
* value, and then ensure the paper is removed from the ledger.
*
* @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer.
*/
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub, ourIdentity: PartyAndCertificate) {
// Add the cash movement using the states in our vault.
Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), ourIdentity, paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
}
}

View File

@ -0,0 +1,351 @@
// So the static extension functions get put into a class with a better name than CashKt
package com.r3.corda.enterprise.perftestcordapp.contracts.asset
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.crypto.NullKeys.NULL_PARTY
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.Emoji
import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.toBase58String
import com.r3.corda.enterprise.perftestcordapp.schemas.CashSchemaV1
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 java.math.BigInteger
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cash
//
/**
* A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour
* (a blend of issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in
* the same transaction.
*
* The goal of this design is to ensure that money can be withdrawn from the ledger easily: if you receive some money
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
* it has passed through in the intervening time.
*
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/
class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
override fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): List<CommandWithParties<Cash.Commands>>
= commands.select<Cash.Commands>()
// DOCSTART 1
/** A state representing a cash claim against some party. */
data class State(
override val amount: Amount<Issued<Currency>>,
/** There must be a MoveCommand signed by this key to claim the amount. */
override val owner: AbstractParty
) : FungibleAsset<Currency>, QueryableState {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: AbstractParty)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey)
override val participants = listOf(owner)
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency>
= copy(amount = amount.copy(newAmount.quantity), owner = newOwner)
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)"
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
infix fun ownedBy(owner: AbstractParty) = copy(owner = owner)
infix fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party))))
infix fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit)))
infix fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is CashSchemaV1 -> CashSchemaV1.PersistentCashState(
owner = this.owner,
pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode,
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
issuerRef = this.amount.token.issuer.reference.bytes
)
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CashSchemaV1)
/** Additional used schemas would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
}
// DOCEND 1
// Just for grouping
interface Commands : CommandData {
/**
* A command stating that money has been moved, optionally to fulfil another contract.
*
* @param contract the contract this move is for the attention of. Only that contract's verify function
* should take the moved states into account when considering whether it is valid. Typically this will be
* null.
*/
data class Move(override val contract: Class<out Contract>? = null) : MoveCommand
/**
* Allows new cash states to be issued into existence.
*/
class Issue : TypeOnlyCommandData()
/**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
data class Exit(val amount: Amount<Issued<Currency>>) : CommandData
}
/**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder, tokenDef: Issued<Currency>, pennies: Long, owner: AbstractParty, notary: Party)
= generateIssue(tx, Amount(pennies, tokenDef), owner, notary)
/**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, owner: AbstractParty, notary: Party)
= generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue())
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: LedgerTransaction) {
// Each group is a set of input/output states with distinct (reference, currency) attributes. These types
// of cash are not fungible and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates { it: Cash.State -> it.amount.token }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val issuer = key.issuer
val currency = key.product
requireThat {
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else {
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
// If we want to remove cash from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
requireThat {
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: CommandWithParties<Commands.Issue>,
currency: Currency,
issuer: PartyAndReference) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
// and the output states must have a deposit reference owned by the signer.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// as-yet-unwritten identity service. See ADP-22 for discussion.
// The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Commands.Issue>()
requireThat {
// TODO: This doesn't work with the trader demo, so use the underlying key instead
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
"output values sum to more than the inputs" using (outputAmount > inputAmount)
"there is only a single issue command" using (cashCommands.count() == 1)
}
}
companion object {
const val PROGRAM_ID: ContractClassName = "com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash"
/**
* Generate a transaction that moves an amount of currency to the given party, and sends any change back to
* sole identity of the calling node. Fails for nodes with multiple identities.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to the recipient party.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
@Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)"))
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
amount: Amount<Currency>,
to: AbstractParty,
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
/**
* Generate a transaction that moves an amount of currency to the given party.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to the recipient party.
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
amount: Amount<Currency>,
ourIdentity: PartyAndCertificate,
to: AbstractParty,
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), ourIdentity, onlyFromParties)
}
/**
* Generate a transaction that moves money of the given amounts to the recipients specified, and sends any change
* back to sole identity of the calling node. Fails for nodes with multiple identities.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param payments A list of amounts to pay, and the party to send the payment to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
@Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)"))
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
payments: List<PartyAndAmount<Currency>>,
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, payments, services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
/**
* Generate a transaction that moves money of the given amounts to the recipients specified.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param payments A list of amounts to pay, and the party to send the payment to.
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
payments: List<PartyAndAmount<Currency>>,
ourIdentity: PartyAndCertificate,
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
// Retrieve unspent and unlocked cash states that meet our spending criteria.
val totalAmount = payments.map { it.amount }.sumOrThrow()
val cashSelection = AbstractCashSelection.getInstance({ services.jdbcSession().metaData })
val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId)
val revocationEnabled = false // Revocation is currently unsupported
// Generate a new identity that change will be sent to for confidentiality purposes. This means that a
// third party with a copy of the transaction (such as the notary) cannot identify who the change was
// sent to
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled)
return OnLedgerAsset.generateSpend(tx, payments, acceptableCoins,
changeIdentity.party.anonymise(),
{ state, quantity, owner -> deriveState(state, quantity, owner) },
{ Cash().generateMoveCommand() })
}
}
}
// Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions.
/** 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) }
/** 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)
/** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */
val Amount<Issued<Currency>>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY)

View File

@ -0,0 +1,374 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset
import net.corda.core.contracts.*
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Generic contract for assets on a ledger
//
/** A simple holder for a (possibly anonymous) [AbstractParty] and a quantity of tokens */
data class PartyAndAmount<T : Any>(val party: AbstractParty, val amount: Amount<T>)
/**
* An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of
* issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in the same
* transaction.
*
* The goal of this design is to ensure that assets can be withdrawn from the ledger easily: if you receive some asset
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
* it has passed through in the intervening time.
*
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
* the issuer/depositRefs and just examine the amount fields.
*/
abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : Contract {
companion object {
val log = loggerFor<OnLedgerAsset<*, *, *>>()
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param acceptableStates a list of acceptable input states to use.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party.
* @param deriveState a function to derive an output state based on an input state, amount for the output
* and public key to pay to.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T : Any> generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: AbstractParty,
acceptableStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, payChangeTo, deriveState, generateMoveCommand)
}
/**
* Adds to the given transaction states that move amounts of a fungible asset to the given parties, using only
* the provided acceptable input states to find a solution (not all of them may be used in the end). A change
* output will be generated if the state amounts don't exactly fit.
*
* The fungible assets must all be of the same type and the amounts must be summable i.e. amounts of the same
* token.
*
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param acceptableStates a list of acceptable input states to use.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party. We use a new confidential identity here so that the recipient is not identifiable.
* @param deriveState a function to derive an output state based on an input state, amount for the output
* and public key to pay to.
* @param T A type representing a token
* @param S A fungible asset state type
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T : Any> generateSpend(tx: TransactionBuilder,
payments: List<PartyAndAmount<T>>,
acceptableStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "vault", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
// TODO: We should be prepared to produce multiple transactions spending inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value.
// TODO: Check that re-running this on the same transaction multiple times does the right thing.
// The notary may be associated with a locked state only.
tx.notary = acceptableStates.firstOrNull()?.state?.notary
// Calculate the total amount we're sending (they must be all of a compatible token).
val totalSendAmount = payments.map { it.amount }.sumOrThrow()
// Select a subset of the available states we were given that sums up to >= totalSendAmount.
val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount)
check(gatheredAmount >= totalSendAmount)
val keysUsed = gathered.map { it.state.data.owner.owningKey }
// Now calculate the output states. This is complicated by the fact that a single payment may require
// multiple output states, due to the need to keep states separated by issuer. We start by figuring out
// how much we've gathered for each issuer: this map will keep track of how much we've used from each
// as we work our way through the payments.
val statesGroupedByIssuer = gathered.groupBy { it.state.data.amount.token }
val remainingFromEachIssuer = statesGroupedByIssuer
.mapValues {
it.value.map {
it.state.data.amount
}.sumOrThrow()
}.toList().toMutableList()
val outputStates = mutableListOf<TransactionState<S>>()
for ((party, paymentAmount) in payments) {
var remainingToPay = paymentAmount.quantity
while (remainingToPay > 0) {
val (token, remainingFromCurrentIssuer) = remainingFromEachIssuer.last()
val templateState = statesGroupedByIssuer[token]!!.first().state
val delta = remainingFromCurrentIssuer.quantity - remainingToPay
when {
delta > 0 -> {
// The states from the current issuer more than covers this payment.
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
remainingFromEachIssuer[0] = Pair(token, Amount(delta, token))
remainingToPay = 0
}
delta == 0L -> {
// The states from the current issuer exactly covers this payment.
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
remainingToPay = 0
}
delta < 0 -> {
// The states from the current issuer don't cover this payment, so we'll have to use >1 output
// state to cover this payment.
outputStates += deriveState(templateState, remainingFromCurrentIssuer, party)
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
remainingToPay -= remainingFromCurrentIssuer.quantity
}
}
}
}
// Whatever values we have left over for each issuer must become change outputs.
for ((token, amount) in remainingFromEachIssuer) {
val templateState = statesGroupedByIssuer[token]!!.first().state
outputStates += deriveState(templateState, amount, payChangeTo)
}
for (state in gathered) tx.addInputState(state)
for (state in outputStates) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
tx.addCommand(generateMoveCommand(), keysUsed)
return Pair(tx, keysUsed)
}
/**
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun <S : FungibleAsset<T>, T : Any> gatherCoins(acceptableCoins: Collection<StateAndRef<S>>,
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
require(amount.quantity > 0) { "Cannot gather zero coins" }
val gathered = arrayListOf<StateAndRef<S>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount) {
log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" }
throw InsufficientBalanceException(amount - gatheredAmount)
}
log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" }
return Pair(gathered, gatheredAmount)
}
/**
* Generate an transaction exiting fungible assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
@Deprecated("Replaced with generateExit() which takes in a party to pay change to")
fun <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey> {
val owner = assetStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amountIssued)
return generateExit(tx, amountIssued, assetStates, owner, deriveState, generateMoveCommand, generateExitCommand)
}
/**
* Generate an transaction exiting fungible assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey> {
require(assetStates.isNotEmpty()) { "List of states to exit cannot be empty." }
val currency = amountIssued.token.product
val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
tx.notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions exiting inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token)
} else {
null
}
val outputs = if (change != null) {
// Add a change output and adjust the last output downwards.
listOf(deriveState(gathered.last().state, change, payChangeTo))
} else emptyList()
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
val moveKeys = gathered.map { it.state.data.owner.owningKey }
val exitKeys = gathered.flatMap { it.state.data.exitKeys }
tx.addCommand(generateMoveCommand(), moveKeys)
tx.addCommand(generateExitCommand(amountIssued), exitKeys)
return (moveKeys + exitKeys).toSet()
}
/**
* Puts together an issuance transaction for the specified state. Normally contracts will provide convenient
* wrappers around this function, which build the state for you, and those should be used in preference.
*/
@JvmStatic
fun <S : FungibleAsset<T>, T : Any> generateIssue(tx: TransactionBuilder,
transactionState: TransactionState<S>,
issueCommand: CommandData): Set<PublicKey> {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
require(transactionState.data.amount.quantity > 0)
val at = transactionState.data.amount.token.issuer
val commandSigner = at.party.owningKey
tx.addOutputState(transactionState)
tx.addCommand(issueCommand, commandSigner)
return setOf(commandSigner)
}
}
abstract fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): Collection<CommandWithParties<C>>
/**
* Generate an transaction exiting assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
@Deprecated("Replaced with generateExit() which takes in a party to pay change to")
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>): Set<PublicKey> {
return generateExit(
tx,
amountIssued,
assetStates,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { -> generateMoveCommand() },
generateExitCommand = { amount -> generateExitCommand(amount) }
)
}
/**
* Generate an transaction exiting assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty): Set<PublicKey> {
return generateExit(
tx,
amountIssued,
assetStates,
payChangeTo,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { -> generateMoveCommand() },
generateExitCommand = { amount -> generateExitCommand(amount) }
)
}
abstract fun generateExitCommand(amount: Amount<Issued<T>>): CommandData
abstract fun generateMoveCommand(): MoveCommand
/**
* Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete
* implementations to have fields in their state which we don't know about here, and we simply leave them untouched
* when sending out "change" from spending/exiting.
*/
abstract fun deriveState(txState: TransactionState<S>, amount: Amount<Issued<T>>, owner: AbstractParty): TransactionState<S>
}

View File

@ -0,0 +1,168 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.*
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import java.sql.*
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Pluggable interface to allow for different cash selection provider implementations
* Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation.
* Custom implementations must implement this interface and declare their implementation in
* META-INF/services/net.corda.contracts.asset.CashSelection
*/
abstract class AbstractCashSelection {
companion object {
val instance = AtomicReference<AbstractCashSelection>()
fun getInstance(metadata: () -> java.sql.DatabaseMetaData): AbstractCashSelection {
return instance.get() ?: {
val _metadata = metadata()
val cashSelectionAlgos = ServiceLoader.load(AbstractCashSelection::class.java).toList()
val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) }
cashSelectionAlgo?.let {
instance.set(cashSelectionAlgo)
cashSelectionAlgo
} ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." +
"\nPlease specify an implementation in META-INF/services/${AbstractCashSelection::class.java}")
}.invoke()
}
val log = loggerFor<AbstractCashSelection>()
}
// 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()
/**
* Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services
* this method determines whether the loaded implementation is compatible and usable with the currently
* loaded JDBC driver.
* Note: the first loaded implementation to pass this check will be used at run-time.
*/
abstract fun isCompatible(metadata: DatabaseMetaData): Boolean
/**
* 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 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.
* @param notary If null the notary source is ignored, if specified then only states marked
* 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,
* otherwise what is available is returned unlocked for informational purposes.
*/
abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet
override abstract fun toString() : String
/**
* Query to gather Cash states that are available and retry if they are temporarily unavailable.
* @param services The service hub to allow access to the database session
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
* @param notary If null the notary source is ignored, if specified then only states marked
* with this notary are included.
* @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.
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
* @return 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.
*/
@Suspendable
fun unconsumedCashStatesForSpending(services: ServiceHub,
amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
notary: Party? = null,
lockId: UUID,
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
for (retryCount in 1..MAX_RETRIES) {
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) {
stateAndRefs.clear()
val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt()
FlowLogic.sleep(durationMillis.millis)
} else {
log.warn("Insufficient spendable states identified for $amount")
}
} else {
break
}
}
return stateAndRefs
}
private fun attemptSpend(services: ServiceHub, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, stateAndRefs: MutableList<StateAndRef<Cash.State>>): Boolean {
spendLock.withLock {
val connection = services.jdbcSession()
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()
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val stateRef = StateRef(txHash, index)
val state = rs.getBlob(3).deserialize<TransactionState<Cash.State>>(context = SerializationDefaults.STORAGE_CONTEXT)
val pennies = rs.getLong(4)
totalPennies = rs.getLong(5)
val rowLockId = rs.getString(6)
stateAndRefs.add(StateAndRef(state, stateRef))
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
}
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]
$e.
""")
} catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
log.warn(e.message)
// retry only if there are locked states that may become available again (or consumed with change)
}
}
return false
}
}

View File

@ -0,0 +1,67 @@
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 CashSelectionH2Impl : AbstractCashSelection() {
companion object {
const val JDBC_DRIVER_NAME = "H2 JDBC Driver"
val log = loggerFor<CashSelectionH2Impl>()
}
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
return metadata.driverName == 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:
// 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<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
connection.createStatement().execute("CALL SET(@t, 0);")
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_pt_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 @t < ?
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 "")
// 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() }
return psSelectJoin.executeQuery()
}
}

View File

@ -0,0 +1,53 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.NotaryException
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import java.util.*
/**
* Initiates a flow that produces an Issue/Move or Exit Cash transaction.
*/
abstract class AbstractCashFlow<out T>(override val progressTracker: ProgressTracker) : FlowLogic<T>() {
companion object {
object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
object GENERATING_TX : ProgressTracker.Step("Generating transaction")
object SIGNING_TX : ProgressTracker.Step("Signing transaction")
object FINALISING_TX : ProgressTracker.Step("Finalising transaction")
fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX)
}
@Suspendable
protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set<Party>, message: String): SignedTransaction {
try {
return subFlow(FinalityFlow(tx, extraParticipants))
} catch (e: NotaryException) {
throw CashException(message, e)
}
}
/**
* Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows.
* Specialised flows for unit tests differ from this.
*
* @param stx the signed transaction.
* @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is
* null for exit transactions). For anonymous transactions this is the confidential identity generated for the
* transaction, otherwise this is the well known identity.
*/
@CordaSerializable
data class Result(val stx: SignedTransaction, val recipient: AbstractParty?)
abstract class AbstractRequest(val amount: Amount<Currency>)
}
class CashException(message: String, cause: Throwable) : FlowException(message, cause)

View File

@ -0,0 +1,61 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import com.typesafe.config.ConfigFactory
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.declaredField
import net.corda.core.internal.div
import net.corda.core.internal.read
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import com.r3.corda.enterprise.perftestcordapp.CHF
import com.r3.corda.enterprise.perftestcordapp.EUR
import com.r3.corda.enterprise.perftestcordapp.GBP
import com.r3.corda.enterprise.perftestcordapp.USD
import com.r3.corda.enterprise.perftestcordapp.flows.ConfigHolder.Companion.supportedCurrencies
import java.nio.file.Path
import java.util.*
// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf
// again to get our config and store it here for access by our flow
@CordaService
class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
companion object {
val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
}
val issuableCurrencies: List<Currency>
init {
// Warning!! You are about to see a major hack!
val baseDirectory = services.declaredField<Any>("serviceHub").value
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
.declaredField<Path>("baseDirectory").value
val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
if (config.hasPath("issuableCurrencies")) {
issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) }
require(supportedCurrencies.containsAll(issuableCurrencies))
} else {
issuableCurrencies = emptyList()
}
}
}
/**
* Flow to obtain cash cordapp app configuration.
*/
@StartableByRPC
class CashConfigDataFlow : FlowLogic<CashConfiguration>() {
@Suspendable
override fun call(): CashConfiguration {
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
}
}
@CordaSerializable
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)

View File

@ -0,0 +1,82 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
import com.r3.corda.enterprise.perftestcordapp.issuedBy
import java.util.*
/**
* Initiates a flow that produces an cash exit transaction.
*
* @param amount the amount of a currency to remove from the ledger.
* @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
* issuer.
*/
@StartableByRPC
class CashExitFlow(private val amount: Amount<Currency>,
private val issuerRef: OpaqueBytes,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes) : this(amount, issueRef, tracker())
constructor(request: ExitRequest) : this(request.amount, request.issueRef, tracker())
companion object {
fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX)
}
/**
* @return the signed transaction, and a mapping of parties to new anonymous identities generated
* (for this flow this map is always empty).
*/
@Suspendable
@Throws(CashException::class)
override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary = null)
val issuer = ourIdentity.ref(issuerRef)
val exitStates = AbstractCashSelection
.getInstance { serviceHub.jdbcSession().metaData }
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
val signers = try {
Cash().generateExit(
builder,
amount.issuedBy(issuer),
exitStates)
} catch (e: InsufficientBalanceException) {
throw CashException("Exiting more cash than exists", e)
}
// Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE)
val inputStates = serviceHub.vaultService.queryBy<Cash.State>(VaultQueryCriteria(stateRefs = builder.inputStates()),
PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
// count as a reason to fail?
val participants: Set<Party> = inputStates
.mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
.toSet()
// Sign transaction
progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(builder, signers)
// Commit the transaction
progressTracker.currentStep = FINALISING_TX
val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
return Result(notarised, null)
}
@CordaSerializable
class ExitRequest(amount: Amount<Currency>, val issueRef: OpaqueBytes) : AbstractRequest(amount)
}

View File

@ -0,0 +1,49 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import java.util.*
/**
* Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
*
* We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
* checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
* and immediately transfer it, so impact of this limitation is considered minimal.
*
* @param amount the amount of currency to issue.
* @param issuerBankPartyRef a reference to put on the issued currency.
* @param notary the notary to set on the output states.
*/
@StartableByRPC
class CashIssueAndPaymentFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val anonymous: Boolean,
val notary: Party,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes,
recipient: Party,
anonymous: Boolean,
notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker())
constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker())
@Suspendable
override fun call(): Result {
subFlow(CashIssueFlow(amount, issueRef, notary))
return subFlow(CashPaymentFlow(amount, recipient, anonymous))
}
@CordaSerializable
class IssueAndPaymentRequest(amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party,
val anonymous: Boolean) : AbstractRequest(amount)
}

View File

@ -0,0 +1,52 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import com.r3.corda.enterprise.perftestcordapp.issuedBy
import java.util.*
/**
* Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
*
* We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
* checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
* and immediately transfer it, so impact of this limitation is considered minimal.
*
* @param amount the amount of currency to issue.
* @param issuerBankPartyRef a reference to put on the issued currency.
* @param notary the notary to set on the output states.
*/
@StartableByRPC
class CashIssueFlow(private val amount: Amount<Currency>,
private val issuerBankPartyRef: OpaqueBytes,
private val notary: Party,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>,
issuerBankPartyRef: OpaqueBytes,
notary: Party) : this(amount, issuerBankPartyRef, notary, tracker())
constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker())
@Suspendable
override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary)
val issuer = ourIdentity.ref(issuerBankPartyRef)
val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary)
progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(builder, signers)
progressTracker.currentStep = FINALISING_TX
// There is no one to send the tx to as we're the only participants
val notarised = finaliseTx(tx, emptySet(), "Unable to notarise issue")
return Result(notarised, ourIdentity)
}
@CordaSerializable
class IssueRequest(amount: Amount<Currency>, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount)
}

View File

@ -0,0 +1,74 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.confidential.SwapIdentitiesFlow
import net.corda.core.contracts.Amount
import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import java.util.*
/**
* Initiates a flow that sends cash to a recipient.
*
* @param amount the amount of a currency to pay to the recipient.
* @param recipient the party to pay the currency to.
* @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties.
* @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false
* for testing purposes.
*/
@StartableByRPC
open class CashPaymentFlow(
val amount: Amount<Currency>,
val recipient: Party,
val anonymous: Boolean,
progressTracker: ProgressTracker,
val issuerConstraint: Set<Party> = emptySet()) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
/** A straightforward constructor that constructs spends using cash states of any issuer. */
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
/** A straightforward constructor that constructs spends using cash states of any issuer. */
constructor(amount: Amount<Currency>, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker())
constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint)
@Suspendable
override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_ID
val txIdentities = if (anonymous) {
subFlow(SwapIdentitiesFlow(recipient))
} else {
emptyMap<Party, AnonymousParty>()
}
val anonymousRecipient = txIdentities[recipient] ?: recipient
progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary = null)
// TODO: Have some way of restricting this to states the caller controls
val (spendTX, keysForSigning) = try {
Cash.generateSpend(serviceHub,
builder,
amount,
ourIdentityAndCert,
anonymousRecipient,
issuerConstraint)
} catch (e: InsufficientBalanceException) {
throw CashException("Insufficient cash for spend: ${e.message}", e)
}
progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning)
progressTracker.currentStep = FINALISING_TX
val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
return Result(notarised, anonymousRecipient)
}
@CordaSerializable
class PaymentRequest(amount: Amount<Currency>,
val recipient: Party,
val anonymous: Boolean,
val issuerConstraint: Set<Party> = emptySet()) : AbstractRequest(amount)
}

View File

@ -0,0 +1,242 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.confidential.IdentitySyncFlow
import net.corda.core.contracts.*
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.seconds
import net.corda.core.utilities.unwrap
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashBy
import java.security.PublicKey
import java.util.*
/**
* This asset trading flow implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
* and seller) and the following steps:
*
* 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
* B to pay. For example this has probably been agreed on an exchange.
* 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
* owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
* it lacks a signature from S authorising movement of the asset.
* 3. S signs it and commits it to the ledger, notarising it and distributing the final signed transaction back
* to B.
*
* Assuming no malicious termination, they both end the flow being in possession of a valid, signed transaction
* that represents an atomic asset swap.
*
* Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine.
*/
object TwoPartyTradeFlow {
// TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this
// and [AbstractStateReplacementFlow].
class UnacceptablePriceException(givenPrice: Amount<Currency>) : FlowException("Unacceptable price: $givenPrice")
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() {
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
}
/**
* This object is serialised to the network and is the first flow message the seller sends to the buyer.
*
* @param payToIdentity anonymous identity of the seller, for payment to be sent to.
*/
@CordaSerializable
data class SellerTradeInfo(
val price: Amount<Currency>,
val payToIdentity: PartyAndCertificate
)
open class Seller(private val otherSideSession: FlowSession,
private val assetToSell: StateAndRef<OwnableState>,
private val price: Amount<Currency>,
private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
companion object {
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
// DOCSTART 3
object VERIFYING_AND_SIGNING : ProgressTracker.Step("Verifying and signing transaction proposal") {
override fun childProgressTracker() = SignTransactionFlow.tracker()
}
// DOCEND 3
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING_AND_SIGNING)
}
// DOCSTART 4
@Suspendable
override fun call(): SignedTransaction {
progressTracker.currentStep = AWAITING_PROPOSAL
// Make the first message we'll send to kick off the flow.
val hello = SellerTradeInfo(price, myParty)
// What we get back from the other side is a transaction that *might* be valid and acceptable to us,
// but we must check it out thoroughly before we sign!
// SendTransactionFlow allows seller to access our data to resolve the transaction.
subFlow(SendStateAndRefFlow(otherSideSession, listOf(assetToSell)))
otherSideSession.send(hello)
// Verify and sign the transaction.
progressTracker.currentStep = VERIFYING_AND_SIGNING
// Sync identities to ensure we know all of the identities involved in the transaction we're about to
// be asked to sign
subFlow(IdentitySyncFlow.Receive(otherSideSession))
// DOCSTART 5
val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) {
override fun checkTransaction(stx: SignedTransaction) {
// Verify that we know who all the participants in the transaction are
val states: Iterable<ContractState> = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data }
states.forEach { state ->
state.participants.forEach { anon ->
require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) {
"Transaction state $state involves unknown participant $anon"
}
}
}
if (stx.tx.outputStates.sumCashBy(myParty.party).withoutIssuer() != price)
throw FlowException("Transaction is not sending us the right amount of cash")
}
}
val txId = subFlow(signTransactionFlow).id
// DOCEND 5
return waitForLedgerCommit(txId)
}
// DOCEND 4
// Following comment moved here so that it doesn't appear in the docsite:
// There are all sorts of funny games a malicious secondary might play with it sends maybeSTX,
// we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
// express flow state machines on top of the messaging layer.
}
open class Buyer(private val sellerSession: FlowSession,
private val notary: Party,
private val acceptablePrice: Amount<Currency>,
private val typeToBuy: Class<out OwnableState>,
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>) :
this(otherSideSession, notary, acceptablePrice, typeToBuy, true)
// DOCSTART 2
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
object VERIFYING : ProgressTracker.Step("Verifying seller assets")
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties") {
override fun childProgressTracker() = CollectSignaturesFlow.tracker()
}
object RECORDING : ProgressTracker.Step("Recording completed transaction") {
// TODO: Currently triggers a race condition on Team City. See https://github.com/corda/corda/issues/733.
// override fun childProgressTracker() = FinalityFlow.tracker()
}
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING)
// DOCEND 2
// DOCSTART 1
@Suspendable
override fun call(): SignedTransaction {
// Wait for a trade request to come in from the other party.
progressTracker.currentStep = RECEIVING
val (assetForSale, tradeRequest) = receiveAndValidateTradeRequest()
// Create the identity we'll be paying to, and send the counterparty proof we own the identity
val buyerAnonymousIdentity = if (anonymous)
serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
else
ourIdentityAndCert
// Put together a proposed transaction that performs the trade, and sign it.
progressTracker.currentStep = SIGNING
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity)
// Now sign the transaction with whatever keys we need to move the cash.
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
// Sync up confidential identities in the transaction with our counterparty
subFlow(IdentitySyncFlow.Send(sellerSession, ptx.toWireTransaction(serviceHub)))
// Send the signed transaction to the seller, who must then sign it themselves and commit
// it to the ledger by sending it to the notary.
progressTracker.currentStep = COLLECTING_SIGNATURES
val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey))
val twiceSignedTx = partSignedTx + sellerSignature
// Notarise and record the transaction.
progressTracker.currentStep = RECORDING
return subFlow(FinalityFlow(twiceSignedTx))
}
@Suspendable
private fun receiveAndValidateTradeRequest(): Pair<StateAndRef<OwnableState>, SellerTradeInfo> {
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(sellerSession)).single()
return assetForSale to sellerSession.receive<SellerTradeInfo>().unwrap {
progressTracker.currentStep = VERIFYING
// What is the seller trying to sell us?
val asset = assetForSale.state.data
val assetTypeName = asset.javaClass.name
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
// prove the owner is a confidential identity of the counterparty.
val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
require(assetForSaleIdentity == sellerSession.counterparty)
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
// identity, so that anonymity is enforced.
val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) ?: it.payToIdentity
require(wellKnownPayToIdentity.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" }
if (it.price > acceptablePrice)
throw UnacceptablePriceException(it.price)
if (!typeToBuy.isInstance(asset))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
it
}
}
@Suspendable
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo, buyerAnonymousIdentity: PartyAndCertificate): SharedTx {
val ptx = TransactionBuilder(notary)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, ourIdentityAndCert, tradeRequest.payToIdentity.party)
// Add inputs/outputs/a command for the movement of the asset.
tx.addInputState(assetForSale)
val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party)
tx.addOutputState(state, assetForSale.state.contract, assetForSale.state.notary)
tx.addCommand(command, assetForSale.state.data.owner.owningKey)
// We set the transaction's time-window: it may be that none of the contracts need this!
// But it can't hurt to have one.
val currentTime = serviceHub.clock.instant()
tx.setTimeWindow(currentTime, 30.seconds)
return SharedTx(tx, cashSigningPubKeys)
}
// DOCEND 1
data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List<PublicKey>)
}
}

View File

@ -0,0 +1,45 @@
package com.r3.corda.enterprise.perftestcordapp.schemas
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
/**
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
*/
object CashSchema
/**
* First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood
* at the time of writing.
*/
@CordaSerializable
object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
@Entity
@Table(name = "contract_pt_cash_states",
indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
Index(name = "pennies_idx", columnList = "pennies")))
class PersistentCashState(
/** X500Name of owner party **/
@Column(name = "owner_name")
var owner: AbstractParty,
@Column(name = "pennies")
var pennies: Long,
@Column(name = "ccy_code", length = 3)
var currency: String,
@Column(name = "issuer_key")
var issuerParty: String,
@Column(name = "issuer_ref")
var issuerRef: ByteArray
) : PersistentState()
}

View File

@ -0,0 +1,52 @@
package com.r3.corda.enterprise.perftestcordapp.schemas
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import java.time.Instant
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table
/**
* An object used to fully qualify the [CommercialPaperSchema] family name (i.e. independent of version).
*/
object CommercialPaperSchema
/**
* First version of a commercial paper contract ORM schema that maps all fields of the [CommercialPaper] contract state
* as it stood at the time of writing.
*/
@CordaSerializable
object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) {
@Entity
@Table(name = "cp_pt_states",
indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"),
Index(name = "maturity_index", columnList = "maturity_instant"),
Index(name = "face_value_index", columnList = "face_value")))
class PersistentCommercialPaperState(
@Column(name = "issuance_key")
var issuanceParty: String,
@Column(name = "issuance_ref")
var issuanceRef: ByteArray,
@Column(name = "owner_key")
var owner: String,
@Column(name = "maturity_instant")
var maturity: Instant,
@Column(name = "face_value")
var faceValue: Long,
@Column(name = "ccy_code", length = 3)
var currency: String,
@Column(name = "face_value_issuer_key")
var faceValueIssuerParty: String,
@Column(name = "face_value_issuer_ref")
var faceValueIssuerRef: ByteArray
) : PersistentState()
}

View File

@ -0,0 +1,40 @@
package com.r3.corda.enterprise.perftestcordapp.utils
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Amount.Companion.sumOrNull
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.contracts.Amount.Companion.sumOrZero
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.FungibleAsset
import net.corda.core.contracts.Issued
import net.corda.core.identity.AbstractParty
import java.util.*
/**
* Sums the cash states in the list belonging to a single owner, throwing an exception
* if there are none, or if any of the cash states cannot be added together (i.e. are
* different currencies or issuers).
*/
fun Iterable<ContractState>.sumCashBy(owner: AbstractParty): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies or issuers).
*/
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
fun Iterable<ContractState>.sumCashOrNull(): Amount<Issued<Currency>>? = filterIsInstance<Cash.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */
fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Issued<Currency>> {
return filterIsInstance<Cash.State>().map { it.amount }.sumOrZero(currency)
}
/** Sums the asset states in the list, returning null if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)

View File

@ -0,0 +1,2 @@
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl

View File

@ -0,0 +1,302 @@
package com.r3.corda.enterprise.perftestcordapp.contracts
import net.corda.core.contracts.*
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.days
import net.corda.core.utilities.seconds
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.`issued by`
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.*
import net.corda.testing.*
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.fillWithSomeTestCash
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.time.Instant
import java.util.*
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
// TODO: The generate functions aren't tested by these tests: add them.
interface CommercialPaperTestTemplate {
fun getPaper(): CommercialPaper.State
fun getIssueCommand(notary: Party): CommandData
fun getRedeemCommand(notary: Party): CommandData
fun getMoveCommand(): CommandData
fun getContract(): ContractClassName
}
class KotlinCommercialPaperTest : CommercialPaperTestTemplate {
override fun getPaper(): CommercialPaper.State = CommercialPaper.State(
issuance = MEGA_CORP.ref(123),
owner = MEGA_CORP,
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
maturityDate = TEST_TX_TIME + 7.days
)
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
override fun getContract() = CommercialPaper.CP_PROGRAM_ID
}
class KotlinCommercialPaperLegacyTest : CommercialPaperTestTemplate {
override fun getPaper(): CommercialPaper.State = CommercialPaper.State(
issuance = MEGA_CORP.ref(123),
owner = MEGA_CORP,
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
maturityDate = TEST_TX_TIME + 7.days
)
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
override fun getContract() = CommercialPaper.CP_PROGRAM_ID
}
@RunWith(Parameterized::class)
class CommercialPaperTestsGeneric {
companion object {
@Parameterized.Parameters @JvmStatic
fun data() = listOf(KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest())
}
@Parameterized.Parameter
lateinit var thisTest: CommercialPaperTestTemplate
val issuer = MEGA_CORP.ref(123)
@Test
fun `trade lifecycle test`() {
val someProfits = 1200.DOLLARS `issued by` issuer
ledger {
unverifiedTransaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE)
output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP)
}
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
output(thisTest.getContract(), "paper") { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timeWindow(TEST_TX_TIME)
this.verifies()
}
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
// that sounds a bit too good to be true!
transaction("Trade") {
attachments(Cash.PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
output(thisTest.getContract(), "alice's paper") { "paper".output<CommercialPaper.State>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
this.verifies()
}
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
transaction("Redemption") {
attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
input("alice's paper")
input("some profits")
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE }
output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP }
}
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
tweak {
outputs(700.DOLLARS `issued by` issuer)
timeWindow(TEST_TX_TIME + 8.days)
this `fails with` "received amount equals the face value"
}
outputs(1000.DOLLARS `issued by` issuer)
tweak {
timeWindow(TEST_TX_TIME + 2.days)
this `fails with` "must have matured"
}
timeWindow(TEST_TX_TIME + 8.days)
tweak {
output(thisTest.getContract()) { "paper".output<CommercialPaper.State>() }
this `fails with` "must be destroyed"
}
this.verifies()
}
}
}
@Test
fun `key mismatch at issue`() {
transaction {
attachment(CP_PROGRAM_ID)
attachment(CP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper() }
command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timeWindow(TEST_TX_TIME)
this `fails with` "output states are issued by a command signer"
}
}
@Test
fun `face value is not zero`() {
transaction {
attachment(CP_PROGRAM_ID)
attachment(CP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
}
@Test
fun `maturity date not in the past`() {
transaction {
attachment(CP_PROGRAM_ID)
attachment(CP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timeWindow(TEST_TX_TIME)
this `fails with` "maturity date is not in the past"
}
}
@Test
fun `issue cannot replace an existing state`() {
transaction {
attachment(CP_PROGRAM_ID)
attachment(CP_PROGRAM_ID)
input(thisTest.getContract(), thisTest.getPaper())
output(thisTest.getContract()) { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
}
/**
* Unit test requires two separate Database instances to represent each of the two
* transaction participants (enforces uniqueness of vault content in lieu of partipant identity)
*/
private lateinit var bigCorpServices: MockServices
private lateinit var bigCorpVault: Vault<ContractState>
private lateinit var bigCorpVaultService: VaultService
private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault<ContractState>
private val notaryServices = MockServices(DUMMY_NOTARY_KEY)
private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY)
private lateinit var moveTX: SignedTransaction
// @Test
@Ignore
fun `issue move and then redeem`() {
initialiseTestSerialization()
val aliceDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
val databaseAlice = aliceDatabaseAndServices.first
aliceServices = aliceDatabaseAndServices.second
aliceVaultService = aliceServices.vaultService
databaseAlice.transaction {
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
aliceVaultService = aliceServices.vaultService
}
val bigCorpDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
val databaseBigCorp = bigCorpDatabaseAndServices.first
bigCorpServices = bigCorpDatabaseAndServices.second
bigCorpVaultService = bigCorpServices.vaultService
databaseBigCorp.transaction {
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
bigCorpVaultService = bigCorpServices.vaultService
}
// Propagate the cash transactions to each side.
aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
// BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
val issuance = bigCorpServices.myInfo.chooseIdentity().ref(1)
val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY)
issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds)
val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder)
val issueTx = notaryServices.addSignature(issuePtx)
databaseAlice.transaction {
// Alice pays $9000 to BigCorp to own some of their debt.
moveTX = run {
val builder = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)
stx
}
}
databaseBigCorp.transaction {
// Verify the txns are valid and insert into both sides.
listOf(issueTx, moveTX).forEach {
it.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(it)
bigCorpServices.recordTransactions(it)
}
}
databaseBigCorp.transaction {
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
val builder = TransactionBuilder(DUMMY_NOTARY)
builder.setTimeWindow(time, 30.seconds)
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices, bigCorpServices.myInfo.chooseIdentityAndCert())
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)
return Pair(stx, builder.lockId)
}
val redeemTX = makeRedeemTX(TEST_TX_TIME + 10.days)
val tooEarlyRedemption = redeemTX.first
val tooEarlyRedemptionLockId = redeemTX.second
val e = assertFailsWith(TransactionVerificationException::class) {
tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
}
// manually release locks held by this failing transaction
aliceServices.vaultService.softLockRelease(tooEarlyRedemptionLockId)
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days).first
validRedemption.toLedgerTransaction(aliceServices).verify()
// soft lock not released after success either!!! (as transaction not recorded)
}
resetTestSerialization()
}
}

View File

@ -0,0 +1,40 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset
import net.corda.core.utilities.getOrThrow
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.flows.CashException
import com.r3.corda.enterprise.perftestcordapp.flows.CashPaymentFlow
import net.corda.testing.chooseIdentity
import net.corda.testing.node.MockNetwork
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
class CashSelectionH2Test {
@Test
fun `check does not hold connection over retries`() {
val mockNet = MockNetwork(threadPerNode = true)
try {
val notaryNode = mockNet.createNotaryNode()
val bankA = mockNet.createNode(configOverrides = { existingConfig ->
// Tweak connections to be minimal to make this easier (1 results in a hung node during start up, so use 2 connections).
existingConfig.dataSourceProperties.setProperty("maximumPoolSize", "2")
existingConfig
})
mockNet.startNodes()
// Start more cash spends than we have connections. If spend leaks a connection on retry, we will run out of connections.
val flow1 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
} finally {
mockNet.stopNodes()
}
}
}

View File

@ -0,0 +1,901 @@
package com.r3.corda.enterprise.perftestcordapp.contracts.asset
import com.r3.corda.enterprise.perftestcordapp.*
import com.r3.corda.enterprise.perftestcordapp.utils.sumCash
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashBy
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrNull
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
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
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.DummyState
import net.corda.testing.contracts.calculateRandomlySizedAmounts
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.security.KeyPair
import java.util.*
import kotlin.test.*
/**
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
* to the vault. This is intended for unit tests. The cash is issued by [DUMMY_CASH_ISSUER] and owned by the legal
* identity key from the storage service.
*
* The service hub needs to provide at least a key management service and a storage service.
*
* @param issuerServices service hub of the issuer node, which will be used to sign the transaction.
* @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary.
* @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!).
*/
fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
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<Cash.State> {
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<SignedTransaction> = amounts.map { pennies ->
val issuance = TransactionBuilder(null as Party?)
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
}
recordTransactions(transactions)
// Get all the StateRefs of all the generated transactions.
val states = transactions.flatMap { stx ->
stx.tx.outputs.indices.map { i -> stx.tx.outRef<Cash.State>(i) }
}
return Vault(states)
}
class CashTests : TestDependencyInjectionBase() {
private val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
private val defaultIssuer = MEGA_CORP.ref(defaultRef)
private val inState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = AnonymousParty(ALICE_PUBKEY)
)
// Input state held by the issuer
private val issuerInState = inState.copy(owner = defaultIssuer.party)
private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
private fun Cash.State.editDepositRef(ref: Byte) = copy(
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
)
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<StateAndRef<Cash.State>>
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), MEGA_CORP_KEY)
val databaseAndServices = makeTestDatabaseAndMockServices(cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
database = databaseAndServices.first
miniCorpServices = databaseAndServices.second
// 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)
}
database.transaction {
vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy<Cash.State>().states
}
resetTestSerialization()
}
@After
fun tearDown() {
database.close()
}
@Test
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() }
this `fails with` "the amounts balance"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { DummyCommandData }
// Invalid command
this `fails with` "required com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash.Commands.Move command"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
command(BOB_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the owning keys are a subset of the signing keys"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "at least one cash input"
}
// Simple reallocation works.
tweak {
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
}
}
@Test
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"
}
}
@Test
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 {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "output states are issued by a command signer"
}
transaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID) {
Cash.State(
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
owner = AnonymousParty(ALICE_PUBKEY)
)
}
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies()
}
}
@Test
fun generateIssueRaw() {
initialiseTestSerialization()
// 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)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
val s = tx.outputsOfType<Cash.State>().single()
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party)
assertEquals(AnonymousParty(ALICE_PUBKEY), s.owner)
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
}
@Test
fun generateIssueFromAmount() {
initialiseTestSerialization()
// Test issuance from an issued amount
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
Cash().generateIssue(this, amount, owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
assertEquals(tx.outputs[0], tx.outputs[0])
}
@Test
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() }
this `fails with` "the amounts balance"
}
// Issue works.
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies()
}
}
// Can't use an issue command to lower the amount.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "there is only a single issue command"
}
this.verifies()
}
}
/**
* Test that the issuance builder rejects building into a transaction with existing
* cash inputs.
*/
@Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() {
initialiseTestSerialization()
// Issue some cash
var ptx = TransactionBuilder(DUMMY_NOTARY)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
val tx = miniCorpServices.signInitialTransaction(ptx)
// Include the previously issued cash in a new issuance command
ptx = TransactionBuilder(DUMMY_NOTARY)
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)
}
@Test
fun testMergeSplit() {
// Splitting value works.
transaction {
attachment(Cash.PROGRAM_ID)
command(ALICE_PUBKEY) { Cash.Commands.Move() }
tweak {
input(Cash.PROGRAM_ID) { inState }
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) output(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) input(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
for (i in 0..1) output(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
for (i in 0..1) input(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
output(Cash.PROGRAM_ID) { inState }
this.verifies()
}
}
}
@Test
fun zeroSizedValues() {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "zero sized inputs"
}
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "zero sized outputs"
}
}
@Test
fun trivialMismatches() {
// Can't change issuer.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't change deposit reference when splitting.
transaction {
attachment(Cash.PROGRAM_ID)
val splits2 = inState.amount.splitEvenly(2)
input(Cash.PROGRAM_ID) { inState }
for (i in 0..1) output(Cash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't mix currencies.
transaction {
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) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) {
inState.copy(
amount = 150.POUNDS `issued by` defaultIssuer,
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"
}
// Can't have superfluous input states from different issuers.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't combine two different deposits at the same issuer.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState.editDepositRef(3) }
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "for reference [01]"
}
}
@Test
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() }
this `fails with` "the amounts balance"
}
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "required com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash.Commands.Move command"
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
}
}
}
@Test
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(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()
}
}
@Test
fun `exit cash not held by its issuer`() {
// Single input/output straightforward case.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
}
@Test
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) }
this `fails with` "the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
this `fails with` "the amounts balance"
}
// This works.
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP }
this.verifies()
}
}
@Test
fun multiCurrency() {
// Check we can do an atomic currency trade tx.
transaction {
attachment(Cash.PROGRAM_ID)
val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) }
input(Cash.PROGRAM_ID) { pounds }
output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) }
output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) }
command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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) =
StateAndRef(
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), 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.
*/
private fun makeExit(serviceHub: ServiceHub, amount: Amount<Currency>, issuer: Party, depositRef: Byte = 1): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party
Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo)
return tx.toWireTransaction(serviceHub)
}
private fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction {
Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest)
}
return tx.toWireTransaction(miniCorpServices)
}
/**
* Try exiting an amount which matches a single state.
*/
@Test
fun generateSimpleExit() {
initialiseTestSerialization()
val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size)
val expectedMove = Cash.Commands.Move()
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
}
/**
* Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
*/
@Test
fun generatePartialExit() {
initialiseTestSerialization()
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
val expectedInputs = WALLET.filter { it.ref == actualInput }
assertEquals(1, expectedInputs.size)
val inputState = expectedInputs.single()
val actualChange = wtx.outputs.single().data as Cash.State
val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity
val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
assertEquals(expectedChange, wtx.getOutput(0))
}
/**
* Try exiting a currency we don't have.
*/
@Test
fun generateAbsentExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
}
/**
* Try exiting with a reference mis-match.
*/
@Test
fun generateInvalidReferenceExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
}
/**
* Try exiting an amount greater than the maximum available.
*/
@Test
fun generateInsufficientExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
}
/**
* Try exiting for an owner with no states
*/
@Test
fun generateOwnerWithNoStatesExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
}
/**
* Try exiting when vault is empty
*/
@Test
fun generateExitWithEmptyVault() {
initialiseTestSerialization()
assertFailsWith<IllegalArgumentException> {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1)
}
}
@Test
fun generateSimpleDirectSpend() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
}
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])
}
}
@Test
fun generateSimpleSpendWithParties() {
initialiseTestSerialization()
database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP))
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
}
}
@Test
fun generateSimpleSpendWithChange() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
val vaultState = vaultStatesUnconsumed.elementAt(0)
val changeAmount = 90.DOLLARS `issued by` defaultIssuer
val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state ->
if (state is Cash.State) {
state.amount == changeAmount
} else {
false
}
}
val changeOwner = (likelyChangeState as Cash.State).owner
assertEquals(1, miniCorpServices.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(amount = changeAmount, owner = changeOwner), wtx.outputs[1].data)
assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendWithTwoInputs() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
}
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])
}
}
@Test
fun generateSpendMixedDeposits() {
initialiseTestSerialization()
val wtx =
database.transaction {
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
assertEquals(3, wtx.inputs.size)
wtx
}
database.transaction {
val vaultState0: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(0)
val vaultState1: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(1)
val vaultState2: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(2)
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])
}
}
@Test
fun generateSpendInsufficientBalance() {
initialiseTestSerialization()
database.transaction {
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_IDENTITY_1)
}
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
assertFailsWith(InsufficientBalanceException::class) {
makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1)
}
}
}
/**
* Confirm that aggregation of states is correctly modelled.
*/
@Test
fun aggregation() {
val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP)
val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP)
val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP)
// Obviously it must be possible to aggregate states with themselves
assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)
// Owner is not considered when calculating whether it is possible to aggregate states
assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token)
// States cannot be aggregated if the deposit differs
assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
// States cannot be aggregated if the currency differs
assertNotEquals(oneThousandDollarsFromMini.amount.token,
Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token)
// States cannot be aggregated if the reference differs
assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token)
assertNotEquals((fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
}
@Test
fun `summing by owner`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP))
}
@Test(expected = UnsupportedOperationException::class)
fun `summing by owner throws`() {
val states = listOf(
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
states.sumCashBy(MINI_CORP)
}
@Test
fun `summing no currencies`() {
val states = emptyList<Cash.State>()
assertEquals(0.POUNDS `issued by` defaultIssuer, states.sumCashOrZero(GBP `issued by` defaultIssuer))
assertNull(states.sumCashOrNull())
}
@Test(expected = UnsupportedOperationException::class)
fun `summing no currencies throws`() {
val states = emptyList<Cash.State>()
states.sumCash()
}
@Test
fun `summing a single currency`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
// Test that summing everything produces the total number of dollars
val expected = 7000.DOLLARS `issued by` defaultIssuer
val actual = states.sumCash()
assertEquals(expected, actual)
}
@Test(expected = IllegalArgumentException::class)
fun `summing multiple currencies`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP)
)
// Test that summing everything fails because we're mixing units
states.sumCash()
}
// Double spend.
@Test
fun chainCashDoubleSpendFailsWith() {
val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), MEGA_CORP_KEY)
ledger(mockService) {
unverifiedTransaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP
)
}
}
transaction {
attachment(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
tweak {
transaction {
attachment(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
// We send it to another pubkey so that the transaction is not identical to the previous one
output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = ALICE))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
this.fails()
}
this.verifies()
}
}
@Test
fun multiSpend() {
initialiseTestSerialization()
val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction {
val payments = listOf(
PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS),
PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS)
)
Cash.generateSpend(miniCorpServices, tx, payments)
}
val wtx = tx.toWireTransaction(miniCorpServices)
fun out(i: Int) = wtx.getOutput(i) as Cash.State
assertEquals(4, wtx.outputs.size)
assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
assertEquals(MINI_CORP, out(0).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(1).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(2).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(3).amount.token.issuer.party)
}
}

View File

@ -0,0 +1,73 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.`issued by`
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
import net.corda.testing.BOC
import net.corda.testing.chooseIdentity
import net.corda.testing.getDefaultNotary
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class CashExitFlowTests {
private lateinit var mockNet : MockNetwork
private val initialBalance = 2000.DOLLARS
private val ref = OpaqueBytes.of(0x01)
private lateinit var bankOfCordaNode: StartedNode<MockNode>
private lateinit var bankOfCorda: Party
private lateinit var notaryNode: StartedNode<MockNode>
private lateinit var notary: Party
@Before
fun start() {
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
notaryNode = mockNet.createNotaryNode()
bankOfCordaNode = mockNet.createPartyNode(BOC.name)
notary = notaryNode.services.getDefaultNotary()
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
mockNet.runNetwork()
notary = bankOfCordaNode.services.getDefaultNotary()
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
mockNet.runNetwork()
future.getOrThrow()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `exit some cash`() {
val exitAmount = 500.DOLLARS
val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture
mockNet.runNetwork()
val exitTx = future.getOrThrow().stx.tx
val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
assertEquals(1, exitTx.inputs.size)
assertEquals(1, exitTx.outputs.size)
val output = exitTx.outputsOfType<Cash.State>().single()
assertEquals(expected, output.amount)
}
@Test
fun `exit zero cash`() {
val expected = 0.DOLLARS
val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture
mockNet.runNetwork()
assertFailsWith<CashException> {
future.getOrThrow()
}
}
}

View File

@ -0,0 +1,65 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.`issued by`
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import net.corda.node.internal.StartedNode
import net.corda.testing.chooseIdentity
import net.corda.testing.getDefaultNotary
import net.corda.testing.BOC
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class CashIssueFlowTests {
private lateinit var mockNet : MockNetwork
private lateinit var bankOfCordaNode: StartedNode<MockNode>
private lateinit var bankOfCorda: Party
private lateinit var notaryNode: StartedNode<MockNode>
private lateinit var notary: Party
@Before
fun start() {
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
notaryNode = mockNet.createNotaryNode()
bankOfCordaNode = mockNet.createPartyNode(BOC.name)
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
notary = notaryNode.services.getDefaultNotary()
mockNet.runNetwork()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `issue some cash`() {
val expected = 500.DOLLARS
val ref = OpaqueBytes.of(0x01)
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
mockNet.runNetwork()
val issueTx = future.getOrThrow().stx
val output = issueTx.tx.outputsOfType<Cash.State>().single()
assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
}
@Test
fun `issue zero cash`() {
val expected = 0.DOLLARS
val ref = OpaqueBytes.of(0x01)
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> {
future.getOrThrow()
}
}
}

View File

@ -0,0 +1,112 @@
package com.r3.corda.enterprise.perftestcordapp.flows
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.`issued by`
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.node.services.trackBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
import net.corda.testing.*
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class CashPaymentFlowTests {
private lateinit var mockNet : MockNetwork
private val initialBalance = 2000.DOLLARS
private val ref = OpaqueBytes.of(0x01)
private lateinit var bankOfCordaNode: StartedNode<MockNode>
private lateinit var bankOfCorda: Party
private lateinit var notaryNode: StartedNode<MockNode>
private lateinit var notary: Party
@Before
fun start() {
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
notaryNode = mockNet.createNotaryNode()
bankOfCordaNode = mockNet.createPartyNode(BOC.name)
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
notary = notaryNode.services.getDefaultNotary()
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
mockNet.runNetwork()
future.getOrThrow()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `pay some cash`() {
val payTo = notaryNode.info.chooseIdentity()
val expectedPayment = 500.DOLLARS
val expectedChange = 1500.DOLLARS
bankOfCordaNode.database.transaction {
// Register for vault updates
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy<Cash.State>(criteria)
val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy<Cash.State>(criteria)
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment,
payTo)).resultFuture
mockNet.runNetwork()
future.getOrThrow()
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
vaultUpdatesBoc.expectEvents {
expect { update ->
require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
val changeState = update.produced.single().state.data
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
}
}
// Check notary node vault updates
vaultUpdatesBankClient.expectEvents {
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.size == 1) { produced.size }
val paymentState = produced.single().state.data
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
}
}
}
}
@Test
fun `pay more than we have`() {
val payTo = notaryNode.info.chooseIdentity()
val expected = 4000.DOLLARS
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
payTo)).resultFuture
mockNet.runNetwork()
assertFailsWith<CashException> {
future.getOrThrow()
}
}
@Test
fun `pay zero cash`() {
val payTo = notaryNode.info.chooseIdentity()
val expected = 0.DOLLARS
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
payTo)).resultFuture
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> {
future.getOrThrow()
}
}
}

View File

@ -0,0 +1,790 @@
package com.r3.corda.enterprise.perftestcordapp.flows
// NB: Unlike the other flow tests in this package, this is not originally copied from net.corda.finance, but
// from net.corda.node.messaging
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.rootCause
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.days
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.unwrap
import com.r3.corda.enterprise.perftestcordapp.DOLLARS
import com.r3.corda.enterprise.perftestcordapp.`issued by`
import com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.CASH
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
import com.r3.corda.enterprise.perftestcordapp.flows.TwoPartyTradeFlow.Buyer
import com.r3.corda.enterprise.perftestcordapp.flows.TwoPartyTradeFlow.Seller
import net.corda.node.internal.StartedNode
import net.corda.node.services.api.WritableTransactionStorage
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.utilities.CordaPersistence
import net.corda.nodeapi.internal.ServiceInfo
import net.corda.testing.*
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.fillWithSomeTestCash
import net.corda.node.services.api.Checkpoint
import net.corda.node.services.api.CheckpointStorage
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices
import net.corda.testing.node.pumpReceive
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import rx.Observable
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.KeyPair
import java.util.*
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
/**
* Copied from DBCheckpointStorageTests as it is required as helper for this test
*/
internal fun CheckpointStorage.checkpoints(): List<Checkpoint> {
val checkpoints = mutableListOf<Checkpoint>()
forEach {
checkpoints += it
true
}
return checkpoints
}
/**
* In this example, Alice wishes to sell her commercial paper to Bob in return for $1,000,000 and they wish to do
* it on the ledger atomically. Therefore they must work together to build a transaction.
*
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
*/
@RunWith(Parameterized::class)
class TwoPartyTradeFlowTests(val anonymous: Boolean) {
companion object {
private val cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts")
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Boolean> {
return listOf(true, false)
}
}
private lateinit var mockNet: MockNetwork
@Before
fun before() {
LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap")
}
@After
fun after() {
mockNet.stopNodes()
LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap")
}
@Test
fun `trade cash for commercial paper`() {
// We run this in parallel threads to help catch any race conditions that may exist. The other tests
// we run in the unit test thread exclusively to speed things up, ensure deterministic results and
// allow interruption half way through.
mockNet = MockNetwork(false, true, cordappPackages = cordappPackages)
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
val notaryNode = mockNet.createNotaryNode()
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bankNode = mockNet.createPartyNode(BOC_NAME)
val alice = aliceNode.info.singleIdentity()
val bank = bankNode.info.singleIdentity()
val notary = notaryNode.services.getDefaultNotary()
val cashIssuer = bank.ref(1)
val cpIssuer = bank.ref(1, 2, 3)
aliceNode.internals.disableDBCloseOnStop()
bobNode.internals.disableDBCloseOnStop()
bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
issuedBy = cashIssuer)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, cpIssuer, alice,
1200.DOLLARS `issued by` bank.ref(0), null, notary).second
}
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
"alice's paper".outputStateAndRef())
// TODO: Verify that the result was inserted into the transaction database.
// assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id])
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
aliceNode.dispose()
bobNode.dispose()
aliceNode.database.transaction {
assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
}
aliceNode.internals.manuallyCloseDB()
bobNode.database.transaction {
assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
}
bobNode.internals.manuallyCloseDB()
}
}
@Test(expected = InsufficientBalanceException::class)
fun `trade cash for commercial paper fails using soft locking`() {
mockNet = MockNetwork(false, true, cordappPackages = cordappPackages)
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
val notaryNode = mockNet.createNotaryNode()
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bankNode = mockNet.createPartyNode(BOC_NAME)
val alice = aliceNode.info.singleIdentity()
val bank = bankNode.info.singleIdentity()
val issuer = bank.ref(1)
val notary = aliceNode.services.getDefaultNotary()
aliceNode.internals.disableDBCloseOnStop()
bobNode.internals.disableDBCloseOnStop()
val cashStates = bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3,
issuedBy = issuer)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, issuer, alice,
1200.DOLLARS `issued by` bank.ref(0), null, notary).second
}
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val cashLockId = UUID.randomUUID()
bobNode.database.transaction {
// lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states)
val refs = cashStates.states.map { it.ref }
if (refs.isNotEmpty()) {
bobNode.services.vaultService.softLockReserve(cashLockId, refs.toNonEmptySet())
}
}
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
"alice's paper".outputStateAndRef())
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
aliceNode.dispose()
bobNode.dispose()
aliceNode.database.transaction {
assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
}
aliceNode.internals.manuallyCloseDB()
bobNode.database.transaction {
assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
}
bobNode.internals.manuallyCloseDB()
}
}
@Test
fun `shutdown and restore`() {
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
val notaryNode = mockNet.createNotaryNode()
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
var bobNode = mockNet.createPartyNode(BOB_NAME)
val bankNode = mockNet.createPartyNode(BOC_NAME)
aliceNode.internals.disableDBCloseOnStop()
bobNode.internals.disableDBCloseOnStop()
val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle
mockNet.runNetwork() // Clear network map registration messages
val notary = notaryNode.services.getDefaultNotary()
val alice = aliceNode.info.singleIdentity()
val bank = bankNode.info.singleIdentity()
val issuer = bank.ref(1, 2, 3)
bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
issuedBy = issuer)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, issuer, alice,
1200.DOLLARS `issued by` bank.ref(0), null, notary).second
}
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val aliceFuture = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult
// Everything is on this thread so we can now step through the flow one step at a time.
// Seller Alice already sent a message to Buyer Bob. Pump once:
bobNode.pumpReceive()
// Bob sends a couple of queries for the dependencies back to Alice. Alice reponds.
aliceNode.pumpReceive()
bobNode.pumpReceive()
aliceNode.pumpReceive()
bobNode.pumpReceive()
aliceNode.pumpReceive()
bobNode.pumpReceive()
// OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
bobNode.database.transaction {
assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1)
}
val storage = bobNode.services.validatedTransactions
val bobTransactionsBeforeCrash = bobNode.database.transaction {
(storage as DBTransactionStorage).transactions
}
assertThat(bobTransactionsBeforeCrash).isNotEmpty
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
bobNode.dispose()
// Alice doesn't know that and carries on: she wants to know about the cash transactions he's trying to use.
// She will wait around until Bob comes back.
assertThat(aliceNode.pumpReceive()).isNotNull()
// FIXME: Knowledge of confidential identities is lost on node shutdown, so Bob's node now refuses to sign the
// transaction because it has no idea who the parties are.
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
// that Bob was waiting on before the reboot occurred.
bobNode = mockNet.createNode(bobAddr.id, object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?, entropyRoot: BigInteger): MockNetwork.MockNode {
return MockNetwork.MockNode(config, network, networkMapAddr, bobAddr.id, notaryIdentity, entropyRoot)
}
}, BOB_NAME)
// Find the future representing the result of this state machine again.
val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second
// And off we go again.
mockNet.runNetwork()
// Bob is now finished and has the same transaction as Alice.
assertThat(bobFuture.getOrThrow()).isEqualTo(aliceFuture.getOrThrow())
assertThat(bobNode.smm.findStateMachines(Buyer::class.java)).isEmpty()
bobNode.database.transaction {
assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
}
aliceNode.database.transaction {
assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
}
bobNode.database.transaction {
val restoredBobTransactions = bobTransactionsBeforeCrash.filter {
bobNode.services.validatedTransactions.getTransaction(it.id) != null
}
assertThat(restoredBobTransactions).containsAll(bobTransactionsBeforeCrash)
}
aliceNode.internals.manuallyCloseDB()
bobNode.internals.manuallyCloseDB()
}
}
// Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order
// of gets and puts.
private fun makeNodeWithTracking(name: CordaX500Name): StartedNode<MockNetwork.MockNode> {
// Create a node in the mock network ...
return mockNet.createNode(nodeFactory = object : MockNetwork.Factory<MockNetwork.MockNode> {
override fun create(config: NodeConfiguration,
network: MockNetwork,
networkMapAddr: SingleMessageRecipient?,
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?,
entropyRoot: BigInteger): MockNetwork.MockNode {
return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) {
// That constructs a recording tx storage
override fun makeTransactionStorage(): WritableTransactionStorage {
return RecordingTransactionStorage(database, super.makeTransactionStorage())
}
}
}
}, legalName = name)
}
@Test
fun `check dependencies of sale asset are resolved`() {
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
val notaryNode = mockNet.createNotaryNode()
val aliceNode = makeNodeWithTracking(ALICE_NAME)
val bobNode = makeNodeWithTracking(BOB_NAME)
val bankNode = makeNodeWithTracking(BOC_NAME)
mockNet.runNetwork()
notaryNode.internals.ensureRegistered()
val notary = aliceNode.services.getDefaultNotary()
val alice = aliceNode.info.singleIdentity()
val bob = bobNode.info.singleIdentity()
val bank = bankNode.info.singleIdentity()
val issuer = bank.ref(1, 2, 3)
ledger(aliceNode.services, initialiseSerialization = false) {
// Insert a prospectus type attachment into the commercial paper transaction.
val stream = ByteArrayOutputStream()
JarOutputStream(stream).use {
it.putNextEntry(ZipEntry("Prospectus.txt"))
it.write("Our commercial paper is top notch stuff".toByteArray())
it.closeEntry()
}
val attachmentID = aliceNode.database.transaction {
attachment(ByteArrayInputStream(stream.toByteArray()))
}
val bobsFakeCash = bobNode.database.transaction {
fillUpForBuyer(false, issuer, AnonymousParty(bob.owningKey), notary)
}.second
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, issuer, alice,
1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second
}
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages
runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
mockNet.runNetwork()
run {
val records = (bobNode.services.validatedTransactions as RecordingTransactionStorage).records
// Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice.
records.expectEvents(isStrict = false) {
sequence(
// Buyer Bob is told about Alice's commercial paper, but doesn't know it ..
expect(TxRecord.Get(alicesFakePaper[0].id)),
// He asks and gets the tx, validates it, sees it's a self issue with no dependencies, stores.
expect(TxRecord.Add(alicesSignedTxns.values.first())),
// Alice gets Bob's proposed transaction and doesn't know his two cash states. She asks, Bob answers.
expect(TxRecord.Get(bobsFakeCash[1].id)),
expect(TxRecord.Get(bobsFakeCash[2].id)),
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know. She asks, Bob answers.
expect(TxRecord.Get(bobsFakeCash[0].id))
)
}
// Bob has downloaded the attachment.
bobNode.database.transaction {
bobNode.services.attachments.openAttachment(attachmentID)!!.openAsJAR().use {
it.nextJarEntry
val contents = it.reader().readText()
assertTrue(contents.contains("Our commercial paper is top notch stuff"))
}
}
}
// And from Alice's perspective ...
run {
val records = (aliceNode.services.validatedTransactions as RecordingTransactionStorage).records
records.expectEvents(isStrict = false) {
sequence(
// Seller Alice sends her seller info to Bob, who wants to check the asset for sale.
// He requests, Alice looks up in her DB to send the tx to Bob
expect(TxRecord.Get(alicesFakePaper[0].id)),
// Seller Alice gets a proposed tx which depends on Bob's two cash txns and her own tx.
expect(TxRecord.Get(bobsFakeCash[1].id)),
expect(TxRecord.Get(bobsFakeCash[2].id)),
expect(TxRecord.Get(alicesFakePaper[0].id)),
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know.
expect(TxRecord.Get(bobsFakeCash[0].id)),
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
// Bob's transactions are valid, so she commits to the database
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!)),
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!)),
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!)),
// Now she verifies the transaction is contract-valid (not signature valid) which means
// looking up the states again.
expect(TxRecord.Get(bobsFakeCash[1].id)),
expect(TxRecord.Get(bobsFakeCash[2].id)),
expect(TxRecord.Get(alicesFakePaper[0].id)),
// Alice needs to look up the input states to find out which Notary they point to
expect(TxRecord.Get(bobsFakeCash[1].id)),
expect(TxRecord.Get(bobsFakeCash[2].id)),
expect(TxRecord.Get(alicesFakePaper[0].id))
)
}
}
}
}
@Test
fun `track works`() {
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
val notaryNode = mockNet.createNotaryNode()
val aliceNode = makeNodeWithTracking(ALICE_NAME)
val bobNode = makeNodeWithTracking(BOB_NAME)
val bankNode = makeNodeWithTracking(BOC_NAME)
mockNet.runNetwork()
notaryNode.internals.ensureRegistered()
val notary = aliceNode.services.getDefaultNotary()
val alice: Party = aliceNode.info.singleIdentity()
val bank: Party = bankNode.info.singleIdentity()
val issuer = bank.ref(1, 2, 3)
ledger(aliceNode.services, initialiseSerialization = false) {
// Insert a prospectus type attachment into the commercial paper transaction.
val stream = ByteArrayOutputStream()
JarOutputStream(stream).use {
it.putNextEntry(ZipEntry("Prospectus.txt"))
it.write("Our commercial paper is top notch stuff".toByteArray())
it.closeEntry()
}
val attachmentID = aliceNode.database.transaction {
attachment(ByteArrayInputStream(stream.toByteArray()))
}
val bobsKey = bobNode.services.keyManagementService.keys.single()
val bobsFakeCash = bobNode.database.transaction {
fillUpForBuyer(false, issuer, AnonymousParty(bobsKey), notary)
}.second
insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, issuer, alice,
1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second
}
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages
val aliceTxStream = aliceNode.services.validatedTransactions.track().updates
val aliceTxMappings = with(aliceNode) {
database.transaction { services.stateMachineRecordedTransactionMapping.track().updates }
}
val aliceSmId = runBuyerAndSeller(notary, aliceNode, bobNode,
"alice's paper".outputStateAndRef()).sellerId
mockNet.runNetwork()
// We need to declare this here, if we do it inside [expectEvents] kotlin throws an internal compiler error(!).
val aliceTxExpectations = sequence(
expect { tx: SignedTransaction ->
require(tx.id == bobsFakeCash[0].id)
},
expect { tx: SignedTransaction ->
require(tx.id == bobsFakeCash[2].id)
},
expect { tx: SignedTransaction ->
require(tx.id == bobsFakeCash[1].id)
}
)
aliceTxStream.expectEvents { aliceTxExpectations }
val aliceMappingExpectations = sequence(
expect<StateMachineTransactionMapping> { (stateMachineRunId, transactionId) ->
require(stateMachineRunId == aliceSmId)
require(transactionId == bobsFakeCash[0].id)
},
expect { (stateMachineRunId, transactionId) ->
require(stateMachineRunId == aliceSmId)
require(transactionId == bobsFakeCash[2].id)
},
expect { (stateMachineRunId, transactionId) ->
require(stateMachineRunId == aliceSmId)
require(transactionId == bobsFakeCash[1].id)
}
)
aliceTxMappings.expectEvents { aliceMappingExpectations }
}
}
@Test
fun `dependency with error on buyer side`() {
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
runWithError(true, false, "at least one cash input")
}
}
@Test
fun `dependency with error on seller side`() {
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
runWithError(false, true, "Issuances have a time-window")
}
}
private data class RunResult(
// The buyer is not created immediately, only when the seller starts running
val buyer: CordaFuture<FlowStateMachine<*>>,
val sellerResult: CordaFuture<SignedTransaction>,
val sellerId: StateMachineRunId
)
private fun runBuyerAndSeller(notary: Party,
sellerNode: StartedNode<MockNetwork.MockNode>,
buyerNode: StartedNode<MockNetwork.MockNode>,
assetToSell: StateAndRef<OwnableState>): RunResult {
val buyerFlows: Observable<out FlowLogic<*>> = buyerNode.internals.registerInitiatedFlow(BuyerAcceptor::class.java)
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
val seller = SellerInitiator(buyerNode.info.chooseIdentity(), notary, assetToSell, 1000.DOLLARS, anonymous)
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
}
@InitiatingFlow
class SellerInitiator(private val buyer: Party,
private val notary: Party,
private val assetToSell: StateAndRef<OwnableState>,
private val price: Amount<Currency>,
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val myPartyAndCert = if (anonymous) {
serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
} else {
ourIdentityAndCert
}
val buyerSession = initiateFlow(buyer)
buyerSession.send(TestTx(notary, price, anonymous))
return subFlow(Seller(
buyerSession,
assetToSell,
price,
myPartyAndCert))
}
}
@InitiatedBy(SellerInitiator::class)
class BuyerAcceptor(private val sellerSession: FlowSession) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val (notary, price, anonymous) = sellerSession.receive<TestTx>().unwrap {
require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" }
it
}
return subFlow(Buyer(sellerSession, notary, price, CommercialPaper.State::class.java, anonymous))
}
}
@CordaSerializable
data class TestTx(val notaryIdentity: Party, val price: Amount<Currency>, val anonymous: Boolean)
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
bobError: Boolean,
aliceError: Boolean,
expectedMessageSubstring: String
) {
val notaryNode = mockNet.createNotaryNode()
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
val bankNode = mockNet.createPartyNode(BOC_NAME)
mockNet.runNetwork()
notaryNode.internals.ensureRegistered()
val notary = aliceNode.services.getDefaultNotary()
val alice = aliceNode.info.singleIdentity()
val bob = bobNode.info.singleIdentity()
val bank = bankNode.info.singleIdentity()
val issuer = bank.ref(1, 2, 3)
val bobsBadCash = bobNode.database.transaction {
fillUpForBuyer(bobError, issuer, bob, notary).second
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(aliceError, issuer, alice,1200.DOLLARS `issued by` issuer, null, notary).second
}
insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode)
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
mockNet.runNetwork()
val e = assertFailsWith<TransactionVerificationException> {
if (bobError)
aliceResult.getOrThrow()
else
bobStateMachine.getOrThrow().resultFuture.getOrThrow()
}
val underlyingMessage = e.rootCause.message!!
if (expectedMessageSubstring !in underlyingMessage) {
assertEquals(expectedMessageSubstring, underlyingMessage)
}
}
private fun insertFakeTransactions(
wtxToSign: List<WireTransaction>,
node: StartedNode<*>,
notaryNode: StartedNode<*>,
vararg extraSigningNodes: StartedNode<*>): Map<SecureHash, SignedTransaction> {
val signed = wtxToSign.map {
val id = it.id
val sigs = mutableListOf<TransactionSignature>()
val nodeKey = node.info.chooseIdentity().owningKey
sigs.add(node.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey))
sigs.add(notaryNode.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1,
Crypto.findSignatureScheme(notaryNode.info.legalIdentities[1].owningKey).schemeNumberID)), notaryNode.info.legalIdentities[1].owningKey))
extraSigningNodes.forEach { currentNode ->
sigs.add(currentNode.services.keyManagementService.sign(
SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(currentNode.info.chooseIdentity().owningKey).schemeNumberID)),
currentNode.info.chooseIdentity().owningKey)
)
}
SignedTransaction(it, sigs)
}
return node.database.transaction {
node.services.recordTransactions(signed)
val validatedTransactions = node.services.validatedTransactions
if (validatedTransactions is RecordingTransactionStorage) {
validatedTransactions.records.clear()
}
signed.associateBy { it.id }
}
}
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
withError: Boolean,
issuer: PartyAndReference,
owner: AbstractParty,
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
val interimOwner = issuer.party
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
// wants to sell to Bob.
val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
// Issued money to itself.
output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy interimOwner }
output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH issuedBy issuer ownedBy interimOwner }
if (!withError) {
command(issuer.party.owningKey) { Cash.Commands.Issue() }
} else {
// Put a broken command on so at least a signature is created
command(issuer.party.owningKey) { Cash.Commands.Move() }
}
timeWindow(TEST_TX_TIME)
if (withError) {
this.fails()
} else {
this.verifies()
}
}
// Bob gets some cash onto the ledger from BoE
val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
input("elbonian money 1")
output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy owner }
command(interimOwner.owningKey) { Cash.Commands.Move() }
this.verifies()
}
val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
input("elbonian money 2")
output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH issuedBy issuer ownedBy owner }
output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } // Change output.
command(interimOwner.owningKey) { Cash.Commands.Move() }
this.verifies()
}
val vault = Vault<ContractState>(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef()))
return Pair(vault, listOf(eb1, bc1, bc2))
}
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller(
withError: Boolean,
issuer: PartyAndReference,
owner: AbstractParty,
amount: Amount<Issued<Currency>>,
attachmentID: SecureHash?,
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", notary = notary) {
CommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)
}
command(issuer.party.owningKey) { CommercialPaper.Commands.Issue() }
if (!withError)
timeWindow(time = TEST_TX_TIME)
if (attachmentID != null)
attachment(attachmentID)
if (withError) {
this.fails()
} else {
this.verifies()
}
}
val vault = Vault<ContractState>(listOf("alice's paper".outputStateAndRef()))
return Pair(vault, listOf(ap))
}
class RecordingTransactionStorage(val database: CordaPersistence, val delegate: WritableTransactionStorage) : WritableTransactionStorage, SingletonSerializeAsToken() {
override fun track(): DataFeed<List<SignedTransaction>, SignedTransaction> {
return database.transaction {
delegate.track()
}
}
val records: MutableList<TxRecord> = Collections.synchronizedList(ArrayList<TxRecord>())
override val updates: Observable<SignedTransaction>
get() = delegate.updates
override fun addTransaction(transaction: SignedTransaction): Boolean {
database.transaction {
records.add(TxRecord.Add(transaction))
delegate.addTransaction(transaction)
}
return true
}
override fun getTransaction(id: SecureHash): SignedTransaction? {
return database.transaction {
records.add(TxRecord.Get(id))
delegate.getTransaction(id)
}
}
}
interface TxRecord {
data class Add(val transaction: SignedTransaction) : TxRecord
data class Get(val id: SecureHash) : TxRecord
}
}

View File

@ -47,4 +47,5 @@ include 'doorman'
include 'verify-enclave'
include 'sgx-jvm/hsm-tool'
include 'signing-server'
include 'perftestcordapp'