Статьи

Сохранение транзакций, в которых подписавшимся является только часть сторон

Мне потребовалось некоторое время, чтобы придумать название, которое могло бы обобщить содержание этого поста, не превращаясь в само предложение. Я думаю, что мне удалось выбрать что-то разборчивое. В любом случае, позвольте мне уточнить, о чем я на самом деле говорю.

Я видел, как несколько человек задавали вопросы, подобные приведенному ниже в Slack:

В этом примере он показывает поток ответчика, когда узел, на котором выполняется поток ответчика, является одним из обязательных подписывающих лиц. Но как насчет случая, когда узел, на котором выполняется поток ответчика, не является обязательным подписывающим лицом (например, один из участников состояния, участвующего в передаче)? Нужно ли писать поток ответчика для такого узла? Если так, как я должен написать поток респондента?

Другими словами:

У меня есть штат, в котором есть набор участников. Некоторые из них должны подписать сделку, некоторые не должны. Как мне структурировать свои потоки, особенно поток респондента, чтобы справиться с этим?

Из-за того, как работают потоки респондента, где каждый контрагент запускает один и тот же код респондента (если не переопределен). Наличие группы контрагентов делает одно, а другое — что-то другое, не обрабатывается простым кодом, найденным в примерах. Ваши потоки должны быть построены, чтобы обрабатывать это явно.

Код для этого достаточно прост, но он может быть неочевиден, если вы некоторое время не разрабатывали с Corda.

Пока у меня есть два решения этой проблемы. Я уверен, что это лучшие решения, доступные в настоящее время, и я не могу думать ни о каких других, которые сработали бы или заслуживают внимания. Эти решения:

  • Отправка флага контрагентам, чтобы сообщить им, подписывают они или нет
  • Использование subflow’d @InitiatingFlow s для сбора подписей или сохранения транзакции

Я буду расширять их в следующих разделах.

Прежде чем я доберусь до них, что произойдет, если вы не учтете разницу между подписавшими и участниками? Типичный поток ответчика будет включать в себя:

01
02
03
04
05
06
07
08
09
10
11
@InitiatedBy(SendMessageFlow::class)
class SendMessageResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    subFlow(object : SignTransactionFlow(session) {
      override fun checkTransaction(stx: SignedTransaction) { }
    })
    return subFlow(ReceiveFinalityFlow(otherSideSession = session))
  }
}

Если инициирующий поток запускает этот респондент для не подписывающего контрагента, возникает ошибка:

01
02
03
04
05
06
07
08
09
10
11
net.corda.core.flows.UnexpectedFlowEndException: Tried to access ended session SessionId(toLong=3446769309292325575) with empty buffer
    at net.corda.node.services.statemachine.FlowStateMachineImpl.processEventsUntilFlowIsResumed(FlowStateMachineImpl.kt:161) ~[corda-node-4.0.jar:?]
    at net.corda.node.services.statemachine.FlowStateMachineImpl.suspend(FlowStateMachineImpl.kt:407) ~[corda-node-4.0.jar:?]
    at net.corda.node.services.statemachine.FlowSessionImpl.receive(FlowSessionImpl.kt:67) ~[corda-node-4.0.jar:?]
    at net.corda.node.services.statemachine.FlowSessionImpl.receive(FlowSessionImpl.kt:71) ~[corda-node-4.0.jar:?]
    at net.corda.core.flows.SignTransactionFlow.call(CollectSignaturesFlow.kt:294) ~[corda-core-4.0.jar:?]
    at net.corda.core.flows.SignTransactionFlow.call(CollectSignaturesFlow.kt:198) ~[corda-core-4.0.jar:?]
    at net.corda.node.services.statemachine.FlowStateMachineImpl.subFlow(FlowStateMachineImpl.kt:290) ~[corda-node-4.0.jar:?]
    at net.corda.core.flows.FlowLogic.subFlow(FlowLogic.kt:314) ~[corda-core-4.0.jar:?]
    at dev.lankydan.tutorial.flows.SendMessageResponder.call(SendMessageFlow.kt:70) ~[main/:?]
    at dev.lankydan.tutorial.flows.SendMessageResponder.call(SendMessageFlow.kt:64) ~[main/:?]

