プール 18
マイナー なし
ワーカー なし

< すべての記事

Ravencoin の KAWPOW バグ:プルーフ・オブ・ワークの検証を自ら選んだブロックヘッダー

高さ 4,487,776 でマイニングされた Ravencoin のブロックが、自身のブロックヘッダーにより低い高さを書き込みました。その数字が 2,383,550 の最終チェックポイントより低かったため、検証は完全なメモリハード検査を実行する代わりに、ヘッダーにすでに入っていた mix_hash をそのまま信頼しました。

8 月の第 2 週、Ravencoin は自らのチェーンをおよそ 3 日分巻き戻しました。原因はマイニングのカルテルでも、ウォレットの脆弱性でも、壊れたハッシュ関数でもありません。2 種類のプルーフ・オブ・ワーク(PoW)検証のどちらを実行するかを決めるときにノードが信頼していた、ブロックヘッダー内のフィールド 1 つでした。以下で引用するコードの断片はすべて、告知ではなく Ravencoin 自身のコンセンサスのソースコードから読んだものです。告知には書かれていてもソースで確認できない内容については、推測せずにその旨を記します。

KAWPOW のヘッダーに含まれるもの

KAWPOW は Ravencoin のプルーフ・オブ・ワークで、コインを一般的な GPU でマイニングし続けられるよう、チェーンが 2020 年 5 月に移行した ProgPoW の派生版です。この移行でブロックヘッダーが変わりました。有効化の時刻より前は、Bitcoin から派生したすべてのチェーンが使う通常の 32 ビット nonce でヘッダーが終わります。それ以降は、シリアライザーがその位置に代わりに 3 つのフィールドを次の順で書き込みます:nHeight、次に nNonce64、そして mix_hash です。

そのうち 2 つは特に変わったものではありません。nNonce64 は単に幅の広い nonce です。mix_hash はアルゴリズムのメモリハードな部分の中間結果で、検証者がゼロから計算をやり直す代わりに照合できる値を持てるよう、ヘッダーに入れられています。興味深いのは 3 つ目です。nHeight は、ブロックが自分のヘッダーの中で、自分がどの高さにいると考えているかを述べる値です。

チェーンにおけるブロックの実際の位置は、主張で決まるものではありません。hashPrevBlock がそれを固定します。どのブロックの上に積むかで行き先が決まり、そこを偽りながらチェーンの先端につながることはできません。nHeight は同じ事実の 2 つ目の、書き換え可能なコピーであり、事実のコピーには、それを原本と比べる検査と同じだけの価値しかありません。

2 つのハッシュ関数、そのうち 1 つは何も検証しない

Ravencoin の hash.cpp は KAWPOW の入口を 2 つ定義しており、どちらの本体を 1 行も読まないうちに、そのシグネチャが話の大半を語っています。1 つ目は uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash) です。2 つ目は uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader) です。

1 つ目は mix_hash を出力パラメーターとして受け取ります。mix を生成するからです。ethash::get_epoch_number(blockHeader.nHeight) でデータセットを選び、そのエポックのコンテキストをまだ持っていなければ構築し、その上で progpow::hash を実行します。返される mix は、実際に作業をしたことの副産物です。これがメモリハードな経路であり、メモリハードであることこそ、KAWPOW が単純なハッシュではなく存在している理由のすべてです。

2 つ目にはそのようなパラメーターがありません。何も生成しないからです。progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64) を通じて、ヘッダーにすでに入っている mix をそのまま使います。上流のライブラリはこの関数に hash_no_verify という名前を付けており、その名前は正確です。渡された値に対して最後の混合ステップを行い、答えを返します。データセットを構築しないため、渡された値が作業をしなければ得られないものだということを確かめることもありません。

どちらの関数も間違いではありません。安価な方には正当な理由があります。ハードコードされたチェックポイントがすでに確定させた何百万もの古いブロックを同期するノードが、誰も異論のない履歴を再確認するために、エポックごとに数 GB のデータセットを作り直す必要はないはずです。このような設計が答えるべき問いは 1 つだけです。どのブロックにこの経路を許すか、です。

関門と、それが信頼したもの

