カンボジア国立銀行が発行しているバコンというCBDCが、許可型のプライベートブロックチェーンをベースに動いているということを聞き、自身も再現してみることにしました。
一般的に、ブロックチェーンは非中央集権的な仕組みを特徴としますが、一方で参加ノードを限定し、取引の承認を特定の主体に制限することで、中央集権的な管理を行いつつも、ブロックチェーンの改ざん耐性といった特性を活かすことが可能になります。
コンセンサスアルゴリズム
中央集権的なブロックチェーンを設計するには、ブロックの生成やチェーンの構築、マイニングの仕組みはビットコインやイーサリアムとは異なるものにする必要があります。
ビットコインやイーサリアムで採用されているプルーフ・オブ・ワーク(PoW)やプルーフ・オブ・ステーク(PoS)といったアルゴリズムは、非中央集権的なシステムに適した設計のため、中央集権的なブロックチェーンには向いていません。そこで、許可型のブロックチェーンでよく採用されるプルーフ・オブ・オーソリティ(PoA)や、ビザンチン耐性を強化したコンセンサスアルゴリズムであるプラクティカル・ビザンチン・フォールト・トレランス(PBFT)などを検討する必要があります。
ちなみにバコンは、YACという独自のどちらかといえばPoAに近いアルゴリズムを採用しているようです。
Hyperledger Fabricの採用
許可型ブロックチェーンの構築には、Linux Foundationがホストする「Hyperledger」というプロジェクト群を使うのが便利なようで、バコンはこの中のIrohaというものをベースに作られています(Irohaはソラミツがバコンのために開発したもの)。
この中でも企業向けのプライベートブロックチェーンとして広く利用されている「Fabric」を今回は採用することにしました。
というのもFabricは、許可型ブロックチェーンとして設計されており、参加者を限定し、取引の承認を特定のノードが行う仕組みを備えています。これにより、管理主体が存在する中央集権的なブロックチェーンを構築しつつ、ブロックチェーンの持つ耐改ざん性や透明性を活用できます。また、Fabricはスマートコントラクト機能(チェーンコード)を備えており、特定の業務ロジックをブロックチェーン上に安全に実装することができます。
環境
- Docker / Docker Compose
- Node.js
- Hyperledger Fabric バイナリとサンプル
以下のコマンドで Fabric サンプルとバイナリを取得します。
# ワークスペースディレクトリに移動
mkdir -p ~/fabric && cd ~/fabric
# fabric-samples リポジトリをクローン
git clone https://github.com/hyperledger/fabric-samples.git
# fabric-samples のバージョンに合わせたバイナリをダウンロード(例: 2.2.0の場合)
cd fabric-samples
curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.2.0
テストネットワークの起動とチャネル作成
fabric-samples 内の test-network
を利用してネットワークを立ち上げます。
cd ~/fabric/fabric-samples/test-network
# ネットワークの起動とチャネル作成(CA を使ったオーソリゼーション付き)
./network.sh up createChannel -ca
チェーンコード(ブロックチェーン部分)の作成
今回は Node.js チェーンコードを作成します。以下のようなディレクトリ構成とファイルで作成しました。
~/fabric/chaincode-payment/
├── package.json
├── index.js
└── paymentContract.js
└── metadata.json
package.json
{
"name": "payment-chaincode",
"version": "1.0.0",
"description": "Chaincode for recording payment transactions",
"main": "index.js",
"scripts": {
"start": "fabric-chaincode-node start"
},
"dependencies": {
"fabric-contract-api": "^2.2.0",
"fabric-shim": "^2.2.0"
}
}
index.js
'use strict';
const PaymentContract = require('./paymentContract');
module.exports.contracts = [ PaymentContract ];
paymentContract.js
'use strict';
const { Contract } = require('fabric-contract-api');
class PaymentContract extends Contract {
// 初期化(サンプル取引データを登録)
async InitLedger(ctx) {
console.info('=== Initializing Ledger ===');
const transactions = [
{
id: 'tx1',
payer: 'user1',
payee: 'merchant1',
amount: 100,
timestamp: new Date().toISOString()
}
];
for (const tx of transactions) {
await ctx.stub.putState(tx.id, Buffer.from(JSON.stringify(tx)));
console.info(`Initialized transaction ${tx.id}`);
}
console.info('=== Ledger Initialized ===');
}
// 送金取引を新規作成する関数
async CreateTransaction(ctx, id, payer, payee, amount) {
console.info('=== Creating Transaction ===');
const exists = await this.TransactionExists(ctx, id);
if (exists) {
throw new Error(`Transaction ${id} already exists`);
}
const tx = {
id,
payer,
payee,
amount: parseFloat(amount),
timestamp: new Date().toISOString()
};
await ctx.stub.putState(id, Buffer.from(JSON.stringify(tx)));
console.info(`Transaction ${id} created`);
return JSON.stringify(tx);
}
// 指定した取引の詳細を取得する関数
async QueryTransaction(ctx, id) {
console.info('=== Querying Transaction ===');
const txAsBytes = await ctx.stub.getState(id);
if (!txAsBytes || txAsBytes.length === 0) {
throw new Error(`Transaction ${id} does not exist`);
}
console.info(`Transaction ${id} retrieved`);
return txAsBytes.toString();
}
// 取引の存在チェック
async TransactionExists(ctx, id) {
const txAsBytes = await ctx.stub.getState(id);
return txAsBytes && txAsBytes.length > 0;
}
}
module.exports = PaymentContract;
metadata.json
{
"contracts": [
{
"name": "PaymentContract",
"version": "1.0",
"transactions": [
{
"name": "CreateTransaction",
"parameters": ["id", "payer", "payee", "amount"]
},
{
"name": "QueryTransaction",
"parameters": ["id"]
}
]
}
]
}
Note:
metadata.jsonがないと、ノードごとに帰ってくるレスポンスがズレることがあるそうです。
ここまで準備できたら、上記ディレクトリ(~/fabric/chaincode-payment
)内で以下のコマンドを実行します:
npm install
チェーンコードのパッケージングとデプロイ
test-network
を使って、先ほど作成した Node.js チェーンコードをネットワークへデプロイします。
cd ~/fabric/fabric-samples/test-network
チェーンコードのデプロイ
下記のコマンドで、チェーンコード名を「payment」としてデプロイします。
※ -ccp
オプションのパスは、先ほど作成したチェーンコードのディレクトリ(例: ~/fabric/chaincode-payment
)に合わせます。
./network.sh deployCC -ccn payment -ccp ../chaincode-payment -ccl javascript
このコマンドにより、パッケージングから承認、コミットまでが実施され、チェーンコードがチャネル上にデプロイされます。
取引の実行(オプション)
ネットワークが起動しチェーンコードがデプロイされたら、CLI またはアプリケーションからトランザクションの呼び出しが可能となります。例えば、CLI で以下のように送金取引を実行できます。
# PaymentChaincode を呼び出して新たな取引を作成
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C mychannel -n payment -c '{"function":"CreateTransaction","Args":["tx2","user2","merchant2","250"]}'
また、作成した取引を確認するには次のように実行します。
peer chaincode query -C mychannel -n payment -c '{"Args":["QueryTransaction","tx2"]}'
大枠としてはこんなところなのですが、このようにはすんなりいかず、ChatGPTでもこの辺りの領域は苦手なようで、かなり試行錯誤しました。
TLS および MSP の環境変数の設定
TLS を使用している場合、以下のような環境変数も正しく設定する必要があるそうです。
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
必要な環境変数の設定
export FABRIC_CFG_PATH=/Users/username/fabric/fabric-samples/config
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_TLS_ROOTCERT_FILE=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export ORDERER_CA=/Users/username/fabric/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
ローカルホストから直接 peer
コマンドを実行する場合、ネットワークの設定や Docker のポートマッピングの影響で接続できないことがあるそうで、CLIコンテナに入ってコマンド叩くようにしました(test-network
ディレクトリ内で):
./network.sh cli
送金を指示するコード:
peer chaincode invoke \
-o localhost:7050 \
--ordererTLSHostnameOverride orderer.example.com \
--tls --cafile "$ORDERER_CA" \
-C mychannel -n payment \
-c '{"function":"CreateTransaction","Args":["tx2","user2","merchant2","250"]}' \
--peerAddresses localhost:7051 \
--tlsRootCertFiles /Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \
--peerAddresses localhost:9051 \
--tlsRootCertFiles /Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
上記をやることで自分は一旦動かすことができました。
チェーンコードの再デプロイ手順
修正後のチェーンコードをデプロイする際は、バージョン番号やシーケンス番号を更新して再パッケージ化、インストール、承認、コミットする必要があります。
1. チェーンコードのパッケージング
新しいバージョン(例:バージョン 1.1 とする)としてパッケージ化します。
peer lifecycle chaincode package payment.tar.gz \
--path ./chaincode/payment \
--lang node \
--label payment_1.1
2. 各組織のピアにチェーンコードパッケージをインストール
ターミナルは二つのタブを開き、片方をORG1用、もう片方をORG2用にすると便利だと思います。
今の場所を確認するコマンド:
echo $CORE_PEER_LOCALMSPID
パッケージのインストール:
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
export CORE_PEER_TLS_ENABLED=true
# TLS ルート証明書(Org1 ピア)のパス例
export CORE_PEER_TLS_ROOTCERT_FILE=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
peer lifecycle chaincode install payment.tar.gz
Org2側も同じようにしますが、ポート番号を9051
にすることに注意です。
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=/Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051 # 変更箇所
export CORE_PEER_TLS_ENABLED=true
どちらかの組織上で下記を実行します。
peer lifecycle chaincode queryinstalled \
--peerAddresses localhost:7051 \
--tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE
3. 各組織によるチェーンコード定義の承認
まずはOrg1 による承認:
peer lifecycle chaincode approveformyorg \
--channelID mychannel \
--name payment \
--version 1.1 \
--sequence 2 \
--package-id payment_1.1:4b3928954522480c38d8e5092b1608eed77d1649bc06d48fc3211ff1083e30b8 \
-o localhost:7050 \
--tls --cafile "$ORDERER_CA"
上記をOrg2でも行います。
4. チェーンコード定義のコミット
すべての組織で承認が完了したら、チェーンコード定義をチャネルにコミットします。ここでは、Org1 と Org2 のピアの情報を両方指定します。
peer lifecycle chaincode commit \
-o localhost:7050 \
--channelID mychannel \
--name payment \
--version 1.1 \
--sequence 2 \
--peerAddresses localhost:7051 \
--tlsRootCertFiles /Users/usernamefabric/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \
--peerAddresses localhost:9051 \
--tlsRootCertFiles /Users/username/fabric/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \
--tls --cafile \"$ORDERER_CA\"
以上になります。
他のコマンド
作成されたブロックの確認(heightが長さ)
peer channel getinfo -c mychannel
ブロックの生成時間などのルールを確認するコマンド
cat /Users/usernamei/fabric/fabric-samples/config/configtx.yaml | grep Batch
まとめ
今まで無縁だと思っていたブロックチェーンの構築ですが、意外とライブラリやフレームワークなども充実しているんだと感じました。ただ日本語文献が多くなく、環境の設定がかなり面倒なので、その点は振り返ってもかなり苦労したと感じています。