Это потому, что не подписавший никогда не отправляет транзакцию для подписания, но, увы, их код находится там, ожидая подписания транзакции, которая никогда не приходит. Как печально & # 55357; & # 56895 ;. Я здесь, чтобы не допустить грусти противников ваших потоков, как этот здесь.

Это также хороший обучающий момент. Если вы когда-либо видели трассировку стека, подобную приведенной выше, это, скорее всего, связано с неуместным send и receive s. Либо они находятся в неправильном порядке, либо отсутствует send или receive . Пробежитесь по вашему коду построчно, и вы должны быть в состоянии точно определить, где находится несоответствие.

Дифференциация по флагу

Это решение пришло ко мне первым, так как его легче понять.

Контрагент уведомляется, сообщая ему, должны ли они подписать транзакцию или нет. Затем их поток ответчика выполнит SignTransactionFlow или пропустит его и сразу перейдет к ReceiveFinalityFlow . Оба пути всегда будут получать флаг и вызывать ReceiveFinalityFlow .

Пример можно найти ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@InitiatingFlow
class SendMessageFlow(private val message: MessageState) :
  FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    val spy = serviceHub.identityService.partiesFromName("Spy", false).first()
 
    val tx = verifyAndSign(transaction(spy))
 
    // initiate sessions with each party
    val signingSession = initiateFlow(message.recipient)
    val spySession = initiateFlow(spy)
 
    // send signing flags to counterparties
    signingSession.send(true)
    spySession.send(false)
 
    val stx = collectSignature(tx, listOf(signingSession))
 
    // tell everyone to save the transaction
    return subFlow(FinalityFlow(stx, listOf(signingSession, spySession))
  }
 
  private fun transaction(spy: Party) =
    TransactionBuilder(notary()).apply {
      // the spy is added to the messages participants
      val spiedOnMessage = message.copy(participants = message.participants + spy)
      addOutputState(spiedOnMessage, MessageContract.CONTRACT_ID)
      addCommand(Command(Send(), listOf(message.recipient, message.sender).map(Party::owningKey)))
    }
}
 
@InitiatedBy(SendMessageFlow::class)
class SendMessageResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    // receive the flag
    val needsToSignTransaction = session.receive<Boolean>().unwrap { it }
    // only sign if instructed to do so
    if (needsToSignTransaction) {
      subFlow(object : SignTransactionFlow(session) {
        override fun checkTransaction(stx: SignedTransaction) { }
      })
    }
    // always save the transaction
    return subFlow(ReceiveFinalityFlow(otherSideSession = session))
  }
}

Важные моменты к коду выше:

  • spy (другая сторона) добавлен в список participants штата
  • Флаги рассылаются участникам, сообщая им, подписывать или нет
  • Поток респондента receive флаг и пропускает SignTransactionFlow если ему
  • ReceiveFinalityFlow вызывается в ожидании, когда инициатор вызовет FinalityFlow

Я оставлю объяснение на этом, так как больше нечего сказать.

Разделение логики дополнительными инициирующими потоками

Это решение немного сложнее из-за косвенности, вызванной тем, что разные потоки смешиваются друг с другом. Это решение действительно необходимо прочитать, прежде чем можно будет сделать какое-либо объяснение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@InitiatingFlow
@StartableByRPC
class SendMessageWithExtraInitiatingFlowFlow(private val message: MessageState) :
  FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    logger.info("Started sending message ${message.contents}")
 
    val spy = serviceHub.identityService.partiesFromName("Spy", false).first()
 
    val tx = verifyAndSign(transaction(spy))
 
    // collect signatures from the signer in a new session
    val stx = subFlow(CollectSignaturesInitiatingFlow(tx, listOf(message.recipient)))
 
    // initiate new sessions for all parties
    val sessions = listOf(message.recipient, spy).map { initiateFlow(it) }
 
    // tell everyone to save the transaction
    return subFlow(FinalityFlow(stx, sessions))
  }
 
  private fun transaction(spy: Party) =
    TransactionBuilder(notary()).apply {
      // the spy is added to the messages participants
      val spiedOnMessage = message.copy(participants = message.participants + spy)
      addOutputState(spiedOnMessage, MessageContract.CONTRACT_ID)
      addCommand(Command(Send(), listOf(message.recipient, message.sender).map(Party::owningKey)))
    }
}
 
