LoginSignup
0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-24

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/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
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の初期化が走るのも嫌なので。。
UpdateAssetResetAssetが似ているようで機能を分けています。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の取引履歴を検索・出力します
asset.go
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つの文字列配列から引数ごとに分離しました。

参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0