Hyperledger fabric で始めるブロックチェーンアプリケーション (3/3)

# はじめに

前回は Hyperledger fabric 公式のチュートリアルを実際に動かしてみました。
今回はいよいよ chaincode を実装していきます。

chaincode は fabric ネットワーク上で動作するので、実装した chaincode の動作を試すためには fabric ネットワークを構築し、1.インストール,2.インスタンス化,3.実行(またはアップグレード)の手順を踏まなくてはなりません。
chaincode を開発するう上で都度手順を踏むのは非常に手間なので、より効率的な開発環境を整えることは重要です。

Hyperledger fabric ではそのような chaincode の開発モードが用意されているため、その実行方法を紹介し、例として hello world プログラムを実装していきます。

また、chaincode を継続的にビルドできる状態を維持するための CI の構築例を紹介します。

そして最後に SDK を使ったアプリケーションを実装して、作った chaincode を実行してみたいと思います。

1. [基礎知識の紹介](https://qiita.com/tatsurou313/private/ca124beb9d9e06cceb77)
1. [サンプルアプリケーションを動かすチュートリアル](https://qiita.com/tatsurou313/items/c474555dfd50650cd0e9)
1. 開発環境の整備 (この記事)
1. CIの構築 (この記事)
1. SDKを使ったアプリケーションの構築(この記事)


# chaincode のライフサイクル

chaincode は fabric ネットワーク上で動作するプログラムです。  
fabric ネットワークを使ったブロックチェーンにおけるビジネスロジックを実装する先が chaincode となります。

chaincode におけるライフサイクルは次のとおりです。
新しいバージョンの chaincode をインスタンス化する場合はインストールから行います。

|インストール時の状態遷移図|アップグレード時の状態遷移図|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206493/1428c917-9456-1999-f822-9aadd24c2aaf.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206493/0d7e9ac8-4e1f-c813-2d65-475738c49784.png)|

- インストールする
    - peer のローカルファイルとして chaincode のパッケージをコピーする操作を指します
- インスタンス化する / アップグレードする
    - パッケージ化された chaincode を docker container として起動させる操作を指します
    - chaincode にはバージョンが設定されており、新しいバージョンをインスタンス化する場合はアップグレードと呼びます
    - 複数インスタンスがある場合最新のバージョンの chaincode が実行されます
- 削除する
    - Docker container を停止・削除する操作と、peer 上の chaincode パッケージを削除する操作を指します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206493/56d75f76-fc71-816c-0535-ee8a87fa86c6.png)


# peer の開発モード

実装した chaincode が動作するのはインスタンス化された後です。  
通常 chaincode のライフサイクルは peer が管理します。

開発中に chaincode を更新したのちに毎回このライフサイクルに応じた処理を peer を通じて行うのは手間です。  
そこで、peer にはインスタンス化・アップグレード操作をユーザーが直接操作できる「開発モード」が用意されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206493/5545de83-e754-188a-0207-0d5764103431.png)


開発モードではインスタンス化した chaincode の container を操作してプログラムを再実行できます。  
そのため、この container に開発中の chaincode を mount してビルドや実行をすればライフサイクルを経ることなく最新の chaincode で動作を確認できます。

https://github.com/ryu-sato/hyperledger_fabric_chaincode_sample にサンプルとなる chaincode と関連ファイルを用意しました。

