Hyperledger FabricのChaincode内での台帳のクエリ方法まとめ
なんの話
- Hyperledger FabricでChaincodeの中で台帳(State DBとブロックチェーン)をクエリする方法をまとめて解説
動機
スマートコントラクトで表現できる/できないあるいは向いている/向いていないロジックを考えるうえで、コントラクト内、つまりトランザクションの中で合意を取れる範囲でどのように台帳データをクエリできるのかが重要だと思っているんですが、これ各ブロックチェーン基盤でけっこう差があるんだな~~と最近気になっています。とりあえずHLF(Hyperledger Fabric)についてどうなってるかここにまとめておきますので、Ethereumでどうなってるか、Cordaでどうなってるかなどはどなたかまとめていただけるとうれしいですね。
もちろん単にChaincode書くためのお勉強用にも大いに役立てていただければと思います。
前提
HLFの台帳はKey-Valueの形式でのレコードの保存を基本としています。ここでは最新のValueを格納しているState DB(a.k.a. World State)と過去バージョンのValueが保管されているブロックチェーンの両者を指して「台帳」と呼んでいます。このへん初耳だわとか忘れたわってひとはいったんHyperledger Fabricの主な構成要素をざっくりと解説 - 空谷に吼えるを読んでから戻ってきてください。
また、各クエリ方法について、台帳を更新するトランザクションの中で使った場合にトランザクションの不整合や意図せざる結果を防ぐためにValidationフェーズで検証がされるかについても触れています。Validationされるかどうかは、すなわち更新トランザクション内で安全に使えるかどうかということになります。Validationされないものについては、Chaincodeおよびアプリケーションのロジックで当該問題が排除できることを保証できない限りは更新トランザクションで使うのはやめましょう。もちろん読み取りオンリーのトランザクションで使うぶんにはぜんぜん問題ありません。このあたりのRead-SetのValidationとかクエリ再実行によるPhantom Read検知とか知らんわ、忘れたわってひとはHyperledger FabricはPhantom Read対策をやってたりやってなかったりする - 空谷に吼えるを読んでください。
クエリ方法解説
getState(key)
これが最もシンプルで基本のクエリです。渡したkeyに対応する現在のValueをState DBから検索して返却してきます。もちろんKeyはユニークなので、返却されるValueは単一になります。
Validation:される
ここで指定したkeyはRead-Setに含まれ、Validationフェーズでバージョンに更新がないか検証されます→Read-Set Validation
getStateByRange(startKey, endKey)
Keyを範囲指定するクエリです。startKey、endKeyはそれぞれ文字列として評価され、間にあるKeyを持つレコードについて複数のKeyと現在のValueのセットがState DBから検索され返却されます。startKeyに指定した文字列は範囲に含まれますが、endKeyに指定した文字列は範囲外として扱われることに注意ください(例:startKeyに1、endKeyに3を指定した場合に返ってくるのは1と2だけで3は含まれない)。startKey、endKeyはどちらもそれぞれ空文字とすることができ、この場合は開始または終了のどちらか、または両方を指定せずに検索されます。
なお、クエリ結果のKey-Valueのセットの数がcore.yamlで設定しているtotalQueryLimit(確かデフォルトは100000)を超える場合には使えません(確かそこまででぶった切られて残りは返却されない)。このような場合は次のページング機能付きのものを使えないか検討しましょう。ただし多くの場合、そもそもこのような大量の結果セットが取得されるような処理をChaincodeに実装すべきではないのでは、とも考えられますのでその点も検討してください。
Validation:される
ここでのクエリ条件と、取得してきたセットをハッシュ化した値(確かMerkleルート)はトランザクションの中に保持されています。Validationフェーズでクエリが再実行されEndorsementフェーズのハッシュ値と差がないか検証されます。したがって、読み取り範囲の中にKeyが増えていたり減っていたり、また、読み取ったValueが更新されている場合にはエラーとして検知可能です。→Phantom Read Validation
getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark)
getStateByRangeのページング版です。結果セットがpageSizeに渡した数ごとに区切られて返却されてます。bookmarkは最初のページを受け取る際にはまず空文字を渡し、次のページを受け取る際には前のページのQueryResponseMetadataに含まれているbookmarkの値を指定、以下全体セットの最後が来るまでループを回す、といったように使います。
Validation:されない
このクエリはValidationフェーズで再実行されないのでPhantom Readや読み取った値の更新が検知できません。
getStateByPartialCompositeKey(objectType, attributes)
KeyをComposite Key(複数要素を複合した文字列から成るキー)にしたレコードについて、部分的な要素を指定して検索を行うことで複数のKey-ValueセットをState DBから取得してくるクエリです。
そもそもComposite Keyを持ったレコードを保存する際、Composite Keyの文字列はcreateCompositeKey(objectType, attributes)の関数で作成します。ここでobjectTypeはStateとして複数の種類(たとえば「車」と「パーツ」)のレコードがある場合に、どの種類のレコードなのかを区別するためのKeyの接頭辞として機能します。そしてattributesは複合でユニークになるべき文字列の配列です。たとえば「車」について「メーカー」、「車種名」、「製造年」、「シリアルNo.」の要素で複合キーとしたい場合、createCompositeKey("Car", []string{"Honda","NSX","2018","1234567"}
といったかたちになります。で、この場合実際には"Car[NULL文字]Honda[NULL文字]NSX[NULL文字]2018[NULL文字]1234567"
の文字列が生成されてきて、それがState DBでKeyとして使われたレコードが保存されることになります。
で、このクエリはそのように保存されている「キー種別+複合値」をKeyとするレコードについて、要素の部分一致条件でState DBを検索します。前述の例でいくと、「製造年とシリアルNo.を問わず、HondaのNSXすべてのレコードを取得したい」という場合にはgetStateByPartialCompositeKey(”Car", []string{"Honda","NSX"})
となります。
注意すべきはここで部分一致条件として指定できるのは複合要素の前方の要素からだけであることです。ここでの例では、「メーカー」、「メーカー+車種名」、「メーカー+車種名+製造年」での指定はできますが、「車種名のみ」、「メーカー+製造年」といった指定はできません。つまり前方一致でしか検索できないよ、ということですね。複合要素の順番を決める際にはこの制約に留意しましょう。
このクエリも結果セットの数がtotalQueryLimitで制限されます。
Validation:される
このクエリもValidationフェーズでクエリが再実行されEndorsementフェーズのハッシュ値と差がないか検証されます。
getStateByPartialCompositeKeyWithPagination(objectType, attributes, pageSize, bookmark)
getStateByPartialCompositeKeyのページング版です。
Validation :されない
このクエリはValidationフェーズで再実行されないのでPhantom Readや読み取った値の更新が検知できません。
getQueryResult(query)
上述のクエリ関数はいずれもKeyに関して単一、範囲指定、部分要素前方一致指定でState DBを検索するものです。一方で、前提としてValueをJSON形式で保存しておけば、KeyだけでなくValueの部分要素(JSONのattribute)を条件に指定した検索を行うこともできます。このようなクエリをHLFではリッチクエリと呼びます。ただし、リッチクエリが使えるのはState DBの実装としてリッチクエリに対応する能力があるデータベースを使っている場合のみです。通常のHLFでState DBとして選択できるLevelDBとCouchDBのうち、リッチクエリを使えるのはCouchDBのみとなっています。
ここでqueryとして渡すのは、State DBとして使っているデータベースネイティブのクエリ文です。つまりCouchDBならCouchDB用のクエリ文を組み立てて渡すということですね。CouchDBではクエリ文も以下のようにJSON形式で渡します。以下の例だとState DBのValueに格納されているJSONのattributeに対して、docTypeがvehicleでありownerがSam Dealerであるものを検索し、modelとmanufacturerの値を取得しています。
{ "fields": ["model", "manufacturer"], "selector": { "docType" : "vehicle", "owner" : "Sam Dealer" } }
複数の結果セットが返ってきます。結果セットの数はtotalQueryLimitで制限されます。
Validation:されない
このクエリはValidationフェーズで再実行されないのでPhantom Readや読み取った値の更新が検知できません。
Oracle Blockchain Platformの場合
Oracle Blockchain PlatformではState DBの実装として独自にBerkeley DBを採用しており、このリッチクエリ利用に関連して以下の利点があります。これらによりgetQueryResult/リッチクエリの使い勝手が大幅に高められており、結果としてOracle Blockchain PlatformではChaincodeでそもそも実装できるロジック、また、運用上許容できる性能で実現できるロジックの範囲が大きく広がっています。
SQLリッチクエリが利用可能
リッチクエリでCouchDB形式のクエリ文(JSONリッチクエリ)に加えて、SQL形式のクエリ文(SQLリッチクエリ)が使えます。
先程のJSONリッチクエリの例と同等のものをSQLリッチクエリで表現すると以下のようになります。
SELECT json_extract(valueJson, '$.model') AS model, json_extract(valueJson, '$.manufacturer') AS manufacturer FROM <state> WHERE json_extract(valueJson, '$.docType') = 'vehicle' AND json_extract(valueJson, '$.owner') = 'Sam Dealer'
これだけだと「そうなんだ、SQLが好きなひとにはいいかもね」くらいなもんなんですが、SQLが使えるということは各種集計関数が使えるわけです。たとえばCOUNTで数える場合だと、
SELECT COUNT(*) AS Count FROM <state> WHERE json_extract(valueJson, '$.docType') = 'vehicle' AND json_extract(valueJson, '$.owner') = 'Sam Dealer'
となります。これを集計関数ナシで実装しようとすると、まずレコードセットを取得してきてからChaincode内でループしてレコード数を数えるしかないので、処理負荷がだいぶ変わってくるはずです。
そんでAVGで平均を数えるには、
SELECT AVG(json_extract(valueJson, '$.price')) AS AveragePrice FROM <state> WHERE json_extract(valueJson, '$.docType') = 'vehicle' AND json_extract(valueJson, '$.owner') = 'Sam Dealer'
となりますね。これも集計関数ナシだと、取得してきたChaincode内でループを回して平均を計算するしかありません。レコード数が増えてくるとだいぶツラそうです。
getQueryResultWithPagination(query, pageSize, bookmark)
getQueryResultのページング版です。
Validation :されない
このクエリはValidationフェーズで再実行されないのでPhantom Readや読み取った値の更新が検知できません。
getHistoryForKey(key)
指定したkeyについての値の履歴(過去~現在バージョンのそれぞれでの値)が取得できます。あるKeyについて複数バージョンがある、つまり、Key-ValueセットのValueが作成以降更新されている場合は複数バージョンの値が返ってきます。
使い方やどういう情報が取得できるのかは↓の記事で紹介されています。
【Hyperledger Fabric】GetHistoryForKeyを使って、StateDBのKeyに紐づくHistoryをGetしよう!! - Qiita
Validation:されない
このクエリの結果はRead-Setにも含まれず、また再実行もされません。ただしこのクエリ使う際には多くの場合、まず他のクエリでKeyを特定するところから始めると思うのでそちら側でのValidationがされるなら更新トランザクションで使っても問題ないはず。
Private Data系のクエリ
Private Dataを検索するためのクエリとして以下が用意されています。いずれもcollectionとして検索対象のPrivate Data Collectionの名称(ID)の引数が増えているだけで、使い方や制約は上述の同等のものとそれぞれ同じです。
- getPrivateData(collection, key)
- getPrivateDataByRange(collection, startKey, endKey)
- getPrivateDataByPartialCompositeKey(collection, objectType, attributes)
- getPrivateDataQueryResult(collection, query)
所感
というわけでここまでHLFのChaincodeの中での台帳のクエリ機能を見てきました。「どのようなクエリが可能なのか」に合わせてKey/Valueのデータ項目をどのように持たせるのか、また、Chaincodeにどこまでロジックを担わせられるのかが変わってきます。クエリはまあまあ種類がありますが、いずれも使いどころが異なるのでValidationに係る制約と合わせて覚えておきましょう。
HLFではリッチクエリが制約付きとはいえ使えるという点が他のブロックチェーン基盤の多くと比べてまあまあイケている点かなと考えています。
参照情報
https://fabric-shim.github.io/master/fabric-shim.ChaincodeStub.html:Node.js版のChaincodeのライブラリの公式ドキュメントです。Go版、Java版はソースコードのコメント読めってかたちなんで、お勉強目的にはこの読みやすいNode.js版がおすすめ。関数名や引数、戻り値などは基本的にNode.js、Go、Javaで共通です。
Marbles Chaincodeサンプル:みんな大好きMarblesのChaincodeサンプルはここで挙げたクエリを一通り使っているのではちゃめちゃに実装の参考になります。