初めに
PS/SLの佐々木です。
PS/SLではweb3の新しい取り組みとしてHyperledger Fabricというコンソーシアム型ブロックチェーンを用いたPoC開発を進めています。
Hyperledger Fabricのキャッチアップをしていく中でローカル環境での開発のやりずらさを感じました。
具体的にはchaincodeをGitでバージョン管理するための方法やチーム開発時の環境構築、すべてがスクリプト化されていてコマンドの挙動検証がしずらいといったことがありました。
そこで今回Hyperledger Fabricのネットワークは用意されているtest-network
を使用し、Chaincodeとクライアントアプリケーションのレイヤーは別のリポジトリに切り出して、ビジネスロジックの実装に集中できる環境を作成したので紹介します。
以下のことは紹介しません。
peer
コマンドのオプションや引数の詳細な説明- chaincodeのコードの解説
- クライアントアプリケーション(API)のコードの詳細な解説
では行きましょう
https://hyperledger-fabric.readthedocs.io/ja/latest/prereqs.html
↑の事前準備は完了しているものとして進みます。
ネットワークの起動
ネットワークの構成は今回意識せずにビジネスロジックの開発に集中したいので事前に用意されているサンプルを使用します。
先ほどから紹介しているサンプルは以下のコマンドを実行することのよって取得することができます。
curl -sSLO <https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh> && chmod +x install-fabric.sh
./install-fabric.sh docker samples binary
ダウンロードが完了したら、test-network
ディレクトリに移動して以下のコマンドを順番に実行します。
cd abric-samples/test-network
// ネットワークを初期化しておく
./network.sh down
// ネットワーク起動
./network.sh up createChannel
// モニタリングに便利
./monitordocker.sh fabric_test
この先でpeer
コマンドを実行したいのでパスを通しておきます。
peer
コマンドはfabric-samples/bin
にあるのでfabric-samples
ディレクトリで以下のコマンドを実行してください。
// peerコマンドを実行するためのパス fabric-sample配下にbinファイルはあるので指定します
export PATH=${PWD}/bin:$PATH
以上でネットワーク準備の完了です。
証明書管理
ネットワークを起動するとPeer, Orderer, Userなどの証明書が作成されます。
しかしfabric-sample
の中にあると扱いが面倒なので自分のアプリケーションリポジトリにコピーします。
まず以下のコマンドで今回使用するアプリケーションのテンプレートをcloneします。
git@github.com:atomic-kanta-sasaki/hyperledger-fabric-client-application.git
続けて以下の証明書をCloneしてきたリポジトリにコピーします。
Ordererの各種証明書
`test-network/organizations/ordererOrganizations` を `hyperledger-fabric-application/certificate` にすべてコピー
Peer, Userの各種証明書
`test-network/organizations/peerOrganizations` を `hyperledger-fabric-application/certificate` にすべてコピー
Userの秘密鍵のファイル名変更(わかりやすくするため)
`certificate/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/` にある秘密鍵をkey.pemに変更
`certificate/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/keystore/` にある秘密鍵をkey.pemに変更
左が今回のアプリケーションテンプレートで右側がHyperledger Fabricのテンプレートです。
証明書に関する作業は以上で終了です。
Chaincode
次に先ほど起動したpeerにchaincodeをinstallしていきましょう。
まず環境変数の設定です。
peer
の設定ファイルcore.yaml
がhyperledger-fabric-application/config
に入っているのでパスを以下のように環境変数設定します。
export FABRIC_CFG_PATH=$PWD/config/
続いてchaincodeをinstallするためにはnode.js(typescript)をパッケージ化する必要があります。
以下のコマンドを実行して下さい。
依存ファイルのinstallとビルド
npm install
npm run build
chaincodeのパッケージ化
peer lifecycle chaincode package basic.tar.gz --path ./ --lang node --label basic_1.0
*これ以降の環境変数をセットするときは hyperledger-fabric-application
ディレクトリで実行します
Org1のPeerにchaincodeをinstallする
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/certificate/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/certificate/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
install
cd chaincode peer lifecycle chaincode install basic.tar.gz
Org2のPeerにchaincodeをinstallする
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/certificate/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/certificate/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
install
cd chaincode
peer lifecycle chaincode install basic.tar.gz
chaincodeの承認
chaincodeがPeerにinstallされたら組織ごとに承認する必要があります。
現在エンドースメントポリシーは設定していないので組織の過半数の承認が必要です。(組織の数が二つなのですべての組織から承認が必要)
また承認フローにinstallしたパッケージIDが必要なので以下のコマンドで確認します。
peer lifecycle chaincode queryinstalled
// 実行結果
Installed chaincodes on peer:
Package ID: basic_1.0:4c7f4fd3119f70763b3ffbeb86566eccb3d2af8d6ab1ebe595eca7d37e9d98a7, Label: basic_1.0
パッケージIDを環境変数にセット
export CC_PACKAGE_ID=basic_1.0:4c7f4fd3119f70763b3ffbeb86566eccb3d2af8d6ab1ebe595eca7d37e9d98a7
OrdererのTLS証明書のパスをセット
export ORDERER_TLS_CERT=${PWD}/certificate/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
Org2 chaincodeの承認
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${PWD}/certificate/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
//以下のログならOK
2024-08-23 16:04:06.118 JST 0001 INFO [chaincodeCmd] ClientWait -> txid [39ecbbee6887f82f5a1684e584d7a65ae0a691f0733ff6b1d803f3ce0853844c] committed with status (VALID) at localhost:9051
Org1 chaincodeの承認
Org1で実行するための環境変数の設定
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=${PWD}/certificate/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/certificate/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_ADDRESS=localhost:7051
承認
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile ${ORDERER_TLS_CERT}
↑一つのコマンドです
chaincodeをチャネルにcommit
チャネルメンバがchaincodeの定義を承認しているかを確認
peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --output json
// 両方の組織が承認していることが確認できる
{
"approvals": {
"Org1MSP": true,
"Org2MSP": true
}
}
commit
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile ${ORDERER_TLS_CERT} --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/certificate/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/certificate/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
↑一つのコマンドです
commitの確認
peer lifecycle chaincode querycommitted --channelID mychannel --name basic --cafile ${ORDERER_TLS_CERT}
// シーケンスとバージョンが返ってくる
Committed chaincode definition for chaincode 'basic' on channel 'mychannel':
Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true, Org2MSP: true]
これによりすべてのPeerでChaincodeを実行することができるようになりました。
クライアントアプリケーション
最後にクライアントアプリケーションの起動です。
今回はnest.jsを使用してAPIを作成しました。
ORMはprismaです。
ディレクトリは hyperledger-fabric-application/application
になります。
依存関係のinstall
npm install
Migrationと初期データ投入
npx prisma migrate dev
npx prisma db seed
アプリケーションの実行
npm run start
これでアプリケーションの起動完了です。
ではchaincodeにも初期データを入れて、データを取得してみましょう
初期化
POST <http://localhost:3000/asset/init>
レスポンスは特にありませんが、初期データの登録が完了しました。
登録したデータの取得
GET <http://localhost:3000/asset/all>
すべてのデータを取得できることが確認できます。
クライアントアプリケーションの実装
chaincodeで実装した関数を引数に渡せば呼び出せるようにしてあります。
chaincodeの呼び出しはRepository層にあり、Repository層でFabricGatewayServiceをDIしています。
FabricServiceGatewayにchaincodeとの接続設定がすでにあるので各Repository層でconnectionを作成しTransactionを発行することができます。
以下実際のソースコードです。
import { Contract } from "@hyperledger/fabric-gateway";
import { FabricGatewayService } from "src/repository/hyperledger/fabric-gateway/fabric-gateway.service";
import { Injectable } from '@nestjs/common';
import { Asset } from "src/domain/asset/asset";
@Injectable()
export class AssetRepository {
private readonly channelName = 'mychannel';
private readonly chaincodeName = 'basic';
private contract: Contract;
private utf8Decoder = new TextDecoder();
constructor(
// private readonly prismaService: PrismaService,
private readonly fabricGatewayService: FabricGatewayService,
) {}
// async onModuleInit() {
// await this.fabricGatewayService.createConnection();
// this.contract = this.fabricGatewayService.getContract(this.channelName, this.chaincodeName);
// }
/**
* TODO 本当はonModuleInitで呼び出すべきだが、ScopeをRequestServiceに定義していると動かないためいったん直接呼び出す方式で対応
*/
private async ensureConnection() {
await this.fabricGatewayService.createConnection();
this.contract = this.fabricGatewayService.getContract(this.channelName, this.chaincodeName);
}
async initLedger() {
await this.ensureConnection()
await this.contract.submitTransaction('InitLedger');
}
async getAllAssets() {
await this.ensureConnection()
const resultBytes = await this.contract.evaluateTransaction('GetAllAssets');
const resultJson = this.utf8Decoder.decode(resultBytes);
return JSON.parse(resultJson);
}
async getAssetById(assetId: string): Promise<Asset> {
await this.ensureConnection()
const resultBytes = await this.contract.evaluateTransaction('ReadAsset', assetId);
if (!resultBytes || resultBytes.length === 0) {
throw new Error(`Asset ${assetId} does not exist`);
}
const resultJson = this.utf8Decoder.decode(resultBytes);
console.log(resultJson);
const json = JSON.parse(resultJson);
return Asset.create(json.ID, json.Color, json.Size, json.Owner, json.AppraisedValue);
}
async createAsset(asset: Asset) {
await this.ensureConnection()
await this.contract.submitTransaction('CreateAsset', asset.getId(), asset.getColor(), asset.getSize(), asset.getOwner(), asset.getValue());
}
async transferAsset(asset: Asset) {
await this.ensureConnection()
console.log('\\n--> Async Submit Transaction: TransferAsset, updates existing asset owner');
const commit = await this.contract.submitAsync('TransferAsset', {
arguments: [asset.getId(), asset.getOwner()],
});
const oldOwner = this.utf8Decoder.decode(commit.getResult());
console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`);
console.log('*** Waiting for transaction commit');
const status = await commit.getStatus();
if (!status.successful) {
throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`);
}
}
}
やや修正したほうが良い箇所はありますが、おおむねこのような形です。
あとは好きなようにUseCase層で呼び、自分たちの作りたいアプリケーションのビジネスロジックを組み立ててもらえればと思います。
課題
環境変数のセットに関しては同じことを複数回繰り返しているのでスクリプト化していきたいです。
またnest.jsの使い方やfabric-gatewayもすべて終えているわけではないので引き続きキャッチアップを進めていきます。
あと、Chaincode実行するまで問題点がわからない(install、承認、commitでは間違いに気づけない)のはちょっと辛いなと思いました。
終わりに
ここまで読んでくださりありがとうございました。
これからも引き続きweb3の有益情報や新しいアウトプットがありましたらこちらで紹介しますのでよろしくお願いいたします。