Статьи

Разработка с Corda

В своем последнем посте я дал обзор того, чего пытается достичь Corda, и проектных решений, которые были приняты для этого. Эту информацию приятно знать, поэтому мы можем получить некоторое представление о платформе, но она не поможет нам в написании системы, которая может использовать Corda. Для этого нам нужно знать, как компоненты Corda работают и сочетаются друг с другом, только тогда мы можем начать писать приложение, которое действительно что-то делает и работает правильно с точки зрения Corda. Я считаю, что это то же самое, что и изучение любой другой инфраструктуры, но нам нужен голос в затылке, чтобы время от времени напоминать нам о том, что мы на самом деле разрабатываем платформу технологии распределенной книги, чтобы мы могли убедиться, что приложения, которые мы создать правильно разработаны.

В этом посте мы рассмотрим написание очень простого приложения Corda.

Corda совместима с любым языком JVM. Я знаю, что вы думаете об этом, но нет, это не написано на Java. Это на самом деле написано на Kotlin. Это может потребовать немного дополнительного обучения, чтобы разобраться с Kotlin, если вы обычно работаете с Java (как я), но это не займет у вас много времени, чтобы освоиться с ним. В связи с этим я лично предлагаю написать свой собственный код на Kotlin, чтобы весь стек оставался на одном языке, поэтому, когда вам нужно будет углубиться в собственный код Corda, он не будет выглядеть чуждо по сравнению с тем, что вы только что написали. Очевидно, что это только мое предложение, и вы могли бы вместо этого использовать Clojure, но вам будет трудно взять какие-либо существующие примеры без предварительного преобразования их в их эквиваленты Clojure.

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

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

Обзор приложения

Прежде чем мы сможем начать писать какой-либо код, нам нужно понять, чего пытается достичь приложение. Вместо того, чтобы придумывать собственный пример, я буду опираться на учебные материалы r3, поскольку пример достаточно прост для понимания. Этот пример — процесс «долгового обязательства» (я должен вам), когда кто-то просит денег, которые будут возвращены позднее. В этом посте мы просто сосредоточимся на выпуске долговых расписок.

Разработка с Corda

Упрощенная схема транзакции векселя

Ниже приведена диаграмма последовательности, содержащая очень упрощенные шаги, связанные с выдачей IOU между двумя людьми:

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

Итак, как этот процесс моделируется в Corda? Опять же, прежде чем мы продолжим, я хочу упомянуть ключевые концепции Corda, так как они чрезвычайно важны для понимания того, что происходит должным образом.

Corda

Схема операции векселя

Чтобы смоделировать процесс в Corda, нам нужно заменить некоторые шаги. Определение того, сколько денег занимать, становится созданием транзакции, содержащей эту информацию. Сторона, которая довольна тем, что предлагается, теперь представлена ​​подписанием транзакции своим ключом. Связь между заемщиком и кредитором заменяется отправкой транзакции между ними. Наконец, запоминание того, кто кому должен, теперь стоит в камне, и обе стороны сохраняют сделку, которая произошла между ними. Изменяя простую диаграмму этой новой информацией, создается новая версия:

Есть больше шагов, но процесс все еще довольно прост. Диаграмма действительно говорит сама за себя, и я не думаю, что могу добавить к ней что-то еще. Что я скажу, так это то, что я еще немного упростил диаграмму, удалив нотариуса, но только для того, чтобы сосредоточиться на процессе, который мы пытаемся смоделировать. Позже мы коснемся того, чем является нотариус очень, очень, очень кратко, но я снова предложу документацию Корды по этому вопросу (я сделаю то же самое позже).

Эти диаграммы должны предоставить нам достаточное руководство, чтобы собрать наше приложение Corda для выдачи долговых расписок между двумя сторонами.

состояния

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

Как упоминалось ранее, я опираюсь на учебные материалы r3 . Ниже IOUState который будет передан с приложением:

