HyperLedgerFablicの公式チュートリアルには
javascript / typesucript / javaで実行するクライアントSDKの記述がありますが、
Ver.1.4.3現在、RestfulAPIで動かすための記述がありません。
Webサーバーを立ててSDKのコードを組み込むしかないの?
と思いきや、公式で紹介しているfablic-samplesの中にも、REST APIサーバーとして動くクライアントがありました。今回はそれをご紹介したいと思います。
※HyperLedgerFablic Composerを使ったり、amazon・microsoftなどのベンダーが公開しているブロックチェーン基盤を使用することで、RESTfulAPIを楽に使うことができるようです。
本記事はfablic-samplesだけで、RESTFulAPIまでやりたいというときのためのガイドです。たぶん、その内公式チュートリアルでも言及されると思いますけども。
対象リポジトリ
balance-transferアプリですね。
上記リポジトリのReadMeに、RESTAPIサーバーの起動まで、分かりやすく手順が記載してあります。
本記事では、RESTAPIサーバーをbalance-transferアプリでなく、fabcarアプリで実行するようにしたいと思います。
組織:2、ピア:2、CA:2、チャンネル:1 の構成です。
準備
fabcarアプリケーションを起動するところから始めます。
2019/11 現在の最新バージョン1.4.3を取得します。
curl -sSL http://bit.ly/2ysbOFE | bash -s 1.4.3
cd fabric-samples/fabcar/
./startFabric.sh
チェーンコードの言語指定は引数なしで、Golangにしておいて下さい。
確認環境
MAC OS 10.14.6
node v8.6.0(※v9以上はNGかも?)
npm v6.12.0
Docker v18.09.2
git v2.11.0
作業開始
- app/
- app.js
- config/
- config.js
- package.json
- artifacts/org1.yaml
- artifacts/org2.yaml
上記のファイルを、balance-transfer/からfirst-network/へコピーします。
cd ../balance-transfer/
cp -rf app* config.* package.json artifacts/org*.yaml ../first-network/
なぜfabcarでなくfirst-networkかというと、fabcarのアプリではネットワークにfirst-networkを使用しているからです。(1.4.0ではbasic-network)
first-network/に移動し、npm installします。
cd ../first-network/
npm install
package.jsonには、expressフレームワークのRESTfuiAPI用のツールなどが記述されています。
無事にインストールできたら、次に進みます。
first-networkの接続プロファイルを変更していきます。
基本的には、balance-transferの接続プロファイル
balance-transfer/articles/network-config.yamlを参考にします。
しかしbalance-transferとfirst-networkでは、プロファイルの構造が違っていて、first-networkはあとから組織が増えていっても動的に出力できる作りになっています。
具体的にはccp-template.yamlの内容を、
ccp-generate.shのシェルを使い
connection-org1.yaml, connection-org2.yaml
といったように組織の数だけ出力しています。
なので、こちらもその作りに合わせた変更が必要になっていきます。
---
name: first-network-org${ORG}
version: 1.0.0
client:
organization: Org${ORG}
connection:
timeout:
peer:
endorser: '300'
# ▼ 追加
channels:
mychannel:
orderers:
- orderer.example.com
peers:
peer0.org1.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.org1.example.com:
endorsingPeer: false
chaincodeQuery: true
ledgerQuery: true
eventSource: false
peer0.org2.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.org2.example.com:
endorsingPeer: false
chaincodeQuery: true
ledgerQuery: true
eventSource: false
chaincodes:
- fabcar:v0
# ▲ 追加
organizations:
Org${ORG}:
mspid: Org${ORG}MSP
peers:
- peer0.org${ORG}.example.com
- peer1.org${ORG}.example.com
certificateAuthorities:
- ca.org${ORG}.example.com
#▼ 追加
adminPrivateKey:
path: crypto-config/peerOrganizations/org${ORG}.example.com/users/Admin@org${ORG}.example.com/msp/keystore/${ADMIN_PRIVATE_KEY}
signedCert:
path: crypto-config/peerOrganizations/org${ORG}.example.com/users/Admin@org${ORG}.example.com/msp/signcerts/Admin@org${ORG}.example.com-cert.pem
#▲ 追加
# ▼ 追加
orderers:
orderer.example.com:
url: grpcs://localhost:7050
grpcOptions:
ssl-target-name-override: orderer.example.com
tlsCACerts:
path: crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
# ▲ 追加
peers:
peer0.org${ORG}.example.com:
url: grpcs://localhost:${P0PORT}
tlsCACerts:
pem: |
${PEERPEM}
grpcOptions:
ssl-target-name-override: peer0.org${ORG}.example.com
hostnameOverride: peer0.org${ORG}.example.com
peer1.org${ORG}.example.com:
url: grpcs://localhost:${P1PORT}
tlsCACerts:
pem: |
${PEERPEM}
grpcOptions:
ssl-target-name-override: peer1.org${ORG}.example.com
hostnameOverride: peer1.org${ORG}.example.com
certificateAuthorities:
ca.org${ORG}.example.com:
url: https://localhost:${CAPORT}
httpOptions:
verify: false
tlsCACerts:
pem: |
${CAPEM}
#▼ 追加
registrar:
- enrollId: admin
- enrollSecret: adminpw
#▲ 追加
caName: ca-org${ORG}
追加した各項目は、RESTAPIサーバーのアプリ上から参照するfirst-networkの構成情報です。
管理者情報も載せていますので、RESTAPIでチェーンコードのインストールやチャンネル作成も可能です。(開発用の機能らしい)
その他の詳細については、balance-transfer/artifacts/network-config.yamlに英語でコメントがありますので、気になりましたらご確認ください。自分でまだきちんと分かっていないので…。
シェルの方は各要素を置き換えているだけです。
touch organizations-template.yaml
touch peers-template.yaml
touch certificate-template.yaml
touch ccp-template-allorg.yaml
とりあえず編集内容をまるっと載せます。
Org${ORG}:
mspid: Org${ORG}MSP
peers:
- peer0.org${ORG}.example.com
- peer1.org${ORG}.example.com
certificateAuthorities:
- ca.org${ORG}.example.com
adminPrivateKey:
path: crypto-config/peerOrganizations/org${ORG}.example.com/users/Admin@org${ORG}.example.com/msp/keystore/${ADMIN_PRIVATE_KEY}
signedCert:
path: crypto-config/peerOrganizations/org${ORG}.example.com/users/Admin@org${ORG}.example.com/msp/signcerts/Admin@org${ORG}.example.comcert.pem
peer0.org${ORG}.example.com:
url: grpcs://localhost:${P0PORT}
tlsCACerts:
pem: |
${PEERPEM}
grpcOptions:
ssl-target-name-override: peer0.org${ORG}.example.com
hostnameOverride: peer0.org${ORG}.example.com
peer1.org${ORG}.example.com:
url: grpcs://localhost:${P1PORT}
tlsCACerts:
pem: |
${PEERPEM}
grpcOptions:
ssl-target-name-override: peer1.org${ORG}.example.com
hostnameOverride: peer1.org${ORG}.example.com
ca.org${ORG}.example.com:
url: https://localhost:${CAPORT}
httpOptions:
verify: false
tlsCACerts:
pem: |
${CAPEM}
registrar:
- enrollId: admin
- enrollSecret: adminpw
caName: ca-org
---
name: first-network-org${ORG}
version: 1.0.0
channels:
mychannel:
orderers:
- orderer.example.com
peers:
peer0.org1.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.org1.example.com:
endorsingPeer: false
chaincodeQuery: true
ledgerQuery: true
eventSource: false
peer0.org2.example.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.org2.example.com:
endorsingPeer: false
chaincodeQuery: true
ledgerQuery: true
eventSource: false
chaincodes:
- fabcar:v0
organizations:
${ORGANIZATIONS}
orderers:
orderer.example.com:
url: grpcs://localhost:7050
grpcOptions:
ssl-target-name-override: orderer.example.com
tlsCACerts:
path: crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
peers:
${PEERS}
certificateAuthorities:
${CERTIFICATE_AUTH}
# !/bin/bash
function one_line_pem {
echo "`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $1`"
}
function json_ccp {
local PP=$(one_line_pem $5)
local CP=$(one_line_pem $6)
sed -e "s/\${ORG}/$1/g" \
-e "s/\${P0PORT}/$2/" \
-e "s/\${P1PORT}/$3/" \
-e "s/\${CAPORT}/$4/" \
-e "s#\${PEERPEM}#$PP#" \
-e "s#\${CAPEM}#$CP#" \
-e "s#\${ADMIN_PRIVATE_KEY}#$7#" \
ccp-template.json
}
function yaml_ccp {
local PP=$(one_line_pem $5)
local CP=$(one_line_pem $6)
sed -e "s/\${ORG}/$1/g" \
-e "s/\${P0PORT}/$2/" \
-e "s/\${P1PORT}/$3/" \
-e "s/\${CAPORT}/$4/" \
-e "s#\${PEERPEM}#$PP#" \
-e "s#\${CAPEM}#$CP#" \
-e "s#\${ADMIN_PRIVATE_KEY}#$7#" \
ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g'
}
function organizations_template {
sed -e "s/\${ORG}/$1/g" \
-e "s#\${ADMIN_PRIVATE_KEY}#$2#" \
organizations-template.yaml | sed -e $'s/\\\\n/\\\n /g'
}
function peers_template {
local PP=$(one_line_pem $4)
sed -e "s/\${ORG}/$1/g" \
-e "s/\${P0PORT}/$2/" \
-e "s/\${P1PORT}/$3/" \
-e "s#\${PEERPEM}#$PP#" \
peers-template.yaml | sed -e $'s/\\\\n/\\\n /g'
}
function certificate_template {
local CP=$(one_line_pem $3)
sed -e "s/\${ORG}/$1/g" \
-e "s/\${CAPORT}/$2/" \
-e "s#\${CAPEM}#$CP#" \
certificate-template.yaml | sed -e $'s/\\\\n/\\\n /g'
}
function yaml_ccp {
local PP=$(one_line_pem $5)
local CP=$(one_line_pem $6)
sed -e "s/\${ORG}/$1/g" \
-e "s/\${P0PORT}/$2/" \
-e "s/\${P1PORT}/$3/" \
-e "s/\${CAPORT}/$4/" \
-e "s#\${PEERPEM}#$PP#" \
-e "s#\${CAPEM}#$CP#" \
-e "s#\${ADMIN_PRIVATE_KEY}#$ADMIN_PRIVATE_KEY#" \
ccp-template.yaml | sed -e $'s/\\\\n/\\\n /g'
}
ADMIN_PRIVATE_KEY=$(cd crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore && ls *_sk)
ORG=1
P0PORT=7051
P1PORT=8051
CAPORT=7054
PEERPEM=crypto-config/peerOrganizations/org1.example.com/tlsca/tlsca.org1.example.com-cert.pem
CAPEM=crypto-config/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem
echo "$(json_ccp $ORG $P0PORT $P1PORT $CAPORT $PEERPEM $CAPEM $ADMIN_PRIVATE_KEY)" > connection-org1.json
echo "$(yaml_ccp $ORG $P0PORT $P1PORT $CAPORT $PEERPEM $CAPEM $ADMIN_PRIVATE_KEY)" > connection-org1.yaml
ORGANIZATIONS=`organizations_template $ORG $ADMIN_PRIVATE_KEY`
PEERS=`peers_template $ORG $P0PORT $P1PORT $PEERPEM`
CERTIFICATE_AUTH=`certificate_template $ORG $CAPORT $PEERPEM`
ADMIN_PRIVATE_KEY=$(cd crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore && ls *_sk)
ORG=2
P0PORT=9051
P1PORT=10051
CAPORT=8054
PEERPEM=crypto-config/peerOrganizations/org2.example.com/tlsca/tlsca.org2.example.com-cert.pem
CAPEM=crypto-config/peerOrganizations/org2.example.com/ca/ca.org2.example.com-cert.pem
echo "$(json_ccp $ORG $P0PORT $P1PORT $CAPORT $PEERPEM $CAPEM $ADMIN_PRIVATE_KEY)" > connection-org2.json
echo "$(yaml_ccp $ORG $P0PORT $P1PORT $CAPORT $PEERPEM $CAPEM $ADMIN_PRIVATE_KEY)" > connection-org2.yaml
ORGANIZATIONS+=`organizations_template $ORG $ADMIN_PRIVATE_KEY`
PEERS+=`peers_template $ORG $P0PORT $P1PORT $PEERPEM`
CERTIFICATE_AUTH+=`certificate_template $ORG $CAPORT $PEERPEM`
source=`cat ccp-template-allorg.yaml`
PARTS1="${ORGANIZATIONS}"
PARTS2="${PEERS}"
PARTS3="${CERTIFICATE_AUTH}"
res="${source/\$\{ORGANIZATIONS\}/$PARTS1}"
res="${res/\$\{PEERS\}/$PARTS2}"
echo "${res/\$\{CERTIFICATE_AUTH\}/$PARTS3}" > connection-org-all.yaml
connection-org-all.yamlが、最終的な接続プロファイルです。
最初のfabcar/statFabric.sh内で、ccp-generate.shが実行されていますが、編集したのでもう一度シェルを実行してください。
./ccp-generate.sh
ちなみに、同じファイルのjson版は作らなくても、
今回のREST API用のクライアントは動くようです。
出力されたconnection-org-all.yamlがREST APIサーバーのアプリから参照されるように、config.jsファイルを修正します。
pr util = require('util');
var path = require('path');
var hfc = require('fabric-client');
//var file = 'network-config%s.yaml';
var file = 'connection-org-all.yaml';
var env = process.env.TARGET_NETWORK;
//if (env)
// file = util.format(file, '-' + env);
//else
// file = util.format(file, '');
// indicate to the application where the setup file is located so it able
// to have the hfc load it to initalize the fabric client instance
//hfc.setConfigSetting('network-connection-profile-path',path.join(__dirname, 'artifacts' ,file));
hfc.setConfigSetting('network-connection-profile-path',path.join(__dirname, file));
//hfc.setConfigSetting('Org1-connection-profile-path',path.join(__dirname, 'artifacts', 'org1.yaml'));
hfc.setConfigSetting('Org1-connection-profile-path',path.join(__dirname, 'org1.yaml'));
//hfc.setConfigSetting('Org2-connection-profile-path',path.join(__dirname, 'artifacts', 'org2.yaml'));
// some other settings the application might need to know
hfc.addConfigFile(path.join(__dirname, 'config.json'));
かなりガッツリとコメントアウトします。
内容的にはconnection-org-all.yamlを見るようにしているだけです。
加えて、チェーンコードの参照パスも修正しましょう。次の2箇所です。
変更前 → "CC_SRC_PATH":"../articles",
変更後 → "CC_SRC_PATH":"../../chaincode",
変更前 → const projDir = path.join(goPath, 'src', chaincodePath);
変更後 → const projDir = path.join(goPath, chaincodePath);
実行
これでAPIサーバーが起動できる状態になりました。
コンソールを2つ使います。
片方でfirst-network/に移動して、試してみましょう。
PORT=4000 node app
[2019-10-27 23:16:35.871] [INFO] SampleWebApp - ****************** SERVER STARTED ************************
[2019-10-27 23:16:35.875] [INFO] SampleWebApp - *************** http://localhost:4000 ******************
上記のように表示されたら起動に成功しています。
もう片方のコンソールからcurlコマンドでリクエストを投げます。
curl -s -X POST http://localhost:4000/users -H "content-type: application/x-www-form-urlencoded" -d 'username=Jim&orgName=Org1'
{"success":true,"secret":"","message":"admin enrolled Successfully","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIyMjE5MDYsInVzZXJuYW1lIjoiYWRtaW4iLCJvcmdOYW1lIjoiT3JnMSIsImlhdCI6MTU3MjE4NTkwNn0.vUkqLz4eIverwM1DI8wENxJqIe3uRBiyAB21ZcIym9k"}
上記のように応答があれば成功です!
このAPIはユーザを組織に参加させ、証明書を発行します。
返却されるJSON Web Token(JWT)の値は控えておいてください。
以降のAPIはこのtokenを付帯してリクエストする必要があります。
例として、新車登録のチェーンコードを実行するリクエストに、authorizationヘッダでJWTを付帯させます。
curl -s -X POST \
http://localhost:4000/channels/mychannel/chaincodes/fabcar \
-H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIyMjIwNDEsInVzZXJuYW1lIjoiYWRtaW4yIiwib3JnTmFtZSI6Ik9yZzEiLCJpYXQiOjE1NzIxODYwNDF9.w0EvpfQ07RGqTanEgTQn1B6N4xRLs_Lpp6T87OqE2kU" \
-H "content-type: application/json" \
-d '{
"peers": ["peer0.org1.example.com", "peer0.org2.example.com"],
"fcn":"createCar",
"args":["CAR999", "Honda", "Accord", "Black", "Tom"]
}'
{"success":true,"message":"Successfully invoked the chaincode Org1 to the channel 'mychannel' for transaction ID: dac7d98a930697cbea61012fadffb3092199935930d41f4658492c77415bcf5c"}
また、fabcarのエンドーズメントポリシーがMSP1とMSP2になっているので
2つのエンドーズメントピア("peer0.org1.example.com", "peer0.org2.example.com")を指定してください。
結果は上記のような感じになります。
その他に、次のようなAPIが実行可能です。
- ユーザ作成/取得
- チャンネル作成
- チャンネル参加
- チェーンコードインストール
- チェーンコードインスタンス化
- チェーンコード実行(POST/GET)
- ブロック参照
- トランザクション参照
- チェーン情報取得
- イントール済みチェーンコード一覧取得
- インスタンス化済みチェーンコード一覧取得
- チャンネル一覧取得
詳しくはこちらをご確認ください。
https://github.com/hyperledger/fabric-samples/tree/release-1.4/balance-transfer
ブロックを眺める
可視性は良くありませんが、上記のAPIでブロックの中身を見ることが可能です。
curl -s -X GET "http://localhost:4000/channels/mychannel?peer=peer0.org1.example.com" -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIyMjIwNDEsInVzZXJuYW1lIjoiYWRtaW4yIiwib3JnTmFtZSI6Ik9yZzEiLCJpYXQiOjE1NzIxODYwNDF9.w0EvpfQ07RGqTanEgTQn1B6N4xRLs_Lpp6T87OqE2kU" -H "content-type: application/json" | grep --color=auto "low"
...
{"height":{"low":5,"high":0,"unsigned":true}
...
curl -s -X GET \
"http://localhost:4000/channels/mychannel/blocks/4?peer=peer0.org1.example.com" \
-H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIyMjIwNDEsInVzZXJuYW1lIjoiYWRtaW4yIiwib3JnTmFtZSI6Ik9yZzEiLCJpYXQiOjE1NzIxODYwNDF9.w0EvpfQ07RGqTanEgTQn1B6N4xRLs_Lpp6T87OqE2kU" \
-H "content-type: application/json" | grep --color=auto "tx_id"
...
{"channel_header":{"type":3,"version":1,"timestamp":"2019-10-27T15:22:53.967Z","channel_id":"mychannel","tx_id":"bc895feb278702eb7e413aaabf3e583e8129ea620e175daad9e21f9e4bceed4f","epoch":"0","extension":{"type":"Buffer","data":[18,8,18,6,102,97,98,99,97,114]},
...
上記によりtx_id(トランザクションID)を控えるか、さきほどのチェーンコード実行のリクエストから返却されたトランザクションIDをパラメータに指定して、ブロック内のトランザクションを問い合わせます。
curl -s -X GET http://localhost:4000/channels/mychannel/transactions/bc895feb278702eb7e413aaabf3e583e8129ea620e175daad9e21f9e4bceed4f?peer=peer0.org1.example.com \
-H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIyMjIwNDEsInVzZXJuYW1lIjoiYWRtaW4yIiwib3JnTmFtZSI6Ik9yZzEiLCJpYXQiOjE1NzIxODYwNDF9.w0EvpfQ07RGqTanEgTQn1B6N4xRLs_Lpp6T87OqE2kU" \
-H "content-type: application/json" | grep --color=auto "writes"
...
"writes":[{"key":"CAR999","is_delete":false,"value":"{\"color\":\"Black\",\"docType\":\"car\",\"make\":\"Honda\",\"model\":\"Accord\",\"owner\":\"Tom\"}"}],
...
これでfabric_samplesのカスタマイズだけでいい感じのシステムが作れそうですね
ちなみにこれまで言及のなかった、org1.yaml, org2.yamlのファイルですが、
この中で、ユーザの秘密鍵・公開鍵・証明書が出力される場所を指定しています。
path: "./fabric-client-kv-org1"
path: "/tmp/fabric-client-kv-org1"
上記デフォルトの設定だと、ユーザ登録のAPIを実行したとき、証明書はfirst-network/fabric-client-kv-org1に、
公開鍵・秘密鍵は/tmp/fabric-client-kv-org1に出力されます。
これを適宜移すことで、既存のクライアントSDKアプリとの連携が可能になります。
たとえばfabcar/javascript-low-level/下のアプリなら、
path: "../fabcar/javascript-low-level/hfc-key-store/"
path: "../fabcar/javascript-low-level/hfc-key-store/"
とすれば連携できます。
fabcar/javascript/などの場合は、証明書・秘密鍵・公開鍵の管理がwalletであるため、
ディレクトリ構造が少し違うので、改造が必要になると思います。
今回説明させて頂いたコードをこちらに置きましたので、参考になれば幸いです。
また、記載した手順をやり直す場合は、ネットワークの初期化を行ってください。
cd fabric-samples/first-network
docker rm -f $(docker ps -aq)
docker rmi -f $(docker images | grep dev | awk '{print $3}')
rm -rf fabric-client-kv-org[1-2]
rm -rf /tmp/fabric-client-kv-org[1-2]
rm -rf channel-artifacts/*
rm -rf crypto-config/*
以上です。おつかれさまでした。