diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index b408708da4..91174c0a28 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -4,11 +4,9 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.requireThat import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.CollectSignaturesFlow -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.SignTransactionFlow +import net.corda.core.flows.* import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceType @@ -29,9 +27,14 @@ import java.security.PublicKey // TODO: Also, the term Deal is used here where we might prefer Agreement. // TODO: Make this flow more generic. object TwoPartyDealFlow { - // This object is serialised to the network and is the first flow message the seller sends to the buyer. + /** + * This object is serialised to the network and is the first flow message the seller sends to the buyer. + * + * @param primaryIdentity the (anonymised) identity of the participant that initiates communication/handshake. + * @param secondaryIdentity the (anonymised) identity of the participant that is recipient of initial communication. + */ @CordaSerializable - data class Handshake(val payload: T, val publicKey: PublicKey) + data class Handshake(val payload: T, val primaryIdentity: AnonymousParty, val secondaryIdentity: AnonymousParty) /** * Abstracted bilateral deal flow participant that initiates communication/handshake. @@ -39,19 +42,25 @@ object TwoPartyDealFlow { abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic() { companion object { + object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") - fun tracker() = ProgressTracker(SENDING_PROPOSAL) + fun tracker() = ProgressTracker(GENERATING_ID, SENDING_PROPOSAL) } abstract val payload: Any abstract val notaryNode: NodeInfo abstract val otherParty: Party + // TODO: This is never read from, and should be removed abstract val myKey: PublicKey @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = GENERATING_ID + val txIdentities = subFlow(TransactionKeyFlow(otherParty)) + val anonymousMe = txIdentities.get(serviceHub.myInfo.legalIdentity) ?: serviceHub.myInfo.legalIdentity.anonymise() + val anonymousCounterparty = txIdentities.get(otherParty) ?: otherParty.anonymise() progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey) + val hello = Handshake(payload, anonymousMe, anonymousCounterparty) // Wait for the FinalityFlow to finish on the other side and return the tx when it's available. send(otherParty, hello) @@ -105,7 +114,7 @@ object TwoPartyDealFlow { progressTracker.currentStep = COLLECTING_SIGNATURES // DOCSTART 1 - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, additionalSigningPubKeys)) // DOCEND 1 logger.trace { "Got signatures from other party, verifying ... " } @@ -138,7 +147,14 @@ object TwoPartyDealFlow { val handshake = receive>(otherParty) progressTracker.currentStep = VERIFYING - return handshake.unwrap { validateHandshake(it) } + return handshake.unwrap { + // Verify the transaction identities represent the correct parties + val wellKnownOtherParty = serviceHub.identityService.partyFromAnonymous(it.primaryIdentity) + val wellKnownMe = serviceHub.identityService.partyFromAnonymous(it.secondaryIdentity) + require(wellKnownOtherParty == otherParty) + require(wellKnownMe == serviceHub.myInfo.legalIdentity) + validateHandshake(it) + } } @Suspendable protected abstract fun validateHandshake(handshake: Handshake): Handshake @@ -155,7 +171,6 @@ object TwoPartyDealFlow { override val payload: AutoOffer, override val myKey: PublicKey, override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() { - override val notaryNode: NodeInfo get() = serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 215946ae9b..bac6013f74 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -5,10 +5,10 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.OwnableState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.withoutIssuer +import net.corda.core.contracts.* import net.corda.core.flows.* -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -48,18 +48,22 @@ object TwoPartyTradeFlow { 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. + /** + * 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 sellerOwner: AbstractParty + val payToIdentity: PartyAndCertificate ) open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val me: AbstractParty, + val me: PartyAndCertificate, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { companion object { @@ -84,12 +88,26 @@ object TwoPartyTradeFlow { // SendTransactionFlow allows otherParty to access our data to resolve the transaction. subFlow(SendStateAndRefFlow(otherParty, listOf(assetToSell))) send(otherParty, 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(otherParty)) + // DOCSTART 5 val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { - if (stx.tx.outputStates.sumCashBy(me).withoutIssuer() != price) + // 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.partyFromAnonymous(anon) != null) { "Transaction state ${state} involves unknown participant ${anon}" } + } + } + + if (stx.tx.outputStates.sumCashBy(me.party).withoutIssuer() != price) throw FlowException("Transaction is not sending us the right amount of cash") } } @@ -114,7 +132,9 @@ object TwoPartyTradeFlow { open class Buyer(val otherParty: Party, val notary: Party, val acceptablePrice: Amount, - val typeToBuy: Class) : FlowLogic() { + val typeToBuy: Class, + val anonymous: Boolean) : FlowLogic() { + constructor(otherParty: Party, notary: Party, acceptablePrice: Amount, typeToBuy: Class): this(otherParty, notary, acceptablePrice, typeToBuy, true) // DOCSTART 2 object RECEIVING : ProgressTracker.Step("Waiting for seller trading info") @@ -139,16 +159,27 @@ object TwoPartyTradeFlow { 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(serviceHub.myInfo.legalIdentityAndCert, false) + else + serviceHub.myInfo.legalIdentityAndCert + // Put together a proposed transaction that performs the trade, and sign it. progressTracker.currentStep = SIGNING - val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest) + 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(otherParty, ptx.toWireTransaction())) + // 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 twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, COLLECTING_SIGNATURES.childProgressTracker())) + val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, cashSigningPubKeys, COLLECTING_SIGNATURES.childProgressTracker())) + // Notarise and record the transaction. progressTracker.currentStep = RECORDING return subFlow(FinalityFlow(twiceSignedTx)).single() @@ -159,33 +190,40 @@ object TwoPartyTradeFlow { val assetForSale = subFlow(ReceiveStateAndRefFlow(otherParty)).single() return assetForSale to receive(otherParty).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.partyFromAnonymous(asset.owner) + require(assetForSaleIdentity == otherParty) + + // 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) + require(wellKnownPayToIdentity?.party == otherParty) { "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): Pair> { + 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, tradeRequest.sellerOwner) + val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.payToIdentity.party) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(assetForSale) - // Just pick some new public key for now. This won't be linked with our identity in any way, which is what - // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to - // reveal who the owner actually is. The key management service is expected to derive a unique key from some - // initial seed in order to provide privacy protection. - val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = assetForSale.state.data.withNewOwner(AnonymousParty(freshKey)) + val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party) tx.addOutputState(state, assetForSale.state.notary) tx.addCommand(command, assetForSale.state.data.owner.owningKey) @@ -193,8 +231,11 @@ object TwoPartyTradeFlow { // But it can't hurt to have one. val currentTime = serviceHub.clock.instant() tx.setTimeWindow(currentTime, 30.seconds) - return Pair(tx, cashSigningPubKeys) + + return SharedTx(tx, cashSigningPubKeys) } // DOCEND 1 + + data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List) } } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 55b1ff406b..f915218555 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.concurrent.map import net.corda.core.internal.rootCause @@ -20,6 +21,7 @@ import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault +import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -145,19 +147,18 @@ class TwoPartyTradeFlowTests { val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val cashIssuer = bankNode.info.legalIdentity.ref(1) - val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val issuer = bankNode.info.legalIdentity.ref(1) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() val cashStates = bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notaryNode.info.notaryIdentity, 3, 3, - issuedBy = cashIssuer) + issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, + fillUpForSeller(false, issuer, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second } @@ -199,8 +200,13 @@ class TwoPartyTradeFlowTests { val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val cashIssuer = bankNode.info.legalIdentity.ref(1) - val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + + // Let the nodes know about each other - normally the network map would handle this + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) } + } aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) @@ -214,10 +220,10 @@ class TwoPartyTradeFlowTests { bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notaryNode.info.notaryIdentity, - issuedBy = cashIssuer) + issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, + fillUpForSeller(false, issuer, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -253,6 +259,9 @@ class TwoPartyTradeFlowTests { // 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(networkMapAddress, bobAddr.id, object : MockNetwork.Factory { @@ -437,7 +446,6 @@ class TwoPartyTradeFlowTests { } ledger(aliceNode.services, initialiseSerialization = false) { - // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -529,13 +537,11 @@ class TwoPartyTradeFlowTests { private fun runBuyerAndSeller(notaryNode: MockNetwork.MockNode, sellerNode: MockNetwork.MockNode, buyerNode: MockNetwork.MockNode, - assetToSell: StateAndRef): RunResult { - val anonymousSeller = sellerNode.services.let { serviceHub -> - serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) - }.party.anonymise() - val buyerFlows: Observable = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) + assetToSell: StateAndRef, + anonymous: Boolean = true): RunResult { + val buyerFlows: Observable> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } - val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymousSeller) + val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymous) val sellerResult = sellerNode.services.startFlow(seller).resultFuture return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id) } @@ -545,10 +551,15 @@ class TwoPartyTradeFlowTests { val notary: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val me: AnonymousParty) : FlowLogic() { + val anonymous: Boolean) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - send(buyer, Pair(notary.notaryIdentity, price)) + val me = if (anonymous) { + serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) + } else { + serviceHub.myInfo.legalIdentityAndCert + } + send(buyer, TestTx(notary.notaryIdentity, price, anonymous)) return subFlow(Seller( buyer, notary, @@ -562,14 +573,17 @@ class TwoPartyTradeFlowTests { class BuyerAcceptor(val seller: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val (notary, price) = receive>>(seller).unwrap { - require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" } + val (notary, price, anonymous) = receive(seller).unwrap { + require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" } it } - return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java)) + return subFlow(Buyer(seller, 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, @@ -581,6 +595,12 @@ class TwoPartyTradeFlowTests { val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + // Let the nodes know about each other - normally the network map would handle this + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) } + } + val bobsBadCash = bobNode.database.transaction { fillUpForBuyer(bobError, issuer, bobNode.info.legalIdentity, notaryNode.info.notaryIdentity).second diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index 65b6bb6ad7..877ae7dd67 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -8,6 +8,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.SchedulableFlow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceType @@ -110,8 +111,7 @@ object FixingFlow { } override val myKey: PublicKey get() { - dealToFix.state.data.participants.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } - return serviceHub.legalIdentityKey + return serviceHub.keyManagementService.filterMyKeys(dealToFix.state.data.participants.map(AbstractParty::owningKey)).single() } override val notaryNode: NodeInfo get() { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index a249b87339..9656d93227 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.transactions.SignedTransaction @@ -42,10 +41,9 @@ class SellerFlow(val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey = serviceHub.keyManagementService.freshKey() + val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first() - progressTracker.currentStep = TRADING // Send the offered amount. @@ -55,7 +53,7 @@ class SellerFlow(val otherParty: Party, notary, commercialPaper, amount, - AnonymousParty(cpOwnerKey), + cpOwner, progressTracker.getChildProgressTracker(TRADING)!!) return subFlow(seller) }