diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 01065d5da9..4863b27c54 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -12,6 +12,7 @@ import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.debug /** * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction @@ -29,44 +30,74 @@ import net.corda.core.utilities.ProgressTracker * The flow returns the same transaction but with the additional signatures from the notary. * * NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow]. - * - * @param transaction What to commit. - * @param sessions A collection of [FlowSession]s who will be given the notarised transaction. This list **must** include - * all participants in the transaction (excluding the local identity). */ // To maintain backwards compatibility with the old API, FinalityFlow can act both as an initiating flow and as an inlined flow. // This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the // presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow. @InitiatingFlow class FinalityFlow private constructor(val transaction: SignedTransaction, - private val extraRecipients: Set, + private val oldParticipants: Collection, override val progressTracker: ProgressTracker, - private val sessions: Collection?) : FlowLogic() { + private val sessions: Collection, + private val newApi: Boolean) : FlowLogic() { @Deprecated(DEPRECATION_MSG) constructor(transaction: SignedTransaction, extraRecipients: Set, progressTracker: ProgressTracker) : this( - transaction, extraRecipients, progressTracker, null + transaction, extraRecipients, progressTracker, emptyList(), false ) @Deprecated(DEPRECATION_MSG) - constructor(transaction: SignedTransaction, extraRecipients: Set) : this(transaction, extraRecipients, tracker(), null) + constructor(transaction: SignedTransaction, extraRecipients: Set) : this(transaction, extraRecipients, tracker(), emptyList(), false) @Deprecated(DEPRECATION_MSG) - constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), null) + constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), emptyList(), false) @Deprecated(DEPRECATION_MSG) - constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, null) + constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, emptyList(), false) - constructor(transaction: SignedTransaction, sessions: Collection, progressTracker: ProgressTracker) : this( - transaction, emptySet(), progressTracker, sessions - ) - constructor(transaction: SignedTransaction, sessions: Collection) : this( - transaction, emptySet(), tracker(), sessions - ) + /** + * Notarise the given transaction and broadcast it to the given [FlowSession]s. This list **must** at least include + * all the non-local participants of the transaction. Sessions to non-participants can also be provided. + * + * @param transaction What to commit. + */ constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this( - transaction, emptySet(), tracker(), listOf(firstSession) + restSessions.asList() + transaction, listOf(firstSession) + restSessions.asList() ) + /** + * Notarise the given transaction and broadcast it to all the participants. + * + * @param transaction What to commit. + * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can + * also be provided. + */ + @JvmOverloads + constructor( + transaction: SignedTransaction, + sessions: Collection, + progressTracker: ProgressTracker = tracker() + ) : this(transaction, emptyList(), progressTracker, sessions, true) + + /** + * Notarise the given transaction and broadcast it to all the participants. + * + * @param transaction What to commit. + * @param sessions A collection of [FlowSession]s for each non-local participant. + * @param oldParticipants An **optional** collection of parties for participants who are still using the old API. + * + * You will only need to use this parameter if you have upgraded your CorDapp from the V3 FinalityFlow API but are required to provide + * backwards compatibility with participants running V3 nodes. If you're writing a new CorDapp then this does not apply and this + * parameter should be ignored. + */ + @Deprecated(DEPRECATION_MSG) + constructor( + transaction: SignedTransaction, + sessions: Collection, + oldParticipants: Collection, + progressTracker: ProgressTracker + ) : this(transaction, oldParticipants, progressTracker, sessions, true) + companion object { private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " + "accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " + - "that takes in existing FlowSessions." + "that requires only FlowSessions." object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") { override fun childProgressTracker() = NotaryFlow.Client.tracker() @@ -81,7 +112,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suspendable @Throws(NotaryException::class) override fun call(): SignedTransaction { - if (sessions == null) { + if (!newApi) { require(CordappResolver.currentTargetVersion < 4) { "A flow session for each external participant to the transaction must be provided. If you wish to continue " + "using this insecure API then specify a target platform version of less than 4 for your CorDapp." @@ -103,30 +134,25 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, transaction.pushToLoggingContext() logCommandData() val ledgerTransaction = verifyTx() - val externalParticipants = extractExternalParticipants(ledgerTransaction) + val externalTxParticipants = extractExternalParticipants(ledgerTransaction) - if (sessions != null) { - val missingRecipients = externalParticipants - sessions.map { it.counterparty } + if (newApi) { + val sessionParties = sessions.map { it.counterparty } + val missingRecipients = externalTxParticipants - sessionParties - oldParticipants require(missingRecipients.isEmpty()) { "Flow sessions were not provided for the following transaction participants: $missingRecipients" } + sessionParties.intersect(oldParticipants).let { + require(it.isEmpty()) { "The following parties are specified both in flow sessions and in the oldParticipants list: $it" } + } } val notarised = notariseAndRecord() - // Each transaction has its own set of recipients, but extra recipients get them all. progressTracker.currentStep = BROADCASTING - if (sessions == null) { - val recipients = externalParticipants + (extraRecipients - serviceHub.myInfo.legalIdentities) - logger.info("Broadcasting transaction to parties ${recipients.joinToString(", ", "[", "]")}.") - for (recipient in recipients) { - logger.info("Sending transaction to party ${recipient.name}.") - val session = initiateFlow(recipient) - subFlow(SendTransactionFlow(session, notarised)) - logger.info("Party $recipient received the transaction.") - } - } else { + if (newApi) { + oldV3Broadcast(notarised, oldParticipants.toSet()) for (session in sessions) { try { subFlow(SendTransactionFlow(session, notarised)) @@ -140,6 +166,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, ) } } + } else { + oldV3Broadcast(notarised, (externalTxParticipants + oldParticipants).toSet()) } logger.info("All parties received the transaction successfully.") @@ -147,6 +175,18 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, return notarised } + @Suspendable + private fun oldV3Broadcast(notarised: SignedTransaction, recipients: Set) { + for (recipient in recipients) { + if (!serviceHub.myInfo.isLegalIdentity(recipient)) { + logger.debug { "Sending transaction to party $recipient." } + val session = initiateFlow(recipient) + subFlow(SendTransactionFlow(session, notarised)) + logger.info("Party $recipient received the transaction.") + } + } + } + private fun logCommandData() { if (logger.isDebugEnabled) { val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct() diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index 8786696354..76251376bf 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -3,6 +3,7 @@ package net.corda.core.flows import com.natpryce.hamkrest.and import com.natpryce.hamkrest.assertion.assert import net.corda.core.flows.mixins.WithFinality +import net.corda.core.flows.mixins.WithFinality.FinalityInvoker import net.corda.core.identity.Party import net.corda.core.internal.cordapp.CordappResolver import net.corda.core.transactions.SignedTransaction @@ -75,15 +76,29 @@ class FinalityFlowTests : WithFinality { @Test fun `allow use of the old API if the CorDapp target version is 3`() { - // We need Bob to load at least one old CorDapp so that its FinalityHandler is enabled - val bob = createBob(cordapps = listOf(cordappWithPackages("com.template").copy(targetPlatformVersion = 3))) - val stx = aliceNode.issuesCashTo(bob) + val oldBob = createBob(cordapps = listOf(tokenOldCordapp())) + val stx = aliceNode.issuesCashTo(oldBob) val resultFuture = CordappResolver.withCordapp(targetPlatformVersion = 3) { @Suppress("DEPRECATION") aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture } resultFuture.getOrThrow() - assertThat(bob.services.validatedTransactions.getTransaction(stx.id)).isNotNull() + assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull() + } + + @Test + fun `broadcasting to both new and old participants`() { + val newCharlie = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) + val oldBob = createBob(cordapps = listOf(tokenOldCordapp())) + val stx = aliceNode.issuesCashTo(oldBob) + val resultFuture = aliceNode.startFlowAndRunNetwork(FinalityInvoker( + stx, + newRecipients = setOf(newCharlie.info.singleIdentity()), + oldRecipients = setOf(oldBob.info.singleIdentity()) + )).resultFuture + resultFuture.getOrThrow() + assertThat(newCharlie.services.validatedTransactions.getTransaction(stx.id)).isNotNull() + assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull() } private fun createBob(cordapps: List = emptyList()): TestStartedNode { @@ -100,4 +115,7 @@ class FinalityFlowTests : WithFinality { Cash().generateIssue(builder, amount, other, notary) return services.signInitialTransaction(builder) } + + /** "Old" CorDapp which will force its node to keep its FinalityHandler enabled */ + private fun tokenOldCordapp() = cordappWithPackages("com.template").copy(targetPlatformVersion = 3) } diff --git a/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt b/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt index 23acfa28e9..45a2363151 100644 --- a/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt +++ b/core/src/test/kotlin/net/corda/core/flows/mixins/WithFinality.kt @@ -17,7 +17,7 @@ import net.corda.testing.node.internal.TestStartedNode interface WithFinality : WithMockNet { //region Operations fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine { - return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet())) + return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet(), emptySet())) } fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction { @@ -25,7 +25,7 @@ interface WithFinality : WithMockNet { } fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle { - return startFlow(::FinalityInvoker, stx, recipients.toSet()).andRunNetwork() + return startFlow(::FinalityInvoker, stx, recipients.toSet(), emptySet()).andRunNetwork() } //endregion @@ -41,11 +41,13 @@ interface WithFinality : WithMockNet { @InitiatingFlow @StartableByRPC class FinalityInvoker(private val transaction: SignedTransaction, - private val recipients: Set) : FlowLogic() { + private val newRecipients: Set, + private val oldRecipients: Set) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val sessions = recipients.map(::initiateFlow) - return subFlow(FinalityFlow(transaction, sessions)) + val sessions = newRecipients.map(::initiateFlow) + @Suppress("DEPRECATION") + return subFlow(FinalityFlow(transaction, sessions, oldRecipients, FinalityFlow.tracker())) } } diff --git a/docs/source/app-upgrade-notes.rst b/docs/source/app-upgrade-notes.rst index f809616aa9..f5f041b0d5 100644 --- a/docs/source/app-upgrade-notes.rst +++ b/docs/source/app-upgrade-notes.rst @@ -125,6 +125,9 @@ This is a three step process: 2. Change or create the flow that will receive the finalised transaction. 3. Make sure your application's minimum and target version numbers are both set to 4 (see step 2). +Upgrading a non-initiating flow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + As an example, let's take a very simple flow that finalises a transaction without the involvement of a counterpart flow: .. container:: codeset @@ -175,10 +178,17 @@ to record the finalised transaction: :end-before: DOCEND SimpleNewResponderFlow :dedent: 4 -For flows which are already initiating counterpart flows then it's a simple matter of using the existing flow session. +.. note:: All the nodes in your business network will need the new CorDapp, otherwise they won't know how to receive the transaction. **This + includes nodes which previously didn't have the old CorDapp.** If a node is sent a transaction and it doesn't have the new CorDapp loaded + then simply restart it with the CorDapp and the transaction will be recorded. + +Upgrading an initiating flow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For flows which are already initiating counterpart flows then it's a matter of using the existing flow session. Note however, the new ``FinalityFlow`` is inlined and so the sequence of sends and receives between the two flows will change and will be incompatible with your current flows. You can use the flow version API to write your flows in a -backwards compatible way. +backwards compatible manner. Here's what an upgraded initiating flow may look like: @@ -212,8 +222,9 @@ finalised transaction. If the initiator is written in a backwards compatible way :end-before: DOCEND ExistingResponderFlow :dedent: 12 -The responder flow may be waiting for the finalised transaction to appear in the local node's vault using ``waitForLedgerCommit``. -This is no longer necessary with ``ReceiveFinalityFlow`` and the call to ``waitForLedgerCommit`` can be removed. +You may already be using ``waitForLedgerCommit`` in your responder flow for the finalised transaction to appear in the local node's vault. +Now that it's calling ``ReceiveFinalityFlow``, which effectively does the same thing, this is no longer necessary. The call to +``waitForLedgerCommit`` should be removed. Step 4. Security: Upgrade your use of SwapIdentitiesFlow --------------------------------------------------------