5
4

More than 3 years have passed since last update.

【Hyperledger Fabric】GetHistoryForKeyを使って、StateDBのKeyに紐づくHistoryをGetしよう!!

Last updated at Posted at 2019-11-05

2019/12/17(火)追記ここから

2019/12/12(木)にHyperledger Fabric v2.0 β が公開されました。
FabTokenの延期等ありますが、この記事で紹介している「GetHistoryForKey」の取得結果が最新から順序付するようになったそうです。
※今現在だと、順序に関する保証がないらしい・・・
※「FAB-16303: GetHistoryForKey returns results from newest to oldest」

その他、コンセンサスのSoloやKafkaは非推奨となりRAFTが推奨されるなど、色々と変更がありそうです。
詳しくはリリースノートをご参照ください

▼ リリースノート
https://github.com/hyperledger/fabric/releases/tag/v2.0.0-beta

2019/12/17(火)追記ここまで

はじめに

みなさま、お久しぶりです。
どーも、のぶこふです。

プロキシという足枷に行く手を阻まれながらも、
どーにかこーにか、生きながらえております。

今回は、Hyperledger Fabricで履歴(History)を取得するやり方です。

諸事情により、Golangでの記述となりますが、Node.jsやJavaでも同様の方法で取得できるかと思います(未確認)。

なお、環境構築やその他チェーンコード箇所等については記載しませんのであしからず。

環境

  • Windows10 Behind Proxy(ホストOS)
  • VirtualBox
  • CentOS7(ゲストOS)
  • Hyperledger Fabric(HLF)1.4.3
  • Golang

登場人物

Fabric-ShimGetHistoryForKey(key string)
▼GoDoc
https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStub.GetHistoryForKey

コード(全量)

公式サンプルのFabcarを修正していきます。
全量は下記になります
※「コメント」「initLedger」「queryAllCars」「changeCarOwner」は削除し、「getHistoryForKey」を追加しています。
shim.Successで返すため、「queryAllCars」と同じ様にbufferに詰め込んで、byteに変換しています。

fabcar.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "strconv"
    "time"

    "github.com/hyperledger/fabric/core/chaincode/shim"
    sc "github.com/hyperledger/fabric/protos/peer"
)

type SmartContract struct {
}

type Car struct {
    Make   string `json:"make"`
    Model  string `json:"model"`
    Colour string `json:"colour"`
    Owner  string `json:"owner"`
}

func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
    return shim.Success(nil)
}

func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
    function, args := APIstub.GetFunctionAndParameters()

    if function == "queryCar" {
        return s.queryCar(APIstub, args)
    } else if function == "createCar" {
        return s.createCar(APIstub, args)
    } else if function == "getHistoryForKey" {
        return s.getHistoryForKey(APIstub, args)
    } 

    return shim.Error("Invalid Smart Contract function name.")
}

func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

    if len(args) != 1 {
        return shim.Error("Incorrect number of arguments. Expecting 1")
    }

    carAsBytes, _ := APIstub.GetState(args[0])
    return shim.Success(carAsBytes)
}

func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

    if len(args) != 5 {
        return shim.Error("Incorrect number of arguments. Expecting 5")
    }

    var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}

    carAsBytes, _ := json.Marshal(car)
    APIstub.PutState(args[0], carAsBytes)

    return shim.Success(nil)
}

func (s *SmartContract) getHistoryForKey(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
        if len(args) != 1 {
                return shim.Error("Incorrect number of arguments. Expecting 1")
        }

        var key = args[0]

        historyIterator, _ := APIstub.GetHistoryForKey(key)
        defer historyIterator.Close()

        var buffer bytes.Buffer
        buffer.WriteString("[")

        bArrayMemberAlreadyWritten := false
        for historyIterator.HasNext() {
                queryResponse, err := historyIterator.Next()
                if err != nil {
                        return shim.Error(err.Error())
                }
                if bArrayMemberAlreadyWritten == true {
                        buffer.WriteString(",")
                }

                t := time.Unix(queryResponse.Timestamp.Seconds, 0)
                const layout = "Monday Jan 02 15:04:05 JST 2006"

                buffer.WriteString("{\"TxID\":")
                buffer.WriteString("\"")
                buffer.WriteString(queryResponse.TxId)
                buffer.WriteString("\"")

                buffer.WriteString(", \"Record\":")
                buffer.WriteString("\"")
                buffer.WriteString(string(queryResponse.Value))
                buffer.WriteString("\"")

                buffer.WriteString(", \"Timestamp\":")
                buffer.WriteString("\"")
                buffer.WriteString(t.Format(layout))
                buffer.WriteString("\"")

                buffer.WriteString(", \"IsDelete\":")
                buffer.WriteString("\"")
                buffer.WriteString(strconv.FormatBool(queryResponse.IsDelete))
                buffer.WriteString("\"")

                buffer.WriteString("}")
                bArrayMemberAlreadyWritten = true
        }
        buffer.WriteString("]")

        return shim.Success(buffer.Bytes())
}