1
2
3
4
5
6
7
data class IOUState(val amount: Amount<Currency>,
                    val lender: Party,
                    val borrower: Party,
                    val paid: Amount<Currency> = Amount(0, amount.token),
                    override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {   
                    override val participants: List<Party> get() = listOf(lender, borrower)
                    }

Из-за концепции «долговые расписки» почти все поля имеют смысл без особых объяснений; amount — это одолженная сумма, lender участник, одалживающий деньги и так далее. Единственное свойство, которое нуждается в объяснении, это linearId который имеет тип UniqueIdentifier , этот класс в основном является UUID, фактически его internalId генерируется из класса UUID .

Состояние расширяет LinearState который является одним из основных типов состояний, которые Corda использует с другим FungibleState . Оба они являются реализациями ContractState . LinearState s используются для представления состояний, которые, цитируя свои документы, «эволюционируют, заменяя себя». Таким образом, когда состояние обновляется, оно должно быть включено в качестве ввода транзакции с выводом более новой версии. Старое состояние теперь будет помечено как CONSUMED из UNCONSUMED при сохранении в хранилище (абстракция Корды над базой данных).

ContractState требует свойство, которое возвращает участников состояния.

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

В приведенном выше коде участники не были включены в конструктор и вместо этого полагаются на определение функции get которая может использоваться для их извлечения. Здесь он возвращает lender и borrower поскольку они являются единственными двумя сторонами, вовлеченными в сделку. Если вы хотите, вы можете добавить participants в конструктор, как показано ниже:

1
2
3
4
5
6
data class IOUState(val amount: Amount<Currency>,
                    val lender: Party,
                    val borrower: Party,
                    val paid: Amount<Currency> = Amount(0, amount.token),
                    override val linearId: UniqueIdentifier = UniqueIdentifier(),
                    override val participants: List<Party> = listOf(lender, borrower)) : LinearState

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

контракты

Далее идут контракты. Контракты используются для проверки состояний ввода и вывода для данной команды всеми сторонами, участвующими в транзакции, например, команда может выдавать состояние или выплачивать причитающиеся деньги. Мы рассмотрим команды чуть позже в этом разделе, но сейчас мы сможем их закрасить.

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

Эти условия используют requireThat DSL, который определяет Corda, чтобы указать условия вместе с сообщениями об ошибках, в которых подробно описывается, что не так с транзакцией. Это позволяет легко и просто пройти контракт и понять, что он делает, поскольку условия кода хорошо дополняются сообщениями на английском языке (или на любом другом языке, на котором вы хотите их написать).

Ниже приведен пример контракта, который используется для проверки состояния IOUState определенного выше, опять же это взято из учебных материалов r3 :

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
class IOUContract : Contract {
    companion object {
        @JvmStatic
        val IOU_CONTRACT_ID = "net.corda.contracts.IOUContract"
    }
 
    interface Commands : CommandData {
        class Issue : TypeOnlyCommandData(), Commands
        class Transfer : TypeOnlyCommandData(), Commands
        class Settle : TypeOnlyCommandData(), Commands
    }
 
    override fun verify(tx: LedgerTransaction) {
        val command = tx.commands.requireSingleCommand<Commands>()
        when (command.value) {
            is Commands.Issue -> requireThat {
                "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
                "Only one output state should be created when issuing an IOU." using (tx.outputs.size == 1)
                val iou = tx.outputStates.single() as IOUState
                "A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)
                "The lender and borrower cannot have the same identity." using (iou.borrower != iou.lender)
                "Both lender and borrower together only may sign IOU issue transaction." using
                        (command.signers.toSet() == iou.participants.map { it.owningKey }.toSet())
            }
            is Commands.Transfer -> requireThat {
                // more conditions
            }
            is Commands.Settle -> {
               // more conditions
            }
        }
    }
}

Для этого поста я упростил контракт, поскольку мы сосредоточимся только на реализации одного типа команд. Давайте начнем с вершины и продолжим наш путь.

IOUContract реализует Contract требующий, чтобы у него теперь была функция verify которая IOUContract для проверки (отсюда и имя) транзакции.

Название класса контракта

Имя класса контракта было включено сюда:

1
2
3
4
companion object {
    @JvmStatic
    val IOU_CONTRACT_ID = "net.corda.contracts.IOUContract"
}

Это используется в других частях Корды, когда требуется отражение. Учебные материалы r3 сделали это таким образом, но я лично считаю, что это немного забавно и должно быть сделано по-другому.

1
2
3
4
companion object {
    @JvmStatic
    val IOU_CONTRACT_ID = IOUContract::class.qualifiedName!!
}

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

команды

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

1
2
3
4
5
interface Commands : CommandData {
    class Issue : TypeOnlyCommandData(), Commands
    class Transfer : TypeOnlyCommandData(), Commands
    class Settle : TypeOnlyCommandData(), Commands
}

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

Поскольку эти команды просты и используются только для указания намерения, TypeOnlyCommandData расширен. Доступны другие абстрактные классы, которые определяют команды, которые мы могли бы использовать, например MoveCommand .

Мы увидим, как использовать команды в следующем разделе.

Осуществление проверки

Вот где происходит большая часть магии, код был скопирован и вставлен ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
override fun verify(tx: LedgerTransaction) {
    val command = tx.commands.requireSingleCommand()
    when (command.value) {
        is Commands.Issue -> requireThat {
            "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
            "Only one output state should be created when issuing an IOU." using (tx.outputs.size == 1)
            val iou = tx.outputStates.single() as IOUState
            "A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)
            "The lender and borrower cannot have the same identity." using (iou.borrower != iou.lender)
            "Both lender and borrower together only may sign IOU issue transaction." using
                    (command.signers.toSet() == iou.participants.map { it.owningKey }.toSet())
        }
        is Commands.Transfer -> requireThat {
            // more conditions
        }
        is Commands.Settle -> {
            // more conditions
        }
    }
}

Функция verify проверяет, является ли предложенная транзакция действительной. Если это так, транзакция будет продолжаться и, скорее всего, будет подписана и передана в бухгалтерскую книгу, но если какое-либо из условий не будет выполнено, IllegalArgumentException что, вероятно, приведет к прекращению предложенной транзакции.

Исключением является то, как Corda справляется с неудовлетворенными требованиями. Когда генерируется исключение, предполагая, что ничто не пытается его перехватить, выполнение прекращается и распространяется до тех пор, пока оно не будет перехвачено или не попадет в вывод консоли. Используя это, он предоставляет простой способ управления потоком транзакции, поскольку ее можно остановить в любом месте во время ее выполнения, даже на узле контрагента, так как исключение будет передано обратно инициатору.

На сам код подтверждения. Команда, которую транзакция выполняет в своих состояниях, извлекается, и в зависимости от типа выполняются различные проверки для проверки действительности транзакции. requireThat DSL, предоставляемый Corda, позволяет вам написать условие, которое должно быть истинным, чтобы продолжить вывод сообщения об ошибке, которое выводится, если условие ложно.

Давайте посмотрим на одно из требований requireThat чуть более подробно:

1
2
val iou = tx.outputStates.single() as IOUState
"A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)

Theres не так много, чтобы объяснить здесь. DSL заботится о цели заявления. То, что я укажу, это синтаксис:

1
<string message of what condition should be met> using <condition it must pass>

Достаточно просто. Точка, которая глупо поймала меня немного: если условие содержит пробелы, оно должно быть заключено в квадратные скобки. Наконец, DSL может содержать код, который отсутствует в выражении условия, что позволяет вам инициализировать переменные, и такой, который затем можно использовать в реальных условиях.

На сегодня достаточно контрактов. Они появятся снова в следующем разделе, когда мы IOUContract в действие.

Потоки

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

Следуя примерам в предыдущих разделах, теперь мы реализуем IOUIssueFlow . Опять же, это взято из учебных материалов r3 . Ниже приведен код в целом, который мы затем разделим и рассмотрим:

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
@InitiatingFlow
@StartableByRPC
class IOUIssueFlow(private val state: IOUState) : FlowLogic() {
 
    @Suspendable
    override fun call(): SignedTransaction {
        val notary = serviceHub.networkMapCache.notaryIdentities.first()
        val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
        val transaction = TransactionBuilder(notary = notary)
        transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID)
        transaction.addCommand(issueCommand)
        transaction.verify(serviceHub)
        val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)
        val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
        val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))
        subFlow(FinalityFlow(allSignedTransaction))
        return allSignedTransaction
    }
}
 
