空谷に吼える

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

Hyperledger FabricはPhantom Read対策をやってたりやってなかったりする

ここの冒頭に書いてるひとこといつも考えるのにけっこう時間かかってるんだけど要る????

なんの話

  • Validationフェーズではクエリを再実行することで、クエリ結果がChaincode実行時と変わっていないかチェックしている(Phantom Readチェック)
  • ただしState DBとしてCouchDBを使用し、リッチクエリを使っている場合はPhantom Readチェックは行われない
  • なのでCouchDB使ってる場合、台帳を更新するトランザクションではリッチクエリを使っちゃダメ

内容

Read-Set Validationの振り返り

前回のエントリではRead-Set Validationについてお勉強しました。これはEndorsementフェーズで実行されたChaincodeの中で行われた台帳(State DB)へのクエリ結果であるRead-Set(Keyとそのバージョンのセット)が、Validationフェーズでも変わっていないことをチェックする、というものでした。

これはつまり、台帳の中のクエリ条件に当てはまったレコードが、EndorsementフェーズとValidationフェーズで更新されていないことをチェックしている、ということになります。ではクエリ条件に当てはまらなかったレコードについてはチェックしなくてもよいのでしょうか?これは反語という修辞法なので「A. よいです」とはならず、今からよくないですという話をします

Phantom Read発生ケース

例えば資産(通貨などではなく、骨董品などのnon-fungibleな資産)とその所有者が記録されているような台帳があったとします。ここで、「Aliceが所有している資産すべてをBobに譲渡する」というようなTxを実行したとします。このTxがコミットされた時点で期待される台帳のStateは、Aliceの資産はひとつも残っていないという状態です。そのようなTxを実現するためのChaincodeは、まず「所有者がAliceであるすべての資産」という条件で台帳をクエリし、そのクエリ結果に対して所有者をBobに更新するというものになるでしょう。

この「Aliceの全資産をBobに譲渡」というTx(Tx②とします)をコミットする直前に、「Aliceの資産としてhogeを追加する」という別のTx(Tx①とします)がコミットされるケースがあります。このような場合、Tx②でEndorsementフェーズでのChaincode実行時の「所有者がAliceであるすべての資産」というクエリ結果に、Tx①で追加された資産hogeは含まれていません。すると、何も対策をしていなければ、Tx②のコミット時点での台帳には、Aliceの資産としてhogeが残ってしまい、前述の期待と異なった結果になってしまっています。

このように、「あるトランザクションのライフサイクル(コミットまで)の中での複数回のクエリ結果が、別のトランザクションによるレコード新規追加により変わってしまう」事態はPhantom Read Anomalyと呼ばれます。そして上記の場合、Tx②のRead-Setには資産hogeが含まれていません。したがってこのようなPhantom Readケースは、Read-Set Validationでは防げないことになります。

Validationでのクエリ再実行(Phantom Read対策)

あんしんしてください、HLFではこのようなPhantom Read対策も行われています。Chaincode内で実行されたクエリをValidationフェーズの中で再実行し、Endorsermentフェーズでのクエリ結果とValidationフェーズでの結果を比較することによりこのようなPhantom Readが発生していないかをチェックしています。Phantom Readが発生した場合、そのTxはInvalidとマークされて台帳(State DB)にはコミットされません。

したがって上記の例のような資産残ってしまう問題は起きません。これは朗報です

なお、クエリ結果をそのまま返却しているとProposalResponseのペイロード(およびProposalResponseを含むTx履歴を格納するブロックサイズ)が大きくなってしまう、また比較にも時間がかかるので、クエリ結果のMerkle Tree Digestを比較しています。

CouchDBのリッチクエリ使用時の問題とそれに伴う制約

さっきHLFではPhantom Read対策が行われていると言ったな、あれは嘘だ。

いやまるっきり嘘というわけではない、ないが、クエリ再実行が行われない場合もあります。State DBとしてCouch DBを使い、リッチクエリを使用している場合にはクエリ再実行が行われない、というものです。

Couch DBではJSON形式でValueを格納することが想定されており、JSONのAttributeをフィルタ条件として指定したリッチクエリを使うことができます。このリッチクエリ自体はすごく便利で、さきほどの例で言うと、Owner : "Alice"となっている資産をクエリしてくるようなことができます。LevelDBだとKeyをowner,asset_idといった複合キーにしておいてRangedQueryするしかないのでわりと扱いづらく、複合キーが増えてくるとフィルタに使えない項目が出てきてちょっとどうするんだということになってしまいます。

しかし、このような便利なリッチクエリを使うと前述のとおりValidationでクエリが再実行されず、Phantom Read発生を検知できない、したがって資産残ってしまう問題が起きてしまう可能性があるということです。これは悲報ですね

というわけで、CouchDBをState DBとして使っている場合も、(アプリケーションレベルでPhantom Readの発生を防ぐ/ハンドルしていない限りは)更新トランザクションの中のクエリとしてリッチクエリを使うな、つまりリッチクエリは照会トランザクションでのみ使え、という制約が設けられています。

この制約は結構真剣に不便なんですが(ワークアラウンドしきれず実装不能になるロジックが出てくる)、以下のHLFのJIRAを見るとこの制約の解消は技術的に困難なようで、少なくともHLF v1.x系の間は解消されないっぽい

[FAB-2878] CouchDB Phantom Read - Hyperledger JIRA