Austin Moothart
Austin Moothart

Reputation: 378

What are the risks of creating multiple transactions within the same Corda flow?

Within a Cordapp I would like to update a second chain as part of normal transaction. As the data is tracked in two separate states with different participants this would need to be done in two transactions.

For purposes of discussion we have two parties A and B. A initiates transaction 1 with B. On receiving transaction 1 party B kicks off transaction 2 to update another state. How can we ensure that both transaction complete successfully?

There are two ways one can go about this:

  1. Initiate the subFlow for transaction 2 inline the flow responder.
  2. Use the vaultTrack to respond to the committed transaction 1 and initiate the subFlow for transaction 2.

Here is some example code of option 1:

class CustomerIssueFlowResponder(val otherPartyFlow: FlowSession) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val signTransactionFlow = object : SignTransactionFlow(otherPartyFlow) {
            override fun checkTransaction(stx: SignedTransaction) = requireThat {
                val output = stx.tx.outputs.single().data
                "This must be an CustomerState." using (output is CustomerState)
            }
        }
        // signing transaction 1
        val stx = subFlow(signTransactionFlow)
        val customerState = stx.tx.outputs.single().data as CustomerState
        // initiating transaction 2
        subFlow(CustomerIssueOrUpdateFlow(customerState))

        return stx
    }
}

What are the pros and cons of each approach?

My concern for option 1 is that two transactions within a single flow are not atomic. That one of the two transactions could fail and the other succeed which would leave the data in an inconsistent state. For example: the subFlow within the responder above could succeed for transaction 2 but transaction 1 could fail on notarization from a double spend issue. In that case the second chain would have been improperly updated.

Using the vaultTrack would be safer because transaction 1 will have succeeded but there is nothing to guarantee that the transaction 2 will eventually complete.

Upvotes: 5

Views: 705

Answers (1)

Joel
Joel

Reputation: 23140

Firstly, you say:

As the data is tracked in two separate states with different participants this would need to be done in two transactions.

This is not necessarily true. Two separate states with different participants can be part of the same transaction. However, let's assume that you have a reason to keep them separate here (e.g. privacy).

As of Corda 4, the platform provides no multi-transaction atomicity guarantees. There is no built-in way to ensure that a given transaction is only committed if another transaction is committed (but see the P.S. below).

So neither of your options would guarantee multi-transaction atomicity. I still believe option 1 would be preferable, as you get the guarantees of the flow framework that the transaction will be invoked. Your concern is that the flow creating the second transaction would be invoked by the responder even if the first transaction fails. This can be avoided using waitForLedgerCommit to ensure transaction 1 is committed before kicking off the flow to create the second transaction:

class CustomerIssueFlowResponder(val otherPartyFlow: FlowSession) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val signTransactionFlow = object : SignTransactionFlow(otherPartyFlow) {
            override fun checkTransaction(stx: SignedTransaction) = requireThat {
                val output = stx.tx.outputs.single().data
                "This must be an CustomerState." using (output is CustomerState)
            }
        }
        // signing transaction 1
        val stx = subFlow(signTransactionFlow)
        val customerState = stx.tx.outputs.single().data as CustomerState
        // initiating transaction 2 once transaction 1 is committed
        waitForLedgerCommit(stx.id)
        subFlow(CustomerIssueOrUpdateFlow(customerState))

        return stx
    }
}

P.S. One possible way to achieve multi-transaction atomicity is using encumbrances, as follows:

  • Imagine we have two transactions: Tx1, which outputs S1, and Tx2, which outputs S2
  • As part of Tx1, encumber S1 so that it can only be spent if you know the notary's signature over Tx2, or reverts to its original state after some time period elapses
  • As part of Tx2, encumber S2 so that it can only be spent if you know the notary's signature over Tx1, or reverts to its original state after some time period elapses

However, one attack that comes to mind is the caller of FinalityFlow for Tx1 not distributing the notary's signature over Tx1, allowing them to claim Tx2 without giving up Tx1. This would be solved if the notary published all its signatures to some bulletin board instead of relying on the caller of FinalityFlow to distribute them.

Upvotes: 0

Related Questions