@InitiatedBy(IOUIssueFlow::class)
class IOUIssueFlowResponder(private val flowSession: FlowSession) : FlowLogic() {
    @Suspendable
    override fun call() {
        val signedTransactionFlow = object : SignTransactionFlow(flowSession) {
            override fun checkTransaction(stx: SignedTransaction) {
                requireThat {
                    val output = stx.tx.outputs.single().data
                    "This must be an IOU transaction" using (output is IOUState)
                }
            }
        }
        subFlow(signedTransactionFlow)
    }
}

Поток этого кода (да, это каламбур) достаточно прост и, возможно, будет одним из типичных потоков, которые вы пишете в своем собственном приложении.

Все, что он делает, это:

  • Создать государство
  • Добавить состояние в новую транзакцию
  • Подтвердите сделку с договором
  • Подпишите сделку
  • Запросить подписи контрагентов
  • Сохранить транзакцию для всех участников

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

Инициирующий Поток

Во-первых, класс потока аннотируется @InitiatingFlow и расширяет FlowLogic . Эта комбинация требуется любым потоком, который запрашивает связь с контрагентом. FlowLogic содержит один абстрактный call функции, который должен быть реализован потоком. Здесь происходит вся магия. Когда поток запускается, что мы рассмотрим позже, выполняется call и любая логика, которую мы поместили в функцию, очевидно, запускается. FlowLogic является универсальным ( FlowLogic<T> ), где T определяет тип возвращаемого call . В приведенном выше примере SignedTransaction но вполне возможно использовать FlowLogic<Unit> если у вас нет желания возвращать что-либо обратно вызывающей стороне потока.