func main() {
    err := shim.Start(new(SmartContract))
    if err != nil {
        fmt.Printf("Error creating new Smart Contract: %s", err)
    }
}

コード(解説)

追加した箇所について解説していきます。

  • 関数の定義と、引数のサイズチェックです
    • Keyを動的に変更させたかったので、引数から取得しています
fabcar.go
func (s *SmartContract) getHistoryForKey(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
        if len(args) != 1 {
                return shim.Error("Incorrect number of arguments. Expecting 1")
        }

        var key = args[0]
  • ShimからGetHistoryForKeyを呼び出しています。
  • deferを使用して、忘れる前にClose処理も入れておきましょう。
fabcar.go
        historyIterator, _ := APIstub.GetHistoryForKey(key)
        defer historyIterator.Close()
  • 取得した内容をbufferに詰め込んでいきます。
    • 行っている内容は「queryAllCars」と同じで、詰めている内容をGetHistoryForKeyに合わせているだけです。
fabcar.go
        var buffer bytes.Buffer
        buffer.WriteString("[")

        bArrayMemberAlreadyWritten := false
        for historyIterator.HasNext() {
                queryResponse, err := historyIterator.Next()
                if err != nil {
                        return shim.Error(err.Error())
                }
                if bArrayMemberAlreadyWritten == true {
                        buffer.WriteString(",")
                }

                t := time.Unix(queryResponse.Timestamp.Seconds, 0)
                const layout = "Monday Jan 02 15:04:05 JST 2006"

                buffer.WriteString("{\"TxID\":")
                buffer.WriteString("\"")
                buffer.WriteString(queryResponse.TxId)
                buffer.WriteString("\"")

                buffer.WriteString(", \"Record\":")
                buffer.WriteString("\"")
                buffer.WriteString(string(queryResponse.Value))
                buffer.WriteString("\"")

                buffer.WriteString(", \"Timestamp\":")
                buffer.WriteString("\"")
                buffer.WriteString(t.Format(layout))
                buffer.WriteString("\"")

                buffer.WriteString(", \"IsDelete\":")
                buffer.WriteString("\"")
                buffer.WriteString(strconv.FormatBool(queryResponse.IsDelete))
                buffer.WriteString("\"")

                buffer.WriteString("}")
                bArrayMemberAlreadyWritten = true
        }
        buffer.WriteString("]")
  • 最後に、bufferをByteへ変換して、Successとして返しています。
fabcar.go
        return shim.Success(buffer.Bytes())
}

呼び出し側

このままでは、使用することができないので、javascript/query.jsにも変更を加えます。

  • 関数名を作成した関数名に変更
  • 動的にKeyを変更させたいので、コンソールから取得するように設定
    • 「node query.js hoge」とすると、process.argv[0]がnode、process.argv[1]がquery.js、process.argv[2]がhoge、となります
javascript/query.js
// 変更前
// const result = await contract.evaluateTransaction('queryAllCars');

//変更後
const result = await contract.evaluateTransaction('getHistoryForKey',  process.argv[2]);

また、登録処理も中身を動的に設定できるように修正します。

javascript/invoke.js
// 変更前
// await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');

//変更後
await contract.submitTransaction('createCar', process.argv[2], process.argv[3], process.argv[4], process.argv[5], process.argv[6]);

実行

準備が整いました。
実際に動かしてみましょう。
※ネットワーク起動、install、instantiate、adminとuser作成等はスキップ

