diff --git a/.idea/compiler.xml b/.idea/compiler.xml index bfeff657cb..1bd2adab38 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -86,6 +86,8 @@ + + diff --git a/perftestcordapp/build.gradle b/perftestcordapp/build.gradle new file mode 100644 index 0000000000..ba8c4a72b0 --- /dev/null +++ b/perftestcordapp/build.gradle @@ -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 +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt new file mode 100644 index 0000000000..29c8d1a5ed --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt @@ -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 AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) +fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) +fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) +fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) +fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) +fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) + +val Int.DOLLARS: Amount get() = DOLLARS(this) +val Double.DOLLARS: Amount get() = DOLLARS(this) +val Int.POUNDS: Amount get() = POUNDS(this) +val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) + +infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) +infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) + diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt new file mode 100644 index 0000000000..c8ec2eb1fd --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt @@ -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>, + 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>): State = copy(faceValue = newFaceValue) + fun withMaturityDate(newMaturityDate: Instant): State = copy(maturityDate = newMaturityDate) + + /** Object Relational Mapping support. */ + override fun supportedSchemas(): Iterable = 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() + 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>, 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, 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, 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) + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt new file mode 100644 index 0000000000..583207edcd --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt @@ -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() { + override fun extractCommands(commands: Collection>): List> + = commands.select() + + // DOCSTART 1 + /** A state representing a cash claim against some party. */ + data class State( + override val amount: Amount>, + + /** There must be a MoveCommand signed by this key to claim the amount. */ + override val owner: AbstractParty + ) : FungibleAsset, QueryableState { + constructor(deposit: PartyAndReference, amount: Amount, 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>, newOwner: AbstractParty): FungibleAsset + = 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 = 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? = 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>) : 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, 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>, owner: AbstractParty, notary: Party) + = generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue()) + + override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + + override fun generateExitCommand(amount: Amount>) = 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().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 = inputs.flatMap { it.exitKeys }.toSet() + val exitCommand = tx.commands.select(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(inputs, tx.commands) + } + } + } + + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: LedgerTransaction, + issueCommand: CommandWithParties, + 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() + 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, + to: AbstractParty, + onlyFromParties: Set = 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, + ourIdentity: PartyAndCertificate, + to: AbstractParty, + onlyFromParties: Set = emptySet()): Pair> { + 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>, + onlyFromParties: Set = 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>, + ourIdentity: PartyAndCertificate, + onlyFromParties: Set = emptySet()): Pair> { + fun deriveState(txState: TransactionState, amt: Amount>, 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.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>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY) diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt new file mode 100644 index 0000000000..190e3c6f7f --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt @@ -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(val party: AbstractParty, val amount: Amount) + +/** + * 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> : Contract { + companion object { + val log = loggerFor>() + + /** + * 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 , T : Any> generateSpend(tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { + 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 , T : Any> generateSpend(tx: TransactionBuilder, + payments: List>, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { + // 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>() + 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 , T : Any> gatherCoins(acceptableCoins: Collection>, + amount: Amount): Pair>, Amount> { + require(amount.quantity > 0) { "Cannot gather zero coins" } + val gathered = arrayListOf>() + 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 , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + 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 , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + 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 , T : Any> generateIssue(tx: TransactionBuilder, + transactionState: TransactionState, + issueCommand: CommandData): Set { + 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>): Collection> + + /** + * 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>, + assetStates: List>): Set { + 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>, + assetStates: List>, + payChangeTo: AbstractParty): Set { + 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>): 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, amount: Amount>, owner: AbstractParty): TransactionState +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt new file mode 100644 index 0000000000..8241e3c607 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt @@ -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() + + 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() + } + + // 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, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : 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, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> { + val stateAndRefs = mutableListOf>() + + 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, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): 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>(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 + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt new file mode 100644 index 0000000000..557d729abb --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -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() + } + + 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, lockId: UUID, notary: Party?, + onlyFromIssuerParties: Set, withIssuerRefs: Set) : 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() + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt new file mode 100644 index 0000000000..119b99d0ea --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt @@ -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(override val progressTracker: ProgressTracker) : FlowLogic() { + 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, 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) +} + +class CashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt new file mode 100644 index 0000000000..4bb1af7659 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt @@ -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 + + init { + // Warning!! You are about to see a major hack! + val baseDirectory = services.declaredField("serviceHub").value + .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } + .declaredField("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() { + @Suspendable + override fun call(): CashConfiguration { + val configHolder = serviceHub.cordaService(ConfigHolder::class.java) + return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) + } +} + +@CordaSerializable +data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt new file mode 100644 index 0000000000..fad3ec7b58 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt @@ -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, + private val issuerRef: OpaqueBytes, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(amount: Amount, 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(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 = 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, val issueRef: OpaqueBytes) : AbstractRequest(amount) +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt new file mode 100644 index 0000000000..97708a0891 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt @@ -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, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(amount: Amount, + 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, + val issueRef: OpaqueBytes, + val recipient: Party, + val notary: Party, + val anonymous: Boolean) : AbstractRequest(amount) +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt new file mode 100644 index 0000000000..d6f71fa1db --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt @@ -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, + private val issuerBankPartyRef: OpaqueBytes, + private val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(amount: Amount, + 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, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount) +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt new file mode 100644 index 0000000000..b60c83ced0 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt @@ -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, + val recipient: Party, + val anonymous: Boolean, + progressTracker: ProgressTracker, + val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) { + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, 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() + } + 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, + val recipient: Party, + val anonymous: Boolean, + val issuerConstraint: Set = emptySet()) : AbstractRequest(amount) +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt new file mode 100644 index 0000000000..7c5c615ef2 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt @@ -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) : 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, + val payToIdentity: PartyAndCertificate + ) + + open class Seller(private val otherSideSession: FlowSession, + private val assetToSell: StateAndRef, + private val price: Amount, + private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party. + override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { + + 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 = 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, + private val typeToBuy: Class, + private val anonymous: Boolean) : FlowLogic() { + constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount, typeToBuy: Class) : + 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, SellerTradeInfo> { + val assetForSale = subFlow(ReceiveStateAndRefFlow(sellerSession)).single() + return assetForSale to sellerSession.receive().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, 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) + } +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt new file mode 100644 index 0000000000..9ec6f97ddf --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt @@ -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() +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt new file mode 100644 index 0000000000..71857824f9 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt @@ -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() +} diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt new file mode 100644 index 0000000000..9a6457b370 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt @@ -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.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().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.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ +fun Iterable.sumCashOrZero(currency: Issued): Amount> { + return filterIsInstance().map { it.amount }.sumOrZero(currency) +} + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + diff --git a/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection new file mode 100644 index 0000000000..c9ce8a6ee9 --- /dev/null +++ b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection @@ -0,0 +1,2 @@ +com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl + diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt new file mode 100644 index 0000000000..dade7aa141 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt @@ -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().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.outputs(aliceGetsBack: Amount>) { + 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() } + 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 + private lateinit var bigCorpVaultService: VaultService + + private lateinit var aliceServices: MockServices + private lateinit var aliceVaultService: VaultService + private lateinit var alicesVault: Vault + + 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 { + 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() + } +} diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt new file mode 100644 index 0000000000..8929387a9e --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt new file mode 100644 index 0000000000..8a67039948 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt @@ -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, + issuerServices: ServiceHub = this, + outputNotary: Party = DUMMY_NOTARY, + atLeastThisManyStates: Int = 3, + atMostThisManyStates: Int = 10, + rng: Random = Random(), + ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), + ownedBy: AbstractParty? = null, + issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault { + val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) + + val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey + val anonParty = AnonymousParty(myKey) + + // We will allocate one state to one transaction, for simplicities sake. + val cash = Cash() + val transactions: List = amounts.map { pennies -> + val issuance = TransactionBuilder(null as Party?) + cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary) + + 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(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> + + @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().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().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(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, 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, 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, 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 { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) } + } + + /** + * Try exiting with a reference mis-match. + */ + @Test + fun generateInvalidReferenceExit() { + initialiseTestSerialization() + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) } + } + + /** + * Try exiting an amount greater than the maximum available. + */ + @Test + fun generateInsufficientExit() { + initialiseTestSerialization() + assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) } + } + + /** + * Try exiting for an owner with no states + */ + @Test + fun generateOwnerWithNoStatesExit() { + initialiseTestSerialization() + assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) } + } + + /** + * Try exiting when vault is empty + */ + @Test + fun generateExitWithEmptyVault() { + initialiseTestSerialization() + assertFailsWith { + 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 = vaultStatesUnconsumed.elementAt(0) + val vaultState1: StateAndRef = vaultStatesUnconsumed.elementAt(1) + val vaultState2: StateAndRef = 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() + 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() + 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().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().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) + } +} diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt new file mode 100644 index 0000000000..fc827ac9c6 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt @@ -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 + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: StartedNode + 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().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 { + future.getOrThrow() + } + } +} diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt new file mode 100644 index 0000000000..9df67f5f29 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt @@ -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 + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: StartedNode + 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().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 { + future.getOrThrow() + } + } +} diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt new file mode 100644 index 0000000000..58aba54e43 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt @@ -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 + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: StartedNode + 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(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy(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 { + 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 { + future.getOrThrow() + } + } +} diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt new file mode 100644 index 0000000000..ee6fc4e0e4 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt @@ -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 { + val checkpoints = mutableListOf() + 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 { + 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 { + override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, + id: Int, notaryIdentity: Pair?, 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 { + // Create a node in the mock network ... + return mockNet.createNode(nodeFactory = object : MockNetwork.Factory { + override fun create(config: NodeConfiguration, + network: MockNetwork, + networkMapAddr: SingleMessageRecipient?, + id: Int, notaryIdentity: Pair?, + 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 { (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>, + val sellerResult: CordaFuture, + val sellerId: StateMachineRunId + ) + + private fun runBuyerAndSeller(notary: Party, + sellerNode: StartedNode, + buyerNode: StartedNode, + assetToSell: StateAndRef): RunResult { + val buyerFlows: Observable> = 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, + private val price: Amount, + private val anonymous: Boolean) : FlowLogic() { + @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() { + @Suspendable + override fun call(): SignedTransaction { + val (notary, price, anonymous) = sellerSession.receive().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, val anonymous: Boolean) + + private fun LedgerDSL.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 { + 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, + node: StartedNode<*>, + notaryNode: StartedNode<*>, + vararg extraSigningNodes: StartedNode<*>): Map { + + val signed = wtxToSign.map { + val id = it.id + val sigs = mutableListOf() + 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.fillUpForBuyer( + withError: Boolean, + issuer: PartyAndReference, + owner: AbstractParty, + notary: Party): Pair, List> { + 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(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef())) + return Pair(vault, listOf(eb1, bc1, bc2)) + } + + private fun LedgerDSL.fillUpForSeller( + withError: Boolean, + issuer: PartyAndReference, + owner: AbstractParty, + amount: Amount>, + attachmentID: SecureHash?, + notary: Party): Pair, List> { + 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(listOf("alice's paper".outputStateAndRef())) + return Pair(vault, listOf(ap)) + } + + + class RecordingTransactionStorage(val database: CordaPersistence, val delegate: WritableTransactionStorage) : WritableTransactionStorage, SingletonSerializeAsToken() { + override fun track(): DataFeed, SignedTransaction> { + return database.transaction { + delegate.track() + } + } + + val records: MutableList = Collections.synchronizedList(ArrayList()) + override val updates: Observable + 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 + } + +} diff --git a/settings.gradle b/settings.gradle index f4058ed729..910a4ee92f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -47,4 +47,5 @@ include 'doorman' include 'verify-enclave' include 'sgx-jvm/hsm-tool' include 'signing-server' +include 'perftestcordapp'