Каковы риски создания нескольких транзакций в одном потоке Corda? - PullRequest
0 голосов
/ 16 января 2019

В Cordapp я хотел бы обновить вторую цепочку как часть обычной транзакции. Поскольку данные отслеживаются в двух отдельных состояниях с разными участниками, это необходимо сделать в двух транзакциях.

В целях обсуждения у нас есть две стороны A и B. A инициирует транзакцию 1 с B. При получении транзакции 1 сторона B запускает транзакцию 2 для обновления другого состояния. Как мы можем гарантировать, что обе транзакции успешно завершены?

Есть два способа сделать это:

  1. Инициируйте subFlow для транзакции 2, встроенной в ответчик потока.
  2. Используйте vaultTrack, чтобы ответить на совершенную транзакцию 1 и инициировать subFlow для транзакции 2.

Вот пример кода варианта 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 в ответчике выше может быть успешным для транзакции 2, но транзакция 1 может завершиться неудачно при нотариальном заверении из-за двойной оплаты. В этом случае вторая цепочка была бы неправильно обновлена.

Использование vaultTrack будет безопаснее, поскольку транзакция 1 будет успешной, но нет ничего, что гарантировало бы, что транзакция 2 в конечном итоге завершится.

1 Ответ

0 голосов
/ 16 января 2019

Во-первых, вы говорите:

Поскольку данные отслеживаются в двух отдельных состояниях с разными Участникам это необходимо будет сделать за две транзакции.

Это не обязательно так. Два отдельных состояния с разными участниками могут быть частью одной транзакции. Однако предположим, что у вас есть причина разделять их здесь (например, конфиденциальность).

Начиная с Corda 4, платформа не предоставляет многоядерных гарантий атомарности. Не существует встроенного способа гарантировать, что данная транзакция фиксируется только в том случае, если зафиксирована другая транзакция (но см. Пункт P.S. ниже).

Так что ни один из ваших вариантов не гарантирует атомарность нескольких транзакций. Я все еще верю, что вариант 1 был бы предпочтительнее, поскольку вы получаете гарантии структуры потока, что транзакция будет вызвана. Вы обеспокоены тем, что респондентом будет вызван поток, создающий вторую транзакцию, даже если первая транзакция завершится неудачно. Этого можно избежать, используя waitForLedgerCommit, чтобы убедиться, что транзакция 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 once transaction 1 is committed
        waitForLedgerCommit(stx.id)
        subFlow(CustomerIssueOrUpdateFlow(customerState))

        return stx
    }
}

P.S. Одним из возможных способов достижения атомарности в нескольких транзакциях является использование обременений следующим образом:

  • Представьте, что у нас есть две транзакции: Tx1, который выводит S1, и Tx2, который выводит S2
  • Как часть Tx1, ограничьте S1, чтобы его можно было потратить только в том случае, если вы знаете подпись нотариуса над Tx2, или возвращается в исходное состояние по истечении некоторого периода времени
  • Как часть Tx2, ограничьте S2, чтобы его можно было потратить только в том случае, если вы знаете подпись нотариуса над Tx1, или возвращается в исходное состояние по истечении некоторого периода времени

Однако, одна атака, которая приходит на ум, это вызывающий FinalityFlow для Tx1, который не распространяет подпись нотариуса по Tx1, что позволяет им требовать Tx2, не отказываясь от Tx1. Это было бы решено, если бы нотариус опубликовал все свои подписи на какой-либо доске объявлений вместо того, чтобы полагаться на вызывающего абонента FinalityFlow для их распространения.

...