diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDoublePayment.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDoublePayment.kt new file mode 100644 index 0000000000..7118f05175 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDoublePayment.kt @@ -0,0 +1,93 @@ +package com.r3.corda.enterprise.perftestcordapp.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.OnLedgerAsset +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.PartyAndAmount +import net.corda.confidential.SwapIdentitiesFlow +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.TransactionState +import net.corda.core.flows.FlowException +import net.corda.core.flows.NotaryError +import net.corda.core.flows.NotaryException +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import java.util.* + +/** + * Initiates a flow that self-issues cash. We then try and send it to another party twice. The flow only succeeds if + * the second payment is rejected by the notary as a double spend. + * + * @param amount the amount of currency to issue. + * @param issueRef a reference to put on the issued currency. + * @param recipient payee Party + * @param anonymous whether to anonymise before the transaction + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class CashIssueAndDoublePayment(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(request: CashIssueAndPaymentFlow.IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) + constructor(amount: Amount, issueRef: OpaqueBytes, payTo: Party, anonymous: Boolean, notary: Party) : this(amount, issueRef, payTo, anonymous, notary, tracker()) + + @Suspendable + override fun call(): AbstractCashFlow.Result { + fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) + + val issueResult = subFlow(CashIssueFlow(amount, issueRef, notary)) + val cashStateAndRef = issueResult.stx.tx.outRef(0) + + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(SwapIdentitiesFlow(recipient)) + } else { + emptyMap() + } + val anonymousRecipient = txIdentities[recipient] ?: recipient + + val changeIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) + + progressTracker.currentStep = GENERATING_TX + val builder1 = TransactionBuilder(notary) + val (spendTx1, keysForSigning1) = OnLedgerAsset.generateSpend(builder1, listOf(PartyAndAmount(anonymousRecipient, amount)), listOf(cashStateAndRef), + changeIdentity.party.anonymise(), + { state, quantity, owner -> deriveState(state, quantity, owner) }, + { Cash().generateMoveCommand() }) + + val builder2 = TransactionBuilder(notary) + val (spendTx2, keysForSigning2) = OnLedgerAsset.generateSpend(builder2, listOf(PartyAndAmount(anonymousRecipient, amount)), listOf(cashStateAndRef), + changeIdentity.party.anonymise(), + { state, quantity, owner -> deriveState(state, quantity, owner) }, + { Cash().generateMoveCommand() }) + + progressTracker.currentStep = SIGNING_TX + val tx1 = serviceHub.signInitialTransaction(spendTx1, keysForSigning1) + val tx2 = serviceHub.signInitialTransaction(spendTx2, keysForSigning2) + + progressTracker.currentStep = FINALISING_TX + val notarised1 = finaliseTx(tx1, setOf(recipient), "Unable to notarise spend first time") + try { + val notarised2 = finaliseTx(tx2, setOf(recipient), "Unable to notarise spend second time") + } catch (expected: CashException) { + val cause = expected.cause + if (cause is NotaryException) { + if (cause.error is NotaryError.Conflict) { + return Result(notarised1, recipient) + } + throw expected // Wasn't actually expected! + } + } + throw FlowException("Managed to do double spend. Should have thrown NotaryError.Conflict.") + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDuplicatePayment.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDuplicatePayment.kt new file mode 100644 index 0000000000..81038639ad --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndDuplicatePayment.kt @@ -0,0 +1,74 @@ +package com.r3.corda.enterprise.perftestcordapp.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.OnLedgerAsset +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.PartyAndAmount +import net.corda.confidential.SwapIdentitiesFlow +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.TransactionState +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import java.util.* + +/** + * Initiates a flow that self-issues cash and then immediately spends it without coin selection. It also then attempts + * to notarise exactly the same transaction again, which should succeed since it is exactly the same notarisation request. + * + * @param amount the amount of currency to issue. + * @param issueRef a reference to put on the issued currency. + * @param recipient payee Party + * @param anonymous whether to anonymise before the transaction + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class CashIssueAndDuplicatePayment(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(request: CashIssueAndPaymentFlow.IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) + constructor(amount: Amount, issueRef: OpaqueBytes, payTo: Party, anonymous: Boolean, notary: Party) : this(amount, issueRef, payTo, anonymous, notary, tracker()) + + @Suspendable + override fun call(): Result { + fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) + + val issueResult = subFlow(CashIssueFlow(amount, issueRef, notary)) + val cashStateAndRef = issueResult.stx.tx.outRef(0) + + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(SwapIdentitiesFlow(recipient)) + } else { + emptyMap() + } + val anonymousRecipient = txIdentities[recipient] ?: recipient + + val changeIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) + + progressTracker.currentStep = GENERATING_TX + val builder = TransactionBuilder(notary) + val (spendTx, keysForSigning) = OnLedgerAsset.generateSpend(builder, listOf(PartyAndAmount(anonymousRecipient, amount)), listOf(cashStateAndRef), + changeIdentity.party.anonymise(), + { state, quantity, owner -> deriveState(state, quantity, owner) }, + { Cash().generateMoveCommand() }) + + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(spendTx, keysForSigning) + + progressTracker.currentStep = FINALISING_TX + val notarised1 = finaliseTx(tx, setOf(recipient), "Unable to notarise spend first time") + val notarised2 = finaliseTx(tx, setOf(recipient), "Unable to notarise spend second time") + + return Result(notarised2, recipient) + } +} \ No newline at end of file diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt index 5df653d49e..1cc43611a1 100644 --- a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt @@ -5,7 +5,9 @@ import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash import com.r3.corda.enterprise.perftestcordapp.contracts.asset.OnLedgerAsset import com.r3.corda.enterprise.perftestcordapp.contracts.asset.PartyAndAmount import net.corda.confidential.SwapIdentitiesFlow -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.TransactionState import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -16,11 +18,7 @@ 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. + * Initiates a flow that self-issues cash and then is immediately sent to another party, without coin selection. * * @param amount the amount of currency to issue. * @param issueRef a reference to put on the issued currency. diff --git a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/NotaryFunctionalSamplers.kt b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/NotaryFunctionalSamplers.kt new file mode 100644 index 0000000000..e14b8eb628 --- /dev/null +++ b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/NotaryFunctionalSamplers.kt @@ -0,0 +1,72 @@ +package com.r3.corda.jmeter + +import com.r3.corda.enterprise.perftestcordapp.POUNDS +import com.r3.corda.enterprise.perftestcordapp.flows.CashIssueAndDoublePayment +import com.r3.corda.enterprise.perftestcordapp.flows.CashIssueAndDuplicatePayment +import net.corda.core.identity.Party +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.OpaqueBytes +import org.apache.jmeter.config.Argument +import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext + + +/** + * A sampler to just issue cash and pay another party, but the flow attempts to spend the cash twice (double spend) and + * only succeeds if that is correctly rejected for the second spend. + * + * Use with 1 iteration and 1 thread in JMeter as a functional test. + */ +class NotariseDoubleSpendSampler : AbstractSampler() { + companion object JMeterProperties { + val otherParty = Argument("otherPartyName", "", "", "The X500 name of the payee.") + } + + lateinit var counterParty: Party + + override fun setupTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) { + getNotaryIdentity(rpcProxy, testContext) + counterParty = getIdentity(rpcProxy, testContext, otherParty) + } + + override fun createFlowInvoke(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext): FlowInvoke<*> { + val amount = 2_000_000.POUNDS + return FlowInvoke(CashIssueAndDoublePayment::class.java, arrayOf(amount, OpaqueBytes.of(1), counterParty, false, notaryIdentity)) + } + + override fun teardownTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) { + } + + override val additionalArgs: Set + get() = setOf(notary, otherParty) +} + + +/** + * A sampler to just issue cash and pay another party. The flow actually submits the same transaction twice to the notary + * which should succeed. + * + * Use with 1 iteration and 1 thread in JMeter as a functional test. + */ +class NotariseDuplicateTransactionSampler : AbstractSampler() { + companion object JMeterProperties { + val otherParty = Argument("otherPartyName", "", "", "The X500 name of the payee.") + } + + lateinit var counterParty: Party + + override fun setupTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) { + getNotaryIdentity(rpcProxy, testContext) + counterParty = getIdentity(rpcProxy, testContext, otherParty) + } + + override fun createFlowInvoke(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext): FlowInvoke<*> { + val amount = 2_000_000.POUNDS + return FlowInvoke(CashIssueAndDuplicatePayment::class.java, arrayOf(amount, OpaqueBytes.of(1), counterParty, false, notaryIdentity)) + } + + override fun teardownTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) { + } + + override val additionalArgs: Set + get() = setOf(notary, otherParty) +} \ No newline at end of file