その問いに答えるのは validation.cpp の CheckBlockHeader で、答えのすぐ上にあるコメントが意図をはっきり述べています:「If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash」。意図は正しいものです。しかし実装は block.nHeight <= (uint32_t)pcheckpoint->nHeight を条件にしており、block.nHeight はブロックが自分自身について書いた数字です。

その下の行は汎用の CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&) を呼び出します。このシグネチャに何が含まれていないかを見てください。mix も nonce もヘッダーもありません。完成したハッシュとターゲットを受け取って 2 つを比べるだけで、渡された数字を 2 つの関数のどちらが生成したのかを知る手段はありません。KAWPOW のブロックでは CBlockHeader::GetHash() が KAWPOWHash_OnlyMix(*this) を返すため、渡される数字は安価な方のものです。

つなげてみると、十分に低い nHeight を宣言したブロックは、自身の mix hash から導かれた値がターゲットを下回るかどうかだけを問われて検証を通過していました。その経路のどこでも、mix がどこから来たのかは問われず、宣言された高さをブロックが実際に占める位置と比べることもありませんでした。

どれだけ低ければ十分かは、微妙な話ではまったくありませんでした。リリースされた mainnet のチェーンパラメーターで最も高いチェックポイントは 2,383,550 です。最初の偽造ブロックは 4,487,776 にありました。そのヘッダーは実際の位置より約 210 万ブロック手前の位置を主張する必要があり、それによって選ばれた分岐は、その差を一度も確認しませんでした。

近道がもたらしたもの、もたらさなかったもの

ここは正確に述べておく価値があります。多くの報道がこの点を曖昧に扱っており、曖昧な説明では、このバグが実際より単純で、しかも奇妙なものに聞こえてしまうからです。

近道は難易度を下げてはいません。nBits は実際の直前ブロックが要求する値と照合して検証され、実際の直前ブロックは hashPrevBlock によって選ばれます。攻撃者はそれを偽りながらチェーンの先端に乗ることはできません。偽造ブロックも、同じ分に正直にマイニングされたブロックとまったく同じターゲットを満たす必要がありました。

変わったのは 1 回の推測にかかるコストです。正直なマイナーは試行のたびに GB 単位のデータセット全体を 1 回通るコストを支払います。これがメモリハードなアルゴリズムの要点であり、KAWPOW が 6 年間 Ravencoin から ASIC を締め出してきた理由です。近道のコストは、ヘッダーのハッシュ 1 回と、攻撃者が自由に選んだ値に対する最後の混合ステップ 1 回だけです。同じくじで、1 枚ごとの当選確率も同じなのに、くじの値段が桁違いに安くなったのです。2Miners はその結果をほぼ作業ゼロのブロックと表現しており、KAWPOW に本来かかるはずのコストと比べれば、妥当な表現です。

ネットワークで起きたこと

知られている最初の無効ブロックは高さ 4,487,776 で、タイムスタンプは 2026-08-07 15:44:01 UTC です。悪用は 8 月 10 日の緊急リリースまで続きました。2Miners は、調べた 2,089 ブロックの区間で影響を受けたブロックを 96 個数えました。つまり偽造ブロックは 1 か所にまとまっておらず分岐全体に散らばっていたため、対処は精密な除去ではなくチェーンの再編成でなければなりませんでした。

ノード運用者にとって、目に見えた症状は無効なブロックが静かに受け入れられることよりも悪いものでした。ノードはこれらのブロックでクラッシュし、再起動後は同期を停止しました。Tron Black がのちにその仕組みを説明しており、見事なまでの巻き添え被害の例です。ブロックインデックスは、ヘッダーが宣言した高さではなく、ブロックが実際に位置する高さを保存します。再起動時にノードはインデックスから各ヘッダーを組み立て直してハッシュを再計算するため、偽造ブロックでは、ヘッダーに偽の高さが入っていた場所に実際の高さを入れてハッシュすることになります。ハッシュはもう一致せず、プルーフ・オブ・ワークの検証が失敗し、LoadBlockIndexGuts が中断します。ブロックは一度受け入れられた後に読み込めなくなり、そのため 2 つ目の症状は 1 つ目より長く残りました。

分岐が解決されるまでの間、複数の取引所が RVN の入出金を停止しました。直近の履歴がこれから書き換えられるチェーンに対する、ごく普通の正しい対応であり、コインに対する評価と受け取るより、起こるものと考えておくべきことです。

