Reputation: 378
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:
subFlow
for transaction 2 inline the flow responder.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
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:
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