Hyperledger Fabricの開発環境テンプレート公開しました

初めに

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.yamlhyperledger-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の有益情報や新しいアウトプットがありましたらこちらで紹介しますのでよろしくお願いいたします。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

2人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です