パッチと、その出どころ

最初の修正は 8 月 10 日にバージョン 4.6.1.1-hf1 として出され、Ravencoin プロジェクトではなく、マイニングプールの 2Miners が作ったものでした。これを導入した人にとっては被害を止めるものでした。1 日後には別のバージョンに置き換えられました。

8 月 11 日に Ravencoin のリポジトリから公開されたバージョン 4.8.0 が、実行すべきリリースです。それより前のもので済ませないでください。このバージョンは、4,487,776 以降でヘッダーに宣言された高さが実際の位置と一致しないブロックを拒否し、4,487,775 を最後の正常なブロックとしてチェックポイントに設定し、壊れたチェーン状態を中断せずに自動で再構築します。さらに、緊急ホットフィックスには含まれていなかったアセット送金のオーバーフロー修正や、その他の上流の変更も取り込んでいます。

このうち本当の修正は最初の 1 つだけです。宣言された高さが実際の位置と一致しないブロックを拒否すれば、関門は閉じます。チェックポイントと自動再構築は復旧のための措置であり、特にチェックポイントは大ざっぱな道具です。特定の 1 つのブロックが正しいブロックだとハードコードで言い切るもので、チェーンとしてはできるだけ少なくしたい種類のものです。ここで使われたのは、正直な分岐を、ハッシュレートだけに任せるよりも早く確定させる必要があったからです。

復旧地点そのものも、自明な選択ではなく判断の問題でした。Tron Black は、再編成を浅くして偽造ブロックは例外として扱えるよう、もっと最近の地点を検討してほしいとプールに求めたと書いています。プールは断り、最もきれいな線を選びました。プルーフ・オブ・ワークのシステムでは、この種の議論はおのずと決着します。正直なハッシュレートの過半数がある履歴を拒否すれば、その履歴は負けます。そして最大の 2 つのプールが合わせてその過半数を持っていました。

3 日分の再編成が実際に意味すること

脆弱なノードが偽造ブロックを受け入れたため、それを含む分岐の上に本物のチェーンが積み重なりました。それを捨てることはチェーンの再編成を意味し、Ravencoin の 1 分というブロック目標で見ると、4,487,776 からパッチまでの区間はおよそ 3 日分のブロックになります。

RVN を保有している場合、実際の意味は、捨てられた分岐で承認された取引が承認済みでなくなるということです。ほとんどは mempool に戻り、残ったチェーンで再びマイニングされますが、それは保証されておらず、まったく戻れない種類の取引もあります。捨てられたブロックの coinbase を使った取引は、残ったチェーン上にもう存在しないコインを使っていることになり、その上に積まれた取引もすべて同様です。これらは保留中ではなく、単に無効です。だからこそ取引所の入出金停止は過剰反応ではなく正しい判断であり、チェーンの先端が 1 つに確定するまで、4,487,775 以降の動きには注意が必要でした。

その週にノードがまったく動かないのを見た方は、知っておく価値のある細かな点があります。Ravencoin のノードには長い再編成を禁止するルールがあり、予期しない過半数攻撃が深い履歴を書き換えるのを防ぐために存在します。動き続けていた古いノードの多くは、やがてこのルールが前提とするピア数を下回り、設計どおりに長い方のチェーンを受け入れました。一部はそうなりませんでした。十分な数のピアを失わず、書かれたルールのうえでは何も違反していなかったからです。保護の仕組みが、本来想定していなかった問題に対して仕様どおりに動いたということです。今回の分裂は過半数への攻撃ではなく検証の欠陥から生じたもので、このルールは両者を区別できません。

RVN をマイニングしている場合にすること

自分で Ravencoin のノードを運用している場合は、4.8.0 を実行してください。この問題の一部だけに対処した以前のリリースは避けてください。ノードを停止し、バイナリを入れ替えて、再び起動します。実行すべき reindex も、手作業の手順もありません。ソフトウェアが必要なものを自分で再構築します。緊急ホットフィックスから移行するノードはすぐに再起動します。パッチ未適用のソフトウェアから移行するノードは再生と再構築が必要で、一般的なハードウェアで数時間かかります。その間に出る bad-blk-height エラーはパッチの不具合ではなく、パッチが働いている証拠です。

