少し前に報告されたtestnetで発生したNeutrinoのフィルタ処理の問題について↓
https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-November/019589.html
Neutrinoの位置付け
LNノード実装の1つであるLNDのブロックチェーンのバックエンドの連携先の1つがNeutrinoで、LNDにバンドルされている。現状LNDは以下のバックエンドが選択できる。
- Neutrino
- Bitcoin Core
- btcd
この内、Neutrinoは軽量クライアントとして位置付けられる。
軽量クライアントは、ブロックチェーン全体をダウンロードすることなく、ウォレットに関連するブロック、Txのみをダウンロードすることで、ノードのリソースコストを最小限に抑え、IoTデバイスやスマートフォンなどの低リソース環境でも動作させることができる。
もともとBitcoinにはBIP-16で定義されたBloom Filterベースのフィルターの仕組みを使ったSPVの仕様があったが、このプロトコルはフルノードへのトラストがネックになる。LNなどのペイメントチャネルでは、自分に関連するTxをオンチェーンで監視する必要があり、正しく検知できないと資金を失うリスクがあるため、このトラストポイントは最小化したい。
そこで、フルノードへのトラストを最小限にし、プライバシーも向上させるように新しく提案された軽量クライアント向けの仕様がBIP-157/BIP158↓
techmedia-think.hatenablog.com
techmedia-think.hatenablog.com
で、このBIPの前身となったのがNeutrino。Neutrinoは実装でもあるものの、Neutrinoプロトコルと呼ばれるのは、このため。
問題の原因
↑のBIP-157/BIP158は、既にBitcoin Coreにも実装されているけど、今回問題となったのはNeutrinoの実装の問題。
BIP-157/BIP158では、フルノードが各ブロック毎に、ブロック内の全トランザクションのインプット(正確にはインプットが参照する前のトランザクションのアウトプット)とアウトプットからフィルターを作成し、そのフィルターをP2Pメッセージで共有するようになっている。
軽量クライアントはブロックヘッダーのダウンロードに加えて、これらのフィルターのヘッダーおよびフィルタ自体をダウンロードする。そして、フィルタに対して自身に関連するトランザクションが含まれているかどうかを軽量クライアント自身がチェックする。この辺りのチェックの仕組みは↓
techmedia-think.hatenablog.com
今回問題になったのは、各ピアから入手したフィルターが競合した場合に、どのフィルターが正しいかチェックする際の実装。
Neutrinoでは競合するフィルターが見つかった場合、対象となるブロックを実際にダウンロードして、フィルターをチェックするようになっている。この振る舞い自体は問題なく、verification.cppに以下のチェックが実装されている。
- ブロック内の全TxのアウトプットのscriptPubkeyがフィルタにマッチするかどうか。
- ブロック内の全Txのインプットが参照するUTXOのscriptPubkeyがフィルタにマッチするかどうか。
このうち1についてはブロック内のトランザクションにscriptPubkeyがあるので問題ないけど、2のscriptPubkeyをどこから取得するのか?というのが課題になる。Neutrinoは軽量クライアントでブロックチェーン自体のデータを持っていないため、2のscriptPubkeyを持っていない。
そこで、NeutrinoはインプットのsciptSig/witnessのデータからscriptPubkeyを計算するというヒューリスティックを採用していた。計算ロジックは↓
- witnessにデータがある=Segwit UTXOである。
- witnessのアイテムが2つで、最後の要素のサイズが公開鍵(33バイト)と等しい場合、P2WPKHと判断して、公開鍵からP2WPKHのscriptPubkeyを計算する。
- それ以外の場合、witnessの最後のデータをwitness scriptとして、P2WSHのscriptPubkeyを計算する。
- witnessではなく、scriptSigにデータがある=非Segwit UTXOである。
- scriptSigのデータサイズが署名+圧縮公開鍵の取る範囲内であれば、P2PKHと判断して、公開鍵からP2PKHのscriptPubkeyを計算する。
- それ以外の場合、P2SHと判断して、scriptSigのデータからredeem scriptを取得しP2SHのscriptPubkeyを計算する。
この内、1のロジックがTaprootに対応しておらず、TaprootのwitnessデータはすべてP2WSHとして判断されてしまい、誤ったscriptPubkeyが計算されてしまう。この結果、インプットが参照する正しいscriptPubkeyとは異なるscriptPubkeyが導出され、それをフィルタにかけるので、本来正しいはずのフィルターを不正なフィルターと判断してまう。
※ コードみた感じだと、P2PKHで非圧縮公開鍵使った場合もフィルターにアンマッチしそうなんだけど、どうなんだろう?
対応
直近の対応
直近の対応としては、↑のインプットのフィルタのチェックはするものの、ミスマッチが発生してもエラーログは吐くが不正なフィルターとは判定しないように修正されたみたい↓
これ対応が入ったLND v0.13.4がリリースされている。
今後の対応
↑で不正なブロック判定はされなくなったものの、じゃあこの振る舞いを今後どうする?という課題が残る。これは別にNeutrinoに限った話ではなく、軽量クライアント全般に言えることだと思う(複数のピアから入手しているので、その中で一致する数の多いフィルターを正とするというアプローチもあると思うけど)。
そこで対策として挙げられているのが↓
- 過去のヘッダーの取り扱いに関する緩和策としては、Neutrinoに10万ブロック毎にフィルターのヘッダーをハードコードしてチェックポイントとする(これは既に実装されている)。
- フルノードからブロックのundoデータを入手できる新しいP2Pメッセージを用意する。ブロックのundoデータには、そのブロックで使用されたUTXOの情報が含まれるので、これをP2Pメッセージで入手できるようにすることで、インプットが参照するscriptPubkeyを補完する。
- 複数のブロックにまたがるフィルターを作成する
- マイナーに自身がマイニングしたブロックのフィルターへのコミットを求める。
- Taprootの構成要素である内部キー/外部キー、Control Block、マークルルート、annexなどを照会可能な新しいフィルタータイプを追加する。
など。