```yaml:chaincode-docker-compose/docker-compose.yaml
version: '2'

services:
  orderer:
    container_name: orderer
    image: hyperledger/fabric-orderer:1.4.7
    environment:
      - FABRIC_LOGGING_SPEC=DEBUG
      - ORDERER_GENERAL_LISTENADDRESS=orderer
      - ORDERER_GENERAL_GENESISMETHOD=file
      - ORDERER_GENERAL_GENESISFILE=genesis.block
      - ORDERER_GENERAL_LOCALMSPID=DEFAULT
      - ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/msp
      - GRPC_VERBOSITY=debug
      - GRPC_TRACE=all=true,
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric
    command: orderer
    volumes:
      - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
      - ${HLF_SMP_CHAINDEV_DIR}/genesis.block:/etc/hyperledger/fabric/genesis.block
    ports:
      - 7050:7050
  peer:
    container_name: peer
    image: hyperledger/fabric-peer:1.4.7
    environment:
      - FABRIC_LOGGING_SPEC=DEBUG
      - CORE_PEER_ID=peer
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
      - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
      - CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      - COUCHDB_USERNAME=couchdb
      - COUCHDB_PASSWORD=password
    volumes:
      - /var/run/:/host/var/run/
      - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
    command: peer node start --peer-chaincodedev=true
    ports:
      - 7051:7051
      - 7053:7053
    depends_on:
      - orderer
      - couchdb
  couchdb:
    image: hyperledger/fabric-couchdb:0.4.10
    environment:
      - COUCHDB_USERNAME=couchdb
      - COUCHDB_PASSWORD=password

  cli:
    container_name: cli
    image: hyperledger/fabric-tools:1.4.7
    tty: true
    environment:
      - FABRIC_LOGGING_SPEC=DEBUG
      - GOPATH=/opt/gopath
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - CORE_PEER_ID=cli
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    working_dir: /opt/gopath/src/chaincodedev
    command:
      - /bin/bash
      - -c
      # Peer が起動終わるまで待つよう sleep 10 を加えている
      - |
        sleep 10 &&
        peer channel create -c mychannel -f mychannel.tx -o orderer:7050 &&
        peer channel join -b mychannel.block &&
        tail -f /dev/null
    volumes:
      - /var/run/:/host/var/run/
      - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
      - ${HLF_SMP_CHAINDEV_DIR}/../chaincode:/opt/gopath/src/chaincodedev/chaincode
      - ${HLF_SMP_CHAINDEV_DIR}/:/opt/gopath/src/chaincodedev/
      - ../:/opt/gopath/src/${GO_MODULE_NAME}
    depends_on:
      - orderer
      - peer
      - couchdb

  chaincode:
    container_name: chaincode
    image: hyperledger/fabric-ccenv:1.4.7
    tty: true
    environment:
      - FABRIC_LOGGING_SPEC=DEBUG
      - GOPATH=/opt/gopath
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - CORE_PEER_ID=example02
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    working_dir: /opt/gopath/src/chaincode
    command: /bin/sh -c 'tail -f /dev/null'
    volumes:
      - /var/run/:/host/var/run/
      - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
      - ${HLF_SMP_CHAINDEV_DIR}/../chaincode:/opt/gopath/src/chaincode
      - ../:/opt/gopath/src/${GO_MODULE_NAME}
    depends_on:
      - orderer
      - peerversion: '2'

services:
  orderer:
    container_name: orderer
    image: hyperledger/fabric-orderer
    environment:
      - FABRIC_LOGGING_SPEC=debug
      - ORDERER_GENERAL_LISTENADDRESS=orderer
      - ORDERER_GENERAL_GENESISMETHOD=file
      - ORDERER_GENERAL_GENESISFILE=orderer.block
      - ORDERER_GENERAL_LOCALMSPID=DEFAULT
      - ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/msp
      - GRPC_TRACE=all=true,
      - GRPC_VERBOSITY=debug
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric
    command: orderer
    volumes:
      - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
      - ${HLF_SMP_CHAINDEV_DIR}/orderer.block:/etc/hyperledger/fabric/orderer.block
    ports:
      - 7050:7050
  peer:
    container_name: peer
    image: hyperledger/fabric-peer
    environment:
      - CORE_PEER_ID=peer
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - FABRIC_LOGGING_SPEC=DEBUG
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    volumes:
        - /var/run/:/host/var/run/
        - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
    command: peer node start --peer-chaincodedev=true
    ports:
      - 7051:7051
      - 7053:7053
    depends_on:
      - orderer

  cli:
    container_name: cli
    image: hyperledger/fabric-tools
    tty: true
    environment:
      - GOPATH=/opt/gopath
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - FABRIC_LOGGING_SPEC=DEBUG
      - CORE_PEER_ID=cli
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    working_dir: /opt/gopath/src/${GO_MODULE_NAME}
    command: /bin/bash -c './script.sh'
    volumes:
        - /var/run/:/host/var/run/
        - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
        - ${HLF_SMP_CHAINDEV_DIR}/:/opt/gopath/src/${GO_MODULE_NAME}
    depends_on:
      - orderer
      - peer

  chaincode:
    container_name: chaincode
    image: hyperledger/fabric-ccenv
    tty: true
    environment:
      - GOPATH=/opt/gopath
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - FABRIC_LOGGING_SPEC=DEBUG
      - CORE_PEER_ID=example02
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    working_dir: /opt/gopath/src/${GO_MODULE_NAME}
    command: /bin/sh -c 'sleep 6000000'
    volumes:
        - /var/run/:/host/var/run/
        - ${HLF_SMP_CHAINDEV_DIR}/msp:/etc/hyperledger/msp
        - ${HLF_SMP_CHAINDEV_DIR}/:/opt/gopath/src/${GO_MODULE_NAME}
    depends_on:
      - orderer
      - peer
```