プール経由でマイニングしている場合、手元で変えることは何もありません。新しいアドレスも、新しいポートも、マイニングソフトの更新も不要です。ここで KAWPOW 自体は問題ではありませんでした。アルゴリズムは設計どおりに動作しており、欠陥はそれをいつ実行するかを決めるコードにありました。古い分岐に向けたハッシュレートは本物の作業でした。ただ、その間に散らばっていた偽造ブロックと一緒に捨てられたブロックに入っただけです。

正しいチェーンにいるかどうかを 1 行で確かめる方法があり、思い込むより実際に確かめる価値があります。ブロック 4,494,143 のハッシュを確認してください。正規のチェーンでは 4c67d6a4 で終わり、プロジェクトがネットワークの復旧を宣言した時点で、独立した Ravencoin のエクスプローラーはすべて同じチェーンを示していました。

このバグの形

細部を取り除くと残るのは、チェーンの内外を問わず絶えず繰り返される形です。外から届いた値が、その値自体をどれだけ慎重に調べるかを決めるのに使われました。ヘッダーは自分の高さを述べ、ノードはその申告をもとに、ヘッダーがこれから通らなければならないプルーフ・オブ・ワークの検証を選んだのです。

正しいやり方は複雑ではなく、パッチは実質的にその 1 行です。宣言された高さに何かを決めさせる前に、まずブロックが実際に占める位置と比べることです。なぜそれが欠けていたのかの方が、より興味深い半分です。近道はすでに信頼されたブロックのために書かれており、その前提の中では、関門は誰かが不正直に答えうる問いではなく、事実を述べた文のように読めます。信頼できないブロックがそのフィールドを埋められるようになった瞬間にそうは読めなくなりますが、コードのどこにも、それが変わった瞬間は記されていませんでした。

どのように見つかったかについて、記録しておくべき点が 2 つあります。Ravencoin のコードは専門のセキュリティ監査を受けていましたが、監査はこの問題も、同時に修正されたアセットのオーバーフローも見つけられませんでした。Tron Black は、どちらもフロンティアモデルの AI によるセキュリティレビューで見つかったのではないかと強く疑っていると書いています。どんなツールだったにせよ、この形と合致します。近道をその前提のまま読んでいる間は欠陥が見えず、近道が信頼するフィールドを誰が埋めるのかを問うたときに初めて現れるのです。

最後に注目すべきは、誰がどの順番で動いたかです。あるマイニングプールが検証の経路を読んで理解し、3 日以内に緊急のバイナリを出しました。続いて最大の 2 つのプールが、都合のよい地点ではなく攻撃期間全体を捨てるチェーンをマイニングすることで合意し、より浅い地点を選ぶよう求められても断りました。プロジェクトは 1 日後、ホットフィックスを置き換え、それ以上のものを修正したリリースで続きました。どちらが先だったかより、両方が起きたことの方が重要です。そして実際に決着をつけたのは、どちらの告知でもなく、分裂の片側に積み重なった本物の作業の重みでした。分散化がホワイトペーパーの中にとどまらず、重みを支えなければならない日には、こういう姿になります。

これらの数字の出どころ

私たちの言葉をうのみにせず、誰でも確かめられるように記します。ヘッダーの構造、2 つのハッシュ関数とそのシグネチャ、エポックの選択、そしてコメント付きの検証の分岐は、Ravencoin 自身のソースである src/primitives/block.h、src/primitives/block.cpp、src/hash.cpp、src/hash.h、src/pow.h、src/validation.cpp から読みました。mainnet で最も高い 2,383,550 を含むチェックポイントの一覧は src/chainparams.cpp から読みました。最初の無効な高さとそのタイムスタンプは、Ravencoin プロジェクト自身のネットワーク告知によるものです。ブロックインデックスのクラッシュの経路、戻れない coinbase 依存の取引、長い再編成を禁止する動作、断られたより浅い復旧地点、監査と AI レビューに関する言及、4.8.0 の内容、そして 4,494,143 での確認方法は、Tron Black が Ravencoin プロジェクトとして記した説明によるものです。緊急ホットフィックスのバージョンと、2,089 ブロック中 96 ブロックが影響を受けたという集計は、2Miners のリリースとその周辺の報道によるものです。出典に書かれていないことは、補わずにこの記事から外しました。