【本文】
"Corda: Frictionless Commerce" by R3CEV
-Cordaで認識相違のない取引を。-
CorDappsとは、R3CEVの分散型台帳基盤(DLともDLTとも言う。)であるCordaネットワーク上で動作する分散型アプリケーション(Dapps)の一種です。(※所謂ブロックチェーンではないのでご注意を。)
最近、CorDapps開発に興味を持ち始めたので、実際、どんなものか先ずはR3から提供されているCordapps作成手順(チュートリアル)に従って、環境セットアップからCorDappsデプロイまで試してみました。
なお、今回使用したOS環境はWindows10となります。
(Macユーザの皆さん、すみません。。。)
また、使用言語はKotlinとなります。(※Javaでも可能でチュートリアルにも両方の言語が記載されている。)
なお、チュートリアルに従ってCorDappsを作成していく過程で自ずと分かってくるのですが、Cordaにおいては以下、3点の要素が胆となっています。おそらく、この3点を如何に正しく理解し開発するかでCorDappsの成否に掛かってくるかと思われます。
- State Object
- Contract
- Flow
個人的には上記の中でも、ビジネスプロセスを定義するFlowが最も重要な要素を占めていると考えていますが、何れ何らかの紹介が出来ればと思います。
先ずは、動かすことが大事なので、チュートリアルをこなす段階ではさらっと流すだけで良いです。
注)今回、紹介するコードは全てR3が提供する開発者向けドキュメントのチュートリアル「Hello, World!」に記載されているものです。
【その前に。。。】
先ずはこれ読もう!
Cordaについて、少し理解が深まったところで、いよいよCorDappsの世界へ。
【環境セットアップ】
基本は、以下のリンクを参考に、WindowsないしMacそれぞれのOS環境に合わせて必要なソフトウェア等をインストール。
(※全て無料で手に入る。)
<セットアップ手順>
(※バージョン情報は、2017年7月7日現在のもの。都度、Cordaの正式なドキュメントを確認のこと。)
<必要なソフトウェア等>
- Oracle JDK 8 JVM - supported version 8u131
- Git
- IntelliJ IDEA - supported versions 2017.1, 2017.2 and 2017.3(※無料のCommunity Editionで良い。)
- Kotlin - supported version 1.1.2
- Gradle - supported version 3.4
【テンプレート導入】
チュートリアルに従って、以下のテンプレートをGitHubから導入。
<テンプレート>
<導入方法>
- Terminal(コマンドプロンプト等)を開き、以下のコマンドよりテンプレートを導入。
チュートリアルに習った導入コマンド# GitHubからテンプレートをクローン: > git clone https://github.com/corda/cordapp-template.git & cd cordapp-template # Milestone一覧を表示: > git branch -a --list *release-M* # 最新版のテンプレートをチェックアウトし導入: > git checkout release-M[*version number*] & git pull (例: M12版なら、「 git checkout releas-M12 & git pull」を実行。)
IntelliJを起動させ、ローカル上に保存したテンプレートのプロジェクト(cordapp-template)を開く。IntelliJのメニューバーより「File」⇒「Open Project」で開くことが可能。(※初めてIntelliJを開く場合のみ、ダイアログが表示される。その場合は、「Open」を選択し、テンプレートのプロジェクトを開く。)
Gradleをプラグインする。(View > Tool windows > Gradle)。IntelliJ画面上の右側に何らからのメッセージが表示されるので、そちらをクリックすると、Gradleがプラグインされる。
【State Object作成】
チュートリアルに従って、IOUState.ktを作成。
(※チュートリアル上は、勉強上の観点で、途中までIOUState作成後、一度、IOUContractの作成に移り、その後、IOUStateを完成させるやり方になっているが、ここでは、完成版を予め記載。)
IOUState.kt(※完成版)package com.template import net.corda.core.contracts.ContractState import net.corda.core.identity.Party class IOUState(val value: Int, val sender: Party, val recipient: Party) : ContractState { override val contract: IOUContract = IOUContract() override val participants get() = listOf(sender, recipient) }
【Contract作成】
チュートリアルに従って、IOUContract.ktを作成。
IOUContract.kt(※完成版)package com.template import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash.Companion.sha256 open class IOUContract : Contract { // Currently, verify() does no checking at all! override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand<Create>() requireThat { // Constraints on the shape of the transaction. "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) "Only one output state should be created." using (tx.outputs.size == 1) // IOU-specific constraints. val out = tx.outputs.single() as IOUState "The IOU's value must be non-negative." using (out.value > 0) "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) // Constraints on the signers. "All of the participants must be signers." using (command.signers.toSet() == out.participants.map { it.owningKey }.toSet()) } } // Our Create command. class Create : CommandData // The legal contract reference - we'll leave this as a dummy hash for now. override val legalContractReference = SecureHash.sha256("Prose contract.") }
【Transactionテスト実行】
チュートリアルに従って、Transactionテストモジュールを作成。
ContractTests.kt(※完成版)package com.template import net.corda.testing.* import org.junit.Test class IOUTransactionTests { @Test fun `transaction must include Create command`() { ledger { transaction { output { IOUState(1, MINI_CORP, MEGA_CORP) } fails() command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } verifies() } } } @Test fun `transaction must have no inputs`() { ledger { transaction { input { IOUState(1, MINI_CORP, MEGA_CORP) } output { IOUState(1, MINI_CORP, MEGA_CORP) } command(MEGA_CORP_PUBKEY) { IOUContract.Create() } `fails with`("No inputs should be consumed when issuing an IOU.") } } } @Test fun `transaction must have one output`() { ledger { transaction { output { IOUState(1, MINI_CORP, MEGA_CORP) } output { IOUState(1, MINI_CORP, MEGA_CORP) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } `fails with`("Only one output state should be created.") } } } @Test fun `sender must sign transaction`() { ledger { transaction { output { IOUState(1, MINI_CORP, MEGA_CORP) } command(MINI_CORP_PUBKEY) { IOUContract.Create() } `fails with`("All of the participants must be signers.") } } } @Test fun `recipient must sign transaction`() { ledger { transaction { output { IOUState(1, MINI_CORP, MEGA_CORP) } command(MEGA_CORP_PUBKEY) { IOUContract.Create() } `fails with`("All of the participants must be signers.") } } } @Test fun `sender is not recipient`() { ledger { transaction { output { IOUState(1, MEGA_CORP, MEGA_CORP) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } `fails with`("The sender and the recipient cannot be the same entity.") } } } @Test fun `cannot create negative-value IOUs`() { ledger { transaction { output { IOUState(-1, MINI_CORP, MEGA_CORP) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } `fails with`("The IOU's value must be non-negative.") } } } }
IntelliJ画面上で、「IOUTransactionTests」を選択後、「Run」実行。
(※ここでテストに失敗した場合、IOUState.ktもしくはIOUContract.ktのどちらかのコード作成で失敗している可能性が大きいので、先ずはコードを確認。)
【Flow作成】
チュートリアルに従って、IOUFlow.ktを作成。
(※完成版)
IOUFlow.kt(※完成版)package com.template import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker // Additional import. import net.corda.core.transactions.TransactionBuilder import net.corda.core.contracts.Command import net.corda.flows.CollectSignaturesFlow import net.corda.flows.FinalityFlow import net.corda.flows.SignTransactionFlow object IOUFlow { @InitiatingFlow @StartableByRPC class Initiator(val iouValue: Int, val otherParty: Party): FlowLogic<SignedTransaction>() { /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ override val progressTracker = ProgressTracker() /** The flow logic is encapsulated within the call() method. */ @Suspendable override fun call(): SignedTransaction { // We create a transaction builder val txBuilder = TransactionBuilder() val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() txBuilder.notary = notaryIdentity // We create the transaction's components. val ourIdentity = serviceHub.myInfo.legalIdentity val iou = IOUState(iouValue, ourIdentity, otherParty) val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) // Adding the item's to the builder. txBuilder.withItems(iou, txCommand) // Verifying the transaction. txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() // Signing the transaction. val partSignedTx = serviceHub.signInitialTransaction(txBuilder) // Gathering the signatures. val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) // Finalising the transaction. return subFlow(FinalityFlow(signedTx)).single() } } @InitiatedBy(Initiator::class) class Acceptor(val otherParty: Party) : FlowLogic<Unit>() { @Suspendable override fun call() { // Stage 1 - Verifying and signing the transaction. subFlow(object : SignTransactionFlow(otherParty, tracker()) { override fun checkTransaction(stx: SignedTransaction) { // Define custom verification logic here. } }) } } }
【Flowテスト実行】
チュートリアルに従って、Flowテストモジュールを作成。
FlowTests.kt(※完成版)package com.template import net.corda.core.contracts.TransactionVerificationException import net.corda.core.getOrThrow import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith class IOUFlowTests { lateinit var net: MockNetwork lateinit var a: MockNode lateinit var b: MockNode lateinit var c: MockNode @Before fun setup() { net = MockNetwork() val nodes = net.createSomeNodes(2) a = nodes.partyNodes[0] b = nodes.partyNodes[1] b.registerInitiatedFlow(IOUFlow.Acceptor::class.java) net.runNetwork() } @After fun tearDown() { net.stopNodes() } @Test fun `flow rejects invalid IOUs`() { val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) val future = a.services.startFlow(flow).resultFuture net.runNetwork() // The IOUContract specifies that IOUs cannot have negative values. assertFailsWith<TransactionVerificationException> {future.getOrThrow()} } @Test fun `SignedTransaction returned by the flow is signed by the initiator`() { val flow = IOUFlow.Initiator(1, b.info.legalIdentity) val future = a.services.startFlow(flow).resultFuture net.runNetwork() val signedTx = future.getOrThrow() signedTx.verifySignatures(b.services.legalIdentityKey) } @Test fun `SignedTransaction returned by the flow is signed by the acceptor`() { val flow = IOUFlow.Initiator(1, b.info.legalIdentity) val future = a.services.startFlow(flow).resultFuture net.runNetwork() val signedTx = future.getOrThrow() signedTx.verifySignatures(a.services.legalIdentityKey) } @Test fun `flow records a transaction in both parties' vaults`() { val flow = IOUFlow.Initiator(1, b.info.legalIdentity) val future = a.services.startFlow(flow).resultFuture net.runNetwork() val signedTx = future.getOrThrow() // We check the recorded transaction in both vaults. for (node in listOf(a, b)) { assertEquals(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id)) } } @Test fun `recorded transaction has no inputs and a single output, the input IOU`() { val flow = IOUFlow.Initiator(1, b.info.legalIdentity) val future = a.services.startFlow(flow).resultFuture net.runNetwork() val signedTx = future.getOrThrow() // We check the recorded transaction in both vaults. for (node in listOf(a, b)) { val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id) val txOutputs = recordedTx!!.tx.outputs assert(txOutputs.size == 1) val recordedState = txOutputs[0].data as IOUState assertEquals(recordedState.value, 1) assertEquals(recordedState.sender, a.info.legalIdentity) assertEquals(recordedState.recipient, b.info.legalIdentity) } } }
「FlowTests」を選択後、以下の「注)」を設定し、「Run」実行。
注)なお、Cordaでは、QUASARというJVM用軽量スレッドが使用されており、Flowテスト実施時には、こちらを有効にしておく必要がある。そこで、IntelliJ画面上で、「FlowTests」を選択後、「Edit Configurations...」を開き、「VM options」に以下のパラメータを設定しておく必要があります。
(※チュートリアル上には直接明記されていないので、注意。)
-javaagent:path-to-quasar-jar.jar
<参考>
【いよいよCorDapp実行!】
Flowテストまで問題なく終了したら、いよいよCorDappをデプロイし実行する。
<デプロイ>
チュートリアルに従って、「kotlin-source/build.gradle」にノード設定を行う。
なお、「build.gradle」で今回修正するのは、「task deployNodes」のみで良い。
(※それ以外の設定箇所は修正不要。)
kotlin-source/build.gradletask deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { directory "./build/nodes" networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" node { name "CN=Controller,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 webPort 10004 cordapps = [] } node { name "CN=NodeA,O=NodeA,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 webPort 10007 cordapps = [] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } node { name "CN=NodeB,O=NodeB,L=New York,C=US" advertisedServices = [] p2pPort 10008 rpcPort 10009 webPort 10010 cordapps = [] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } node { name "CN=NodeC,O=NodeC,L=Paris,C=FR" advertisedServices = [] p2pPort 10011 rpcPort 10012 webPort 10013 cordapps = [] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } }
Terminal上で以下のコマンドを実行し、nodesをビルドする。
(※これでCorDappのデプロイ完了!)
> gradlew clean deployNodes
<CorDapp実行>
Terminal上で、以下のコマンド実行し設定したノードを起動させる。
(※計8つのTerminal画面が表示され、ノード起動処理が実行される。
ノード用Terminal画面:計4つ、各ノードのログ出力用画面:計4つ)
> cd kotlin-source/build/nodes > runnodes.bat
以下のコマンドを実行し、NodeAからNodeBにIOUを「99」渡す。
(※IOU:"I owe You"の略で、ここでは金額のことだと考えてよい。)
NodeA用のTerminal上start IOUFlow iouValue: 99, otherParty: "CN=NodeB,O=NodeB,L=New York,C=US"
注)チュートリアル上では、「start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US"」となっているが、「arg0」と「arg1」は、それぞれ「IOUFlow.kt」で定義した変数名を入力。
多分、NodeAのTerminal上で、以下の文言が出力されるが基本的に問題ないと考え、後続の処理を行う。
実行後出力される文言UnStarted Done
実際に、NodeAからNodeBにIOUが「99」渡り両者が合意済みであるか、以下のコマンドを実行し検証。
NodeAとNodeBのTerminal上でそれぞれ実行run vaultAndUpdates
以下のような実行結果が出力される(はず)。
NodeAとNodeBの検証結果first: - state: data: value: 99 sender: "CN=NodeA,O=NodeA,L=London,C=GB" recipient: "CN=NodeB,O=NodeB,L=New York,C=US" contract: legalContractReference: "E80AA017E7F0E03061098BEA470B411CF2372C2E11DE0FE7705145308D081F1A" participants: - "CN=NodeA,O=NodeA,L=London,C=GB" - "CN=NodeB,O=NodeB,L=New York,C=US" notary: "CN=Controller,O=R3,OU=corda,L=London,C=GB,OU=corda.notary.validating" encumbrance: null ref: txhash: "5DCA019E1B8300E04F54EB425161F2B4B5CD6B4F69CC942635E2DDCB70DED75E" index: 0 second: "(observable)"
なお、Cordaはブロックチェーンではなく、同じCordaネットワーク上に存在するノードであっても、当事者同士以外のトランザクションは分散型台帳上に記録されない。
この点がCordaの特徴的な部分の一つであり、今回、NodeAとNodeBの間で共有されたトランザクションデータがNodeCには共有されていないことが、NodeCのTerminal上で「run vaultAndUpdates」コマンド実行によって確認可能である。
NodeCの検証結果first: [] second: "(observable)"
以上、チュートリアル「Hello, World!」によるCorDapp作成事例の紹介まで。
【終わりに】
Cordaをコードレベルで触り始めたのはつい数日前からで、自分にもまだまだ分からないことが多いです。
ただ、Cordaはドキュメントも豊富で、Corda ForumやSlackも盛況なので、そちらを参考にしながら勉強していくとある程度独学でも行けそうな感触を得ています。
また、CordaはKotlinというJavaライクな言語でほぼ構成されており、Javaとの互換性もあることから、開発者には親しみやすい印象を得ています。
(※尤も、私自身は新人研修以来、まともにJavaを触る機会がなかったので、恥ずかしながらKotlinを初歩レベルから勉強中ですが。。。)
Kotlinについては、以下書籍が結構参考になっています。
また、Cordaについて動画も豊富に用意されているので、こちらも参考になります。
最後に一言。チュートリアルの「Hello, World!」が全然、"Hello, World!"のレベルじゃありません。まともに理解しようとすると、結構時間かかりそうです。
(※"Hello, World!"って、もっと簡単なものですよね。。。)
以上。