Hyperledger FabricでState DBのKeyを再利用させない(実はできない)
Hyperledger Fabric(以下HF)で一度削除(DelState
)したKeyで再登録(PutState
)したらどうなるんだろう?という疑問から、次のようなシナリオを考えました。またミラ・キータ(Mira Qiita)に登場してもらいます。
- Mira QiitaをKey="JMYMIRAGINO200302"で登録(
PutState
) - 1のKeyでクエリ(
GetState
) - 1のKeyで削除(
DelState
) - 1のKeyでクエリ(
GetState
) - 1のKeyで登録(
PutState
) - 1のKeyで履歴を参照(
GetHistoryForKey
)
どうなるんだろう?
あと、この投稿にてGoで書いたソースコードやシェルスクリプトを掲載します。
環境について
動作環境については次の通りです。
- Ubuntu 18.04.4 LTS
- docker-compose 1.26.0
- docker 19.03.11
- HF 2.1.1
- go 1.14.4
- PostgreSQL(Dockerイメージ) 12.3(latest)
実際にやってみる
# ./CreateAsset.sh <----- 初期登録
2020-06-24 17:50:15.434 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
# ./QueryAsset.sh <----- 一度クエリ
{"year":"2003","month":"02","mileage":43871,"battery":100,"Location":"QIITA東京販売"}
# ./DeleteAsset.sh <----- 削除
2020-06-24 17:53:11.100 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
# ./QueryAsset.sh <----- 削除後のクエリ
Error: endorsement failure during query. response: status:500 message:"JMYMIRAGINO200302 does not exist"
# ./CreateAsset.sh <----- 同じKeyで登録
2020-06-24 17:53:26.301 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
# ./QueryAsset.sh <----- またクエリ
{"year":"2003","month":"02","mileage":43871,"battery":100,"Location":"QIITA東京販売"}
#
Oh...
削除後のクエリではdoes not exist
が返ってきますが、削除したKeyで再登録ができてクエリもできちゃいました。履歴はどうなっているのでしょう?
見てみましょう!
# ./GetHistoryOfAsset.sh | jq
[
{
"TxId": "fed9df47d31bb4c6cec4be6c9bf154f8655f13d994eebfd709e564e36e011945",
"Timestamp": "2020-06-24 08:53:26 +0000 UTC",
"IsDelete": false,
"Record": {
"year": "2003",
"month": "02",
"mileage": 43871,
"battery": 100,
"Location": "QIITA東京販売"
}
},
{
"TxId": "7d2853ede65564e082ab81c56e7a12331493c841993caea4ad88261706f6da9c",
"Timestamp": "2020-06-24 08:53:11 +0000 UTC",
"IsDelete": true,
"Record": {
"year": "",
"month": "",
"mileage": 0,
"battery": 0,
"Location": ""
}
},
{
"TxId": "854ce2f67ecb11aba6ae103841da9cbf9ddb17ed412df661a8d76abc4b39a35c",
"Timestamp": "2020-06-24 08:50:15 +0000 UTC",
"IsDelete": false,
"Record": {
"year": "2003",
"month": "02",
"mileage": 43871,
"battery": 100,
"Location": "QIITA東京販売"
}
}
]
#
Oh...
履歴は下から読んでください。
廃車(DelState
)前からの履歴が残って出力されています。
Keyやidの桁数が足りなくて、削除後に一定期間が過ぎると再利用というのはモノによってはあり(携帯電話の番号がそうでしたね)ですが、ブロックチェーンでは思想的になしだと思います。
対策を探してみる(なかった…)
残念ながらHFのAPIで解決できるものではなさそうです。調べた中でベストプラクティスはDelState
を使わないです。
同じ疑問を持った人がstackoverflow
で質問していて、その回答が意外なものでした。
つまり、レコードを削除しないで「削除したフラグ」をレコードに持つ、ということですね。ちょっとだけ目からウロコ。HFがバージョンアップしたらできるようになるかもです。
もしかしたら調査不足かもしれません。偉い人教えて下さい:-)
ここまでのソースコード
先ずはHFのサンプルコードをインストールします。こちらにインストール方法が書いてあります。現時点でv2.1.1は最新版なのでcurl -sSL https://bit.ly/2ysbOFE | bash -s
でインストールできます。
ディレクトリfabric-samples/
に任意な名前(今回はasset
で作っています)のディレクトリを作って、そこを作業場所にします。そこへfabcar/
からnetworkDown.sh
とstartFabric.sh
をコピーしてください。その他の環境については以前の投稿に書いてあります。
- startFabric.sh: docker-composeを使って必要なサービスを起動してくれます
- networkDown.sh: 全てを無にしてくれます
シェルスクリプトについて
invoke
系の機能とquery
系の機能で呼び出し方が違います。CreateAsset.sh
とCreateAsset.sh
があれば、他の機能を呼び出すシェルもコピペで作れます。function名と引数の違いだけです。
-
invoke
系代表CreateAsset.sh
#!/bin/bash
cd ../test-network
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
key='"JMYMIRAGINO200302"'
year='"2003"'
month='"02"'
mileage='"43871"'
battery='"100"'
location='"QIITA東京販売"'
args=${key},${year},${month},${mileage},${battery},${location}
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n asset --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAsset","Args":['${args}']}'
-
query
系代表QueryAsset.sh
#!/bin/bash
cd ../test-network
export PATH=${PWD}/../bin:$PATH
export FABRIC_CFG_PATH=$PWD/../config
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
export CHANNEL_NAME=mychannel
key='"JMYMIRAGINO20030Z"'
peer chaincode query -C $CHANNEL_NAME -n asset -c '{"Args":["QueryAsset",'${key}']}'
肝は必ずカレントディレクトリをtest-network/
にすることです。
goソースコードについて
もともとはHF1.4の時に書いたコードでHF2.1.1にあわせて書き直しました。祖先はHF1.4のfabcar.go
です。以下に掲載します。長いです。。
DBの初期化はInitLedger
からInitDB
に移動しています。chaincodeのバージョンアップで毎回DBの初期化が走るのも嫌なので。。
UpdateAsset
とResetAsset
が似ているようで機能を分けています。UpdateAsset
には資産価値が上がるような変更にガードを入れています。ResetAsset
は「工場でバッテリーを新品に交換する」みたいなシナリオを想定したものです。
コード中にコメントがなくてスミマセン:-(
個々でやってることは大したことないです。基本的には受け取った引数をHFのAPIへスルーパスしてるだけです。
メソッド一覧です。引数は全て文字列で渡します。
メソッド | 引数 | 機能 |
---|---|---|
InitLedger | なし | HFの初期化 |
InitDB | なし | State DBの初期登録。初期データはPostgre SQLから読み込んでいます |
QueryAsset | key | keyからState DBの内容を検索・出力します |
QueryAssetWithOwner | key | QueryAssetの出力に加え、Postgre SQLからの個人情報も検索・出力します |
CreateAsset | key, year, month, mileage, battery, location | 新しいassetを作成・登録します |
UpdateAsset | key, mileage, battery, location | assetの内容を更新します。値によっては更新できません |
ResetAsset | key, mileage, battery, location | assetの内容を入力の値で強制的に更新します |
QueryAllAssets | なし | State DB内の全レコードを検索します |
QueryRangeAssets | pagesize, key | keyから始まるレコードをpagesize数分検索・出力します |
DeleteAsset | key | keyのレコードを削除します。履歴は残ります |
GetHistoryOfAsset | key | keyの取引履歴を検索・出力します |
package main
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"database/sql"
_ "github.com/lib/pq"
)
// Define the Smart Contract structure
type SmartContract struct {
contractapi.Contract
}
// データ構造の定義
type Asset struct {
Year string `json:"year"` // 初度登録年
Month string `json:"month"` // 初度登録月
Mileage int `json:"mileage"` // 走行距離(km)
Battery int `json:"battery"` // バッテリーライフ(%)
Location string `jasn:"location"` // 位置
}
type AssetWithOwner struct {
Name string // 名前
Country string // 国
City string // 都道府県
Addr string // 市区町村
Record *Asset
}
// クエリ結果(レーコード検索)
type QueryResult struct {
Key string // レコードID(VINコード)
Record *Asset
}
// クエリ結果(履歴検索)
type GetHisResult struct {
TxId string // トランザクションID
Timestamp string // タイムスタンプ
IsDelete bool // 削除(廃車)フラグ
Record *Asset
}
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
fmt.Println("InitLedger")
return nil
}
func (s *SmartContract) InitDB(ctx contractapi.TransactionContextInterface) error {
fmt.Println("InitDB")
db, err := sql.Open("postgres", "host=pgsql port=5432 user=postgres password=secret dbname=asset sslmode=disable")
defer db.Close()
if err != nil {
return fmt.Errorf("sql.Open: %s", err.Error())
}
rows, err := db.Query("SELECT * FROM asset;")
if err != nil {
return fmt.Errorf("sql.Query: %s", err.Error())
}
var id string
for rows.Next() {
var asset Asset
rows.Scan(&id, &asset.Year, &asset.Month, &asset.Mileage, &asset.Battery, &asset.Location)
assetAsBytes, _ := json.Marshal(asset)
ctx.GetStub().PutState(id, assetAsBytes)
}
return nil
}
func (s *SmartContract) QueryAsset(ctx contractapi.TransactionContextInterface, key string) (*Asset, error) {
fmt.Println("QueryAsset")
assetAsBytes, err := ctx.GetStub().GetState(key)
if err != nil {
return nil, fmt.Errorf("Failed to read from world state. %s", err.Error())
}
if assetAsBytes == nil {
return nil, fmt.Errorf("%s does not exist", key)
}
asset := new(Asset)
_ = json.Unmarshal(assetAsBytes, asset)
return asset, nil
}
func (s *SmartContract) QueryAssetWithOwner(ctx contractapi.TransactionContextInterface, key string) (*AssetWithOwner, error) {
fmt.Println("QueryAssetWithOwner")
assetAsBytes, err := ctx.GetStub().GetState(key)
if err != nil {
return nil, fmt.Errorf("Failed to read from world state. %s", err.Error())
}
if assetAsBytes == nil {
return nil, fmt.Errorf("%s does not exist", key)
}
asset := new(Asset)
_ = json.Unmarshal(assetAsBytes, asset)
db, err := sql.Open("postgres", "host=pgsql port=5432 user=postgres password=secret dbname=asset sslmode=disable")
defer db.Close()
if err != nil {
return nil, fmt.Errorf("sql.Open: %s", err.Error())
}
sql := "SELECT * FROM owner WHERE id = '" + key + "';"
rows, err := db.Query(sql)
if err != nil {
return nil, fmt.Errorf("db.Query: %s", err.Error())
}
var id string
awo := new(AssetWithOwner)
for rows.Next() {
rows.Scan(&id, &awo.Name, &awo.Country, &awo.City, &awo.Addr)
awo.Record = asset
}
return awo, nil
}
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, key string, year string, month string, sMileage string, sBattery string, location string) error {
fmt.Println("CreateAsset")
mileage, _ :=strconv.Atoi(sMileage)
battery, _ :=strconv.Atoi(sBattery)
asset := Asset{
Year: year,
Month: month,
Mileage: mileage,
Battery: battery,
Location: location,
}
assetAsBytes, _ := json.Marshal(asset)
return ctx.GetStub().PutState(key, assetAsBytes)
}
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, key string, sMileage string, sBattery string, location string) error {
fmt.Println("UpdateAsset")
assetAsBytes, _ := ctx.GetStub().GetState(key)
asset := new(Asset)
json.Unmarshal(assetAsBytes, &asset)
if sMileage != "" {
mileage, _ := strconv.Atoi(sMileage) // input mileage (km)
if mileage < asset.Mileage {
return fmt.Errorf("Invalid argument.")
}
asset.Mileage = mileage
}
if sBattery != "" {
battery, _ := strconv.Atoi(sBattery) // input battery (%)
if battery > (asset.Battery + 5) {
return fmt.Errorf("Invalid argument.")
}
asset.Battery = battery
}
if location != "" {
asset.Location = location
}
assetAsBytes, _ = json.Marshal(asset)
return ctx.GetStub().PutState(key, assetAsBytes)
}
func (s *SmartContract) ResetAsset(ctx contractapi.TransactionContextInterface, key string, sMileage string, sBattery string, location string) error {
fmt.Println("ResetAsset")
assetAsBytes, _ := ctx.GetStub().GetState(key)
asset := new(Asset)
json.Unmarshal(assetAsBytes, &asset)
if sMileage != "" {
mileage, _ := strconv.Atoi(sMileage) // input mileage (km)
asset.Mileage = mileage
}
if sBattery != "" {
battery, _ := strconv.Atoi(sBattery) // input battery (%)
asset.Battery = battery
}
if location != "" {
asset.Location = location
}
assetAsBytes, _ = json.Marshal(asset)
return ctx.GetStub().PutState(key, assetAsBytes)
}
func (s *SmartContract) QueryAllAssets(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) {
fmt.Println("QueryAllAssets")
startKey := ""
endKey := ""
resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)
if err != nil {
return nil, fmt.Errorf("ctx.GetStub().GetStateByRange: %s", err.Error())
}
defer resultsIterator.Close()
results := []QueryResult{}
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
asset := new(Asset)
_ = json.Unmarshal(queryResponse.Value, asset)
queryResult := QueryResult{Key: queryResponse.Key, Record: asset}
results = append(results, queryResult)
}
return results, nil
}
func (s *SmartContract) QueryRangeAssets(ctx contractapi.TransactionContextInterface, sPageSize string, bookmark string) ([]QueryResult, error) {
fmt.Println("QueryRangeAssets")
startKey := ""
endKey := ""
var pageSize int32
tmpSize, _ := strconv.Atoi(sPageSize)
pageSize = int32(tmpSize)
resultsIterator, _, err := ctx.GetStub().GetStateByRangeWithPagination(startKey, endKey, pageSize, bookmark)
if err != nil {
return nil, fmt.Errorf("ctx.GetStub().GetStateByRangeWithPagination: %s", err.Error())
}
defer resultsIterator.Close()
results := []QueryResult{}
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
asset := new(Asset)
_ = json.Unmarshal(queryResponse.Value, asset)
queryResult := QueryResult{Key: queryResponse.Key, Record: asset}
results = append(results, queryResult)
}
return results, nil
}
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, key string) error {
fmt.Println("DeleteAsset")
return ctx.GetStub().DelState(key)
}
func (s *SmartContract) GetHistoryOfAsset(ctx contractapi.TransactionContextInterface, key string) ([]GetHisResult, error) {
fmt.Println("GetHistoryOfAsset")
resultsIterator, err := ctx.GetStub().GetHistoryForKey(key)
if err != nil {
return nil, fmt.Errorf("ctx.GetStub().GetHistoryForKey: %s", err.Error())
}
defer resultsIterator.Close()
results := []GetHisResult{}
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
asset := new(Asset)
_ = json.Unmarshal(queryResponse.Value, asset)
t := time.Unix(queryResponse.Timestamp.Seconds, 0).String()
//t := time.Unix(queryResponse.Timestamp.Seconds, int64(queryResponse.Timestamp.Nanos)).String()
queryResult := GetHisResult{TxId:queryResponse.TxId, Timestamp:t, IsDelete:queryResponse.IsDelete, Record: asset}
results = append(results, queryResult)
}
return results, nil
}
func main() {
chaincode, err := contractapi.NewChaincode(new(SmartContract))
if err != nil {
fmt.Printf("Error create asset chaincode: %s", err.Error())
return
}
if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting asset chaincode: %s", err.Error())
}
}
最後に
HFが1.4から2.1.1になってコードがシンプルに書きやすくなりました。1.4までは文字列バッファに自分でゴリゴリとデータを詰めていたのですが、その辺りがスマートになっています。引数も1つの文字列配列から引数ごとに分離しました。
参考になれば幸いです。