Далее @StartableByRPC аннотация @StartableByRPC . Это позволяет вызывать поток из соединения RPC, которое является интерфейсом между внешней частью узла Corda и его внутренними компонентами. Мы еще немного коснемся этого, когда рассмотрим запуск потока.

Еще одна аннотация появляется. @Suspendable самом деле происходит из quasar-core а не из одной из собственных библиотек Corda. Эта аннотация важна, и если вы забудете добавить ее, вы можете столкнуться с ошибками, которые не обязательно указывают на то, что идет не так. Это необходимо для всех функций, которые взаимодействуют с контрагентом. Как следует из названия «приостановлено», аннотация позволяет приостановить функцию, пока контрагент имеет дело со своей стороной транзакции. Здесь происходит немного магии, и это кратко затронуто в документации Corda по потокам .

Теперь мы закончили с аннотациями, которые мы можем посмотреть на содержание call . Я вставил его ниже, чтобы сэкономить энергию при прокрутке:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Suspendable
    override fun call(): SignedTransaction {
    val notary = serviceHub.networkMapCache.notaryIdentities.first()
    val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
    val transaction = TransactionBuilder(notary = notary)
    transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID)
    transaction.addCommand(issueCommand)
    transaction.verify(serviceHub)
    val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)
    val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
    val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))
    subFlow(FinalityFlow(allSignedTransaction))
    return allSignedTransaction
}

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

Создание транзакции

Сначала рассмотрим построение предложенной транзакции, соответствующий код был извлечен ниже:

