はじめに
前回は Hyperledger fabric 公式のチュートリアルを実際に動かしてみました。
今回はいよいよ chaincode を実装していきます。
chaincode は fabric ネットワーク上で動作するので、実装した chaincode の動作を試すためには fabric ネットワークを構築し、1.インストール,2.インスタンス化,3.実行(またはアップグレード)の手順を踏まなくてはなりません。
chaincode を開発するう上で都度手順を踏むのは非常に手間なので、より効率的な開発環境を整えることは重要です。
Hyperledger fabric ではそのような chaincode の開発モードが用意されているため、その実行方法を紹介し、例として hello world プログラムを実装していきます。
また、chaincode を継続的にビルドできる状態を維持するための CI の構築例を紹介します。
そして最後に SDK を使ったアプリケーションを実装して、作った chaincode を実行してみたいと思います。
- 基礎知識の紹介
- サンプルアプリケーションを動かすチュートリアル
- 開発環境の整備 (この記事)
- CIの構築 (この記事)
- SDKを使ったアプリケーションの構築(この記事)
chaincode のライフサイクル
chaincode は fabric ネットワーク上で動作するプログラムです。
fabric ネットワークを使ったブロックチェーンにおけるビジネスロジックを実装する先が chaincode となります。
chaincode におけるライフサイクルは次のとおりです。
新しいバージョンの chaincode をインスタンス化する場合はインストールから行います。
インストール時の状態遷移図 | アップグレード時の状態遷移図 |
---|---|
- インストールする
- peer のローカルファイルとして chaincode のパッケージをコピーする操作を指します
- インスタンス化する / アップグレードする
- パッケージ化された chaincode を docker container として起動させる操作を指します
- chaincode にはバージョンが設定されており、新しいバージョンをインスタンス化する場合はアップグレードと呼びます
- 複数インスタンスがある場合最新のバージョンの chaincode が実行されます
- 削除する
- Docker container を停止・削除する操作と、peer 上の chaincode パッケージを削除する操作を指します
peer の開発モード
実装した chaincode が動作するのはインスタンス化された後です。
通常 chaincode のライフサイクルは peer が管理します。
開発中に chaincode を更新したのちに毎回このライフサイクルに応じた処理を peer を通じて行うのは手間です。
そこで、peer にはインスタンス化・アップグレード操作をユーザーが直接操作できる「開発モード」が用意されています。
開発モードではインスタンス化した chaincode の container を操作してプログラムを再実行できます。
そのため、この container に開発中の chaincode を mount してビルドや実行をすればライフサイクルを経ることなく最新の chaincode で動作を確認できます。
https://github.com/ryu-sato/hyperledger_fabric_chaincode_sample にサンプルとなる chaincode と関連ファイルを用意しました。
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
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)にもあります)
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 を実行してみましょう。
$ git clone https://github.com/ryu-sato/hyperledger_fabric_chaincode_sample
$ cd yperledger_fabric_chaincode_sample/misc
$ docker-compose up -d
# ターミナル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 しないとマージできない
- TOPIC ブランチはキリの良いタイミングで
尚、chaincode のテスト方法は分からないため、ビルドができることだけを確認することにします。
また、Chacindoe は go 言語で開発されていることを前提とします。
CI (GitHub Actions)
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用 と Java用 が提供されています。
ここでは Node.js を使ったアプリケーション例を紹介します。
アプリケーション例は https://github.com/ryu-sato/hyperledger_fabric_app_sample でも公開しています。
アプリケーション理解に必要な概念
-
Wallet
- identity (ユーザーや peer/orderer 等、fabric ネットワークの識別子) を読み書きするインターフェースです
- FileSystem上のデータ、メモリー上のデータ、CouchDB上のデータを操作する用のクラスがそれぞれこのインターフェースを実装しています
- 1 つの wallet インタフェースを通じて、「複数」の identity を読み書きします (wallet というと「個人」の identity を読み書きするように感じますが「複数」のidentity を読み書きします)
-
Gateway
- Fabric ネットワークへの接続・切断や、Peer のリスト、Channel にアクセスして transaction を発行するためのクラスインスタンスを返すクラスです
SDKパッケージ
Node.js における SDK パッケージは fabric-network
です。
予め package.json へ追加しておきましょう。(Hyperledger fabric 1.4系を使う前提です)
$ yarn add fabric-network@^1.4.6
Wallet の作成
Fabric ネットワークに接続するためには、wallet に予めユーザーの identity を保存しておく必要があります。
Wallet に証明書と秘密鍵を登録する簡単なサンプルを紹介します。
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 のデータを保存します。
インポートする証明書と秘密鍵は環境変数で指定します。
以下、実行例です。
$ 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 を発行するアプリケーションの例は次のようになります。
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 に投稿された記事の転載です。
Tips wiki では、IT企業の技術的な情報やプロジェクトの情報を公開可能な範囲で公開してます。