はじめに
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構築の流れは大きくは以下の通りです。
- Transactionを構築する
- Transactionを検証し署名を集める
- Transactionを関係者間で記録する
各ステップごとに見ていきましょう。
1.トランザクション構築
Transactionの構成要素は全部で次の6つです。
- 0個以上のInputState
- 0個以上のOutputState
- 0個以上のReference Input State
- 1個以上のCommand
- 0個以上のアタッチメント
- 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の署名は以下の手順で行われます。
- 自分による検証と署名
- 関係者による検証
- 関係者による署名
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の役割については別の記事で解説しています。こちらも併せてご覧ください。