1
2
3
4
5
val notary = serviceHub.networkMapCache.notaryIdentities.first()
val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
val transaction = TransactionBuilder(notary = notary)
transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID.toString())
transaction.addCommand(issueCommand)

Для целей этого поста мы будем предполагать, что есть только один нотариус, который позволяет нам быть ленивым и просто получить первый из списка. Если вы не знаете, что такое нотариус, как и раньше, я предлагаю вам ознакомиться с основными понятиями Corda, чтобы получить хорошее объяснение по этой теме. А пока я предоставлю вам минимум для продолжения. Нотариус — это узел, единственной целью которого является проверка того, что в транзакции, отправленной ему, не было двойных расходов, и дополнительная проверка также может быть выполнена, если он настроен на это.

serviceHub поставляется с тех пор, как мы расширили FlowLogic ; функция networkMapCache затем предоставляет нам идентификационные данные сторон в сети, а notaryIdentities еще больше notaryIdentities ее. Как я упоминал ранее, мы будем ленивы и просто возьмем первый из этого списка. Способ получения нотариуса, который вы хотите использовать в транзакции, может измениться в зависимости от ваших требований.

Затем мы создаем команду, которая представляет цель транзакции. В этом случае мы используем IOUContract.Commands.Issue который мы определили ранее. При создании команды нам также необходимо предоставить открытые ключи сторон, необходимых для подписания транзакции. it Party а owningKey представляет их открытый ключ. Единственные подписанты в этой транзакции содержатся в свойстве participants состояния, но вместо этого может быть передан независимый список.

Все компоненты, которые нам нужны для нашей транзакции, теперь получены или созданы. Теперь нам нужно начать собирать все вместе. TransactionBuilder делает именно это. Полученный нами нотариус может быть передан только через конструктор TransactionBuilder , в то время как другие имеют различные методы добавления, а также могут быть включены в конструктор. addOutputState принимает state переданное в поток, вместе с именем контракта, которое его проверит. Помните, я упомянул два способа получить это имя; через открытое свойство в объекте, как обычно это делает Corda, или добавляя имя класса вручную, в любом случае конечная цель одинакова. Последний компонент, который мы добавляем к этой транзакции, это команда, которую мы создали.

Проверка и подписание транзакции

Следующий блок кода посвящен проверке и подписанию транзакции, опять же соответствующий код был вставлен ниже:

1
2
transaction.verify(serviceHub)
val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)

Как только мы будем рады, что все, что мы хотим включить в транзакцию, включено, мы должны это проверить. Просто вызовите функцию verify предоставляет TransactionBuilder . Эта функция приводит к проверке внутри контракта для транзакции. Как упоминалось ранее в разделе о контракте, если какое-либо из условий контракта не выполняется, создается исключение. Поскольку в этом коде нет попыток перехватить исключение, поток завершится ошибкой, поскольку исключение распространяется вверх по стеку.

После того, как транзакция прошла проверку, для нас (инициатора) транзакция готова для совместного использования с другими сторонами. Для этого serviceHub.signInitialTransaction . Это оставляет нас с новой SignedTransaction которая в настоящее время только подписана нами. Подписание этой транзакции станет важным позже, когда нотариус проверит, что транзакция была подписана всеми заинтересованными сторонами.

Сбор подписей контрагентов

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

Ниже приведен код, отвечающий за сбор подписей:

1
2
val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))

Контрагенты по данной сделке определяются сторонами в списке participants . Если вспомнить, как было построено поле participants в штате, в нем содержались только две стороны, поэтому только две стороны должны будут подписать транзакцию. Хотя это утверждение верно, транзакция уже подписана инициатором, поэтому теперь только один контрагент ( lender ) должен подписать ее.

Чтобы отправить транзакцию контрагенту, нам сначала нужно создать с ними сеанс. initiateFlow делает именно это. Он принимает FlowSession и возвращает FlowSession который будет использоваться для связи. Как уже упоминалось, инициатору не нужно снова подписывать транзакцию с помощью этой конструкции, поэтому в коде они были удалены от сторон, для которых создаются сеансы связи. Из-за того, что мы знаем, кто участвует в этой транзакции, можно было бы написать ниже:

