Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 2889

Hyperledger FabricのKeyゾンビ化を防ぐ(全ソースコード掲載)

$
0
0

Hyperledger FabricでState DBのKeyを再利用させない(実はできない)

Hyperledger Fabric(以下HF)で一度削除(DelState)したKeyで再登録(PutState)したらどうなるんだろう?という疑問から、次のようなシナリオを考えました。またミラ・キータ(Mira Qiita)に登場してもらいます。

  1. Mira QiitaをKey="JMYMIRAGINO200302"で登録(PutState)
  2. 1のKeyでクエリ(GetState)
  3. 1のKeyで削除(DelState)
  4. 1のKeyでクエリ(GetState)
  5. 1のKeyで登録(PutState)
  6. 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.shstartFabric.shをコピーしてください。その他の環境については以前の投稿に書いてあります。
- startFabric.sh: docker-composeを使って必要なサービスを起動してくれます
- networkDown.sh: 全てを無にしてくれます

シェルスクリプトについて

invoke系の機能とquery系の機能で呼び出し方が違います。CreateAsset.shCreateAsset.shがあれば、他の機能を呼び出すシェルもコピペで作れます。function名と引数の違いだけです。

  • invoke系代表CreateAsset.sh
CreateAsset.sh
#!/bin/bashpushd ../test-network > /dev/null

export PATH=${PWD}/../bin:$PATHexport 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}']}'popd> /dev/null
  • query系代表CreateAsset.sh
CreateAsset.sh
#!/bin/bashpushd ../test-network > /dev/null

export PATH=${PWD}/../bin:$PATHexport 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}']}'popd> /dev/null

肝は必ずカレントディレクトリをtest-networkにすることです。pushdで移動してpopdで戻っています。

goソースコードについて

もともとはHF1.4の時に書いたコードでHF2.1.1にあわせて書き直しました。祖先はHF1.4のfabcar.goです。以下に掲載します。長いです。。
DBの初期化はInitLedgerからInitDBに移動しています。chaincodeのバージョンアップで毎回DBの初期化が走るのも嫌なので。。
UpdateAssetResetAssetが似ているようで機能を分けています。UpdateAssetには資産価値が上がるような変更にガードを入れています。ResetAssetは「工場でバッテリーを新品に交換する」みたいなシナリオを想定したものです。

