空谷に吼える

ブロックチェーン/DLTまわりのなにかしらを書いていく所存

Hyperledger Fabricでアセットの履歴を台帳上にどのように持っておくかを考える際のポイント

台帳上でなにかしらのアセットについての状態が管理されており、その最新の状態だけではなく過去の状態(過去バージョンの値、履歴)もちょくちょく参照したいんだよな~ということがあります。

例:NFTトークンのユースケースで、あるNFTトークンについて過去の所有者変遷の履歴を参照したい

また、単に参照するだけならば台帳のデータを外部のDBにIndexingしておいて、その外部DB上で好きなように検索、参照してやってもいいんですが、更新トランザクションにおいて過去バージョンの値をなんらかの条件などに用いたいという場合にはそうもいきません(外部DBと台帳の一致は保証できないため、必ず台帳上の過去バージョンの値を用いる必要があるため)。

例:あるNFTトークンの所有権を移転するトランザクションで、過去に特定の人物が一度でもそのトークンを所有していた場合にはそれ以上の所有権の移転を不可としたい(AMLでブラックリストを管理しているケースなどを想定)

そういった場合にHLF(Hyperledger Fabric)ではどういうかたちでアセットの履歴のデータを台帳(State DB/World Stateとブロックチェーン)上で表現しておくべきかと検討することになります。また、特に更新トランザクションで過去バージョンの値をなんらか用いる場合には、履歴の持ち方を工夫しておかないと後述するPhantom Read検出不能問題にあたり整合性の担保が実装不能になったりするので、これは非常に重要なポイントになります。

そうした検討において結局毎度同じようなことを考えているので自分用の備忘として&同様の検討をされる方用のヒントとしてまとめておきます。

選択肢

前提として、HLFでは台帳上のあるKey-ValueセットについてValueを更新していった場合に、その値の履歴(過去バージョン~現在バージョン)がブロックチェーン上に残っており、この履歴はChaincode内でgetHistoryForKey(key)を使うことで一括取得してくることができます。↓のブログを見るとどういうかたちで取得できるのかイメージがわかります。

qiita.com

で、「なら履歴はこれでいいじゃん」とするのがHistoryを利用する方式です。この場合、State DB/World State上にはあるアセットの最新の状態のみが保持されているということになります。レコードの形式はたとえばこんな感じ↓

Key : アセットID
Value : 
 所有者,
 色,
 サイズ

それに対して、過去バージョンも含め、State DBに複数バージョンを保持する方式もあります。この場合には、台帳でのKeyはアセットIDバージョンの複合キーとしておき、あるアセットについての状態の更新を表現する際には、台帳上の既存のKey-ValueセットのValueを更新するのではなく、新たなKey-Valueセットとして挿入することになります。レコードの形式はたとえばこんな感じ↓(→複合キーの要素にバージョンを含める方式)

Key : アセットID, バージョン(2要素の複合キー)
Value : 
 所有者,
 色,
 サイズ

あるいは、Keyには特定のアセットを指示する意味を持たせずにValueの中にアセットIDとバージョンを持っておくこういうパターン↓も有り得ますね(→Valueの要素にバージョンを含める方式)。ただし、後述しますが、このやり方は必ずリッチクエリのPhantom Read検出不能問題にぶち当たることになります。

Key : 特に意味のないID(Tx IDとか)
Value : 
 アセットID,
 バージョン,
 OLDフラグ,
 所有者,
 色,
 サイズ

で、これらのHistoryを利用する方式State DBに複数バージョンを保持する方式どっちにしましょうかねというところを考えることになります。

Historyを利用する方式

メリット

State DBのKey-Valueセット数を少なく保てる

State DBに保持されるあるアセットについての状態を表すKey-Valueセットが常に単一なので、アセットの数が大量で、かつ、それぞれに対して更新が高頻度に発生するような場合でも、State DB上のKey-Valueセット数を最小に保てます。これにより、将来的なKey-Valueセット数増大によるクエリの性能劣化などの懸念を最小限にできるでしょう。

最新バージョンの取得がシンプルで速い

あるアセットについて最新の状態を取得したければ、単にアセットIDを指定してState DBに対してgetState(key)すればいいだけなので最もシンプルかつ高速になります。

デメリット

過去バージョンについての検索能力が貧弱、低速

履歴に属する過去バージョンに用がある場合、getHistoryForKeyを使って全件取ってきてからChaincode内で件数分ループしてフィルタして取得するしかありません。State DB側でフィルタしてから取得するのに比較して処理コストが高くなるでしょう。なお、getHistoryForKeyを使って取ってきたHistoryはReadセットに載らないことは頭の片隅に置いておきましょう(更新トランザクションで最新バージョンを読まずにHistoryだけ読むといった使い方をすると、Phantom Readによる不整合が発生するパターンを作り込む可能性があります)。

State DBに複数バージョンを保持する方式

メリット

過去バージョンについての検索能力が便利、高速

過去バージョンもState DB上にあるので、リッチクエリでのフィルタが使えます。Chaincode内でループしてフィルタするよりは速いはず。また、バージョンおよびアセット横断的な検索も得意で、例えば、「最新の状態であるかに関わらず、所有者がAliceであったアセットすべてを取得する」といったことをやりたい場合にも、単に所有者 = 'Alice'でリッチクエリを発行すれば良いだけです。

デメリット

State DB上のKey-Valueセットが余分に増えていく

あるアセットについての状態を表すKey-Valueセットがバージョン数に応じて余分に増えていきます。将来的なKey-Valueセット数増大によるクエリの性能劣化の懸念が大きくなります。

最新バージョンの取得が面倒、遅い

アセットを一位に指示するアセットIDだけからでは最新のKey-Valueセットがどれなのかわからず、また、最新のバージョンの数字もわからないので、複合キーの要素にバージョンを含める方式でComposite Keyによる検索をする場合には、あるアセットIDを持つ複数バージョンを一括取得したあと、最新バージョンはどれなのかを選別するという2工程が必要です。

Valueの要素にバージョンを含める方式を用いている場合は、OLDフラグが立っていないものという条件をリッチクエリに付与すればState DB側でフィルタできますが、これはこれでリッチクエリのPhantom Read検出不能問題があるので更新トランザクションでは使えないです。そもそも最新バージョンを挿入する更新トランザクションでは必ず過去バージョンをリッチクエリで取得してReadすることになるはずなので、基本使えないということになります。

考察

というわけでいずれの方式もメリットとデメリットがあります。まずは更新トランザクションにおいて過去バージョンを用いるかどうか、用いるのであればどのように用いるか、整合性を担保しつつそれを実現できるようにするためにはどうすべきかを考慮して検討しましょう。

その上で、アセットの量はどれくらいか、更新頻度はどの程度か、履歴に対してどのような検索をしたいか、性能要求はどのくらいかなどを考慮して決めるといいんじゃないでしょうか。