在同一Corda流程中创建多个交易的风险是什么?

5

在Cordapp中,我希望在正常事务的一部分中更新第二个链。由于数据在具有不同参与者的两个单独状态中被跟踪,因此这需要在两个事务中完成。

为了讨论,我们有A和B两方。 A与B启动交易1。在收到交易1后,B方启动交易2以更新另一个状态。如何确保两个交易都成功完成?

有两种方法可以解决:

  1. 内联响应流程中启动交易2的subFlow
  2. 使用vaultTrack来响应已提交的交易1并启动交易2的subFlow

以下是选项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
    }
}

每种方法的利弊是什么?

我对选项1的担忧在于,单个流程中涉及的两个事务不是原子性的。其中一个事务可能会失败,而另一个成功,这将导致数据处于不一致的状态。例如:上面响应器中的subFlow可能会成功地完成第二个事务,但第一个事务可能因双重支付问题的记账失败。在这种情况下,第二条链将被错误地更新。

使用vaultTrack会更安全,因为第一个事务将会成功,但不能保证第二个事务最终会完成。


看起来你想要一个原子 Tx。如果是这样,在 Corda 中,Tx 是原子的。因此,任何需要原子性的内容都将成为单个 Tx 的一部分。你需要做的就是创建一个单独的 Tx,将所有想要的更改添加到其中,即输入和输出状态,然后提交该 Tx。 - Kid101
根据Joel的回答,你所说的可能是有可能的。如果我的问题回答者被转换为发起人,并且将客户数据发送给他们,他们可以创建一个带有两个输出的tx:一个是共享的客户数据(两个参与者),另一个是他们自己的私有链(发起人作为参与者)。 - Austin Moothart
1个回答

0
首先,你说:
引用: 由于数据在两个具有不同参与者的状态中被跟踪,因此需要进行两个事务。 这不一定是正确的。具有不同参与者的两个独立状态可以成为同一交易的一部分。然而,假设你在这里有一个保持它们分开的理由(例如隐私)。
自Corda 4以来,该平台没有提供多事务原子性保证。目前没有内置的方法确保只有在另一个事务提交后才提交给定的事务(但请参见下面的P.S.)。
因此,你的两个选项都不能保证多事务的原子性。我仍认为选项1更可取,因为你可以得到流框架的保证,即事务将被调用。你担心的是,即使第一个事务失败,响应方仍会调用创建第二个事务的流。可以使用waitForLedgerCommit来确保在启动创建第二个事务的流之前提交了第一个事务,从而避免这种情况:
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
    }
}

提示:实现多事务原子性的一种可能的方法是使用负担,具体如下:

  • 假设我们有两个事务:Tx1 输出 S1,Tx2 输出 S2
  • 在 Tx1 的过程中,对 S1 进行负担,只有在知道 Tx2 上的公证人签名或经过一段时间后才能花费 S1,否则将恢复到原始状态
  • 在 Tx2 的过程中,对 S2 进行负担,只有在知道 Tx1 上的公证人签名或经过一段时间后才能花费 S2,否则将恢复到原始状态

然而,一个可能的攻击是调用 FinalityFlow 的 Tx1 的调用方未分发 Tx1 的公证人签名,从而允许他们声称 Tx2 而不放弃 Tx1。如果公证人将所有签名发布到某个公告牌上而不是依赖于 FinalityFlow 的调用方来分发它们,则可以解决此问题。


更详细地说,我认为这必须是多个交易的原因是原始交易是由发行人创建的,而发行人不知道第二个交易。响应者正在开始第二个链来跟踪他们自己账本中的相同数据。 - Austin Moothart
对于 waitForLedgerCommit:它如何按时解决?流程响应者需要将 stx 返回给发起者,并让他们运行 finalityFlow。我原本以为第二个子流程会阻止进一步处理,因此发起者永远没有机会恢复。 - Austin Moothart
发起方只是在等待作为“CollectSignaturesFlow”一部分返回签名。然后它将继续并运行“FinalityFlow”。 “FinalityFlow”然后使用一个启动子流来分发交易,因此接收方将在不同的流响应器中记录交易。 “CustomerIssueFlowResponder”偶尔会醒来检查另一个流是否已将状态记录到保险库中。然后它继续。顺便说一下,在Corda 4中,“ReceiveFinalityFlow”的添加简化了事情,并消除了“waitForLedgerCommit”的需要。 - Joel
仍然不清楚的是,发起者如何从响应者那里获取已签名的交易。难道响应者被waitForLedgerCommit和最后的subFlow阻塞了吗?在纸面上看起来像是死锁。 - Austin Moothart
当流程遇到waitForLedgerCommit时,流程会被暂停,从而允许其他流程(包括接收并存储交易作为FinalityFlow响应的流程回应者)运行。 - Joel
请注意,在Corda 4中,如果您使用新的FinalityFlow API,则不再需要waitForLedgerCommit - Mike Hearn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接