こんにちは、(株)日立製作所 研究開発グループ サービスコンピューティング研究部の池川です。
はじめに
OSSのブロックチェーン基盤であるHyperledger Fabricは1つのブロックチェーンネットワーク上に複数のチェーンコードをインストールすることができます。
この記事では、チェーンコードを2つインストールし、一方のチェーンコードから他のチェーンコードを呼び出す方法について紹介します。
この方法を使うことで、条件によって他のチェーンコードを読み出し・書き込みを行うことができるようになります。
例えばユーザからデータを参照するリクエストがあった場合、データ本体を管理しているチェーンコードからデータのアクセス権を管理しているチェーンコードを呼び出してアクセス権を確認してからユーザにデータを渡すという処理を実現することができます。
また、ユーザからデータを新たに書き込むリクエストがあった場合、データの内容が特定の条件に一致した場合は他のチェーンコードを呼び出して台帳に書き込みをするという処理を実現することができます。
Hyperledger Fabricではchannelという概念があり、channelごとにどの組織とデータを共有するかを設定することができます。
今回は、同じchannel上に2つのチェーンコードをインストールする場合の例を示します。
同一のブロックチェーンネットワーク上に2つ以上のチェーンコードをインストールする場合、「同じチャネル上にインストールした場合と違うチャネルにインストールした場合」および「他のチェーンコード内の関数を呼び出して台帳の参照のみをする場合と更新をする場合」で挙動が異なるそうです。
詳しくは以下の記事に詳しくまとめてくれている方がいらっしゃるので参考にしましょう。
Hyperledger FabricでChaincode内Chaincode呼び出しするときに理解しておくべきこと(2019年12月23日参照)
環境構築
AWSのEC2を用いてUbuntu Server 18.04 LTSを搭載したサーバを1台立ち上げます。
以後、立ち上げたインスタンスにssh接続し作業をしました。
1. 前提環境構築
Hyperledger Fabricを動作させるために使用したパッケージを以下の表に示します。
使用したGoのバージョンは推奨されているものと比べて低いバージョンを使用しましたが問題なく動作しました。
パッケージ | 使用バージョン | 推奨バージョン |
---|---|---|
Go | v1.10.4 | v1.12.7+ |
Node.js | v8.10.0 | v8.10+ |
Python | v2.7.17 | v2.7 |
Docker | v19.03.5 | v19.03.0+ |
docker-compose | v1.25.0 | v1.24.1+ |
各パッケージをインストールした際のコマンドを以下に示します。
# ホームディレクトリで作業を行います
cd ~
# docker, docker-composeのインストール
curl -sSL https://get.docker.com | bash -s
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
### sudo snap install docker #でもOK.一気にdockerとdocker-composeをインストール可能.
# sudoなしでdockerコマンドを使えるようにする
sudo addgroup --system docker
sudo usermod -aG docker $(whoami)
# その他パッケージのインストール
sudo apt install -y golang nodejs npm python gcc make git
# ここで一旦再起動
sudo reboot
2. Hyperledger Fabricの環境構築および動作確認
次に、Hyperledger Fabricと、そのサンプルプログラム(fabric-samples)をダウンロードおよびインストールする手順を以下に示します。
# ホームディレクトリに移動
cd ~
# Hyperledger Fabricの環境構築
curl -sSL http://bit.ly/2ysbOFE | bash -s
# バージョンを指定する場合
## curl -sSL http://bit.ly/2ysbOFE | bash -s -- <fabric_version> <fabric-ca_version> <thirdparty_version>
## curl -sSL http://bit.ly/2ysbOFE | bash -s -- 1.4.1 1.4.1 0.4.15
cd fabric-samples
curl -sS https://raw.githubusercontent.com/hyperledger/fabric/master/scripts/bootstrap.sh -o ./scripts/bootstrap.sh
chmod +x ./scripts/bootstrap.sh
./scripts/bootstrap.sh
以上の手順が終了すると、fabric-samplesの中にある各サンプルを動作させることができる。
例としてfabcarアプリケーションを起動するスクリプトを実行し、正しくFabricブロックチェーンネットワークが立ち上がっているかを確認しましょう。
# fabcarアプリケーションのディレクトリに移動しスクリプトを実行
cd ~/fabric-samples/fabcar
./startFabric.sh
正しく立ち上がると、JavascriptやTypescript、Javaを用いたFabCarアプリケーションの操作方法が表示されます。
今回はfabcarアプリケーションの操作方法は割愛します。
3. チェーンコードの準備
ここからが本題になります。
チェーンコードから他のチェーンコードを呼び出したいので、2つのチェーンコードを用意します。
今回はfabcar.go
とfabcar.goをベースに変更を加えたcc2cc.go
の2つのチェーンコードを用います。
以下のコマンドを実行し、fabcarをコピーした後に新たに作成したディレクトリの名前およびチェーンコードの名前を変更してください。
# fabcarをコピー
cd ~/fabric-samples/chaincode/
cp -r fabcar cc2cc
# チェーンコードの名前をcc2cc.goに変更
cd ~/fabric-samples/chaincode/cc2cc/go/
mv fabcar.go cc2cc.go
cc2cc.goを書き換えます。
cc2cc.goのcreateCar関数を以下のように書き換えてください。
今回の例では、車のメーカが"T"だった場合はfabcarチェーンコードのcreateCar関数を呼び出してinvokeを行い、その他の場合はcc2ccからinvokeを行います。
//省略
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 5 {
return shim.Error("Incorrect number of arguments. Expecting 5")
}
if args[1] == "T" {
argBytes := [][]byte{[]byte("createCar"), []byte(args[0]), []byte(args[1]), []byte(args[2]), []byte(args[3]), []byte(args[4])}
res := APIstub.InvokeChaincode("fabcar", argBytes, "mychannel")
if res.Status != 200 {
argsErrMsg := "Status of InvokeChaincode is " + string(res.Status)
return shim.Error(argsErrMsg)
}
return shim.Success(res.Payload)
}
var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}
carAsBytes, _ := json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
//省略
以上でチェーンコードの準備は完了です。
変更したチェーンコードにシンタックスエラーが無いかを確認するには以下を実行してください。
# チェーンコードがあるディレクトリに移動
~/fabric-samples/chaincode/cc2cc/go/
# 以下を実行し、エラーなく"go"ファイルが作成できれば問題ない
go build
4. Hyperledger Fabricのコンテナを立ち上げ、チェーンコードのインストールおよびインスタンス化
続いて、Hyperledger Fabricのコンテナを立ち上げ、チェーンコードのインストールおよびインスタンス化をします。
startFabric.shを以下に記載の通り書き換えてください。
このスクリプトではpeer0.org1およびpeer0.org2に対してfabcarおよびcc2ccのインストールをした後にそれぞれのノードでインストールしたチェーンコードのインスタンス化を行っています。
#!/bin/bash
#
# Copyright IBM Corp All Rights Reserved
#
# SPDX-License-Identifier: Apache-2.0
#
# Exit on first error
set -e
# don't rewrite paths for Windows Git Bash users
export MSYS_NO_PATHCONV=1
starttime=$(date +%s)
CC_SRC_LANGUAGE=${1:-"go"}
CC_SRC_LANGUAGE=`echo "$CC_SRC_LANGUAGE" | tr [:upper:] [:lower:]`
if [ "$CC_SRC_LANGUAGE" = "go" -o "$CC_SRC_LANGUAGE" = "golang" ]; then
CC_RUNTIME_LANGUAGE=golang
# --------------------------追加変更点--------------------------
# fabcarのパス
CC_SRC_PATH01=github.com/chaincode/fabcar/go
# cc2ccのパス
CC_SRC_PATH02=github.com/chaincode/cc2cc/go
# --------------------------------------------------------------
elif [ "$CC_SRC_LANGUAGE" = "java" ]; then
CC_RUNTIME_LANGUAGE=java
CC_SRC_PATH=/opt/gopath/src/github.com/chaincode/fabcar/java
elif [ "$CC_SRC_LANGUAGE" = "javascript" ]; then
CC_RUNTIME_LANGUAGE=node # chaincode runtime language is node.js
CC_SRC_PATH=/opt/gopath/src/github.com/chaincode/fabcar/javascript
elif [ "$CC_SRC_LANGUAGE" = "typescript" ]; then
CC_RUNTIME_LANGUAGE=node # chaincode runtime language is node.js
CC_SRC_PATH=/opt/gopath/src/github.com/chaincode/fabcar/typescript
echo Compiling TypeScript code into JavaScript ...
pushd ../chaincode/fabcar/typescript
npm install
npm run build
popd
echo Finished compiling TypeScript code into JavaScript
else
echo The chaincode language ${CC_SRC_LANGUAGE} is not supported by this script
echo Supported chaincode languages are: go, javascript, and typescript
exit 1
fi
# clean the keystore
rm -rf ./hfc-key-store
# launch network; create channel and join peer to channel
cd ../first-network
echo y | ./byfn.sh down
echo y | ./byfn.sh up -a -n -s couchdb
CONFIG_ROOT=/opt/gopath/src/github.com/hyperledger/fabric/peer
ORG1_MSPCONFIGPATH=${CONFIG_ROOT}/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
ORG1_TLS_ROOTCERT_FILE=${CONFIG_ROOT}/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
ORG2_MSPCONFIGPATH=${CONFIG_ROOT}/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
ORG2_TLS_ROOTCERT_FILE=${CONFIG_ROOT}/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
ORDERER_TLS_ROOTCERT_FILE=${CONFIG_ROOT}/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
set -x
# --------------------------追加変更点--------------------------
# peer0.org1.example.comに対してfabcarチェーンコードをインストール
echo "Installing smart contract on peer0.org1.example.com"
docker exec \
-e CORE_PEER_LOCALMSPID=Org1MSP \
-e CORE_PEER_ADDRESS=peer0.org1.example.com:7051 \
-e CORE_PEER_MSPCONFIGPATH=${ORG1_MSPCONFIGPATH} \
-e CORE_PEER_TLS_ROOTCERT_FILE=${ORG1_TLS_ROOTCERT_FILE} \
cli \
peer chaincode install \
-n fabcar \
-v 1.0 \
-p "$CC_SRC_PATH01" \
-l "$CC_RUNTIME_LANGUAGE"
# peer0.org2.example.comに対してfabcarチェーンコードをインストール
echo "Installing smart contract on peer0.org2.example.com"
docker exec \
-e CORE_PEER_LOCALMSPID=Org2MSP \
-e CORE_PEER_ADDRESS=peer0.org2.example.com:9051 \
-e CORE_PEER_MSPCONFIGPATH=${ORG2_MSPCONFIGPATH} \
-e CORE_PEER_TLS_ROOTCERT_FILE=${ORG2_TLS_ROOTCERT_FILE} \
cli \
peer chaincode install \
-n fabcar \
-v 1.0 \
-p "$CC_SRC_PATH01" \
-l "$CC_RUNTIME_LANGUAGE"
# peer0.org1.example.comにインストールしたfabcarチェーンコードをインスタンス化
echo "Instantiating smart contract on mychannel"
docker exec \
-e CORE_PEER_LOCALMSPID=Org1MSP \
-e CORE_PEER_MSPCONFIGPATH=${ORG1_MSPCONFIGPATH} \
cli \
peer chaincode instantiate \
-o orderer.example.com:7050 \
-C mychannel \
-n fabcar \
-l "$CC_RUNTIME_LANGUAGE" \
-v 1.0 \
-c '{"Args":[]}' \
-P "AND('Org1MSP.member','Org2MSP.member')" \
--tls \
--cafile ${ORDERER_TLS_ROOTCERT_FILE} \
--peerAddresses peer0.org1.example.com:7051 \
--tlsRootCertFiles ${ORG1_TLS_ROOTCERT_FILE}
# peer0.org1.example.comに対してcc2ccチェーンコードをインストール
echo "Installing smart contract on peer0.org1.example.com"
docker exec \
-e CORE_PEER_LOCALMSPID=Org1MSP \
-e CORE_PEER_ADDRESS=peer0.org1.example.com:7051 \
-e CORE_PEER_MSPCONFIGPATH=${ORG1_MSPCONFIGPATH} \
-e CORE_PEER_TLS_ROOTCERT_FILE=${ORG1_TLS_ROOTCERT_FILE} \
cli \
peer chaincode install \
-n cc2cc \
-v 1.0 \
-p "$CC_SRC_PATH02" \
-l "$CC_RUNTIME_LANGUAGE"
# peer0.org2.example.comに対してcc2ccチェーンコードをインストール
echo "Installing smart contract on peer0.org2.example.com"
docker exec \
-e CORE_PEER_LOCALMSPID=Org2MSP \
-e CORE_PEER_ADDRESS=peer0.org2.example.com:9051 \
-e CORE_PEER_MSPCONFIGPATH=${ORG2_MSPCONFIGPATH} \
-e CORE_PEER_TLS_ROOTCERT_FILE=${ORG2_TLS_ROOTCERT_FILE} \
cli \
peer chaincode install \
-n cc2cc \
-v 1.0 \
-p "$CC_SRC_PATH02" \
-l "$CC_RUNTIME_LANGUAGE"
# peer0.org1.example.comにインストールしたcc2ccチェーンコードをインスタンス化
echo "Instantiating smart contract on mychannel"
docker exec \
-e CORE_PEER_LOCALMSPID=Org1MSP \
-e CORE_PEER_MSPCONFIGPATH=${ORG1_MSPCONFIGPATH} \
cli \
peer chaincode instantiate \
-o orderer.example.com:7050 \
-C mychannel \
-n cc2cc \
-l "$CC_RUNTIME_LANGUAGE" \
-v 1.0 \
-c '{"Args":[]}' \
-P "AND('Org1MSP.member','Org2MSP.member')" \
--tls \
--cafile ${ORDERER_TLS_ROOTCERT_FILE} \
--peerAddresses peer0.org1.example.com:7051 \
--tlsRootCertFiles ${ORG1_TLS_ROOTCERT_FILE}
echo "Finish..."
# --------------------------------------------------------------
5. チェーンコードから他のチェーンコードを呼び出し
最後に、チェーンコードから他のチェーンコードを呼び出すサンプルを実行してみましょう。
まず、以下のディレクトリに移動してください。
# ディレクトリの移動
cd ~/fabric-samples/fabcar/javascript
チェーンコードから他のチェーンコードを呼び出す確認をします。
invoke.jsを以下のように書き換えてください。
/*
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const { FileSystemWallet, Gateway } = require('fabric-network');
const path = require('path');
const ccpPath = path.resolve(__dirname, '..', '..', 'first-network', 'connection-org1.json');
async function main() {
try {
// Create a new file system based wallet for managing identities.
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = new FileSystemWallet(walletPath);
console.log(`Wallet path: ${walletPath}`);
// Check to see if we've already enrolled the user.
const userExists = await wallet.exists('user1');
if (!userExists) {
console.log('An identity for the user "user1" does not exist in the wallet');
console.log('Run the registerUser.js application before retrying');
return;
}
// Create a new gateway for connecting to our peer node.
const gateway = new Gateway();
await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: true } });
// Get the network (channel) our contract is deployed to.
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network.
const contract = network.getContract('cc2cc');
// Submit the specified transaction.
// createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom')
// changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR10', 'Dave')
// --------------------------追加変更点--------------------------
// 3つのトランザクションを送信する
await contract.submitTransaction('createCar', 'CAR01', 'T', 'Prius', 'White', 'Ikegawa');
await contract.submitTransaction('createCar', 'CAR02', 'M', 'CX-5', 'Gunmetal grey', 'Nemoto');
await contract.submitTransaction('createCar', 'CAR03', 'N', 'March', 'Blue', 'Takada');
// --------------------------------------------------------------
console.log('Transaction has been submitted');
// Disconnect from the gateway.
await gateway.disconnect();
} catch (error) {
console.error(`Failed to submit transaction: ${error}`);
process.exit(1);
}
}
main();
続いて、query.jsを以下のように書き換えてください。
/*
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
const { FileSystemWallet, Gateway } = require('fabric-network');
const path = require('path');
const ccpPath = path.resolve(__dirname, '..', '..', 'first-network', 'connection-org1.json');
async function main() {
try {
// Create a new file system based wallet for managing identities.
const walletPath = path.join(process.cwd(), 'wallet');
const wallet = new FileSystemWallet(walletPath);
console.log(`Wallet path: ${walletPath}`);
// Check to see if we've already enrolled the user.
const userExists = await wallet.exists('user1');
if (!userExists) {
console.log('An identity for the user "user1" does not exist in the wallet');
console.log('Run the registerUser.js application before retrying');
return;
}
// Create a new gateway for connecting to our peer node.
const gateway = new Gateway();
await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: true } });
// Get the network (channel) our contract is deployed to.
const network = await gateway.getNetwork('mychannel');
// Get the contract from the network.
const contract = network.getContract('fabcar');
// Evaluate the specified transaction.
// queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4')
// queryAllCars transaction - requires no arguments, ex: ('queryAllCars')
const result = await contract.evaluateTransaction('queryAllCars');
console.log(`Transaction has been evaluated, fabcar queryAllCars result is: ${result.toString()}`);
// --------------------------追加変更点--------------------------
// 新たに追加したチェーンコードのqueryAllCarsを呼び出して結果を表示
const contract2 = network.getContract('cc2cc');
const result2 = await contract2.evaluateTransaction('queryAllCars');
console.log(`Transaction has been evaluated, cc2cc queryAllCars result is: ${result2.toString()}`);
// --------------------------------------------------------------
} catch (error) {
console.error(`Failed to evaluate transaction: ${error}`);
process.exit(1);
}
}
main();
実際に動かしてみましょう。
# ディレクトリ移動
cd ~/fabric-samples/fabcar/javascript
# npmパッケージをインストール
npm install
# 管理者登録
$ node enrollAdmin.js
Wallet path: /home/ubuntu/fabric-samples/fabcar/javascript/wallet
Successfully enrolled admin user "admin" and imported it into the wallet
# ユーザ登録
$ node registerUser.js
Wallet path: /home/ubuntu/fabric-samples/fabcar/javascript/wallet
Successfully registered and enrolled admin user "user1" and imported it into the wallet
# Invoke(初回の実行時は時間がかかることがあります)
$ node invoke.js
Wallet path: /home/ubuntu/fabric-samples/fabcar/javascript/wallet
Transaction has been submitted
# Query(fabcarから保存したデータとcc2ccから保存したデータをそれぞれ参照する)
$ node query.js
Wallet path: /home/ubuntu/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, fabcar queryAllCars result is: [{"Key":"CAR01", "Record":{"colour":"White","make":"T","model":"Prius","owner":"Ikegawa"}}]
Transaction has been evaluated, cc2cc queryAllCars result is: [{"Key":"CAR02", "Record":{"colour":"Gunmetal grey","make":"M","model":"CX-5","owner":"Nemoto"}},{"Key":"CAR03","Record":{"colour":"Blue","make":"N","model":"March","owner":"Takada"}}]
各コマンドを実行すると、上記のような結果が得られると思います。
Queryの結果をみると、指定した条件(車のメーカが"T"だった場合fabcarチェーンコードから台帳に書き込まれる)通りにデータの書き込みが振り分けられていることが確認できます。
まとめ
以上のように、チェーンコードを2つ準備しインストールおよびインスタンス化した後に、一方のチェーンコードから他のチェーンコードを呼び出す動作を確認できました。
ブロックチェーンを用いたシステムを開発していく上で、チェーンコードを複数インストールして相互にやり取りを行う必要が出てきた場合の参考になれば幸いです。
参考
ブロックチェーンの特徴やユースケースについて (2019年12月23日参照)
ブロックチェーンのビジネス実装へ、すぐに使えるソリューションを一気にご紹介 (2019年12月23日参照)