Develop with pleasure!

福岡でCloudとかBlockchainとか。

lnd v0.15.1-beta以下で発生したチェーン同期の障害

lnd v0.15.1-beta以下で発生したlndでチェーンの同期ができなくなる不具合↓についてまとめておく。

github.com

正確には、btcdの不具合↓

github.com

障害の内容

今回発生した障害は、lndでブロックチェーンの同期ができなくなるというもの。そのため、その間、新しいチャネルの開設やクローズができなくなった。※ 稼働中のチャネルを利用したオフチェーン支払いは可能なまま。

実際、自宅で運用してるUmbrelでも、lndの以下のログを吐いてた↓

2022-10-09 21:04:09.448 [ERR] LNWL: Unable to deserialize transaction: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]
2022-10-09 21:04:09.559 [ERR] LNWL: Unable to deserialize block: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]
...
2022-10-11 00:05:36.001 [ERR] LNWL: Unable to process chain reorg: unable to get block 0000000000000000000400a35a007e223a7fb8a622dc7b5aa5eaace6824291fb: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]

トリガーとなったのは、ブロック757922内の以下のトランザクション

7393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef73

インプットが参照するUTXOはTaprootのアウトプットで、それをscript-pathで998-of-999のマルチシグを使ってアンロックしている。この結果、インプットのscriptWitnessには、999個の公開鍵と998個の署名データが含まれ、合計98,881バイトのデータになる。

※ ちなみに、998-of-999のマルチシグの場合、今回のケースとは別に998-of-998の組み合わせを999個作って1つの公開鍵と1つの署名に集約するというアプローチを採れば、データは150バイト以内に収められる。

このトランザクションをbtcdが正常にパースできず、チェーンの同期が失敗するようになった。

不具合の原因

問題となったbtcdのコードは、ピアからtxメッセージやblockメッセージを受け取って、そのトランザクションをパースする以下の部分↓

https://github.com/btcsuite/btcd/blob/v0.22.0-beta/wire/msgtx.go#L589-L594

maxWitnessItemSizeが11,000バイトに設定されており、このサイズを超えるscriptWitnessはエラーになる。このため、↑のscriptWitnessが98,881バイトあるトランザクションはパースに失敗する。

もともとこのチェックは、P2WSHの仕様↓から来てるっぽい(ただ、1,000バイトの差は何だろう?)。

The witnessScript (≤ 10,000 bytes) is popped off the initial witness stack. SHA256 of the witnessScript must match the 32-byte witness program.

本来Taprootにはこのルールが適用されないが↓*1、このルールが適用されたまま上限を超えるTxが登場したので今回問題が顕在化した。

Script size limit The maximum script size of 10000 bytes does not apply. Their size is only implicitly bounded by the block weight limit.

ちなみに、Bitcoin Coreの場合、このチェックはScriptを評価するこのコードに実装され、P2SHよびP2WSHに限定されている↓

if ((sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) && script.size() > MAX_SCRIPT_SIZE) {
    return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);
}

不具合の修正内容

不具合の修正内容は単純で、maxWitnessItemSizeの値をブロックの最大サイズである4,000,000に引き上げるというもの。

ただ、この修正でちょっと気になったのが

  • このチェックはトランザクションレベルのサイズチェックで済む話なので、あえてこのインプット毎のチェックとして必要なのか?
  • チェックを残すにしても、P2WSHについては↑のチェック回避することになるけど、そこは問題はないのか?

どうしてlndで影響が出たのか?

今回の障害が発生した際、btcdの不具合で、lndの接続先にBitcoin Coreを選択してる自分には関係ない話かな?と思ったけど、そうでもなかった。

lndは、Bitcoinのノード実装だけでなく、ライブラリとしてbtcdのコードを多く利用している。今回も、フルノードから受け取ったブロックのパースに↑のbtcdのコードを使っているため、パースに失敗し新しいブロックを処理できなくなり、今回の障害に至ったと。

チャネルの開設やクローズのオペレーションができないくらいならまだ良いけど、不正なTxがブロードキャストされたのを検知できなくなる方が問題かな。今回みたいなケースを想定すると、Watchtowerの実装はLNノードの実装とはまた別のコードベースのものを使いたいと思うようになる。

*1:この制限がどうしてなくなったかというと、P2WSHまでは署名対象のメッセージダイジェスト(つまりトランザクションのダイジェスト)を計算する際に、アンロックに使用するScriptのデータもその計算に含まれていた(scriptCodeとして)。Scriptのサイズが増加するとメッセージダイジェストを計算するコストも比例して増加してしまうので、この計算リソースを限定するためにScriptのサイズに最大サイズの制限が設定されていた。Taprootでは、メッセージダイジェストを計算する際のデータ項目が変更され、直接Scriptをメッセージダイジェストに含めなくなったため、このリソース要件をなくした模様。ただ、実行するScriptがTaprootツリーのリーフハッシュと一致することを検証する必要はあるので、いずれにせよハッシュ計算は発生するんだけど。