@InitiatedBy(SendMessageWithExtraInitiatingFlowFlow::class)
class SendMessageWithExtraInitiatingFlowResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    // save the transaction and nothing else
    return subFlow(ReceiveFinalityFlow(otherSideSession = session))
  }
}
 
@InitiatingFlow
class CollectSignaturesInitiatingFlow(
  private val transaction: SignedTransaction,
  private val signers: List<Party>
) : FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    // create new sessions to signers and trigger the signing responder flow
    val sessions = signers.map { initiateFlow(it) }
    return subFlow(CollectSignaturesFlow(transaction, sessions))
  }
}
 
@InitiatedBy(CollectSignaturesInitiatingFlow::class)
class CollectSignaturesResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
 
  @Suspendable
  override fun call(): SignedTransaction {
    // sign the transaction and nothing else
    return subFlow(object : SignTransactionFlow(session) {
      override fun checkTransaction(stx: SignedTransaction) { }
    })
  }
}

Поток кода выше выглядит следующим образом:

  • spy (другая сторона) добавлен в список participants штата
  • Подпись подписавшего собирается путем вызова CollectSignaturesInitiatingFlow
  • CollectSignaturesInitiatingFlow создает новый сеанс и вызывает CollectSignaturesFlow
  • CollectSignaturesResponder подписывает транзакцию, отправленную CollectSignaturesInitiatingFlow
  • Больше сеансов инициируется для каждого участника
  • FinalityFlow который запускает SendMessageWithExtraInitiatingFlowResponder связанный с исходным потоком / потоком верхнего уровня

Приведенный выше код основан на том факте, что любой поток, аннотированный @InitiatingFlow будет перенаправлен своему партнеру @InitiatedBy и это будет сделано в новом сеансе . Использование этого позволяет добавлять поток ответчика, который запускается только для необходимых подписчиков. Сеансы все еще создаются для потока верхнего уровня ( SendMessageWithExtraInitiatingFlowFlow ) и используются в FinalityFlow .

Есть несколько других вещей, которые происходят под обложками, но они не нужны для контекста этого поста.

Как лучше?

Трудно сказать на данный момент. Я должен был бы сделать небольшое тестирование производительности и поиграть с кодом еще немного…

Мое текущее мнение, что дополнительные инициирующие потоки работают немного лучше. Это устраняет необходимость дополнительной поездки по сети от инициатора к каждому отдельному контрагенту. Он добавляет немного дополнительного стандартного кода, но также извлекает логику подписи из остальной части кода ответчика / контрагента.

Честно говоря, как я сказал минуту назад, мне действительно нужно использовать его и придумать более сложные варианты использования, прежде чем я смогу дать хороший ответ. Я сомневаюсь, что когда-нибудь вернусь к этому, хотя …;

Вывод

Какой бы маршрут вы ни выбрали, или даже если вам удастся придумать другой, сохранение транзакций, в которых только подписчики являются подписавшими сторонами, может быть сделано на Corda на 100%. Я был бы очень разочарован, если бы это было невозможно. Пока у вас есть процесс для изменения логики в потоках респондента для обработки различных send и receive которые нужны подписывающим и не подписывающим сторонам. Тебе должно быть хорошо идти.

Если вам понравился этот пост или вы нашли его полезным (или и тем, и другим), пожалуйста, не стесняйтесь, следите за мной в Твиттере на @LankyDanDev и не забудьте поделиться с кем-либо, кто может найти это полезным!

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . См. Оригинальную статью здесь: Сохранение транзакций, когда только часть сторон являются подписавшими сторонами.

Мнения, высказанные участниками Java Code Geeks, являются их собственными.