LoginSignup
9
8

More than 5 years have passed since last update.

【CorDapps!】Hallo, World!(Cordaをチュートリアルに従って触ってみた。)

Posted at

【本文】

"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.gradle
task 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!"って、もっと簡単なものですよね。。。)

以上。

9
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8