CordaのFlowの構築手順を分かりやすく解説する

CordaのFlowの構築手順を分かりやすく解説する

はじめに

CordaのFlowを書くにはState, Contract, Command, Transactionなど様々なCordaの基本概念を理解している必要があります。また、Flowのコードには様々なクラスが出てくるので、最初のうちは難解に感じるところがあります。

この記事では難解なコードの概要を理解するため、APIの詳細には触れず全体像からコードの大きな流れを解説します。

CordaのFlowの流れ

詳細はあとで説明しますが、Flow全体の流れは以下のようになります。

今回読んでみるコード

AさんからBさんにオンラインクーポンを送るFlowを例に解説していきます。

@InitiatingFlow
@StartableByRPC
class InitiatorFlow(
        private val inputId: UniqueIdentifier,
        private val newOwner: Party
) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        /* Query input state */
        val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(inputId))
        val inputStateRef =
                serviceHub.vaultService.queryBy(CouponState::class.java, query).states.single()
        val notary = inputStateRef.state.notary

        /* Start building transaction */
        val txBuilder = TransactionBuilder(notary)

        /* Add input */
        txBuilder.addInputState(inputStateRef)

        /* Add output */
        val inputData = inputStateRef.state.data
        val outputState = inputData.copy(owner = newOwner)
        txBuilder.addOutputState(outputState)

        /* Add command */
        val txCommand =
                Command(
                        Send(),
                        inputData.participants.map { it.owningKey }
                )
        txBuilder.addCommand(txCommand)

        /* Verify */
        txBuilder.verify(serviceHub) // Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.

        /* Sign by initiator(A)*/
        val partSignedTx = serviceHub.signInitialTransaction(txBuilder)

        /* Create session with B */
        val otherPartySession = initiateFlow(newOwner)

        /* Collect sign from B */
        val fullySignedTx =
                subFlow(CollectSignaturesFlow(partSignedTx, setOf(otherPartySession)))
        /* Finalize and Record transaction */
        return subFlow(FinalityFlow(fullySignedTx, setOf(otherPartySession)))

    }
}

@InitiatedBy(InitiatorFlow::class)
class ResponderFlow(val otherPartySession: FlowSession) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
            override fun checkTransaction(stx: SignedTransaction) = requireThat {
                /* Verification Logic */
            }
        }
        val txId = subFlow(signTransactionFlow).id
        return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = txId))
    }
}

Transaction生成の流れ

FlowはTransactionを作成し実行するためにあります。Transaction構築の流れは大きくは以下の通りです。

  1. Transactionを構築する
  2. Transactionを検証し署名を集める
  3. Transactionを関係者間で記録する

各ステップごとに見ていきましょう。

1.トランザクション構築

Transactionの構成要素は全部で次の6つです。

  1. 0個以上のInputState
  2. 0個以上のOutputState
  3. 0個以上のReference Input State
  4. 1個以上のCommand
  5. 0個以上のアタッチメント
  6. 1個以下のタイムウィンドウ

上記のうち、特に重要なのがInputState, OutputState, Commandの3点です(その他は補助的な機能となるので今回は説明を割愛します)。

Transactionは特定の状態(InputState)を消費して、新しい状態(OutputState)を生成するために実行されます。そして、その消費アクションの意図を示すものがCommandです。

オンラインクーポンを送るFlowの場合、具体的には以下のようなInputState, OutputState, Commandになります。

  • InputState(特定の状態):Aさんが所持するオンラインクーポン100円
  • OutputState(新しい状態):Bさんが所持するオンラインクーポン100円
  • Command:送金コマンド

InputStateとOutputStateは、それぞれのクラスが定義された時点でContractに紐付けられます。Contractは明示的にTransactionに含めずとも、それぞれのStateの紐付きから全てのContractがそれぞれ呼び出されることになります。

またContractは、Commandの種類に応じて事前に指定されたロジックにより、Stateの内容を検証します。

トランザクション構築のコードを見てみる

InputState, OutputState, Commandを指定している部分が以下のコードとなります。

InputStateを基準として使用するNotaryやOutputStateが決まるので、まずはInputStateを検索するところから始まります。

次にTransactionを生成するためのObject(TransactionBuilder)を生成し、InputとOutputとCommandを付与しています。