コード中にコメントがなくてスミマセン:-(
個々でやってることは大したことないです。基本的には受け取った引数をHFのAPIへスルーパスしてるだけです。

asset.go
packagemainimport("encoding/json""fmt""strconv""time""github.com/hyperledger/fabric-contract-api-go/contractapi""database/sql"_"github.com/lib/pq")// Define the Smart Contract structuretypeSmartContractstruct{contractapi.Contract}// データ構造の定義typeAssetstruct{Yearstring`json:"year"`// 初度登録年Monthstring`json:"month"`// 初度登録月Mileageint`json:"mileage"`// 走行距離(km)Batteryint`json:"battery"`// バッテリーライフ(%)Locationstring`jasn:"location"`// 位置}typeAssetWithOwnerstruct{Namestring// 名前Countrystring// 国Citystring// 都道府県Addrstring// 市区町村Record*Asset}// クエリ結果(レーコード検索)typeQueryResultstruct{Keystring// レコードID(VINコード)Record*Asset}// クエリ結果(履歴検索)typeGetHisResultstruct{TxIdstring// トランザクションIDTimestampstring// タイムスタンプIsDeletebool// 削除(廃車)フラグRecord*Asset}func(s*SmartContract)InitLedger(ctxcontractapi.TransactionContextInterface)error{fmt.Println("InitLedger")returnnil}func(s*SmartContract)InitDB(ctxcontractapi.TransactionContextInterface)error{fmt.Println("InitDB")db,err:=sql.Open("postgres","host=pgsql port=5432 user=postgres password=secret dbname=asset sslmode=disable")deferdb.Close()iferr!=nil{returnfmt.Errorf("sql.Open: %s",err.Error())}rows,err:=db.Query("SELECT * FROM asset;")iferr!=nil{returnfmt.Errorf("sql.Query: %s",err.Error())}varidstringforrows.Next(){varassetAssetrows.Scan(&id,&asset.Year,&asset.Month,&asset.Mileage,&asset.Battery,&asset.Location)assetAsBytes,_:=json.Marshal(asset)ctx.GetStub().PutState(id,assetAsBytes)}returnnil}func(s*SmartContract)QueryAsset(ctxcontractapi.TransactionContextInterface,keystring)(*Asset,error){fmt.Println("QueryAsset")assetAsBytes,err:=ctx.GetStub().GetState(key)iferr!=nil{returnnil,fmt.Errorf("Failed to read from world state. %s",err.Error())}ifassetAsBytes==nil{returnnil,fmt.Errorf("%s does not exist",key)}asset:=new(Asset)_=json.Unmarshal(assetAsBytes,asset)returnasset,nil}func(s*SmartContract)QueryAssetWithOwner(ctxcontractapi.TransactionContextInterface,keystring)(*AssetWithOwner,error){fmt.Println("QueryAssetWithOwner")assetAsBytes,err:=ctx.GetStub().GetState(key)iferr!=nil{returnnil,fmt.Errorf("Failed to read from world state. %s",err.Error())}ifassetAsBytes==nil{returnnil,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")deferdb.Close()iferr!=nil{returnnil,fmt.Errorf("sql.Open: %s",err.Error())}sql:="SELECT * FROM owner WHERE id = '"+key+"';"rows,err:=db.Query(sql)iferr!=nil{returnnil,fmt.Errorf("db.Query: %s",err.Error())}varidstringawo:=new(AssetWithOwner)forrows.Next(){rows.Scan(&id,&awo.Name,&awo.Country,&awo.City,&awo.Addr)awo.Record=asset}returnawo,nil}func(s*SmartContract)CreateAsset(ctxcontractapi.TransactionContextInterface,keystring,yearstring,monthstring,sMileagestring,sBatterystring,locationstring)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)returnctx.GetStub().PutState(key,assetAsBytes)}func(s*SmartContract)UpdateAsset(ctxcontractapi.TransactionContextInterface,keystring,sMileagestring,sBatterystring,locationstring)error{fmt.Println("UpdateAsset")assetAsBytes,_:=ctx.GetStub().GetState(key)asset:=new(Asset)json.Unmarshal(assetAsBytes,&asset)ifsMileage!=""{mileage,_:=strconv.Atoi(sMileage)// input mileage (km)ifmileage<asset.Mileage{returnfmt.Errorf("Invalid argument.")}asset.Mileage=mileage}ifsBattery!=""{battery,_:=strconv.Atoi(sBattery)// input battery (%)ifbattery>(asset.Battery+5){returnfmt.Errorf("Invalid argument.")}asset.Battery=battery}iflocation!=""{asset.Location=location}assetAsBytes,_=json.Marshal(asset)returnctx.GetStub().PutState(key,assetAsBytes)}func(s*SmartContract)ResetAsset(ctxcontractapi.TransactionContextInterface,keystring,sMileagestring,sBatterystring,locationstring)error{fmt.Println("ResetAsset")assetAsBytes,_:=ctx.GetStub().GetState(key)asset:=new(Asset)json.Unmarshal(assetAsBytes,&asset)ifsMileage!=""{mileage,_:=strconv.Atoi(sMileage)// input mileage (km)asset.Mileage=mileage}ifsBattery!=""{battery,_:=strconv.Atoi(sBattery)// input battery (%)asset.Battery=battery}iflocation!=""{asset.Location=location}assetAsBytes,_=json.Marshal(asset)returnctx.GetStub().PutState(key,assetAsBytes)}func(s*SmartContract)QueryAllAssets(ctxcontractapi.TransactionContextInterface)([]QueryResult,error){fmt.Println("QueryAllAssets")startKey:=""endKey:=""resultsIterator,err:=ctx.GetStub().GetStateByRange(startKey,endKey)iferr!=nil{returnnil,fmt.Errorf("ctx.GetStub().GetStateByRange: %s",err.Error())}deferresultsIterator.Close()results:=[]QueryResult{}forresultsIterator.HasNext(){queryResponse,err:=resultsIterator.Next()iferr!=nil{returnnil,err}asset:=new(Asset)_=json.Unmarshal(queryResponse.Value,asset)queryResult:=QueryResult{Key:queryResponse.Key,Record:asset}results=append(results,queryResult)}returnresults,nil}func(s*SmartContract)QueryRangeAssets(ctxcontractapi.TransactionContextInterface,sPageSizestring,bookmarkstring)([]QueryResult,error){fmt.Println("QueryRangeAssets")startKey:=""endKey:=""varpageSizeint32tmpSize,_:=strconv.Atoi(sPageSize)pageSize=int32(tmpSize)resultsIterator,_,err:=ctx.GetStub().GetStateByRangeWithPagination(startKey,endKey,pageSize,bookmark)iferr!=nil{returnnil,fmt.Errorf("ctx.GetStub().GetStateByRangeWithPagination: %s",err.Error())}deferresultsIterator.Close()results:=[]QueryResult{}forresultsIterator.HasNext(){queryResponse,err:=resultsIterator.Next()iferr!=nil{returnnil,err}asset:=new(Asset)_=json.Unmarshal(queryResponse.Value,asset)queryResult:=QueryResult{Key:queryResponse.Key,Record:asset}results=append(results,queryResult)}returnresults,nil}func(s*SmartContract)DeleteAsset(ctxcontractapi.TransactionContextInterface,keystring)error{fmt.Println("DeleteAsset")returnctx.GetStub().DelState(key)}func(s*SmartContract)GetHistoryOfAsset(ctxcontractapi.TransactionContextInterface,keystring)([]GetHisResult,error){fmt.Println("GetHistoryOfAsset")resultsIterator,err:=ctx.GetStub().GetHistoryForKey(key)iferr!=nil{returnnil,fmt.Errorf("ctx.GetStub().GetHistoryForKey: %s",err.Error())}deferresultsIterator.Close()results:=[]GetHisResult{}forresultsIterator.HasNext(){queryResponse,err:=resultsIterator.Next()iferr!=nil{returnnil,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)}returnresults,nil}funcmain(){chaincode,err:=contractapi.NewChaincode(new(SmartContract))iferr!=nil{fmt.Printf("Error create asset chaincode: %s",err.Error())return}iferr:=chaincode.Start();err!=nil{fmt.Printf("Error starting asset chaincode: %s",err.Error())}}

最後に

HFが1.4から2.1.1になってコードがシンプルに書きやすくなりました。1.4までは文字列バッファに自分でゴリゴリとデータを詰めていたのですが、その辺りがスマートになっています。引数も1つの文字列配列から引数ごとに分離しました。

参考になれば幸いです。


Viewing all articles
Browse latest Browse all 2889

Trending Articles