1
2
val session = initiateFlow(state.lender)
val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, listOf(session)))

Вместо того чтобы полагаться на список участников, мы просто создаем сессию для lender поскольку наши знания о состоянии указывают на то, что они являются единственным контрагентом.

FlowSession необходимо использовать внутри CollectSignaturesFlow вместе с SignedTransaction которая на данный момент подписана только инициатором. Это наша первая встреча с subFlow s. Это потоки, похожие на те, которые мы рассматриваем в этом посте, которые вызываются из другого потока. CollectSignaturesFlow не может быть запущен сам по себе, так как он не аннотирован @InitiatingFlow , поэтому его можно использовать только из subFlow . Большинство потоков, предоставляемых Corda из коробки, попадают в эту же категорию.

Все, subFlow делает subFlow , — это запускает поток, передаваемый в него, вызывая функцию вызова потока и возвращая то, что поток обычно возвращает. Поток не требует ничего особенного для передачи в subFlow , если нам когда-либо понадобится, IOUIssueFlow может быть передан в него из другого потока.

Corda предоставляет потоки для многих типичных операций, которые необходимо повторять в наших собственных потоках. Они вызываются через subFlow и включают (и многие другие): CollectSignaturesFlow , SignTransactionFlow , SendTransactionFlow и ReceiveTransactionFlow .

Во всяком случае, вернемся к потоку под рукой! CollectSignaturesFlow отправляет SignedTransaction контрагенту и ожидает его ответа. Мы посмотрим, как ответ будет отправлен обратно в следующем разделе. После возврата SignedTransaction завершена, так как подписана всеми и теперь может быть сохранена.

Сохранение подписанной транзакции

Это самый маленький фрагмент, который мы увидим во время этой разбивки, целая строка:

1
subFlow(FinalityFlow(allSignedTransaction))

Хотя, для одного лайнера, этот кусок кода упакован довольно удар. FinalityFlow , скорее всего, всегда будет вызываться в конце ваших потоков, по крайней мере, для простых потоков в любом случае.

Вызов FinalityFlow будет:

  • Отправить транзакцию нотариусу (при необходимости)
  • Сохранить транзакцию в хранилище инициатора
  • Трансляция участникам транзакции, чтобы сохранить ее в своих хранилищах

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

Ответный поток

Все в потоке, на который мы смотрели до сих пор, находится на стороне инициатора процесса. Во время примера было несколько раз, когда транзакция была отправлена ​​контрагенту, и происходили некоторые «вещи». В этом кратком разделе мы рассмотрим код, который будет запускать контрагент:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@InitiatedBy(IOUIssueFlow::class)
class IOUIssueFlowResponder(private val flowSession: FlowSession) : FlowLogic() {
    @Suspendable
    override fun call() {
        val signedTransactionFlow = object : SignTransactionFlow(flowSession) {
            override fun checkTransaction(stx: SignedTransaction) {
                requireThat {
                    val output = stx.tx.outputs.single().data
                    "This must be an IOU transaction" using (output is IOUState)
                }
            }
        }
        subFlow(signedTransactionFlow)
    }
}

Как и прежде, этот код был включен ранее в пост.

Наиболее важной строкой в ​​этом классе является аннотация @InitiatedBy которая указывает, к какому потоку он принимает запросы и отвечает обратно, в данном примере это IOUIssueFlow который мы уже прошли.

Поскольку IOUIssueFlowResponder также является потоком, он расширяет FlowLogic и должен будет реализовать свою собственную версию call . FlowSession в конструкторе — это сеанс, который использовался инициатором для связи с этим потоком. @Suspendable также используется при call как это было в @Suspendable потоке.