▼登録
※代わり映えがしないって?

複数回登録を行っておきます
# node invoke.js test Zeon MS-06S Red Char
# node invoke.js test Zeon MSM-07S Red Char
# node invoke.js test Zeon MSN-00100 Gold Char
# node invoke.js test Zeon MSN-04 Red Char

▼検索

Keyはtestです
# node query.js test
Wallet path: /opt/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: [{"TxID":"88cf49dbae85c890d6c760adcbc1a508381b7120c96aff500ba129ccb57ccb5b", "Record":"{"make":"Zeon","model":"MS-06S","colour":"Red","owner":"Char"}", "Timestamp":"Tuesday Nov 05 07:41:32 JST 2019", "IsDelete":"false"},{"TxID":"01a1f67af0a574914f68067a16abee9b1015a7e04471c151822951fc3dff432b", "Record":"{"make":"Zeon","model":"MSM-07S","colour":"Red","owner":"Char"}", "Timestamp":"Tuesday Nov 05 07:41:37 JST 2019", "IsDelete":"false"},{"TxID":"861c0d8b47af62d4c4752beaef2d3fd782fd2a982a4bbd959c2bc6adb8cebb10", "Record":"{"make":"Zeon","model":"MSN-00100","colour":"Gold","owner":"Char"}", "Timestamp":"Tuesday Nov 05 07:41:43 JST 2019", "IsDelete":"false"},{"TxID":"660989fbc24f8ee7b5612fb32a3388a8ae193d5a2138fc7d70d99c18e325e7ce", "Record":"{"make":"Zeon","model":"MSN-04","colour":"Red","owner":"Char"}", "Timestamp":"Tuesday Nov 05 07:41:48 JST 2019", "IsDelete":"false"}]

上記だとわかりにくいので、resultの部分をJSONっぽく表示してみます。

"{ と }" を { と } に置換してます
[
  {
    "TxID": "88cf49dbae85c890d6c760adcbc1a508381b7120c96aff500ba129ccb57ccb5b",
    "Record": {
      "make": "Zeon",
      "model": "MS-06S",
      "colour": "Red",
      "owner": "Char"
    },
    "Timestamp": "Tuesday Nov 05 07:41:32 JST 2019",
    "IsDelete": "false"
  },
  {
    "TxID": "01a1f67af0a574914f68067a16abee9b1015a7e04471c151822951fc3dff432b",
    "Record": {
      "make": "Zeon",
      "model": "MSM-07S",
      "colour": "Red",
      "owner": "Char"
    },
    "Timestamp": "Tuesday Nov 05 07:41:37 JST 2019",
    "IsDelete": "false"
  },
  {
    "TxID": "861c0d8b47af62d4c4752beaef2d3fd782fd2a982a4bbd959c2bc6adb8cebb10",
    "Record": {
      "make": "Zeon",
      "model": "MSN-00100",
      "colour": "Gold",
      "owner": "Char"
    },
    "Timestamp": "Tuesday Nov 05 07:41:43 JST 2019",
    "IsDelete": "false"
  },
  {
    "TxID": "660989fbc24f8ee7b5612fb32a3388a8ae193d5a2138fc7d70d99c18e325e7ce",
    "Record": {
      "make": "Zeon",
      "model": "MSN-04",
      "colour": "Red",
      "owner": "Char"
    },
    "Timestamp": "Tuesday Nov 05 07:41:48 JST 2019",
    "IsDelete": "false"
  }
]

         \表示できました/

おわりに

サプライチェーンを考える上で、トレースは欠かせないですよね。
本当はブロックの中身をいじくり回したいのですが、いかんせんSDKをインポートできない病にかかってまして、とりあえずトレースする方法を記述しました。

HLF1.4のLTSもそろそろ切れる頃だし、HLF2.0が正式リリースされないかなぁーーー|д゚)チラッ

あー、Nodeで書きたいよー。
あと、時たま、初期のインストール&インスタンス化ではチェーンコードが反映されていなくて、手動アップグレードが必要だったりするけど、あれはなんじゃろなーーー

※がんだむはくわしくありません

今回はここまでです。
ありがとうございました。

5
4
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
5
4