/* Query input state */
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(inputId))
val inputStateRef =
        serviceHub.vaultService.queryBy(CouponState::class.java, query).states.single()
val notary = inputStateRef.state.notary

/* Start building transaction */
val txBuilder = TransactionBuilder(notary)

/* Add input */
txBuilder.addInputState(inputStateRef)

/* Add output */
val inputData = inputStateRef.state.data
val outputState = inputData.copy(owner = newOwner)
txBuilder.addOutputState(outputState)

/* Add command */
val txCommand =
        Command(
                Send(),
                inputData.participants.map { it.owningKey }
        )
txBuilder.addCommand(txCommand)

2.Transactionの署名

Transactionの署名は以下の手順で行われます。

  1. 自分による検証と署名
  2. 関係者による検証
  3. 関係者による署名

Transactionの署名のコードを見てみる

Transactionの署名のコードを見てみましょう。

InitiatorFlow

/* Verify */
txBuilder.verify(serviceHub) // Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.

/* Sign by initiator(A)*/
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)

/* Create session with B */
val otherPartySession = initiateFlow(newOwner)

/* Collect sign from B */
val fullySignedTx =
        subFlow(CollectSignaturesFlow(partSignedTx, setOf(otherPartySession)))

ResponderFlow

@InitiatedBy(InitiatorFlow::class)
class ResponderFlow(val otherPartySession: FlowSession) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
                /* SignTransactionFlow is called in response to CollectSignaturesFlow */
        val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
            override fun checkTransaction(stx: SignedTransaction) = requireThat {
                /* Verification Logic */
            }
        }
        val txId = subFlow(signTransactionFlow).id
                return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = txId))
    }
}

上記のコードを理解するためには、Session, InitiatorFlow, ResponderFlowとSubFlowの理解が必要になるので、それぞれ説明していきます。

Session

initiateFlowによって特定の当事者と通信するためのSessionオブジェクトが作成されます。Sessionオブジェクト作成時点ではまだ通信は行われず、後のSubFlowをSessionオブジェクトに対して実行することで通信が開始されます。

InitiatorFlowとResponderFlow

Flowは呼び出し側のInitiatorFlowとFlowにレスポンスするResponderFlowがペアで定義されます。@InitiatedByによってInitiatorとペアとなるResponderが指定されています。

SubFlow

SubFlowはFlowの中からさらに呼び出される子のFlowです。SubFlowにも、呼び出し側とペアとなるSubFlowが定義されています。以下のSubFlowペアは事前にCordaで定義されているものです。

  • CollectSignaturesFlow/SignTransactionFlow
  • FinalityFlow/ReceiveFinalityFlow

CollectSignaturesFlow/SignTransactionFlowは署名のリクエストと検証のペアで、FinalityFlow/ReceiveFinalityFlowはトランザクションをNotarizeして記録するためのFlowです。

CollectSignaturesFlow/SignTransactionFlow

CollectSignaturesFlowを実行したタイミングで、ResponderFlowのCallと対応するSignTransactionFlow.checkTransactionが実行され、Flowの内容が検証されます。またこのタイミングで自動的にContractも実行され、検証されることになります。

3.Transactionの記録

Flow最後のステップはTransactionの記録です。必要な関係者すべてからの署名が集まったら、最後にTransactionが記録されます。

Transactionの記録のコードを見てみる

FinalityFlow/ReceiveFinalityFlowのペアによってNotarizeされ、関係者のLedgerにTransactionとStateが記録されます。

FinalityFlowとReceiveFinalityFlowの実行順序は、先にReceiveFinalityFlowが実行されてFinalityFlowを待ち受ける形になります。

InitiatorFlow

return subFlow(FinalityFlow(fullySignedTx, setOf(otherPartySession)))

ResponderFlow

@InitiatedBy(InitiatorFlow::class)
class ResponderFlow(val otherPartySession: FlowSession) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
                ...
                return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = txId))
    }
}

あらためてFlowのコード全体をおさらいしてみましょう。最初に見たときに比べて理解できるようになっているはずです。

関連記事

CordaのFlowの役割については別の記事で解説しています。こちらも併せてご覧ください。

ブロックチェーンカテゴリの最新記事