```bash:.env
HLF_SMP_CHAINDEV_DIR=..
GO_MODULE_NAME=github.com/ryu-sato/hyperledger_fabric_chaincode_sample
```

なお、 cryptogen コマンドが作詞絵する証明書の有効期限は 10 年間です。

# hello chaincode を作る

peer を開発モードで実行するために必要なファイルが準備できました。

いよいよ chaincode を作成、実行してみましょう。

hello chaincode となるコードを紹介します。 (https://github.com/ryu-sato/hyperledger_fabric_chaincode_sample)にもあります)

```go:chaincode_sample.go
package main

import (
        "encoding/json"
        "fmt"
        "os"
        "reflect"

        "github.com/hyperledger/fabric/core/chaincode/shim"
        "github.com/hyperledger/fabric/protos/peer"
)

var logger = shim.NewLogger("chaincode_sample")

type SmartContract struct {
}

// chaincode の初期化
func (t *SmartContract) Init(stub shim.ChaincodeStubInterface) peer.Response {
        logger.Info("Chaincode initialized.")
        return shim.Success(nil) // 何もしない
}

// トランザクションの実行
// args[0]: トランザクション名
// args[1..*]: コマンドに渡す引数
func (t *SmartContract) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
        cmdMap := map[string]interface{}{
                "helloChaincode": HelloChaincode,
        }
        cmdName, cmdArgs := stub.GetFunctionAndParameters()

        if cmdInterface, exist := cmdMap[cmdName]; exist {
                cmd := reflect.ValueOf(cmdInterface)
                out := cmd.Call([]reflect.Value{reflect.ValueOf(stub), reflect.ValueOf(cmdArgs)})
                if ret, ok := out[0].Interface().(peer.Response); ok {
                        return ret
                }
                return shim.Error(fmt.Sprintf("Invoke: Wrong response type (given %T, expected peer.Response)", out[0].Interface()))
        }

        keys := reflect.ValueOf(cmdMap).MapKeys()
        return shim.Error(fmt.Sprintf("Invoke: Invalid command (given %s, expexted %v", cmdName, keys))
}

func HelloChaincode(stub shim.ChaincodeStubInterface, args []string) peer.Response {
        bytes, _ := json.Marshal("hello chaincode!");
        return shim.Success(bytes);
}

func main() {
        logLevel := shim.LogInfo
        if os.Getenv("SHIM_LOGGING_LEVEL") != "" {
                logLevel, _ = shim.LogLevel(os.Getenv("SHIM_LOGGING_LEVEL"))
        }
        logger.SetLevel(logLevel)
        shim.SetLoggingLevel(logLevel)

        err := shim.Start(new(SmartContract))
        if err != nil {
                logger.Errorf("Error hello chaincode: %s\n", err)
        }
}
```

chaincode では Init メソッドと Invoke メソッドを実装し、 `shim.Start` を実行することになります。
Invoke は GRPC により実行されるメソッドで、引数にトランザクション名とその引数が指定されます。
トランザクションの種類が増えるたびに Invoke で定義するトランザクションが増えるため、hello では工夫して reflection を使って汎用化しています。
もし hello chaincode を拡張する場合は `cmdMap` にトランザクション名と対応するメソッドを記述すれば簡単にできます。

hello chaincode のメインとなる処理は cmdMap の "helloChaincode" というトランザクション名と、対応する `HelloChaincode` メソッドです。
HelloChaincode メソッドでは処理は何もせず、成功したという結果と "hello chaincode!" という文字列を応答しています。

# chaincode を開発モードで実行する

hello chaincode を実行してみましょう。

```bash
$ git clone https://github.com/ryu-sato/hyperledger_fabric_chaincode_sample
$ cd yperledger_fabric_chaincode_sample/misc
$ docker-compose up -d
```

```bash
# ターミナル1 (c は Docker container のプロンプトを意味する)
## chaincode を変更する度に実行しなおす
$ docker exec -it chaincode bash
(c)$ go build -o hello
(c)$ CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=hello:0 ./hello

# ターミナル2 (c は Docker container のプロンプトを意味する)
$ docker exec -it cli bash
(c)$ go get  # 依存関係の解決
(c)$ peer chaincode install -p github.com/ryu-sato/hyperledger_fabric_chaincode_sample/ -n hello -v 0
(c)$ peer chaincode instantiate -n hello -v 0 -c '{"Args":[""]}' -C myc
(c)$ peer chaincode invoke -n hello -c '{"Args":["helloChaincode"]}' -C myc
```


# chaincodeのCI環境構築

チームでアプリケーションを開発する場合は特に当てはまりますが、アプリケーションが動作する状態であるか保証できることは重要です。  
ここで、最低限ビルドができることを継続的に確認すること、必要に応じてテストがパスすることを継続的に確認することを CI と呼ぶこととします。

CI を含む、VCS を使ったチーム開発フローは様々なバリエーションがあると思いますので、今回は以下のような開発フローを前提とします。

## 前提

* `main` ブランチが最新である
    * 常にビルドができる状態を維持できている
* 開発者は `main` ブランチから TOPIC ブランチを派生させてコミットする
    * TOPIC ブランチはキリの良いタイミングで `main` ブランチにマージさせる
    * `main` ブランチへマージする際、CI を pass しないとマージできない

尚、chaincode のテスト方法は分からないため、ビルドができることだけを確認することにします。  
また、Chacindoe は go 言語で開発されていることを前提とします。

## CI (GitHub Actions)

```yaml
name: chaincode CI
on: push

jobs:
  build-chaincode:
    runs-on: ubuntu-latest
    container:
      image: hyperledger/fabric-ccenv:1.4.7
      env:
        CHAINCODE_DIR: ./
    steps:
    - name: Check out code into the Go module directory
      uses: actions/checkout@v2
    - name: Build
      run: |
        cd "$CHAINCODE_DIR"
        go get -v -t -d ./...
        go build -v .

    # [オプション]
    # Actionの実行結果をSlack通知をする場合は GitHub の secret に以下を追加する
    #   SLACK_WEBHOOK_URL: SlackのWEBHOOK URL
    #   SLACK_CHANNEL    : Slackのチャンネル名(ex. "#SOME_CHANNEL_NAME")
    # 
    # - name: Slack Notification
    #   uses: weseek/ghaction-slack-notification@master
    #   if: always()
    #   with:
    #     type: ${{ job.status }}
    #     job_name: '*build chaincode*'
    #     channel: ${{ secrets.SLACK_CHANNEL }}
    #     isCompactMode: true
    #     url: ${{ secrets.SLACK_WEBHOOK_URL }}
    #     icon_emoji: 'blockchainco'
```


# SDKを使ったアプリケーション例

Hyperledger fabric のアプリケーションは SDK を使って、chaincode で定義した transaction を実行します。

Hyperledger fabric 1.4 において SDK は [Node.js用](https://hyperledger.github.io/fabric-sdk-node/) と [Java用](https://hyperledger.github.io/fabric-gateway-java/) が提供されています。

ここでは Node.js を使ったアプリケーション例を紹介します。
アプリケーション例は https://github.com/ryu-sato/hyperledger_fabric_app_sample でも公開しています。

## アプリケーション理解に必要な概念

* [Wallet](https://hyperledger.github.io/fabric-sdk-node/release-1.4/module-fabric-network.Wallet.html)
    * identity (ユーザーや peer/orderer 等、fabric ネットワークの識別子) を読み書きするインターフェースです
    * [FileSystem上のデータ](https://hyperledger.github.io/fabric-sdk-node/release-1.4/module-fabric-network.FileSystemWallet.html)、[メモリー上のデータ](https://hyperledger.github.io/fabric-sdk-node/release-1.4/module-fabric-network.InMemoryWallet.html)、[CouchDB上のデータ](https://hyperledger.github.io/fabric-sdk-node/release-1.4/module-fabric-network.CouchDBWallet.html)を操作する用のクラスがそれぞれこのインターフェースを実装しています
    * 1 つの wallet インタフェースを通じて、「複数」の identity を読み書きします (wallet というと「個人」の identity を読み書きするように感じますが「複数」のidentity を読み書きします)
* [Gateway](https://hyperledger.github.io/fabric-sdk-node/release-1.4/module-fabric-network.Gateway.html)
    * Fabric ネットワークへの接続・切断や、Peer のリスト、Channel にアクセスして transaction を発行するためのクラスインスタンスを返すクラスです

## SDKパッケージ

Node.js における SDK パッケージは `fabric-network` です。

予め package.json へ追加しておきましょう。(Hyperledger fabric 1.4系を使う前提です)

```bash
$ yarn add fabric-network@^1.4.6
```

## Wallet の作成

Fabric ネットワークに接続するためには、wallet に予めユーザーの identity を保存しておく必要があります。

Wallet に証明書と秘密鍵を登録する簡単なサンプルを紹介します。

```js
const { FileSystemWallet, X509WalletMixin } = require('fabric-network');
const path = require('path');

const name = process.env.IDENTITY_NAME || 'user';
const certificate = process.env.IDENTITY_CERTIFICATE || 'INVALID_CERT';
const privateKey = process.env.IDENTITY_PRIVATE_KEY || 'INVALID_KEY';
const mspID = process.env.MSP_ID || 'DEFAULT';

async function importToWallt() {
    const walletPath = path.join(process.cwd(), 'wallet');
    const wallet = new FileSystemWallet(walletPath);
    walletMixin = X509WalletMixin.createIdentity(mspID, certificate, privateKey);
    await wallet.import(name, walletMixin)
}

importToWallt();
```

このサンプルではカレントディレクトリにある `wallet` ディレクトリに Wallet のデータを保存します。
インポートする証明書と秘密鍵は環境変数で指定します。

以下、実行例です。

```bash
$ export IDENTITY_CERTIFICATE=$(cat ryu-sato/hyperledger_fabric_chaincode_sample/msp/admincerts/admincert.pem)
$ export IDENTITY_PRIVATE_KEY=$(cat ryu-sato/hyperledger_fabric_chaincode_sample/msp/keystore/key.pem)
$ export MSP_ID=DEFAULT
$ export IDENTITY_NAME=admin
$ yarn run wallet:import
```

ryu-sato/hyperledger_fabric_chaincode_sample/msp 配下にある秘密鍵と証明書を import しています。

各自の環境に合わせて適宜修正してください。

## アプリケーション例

Wallet の準備が出来たらいよいよアプリケーションを実装できる準備が整いました。

transaction を発行するアプリケーションの例は次のようになります。

```javascript
const { FileSystemWallet, Gateway } = require('fabric-network');
const path = require('path');

const connectionProfile = {
    "name": "hyperledger_fabric_app_sample",
    "version": "1.0.0",
    "channels": {
        "myc": {
            "orderers": [
                "orderer1"
            ],
            "peers": {
                "peer1": {
                    "endosingPeer": true,
                    "chaincodeQuery": true,
                    "ledgerQuery": true,
                    "eventSource": true,
                    "discover": false
                }
            }
        }
    },
    "peers": {
        "peer1": {
            "url": "grpc://localhost:7051"
        }
    },
    "orderers": {
        "orderer1": {
            "url": "grpc://localhost:7050"
        }
    }
};

async function invoke() {
    // Walletを読み込む
    const walletPath = path.join(process.cwd(), 'wallet');
    const wallet = new FileSystemWallet(walletPath);
    const adminExists = await wallet.exists('admin');
    if (!adminExists) {
        console.log('"admin" is not exists. You need import wallet before invoke transaction.');
        return;
    }

    // Fabric ネットワークへ接続する
    const gateway = new Gateway();
    await gateway.connect(connectionProfile, { wallet, identity: 'admin', discovery: { enabled: false, asLocalhost: true } });

    // helloChaincode トランザクションを実行する
    const network = await gateway.getNetwork('myc');
    const contract = network.getContract('hello');
    const result = await contract.submitTransaction('helloChaincode');
    console.log(`helloChaincode returns ${result}`);

    // Fabric ネットワークを切断する
    await gateway.disconnect();
}

invoke();
```

メインとなる処理は `invoke()` メソッドに書かれた内容です。
ここでは、Wallet から `admin` ユーザーの identity を読み込み、Gateway クラスを使って Fabric ネットワークに接続した後、`helloChaincode` トランザクションを実行し、トランザクションの実行結果を console.log で出力してから Gateway を切断しています。



# まとめ

chaincode を開発する際に有用な peer の開発モードを使って開発環境を構築する方法と、chaincode の CI 構築例について紹介しました。

そして SDK を使い、chaincode を実行する Hyperledger fabric アプリケーションの例を紹介しました。

以上で「Hyperledger fabric で始めるブロックチェーンアプリケーション」シリーズは終了です。

※この記事は [WESEEK Tips wiki](https://tips.weseek.co.jp/) に投稿された記事の転載です。
Tips wiki では、IT企業の技術的な情報やプロジェクトの情報を公開可能な範囲で公開してます。