SignTransactionFlow — это другая половина CollectSignaturesFlow которая была вызвана в инициаторе. Это абстрактный класс, который требует реализации checkTransaction . Он содержит любую дополнительную проверку, которую контрагент может захотеть выполнить с транзакцией. SignTransaction call SignTransaction будет по-прежнему проверять транзакцию по контракту, так что это шанс для всего остального; обеспечение соответствия транзакции стандартам контрагента. Сказав это, checkTransaction также может содержать как можно меньше кода и даже может быть пустым, если проверки договора достаточно. Вместо того, чтобы показывать вам, как это будет выглядеть, я позволю вам использовать свое яркое воображение, чтобы представить пустую функцию …

Наконец, subFlow вызывается для реализации SignTransactionFlow приводящей к его выполнению. Проверка в контракте выполняется, после чего следует содержимое checkTransaction и если все проверки возвращаются в порядке, транзакция подписывается и отправляется туда, откуда она пришла.

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

Как бы я структурировал Инициирующий Поток

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

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
@InitiatingFlow
@StartableByRPC
class IOUIssueFlow(private val state: IOUState) : FlowLogic() {
    @@Suspendable
    override fun call(): SignedTransaction {
        val stx =  collectSignatures(verifyAndSign(transaction()))
        return subFlow(FinalityFlow(stx))
    }
 
    @Suspendable
    private fun collectSignatures(transaction: SignedTransaction): SignedTransaction {
        val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
        return subFlow(CollectSignaturesFlow(transaction, sessions))
    }
 
    private fun verifyAndSign(transaction: TransactionBuilder): SignedTransaction {
        transaction.verify(serviceHub)
        return serviceHub.signInitialTransaction(transaction)
    }
 
    private fun transaction() = TransactionBuilder(notary()).apply {
        addOutputState(state, IOUContract.IOU_CONTRACT_ID)
        addCommand(Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey }))
    }
 
    private fun notary() = serviceHub.networkMapCache.notaryIdentities.first()
}

Я не думаю, что я сделал что-то особенное здесь, но я думаю, что это разделение делает каждый шаг яснее. call сокращается до двух строк (одной, если вы действительно этого хотите), и я даже не буду объяснять, что делает каждый метод, поскольку мы уже прошли через код, а имена функций так точно описывают, что они делают. Во всяком случае, если вы предпочитаете писать это так, то отлично. Если нет, то делай что хочешь; Я не расстроюсь, обещаю …

Начиная поток

В этом последнем разделе мы рассмотрим, как вызвать поток из-за пределов узла Corda.

Есть несколько способов сделать это, каждый работает немного по-своему. Но давайте сохраним этот краткий и приятный вопрос и рассмотрим только стандартную функцию startFlow :

1
proxy.startFlow(::IOUIssueFlow, state)

Вот и все. Как я уже сказал, коротко и сладко. proxy имеет тип CordaRPCOps который содержит множество функций, вращающихся вокруг взаимодействия с узлом Corda через RPC. startFlow — одна из таких функций. Он принимает имя класса потока вместе с любыми аргументами, которые являются частью конструктора потока. Таким образом, в этом примере функция вызова IOUState будет вызываться с IOUState для использования в потоке.

FlowHandle<T> где T — это тот же универсальный тип вызванного потока, в данном случае SignedTransaction . returnValue можно returnValue для получения CordaFuture , позволяя получить результат, как только он станет доступен. CordaFuture — это подтип стандартного Future с несколькими доступными дополнительными методами, одним из которых является toCompletableFuture который может быть или не быть полезным для вас (в любом случае, это было полезно для меня).

Завершение

Вот и мы, наконец, в конце.

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

Я планирую написать больше постов о разработке на Corda, сосредоточив внимание на более сложных функциях и углубившись в то, что происходит за кулисами.

Если вы нашли этот пост полезным и хотите не отставать от моих постов, когда я пишу их, вы можете подписаться на меня в Twitter по адресу @LankyDanDev .

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

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