EthereumがETHとETCにハードフォークした際に問題となったリプレイ攻撃。
ブロックチェーンでハードフォークが発生すると、ハードフォークの前に持っていた通貨は、分岐した両方のチェーンで有効な状態になる。仮にAコインがハードフォークしてAコイン(元のコイン)とBコイン(フォークしてできたチェーンのコイン)になったとする。
ハードフォークした直後は、ハードフォーク前にAコインを持っていたユーザーは、同量のBコインも持っている状態になる。その後Aコインを使って買い物をする場合、ユーザーはECサイトにAコインを支払うトランザクションをAのネットワークにブロードキャストする。このトランザクションはBのネットワークでも有効なトランザクションなので、ユーザーとは別に悪意ある誰かがこのトランザクションをBのネットワークにブロードキャストするとユーザーがもっていたBコインもBのチェーンのECサイトのアドレス宛に送られてしまう。これをリプレイ攻撃という。
BIP-115ではこのリプレイ攻撃を防止する仕組みとして、新しいopcode = OP_CHECKBLOCKATHEIGHT
を導入し、特定のブロックチェーンでのみ有効なトランザクションを構築する方法を提案している↓
bips/bip-0115.mediawiki at master · bitcoin/bips · GitHub
仕様
OP_CHECKBLOCKATHEIGHT
は既存のOP_NOP5
opcodeを再定義する。
このopcodeは以下のように動作する。
- スタック上の要素数が2未満の場合、スクリプトは失敗する。
- スタックの一番上の要素が32bitのCScriptNumとして解釈できない場合、スクリプトは失敗する。
- スタックの一番上の要素はブロック高(ParamHeight)として解釈される。
- このスクリプトを実行しているブロックチェーンに、ParamHeightで指定したブロック高のブロックが存在しない場合、スクリプトは失敗する。
- ParamHeightがチェーン内の52596ブロックより深い(マイナスを含む)を指定していた場合、opcodeは正常に完了し、スクリプトは続きの処理を実行する。
- スタックの上から2つめの要素は、ブロックハッシュ(ParamBlockHash)として解釈される。
- ParamBlockHashが28バイトより大きい場合、スクリプトは失敗する。
- ParamBlockHashがParamBlockHashで指定したブロックのブロックハッシュの後半部分と一致しない場合、スクリプトは失敗する。
上記でスクリプトが失敗しなければ、そのまま残りのスクリプトが実行される。
デプロイ
このBIPは、cbahという名称で、BIP-9のversion bitsを使ってデプロイされる。
使用するbitやデプロイの開始時間、タイムアウトはまだ定義されていない。
動機
二重使用からの安全な復元
状況によっては、ユーザーが受け取ったビットコインについて、ブロックに格納される前に使いたい(Tx B1)ケースがある。ただこの場合、ビットコインを送信したトランザクション(Tx A1)が二重使用されているものであれば、ウォレットは必要に応じてTx A1を使用するトランザクションを再発行する(Tx B2)必要がある。二重使用のトランザクション(Tx A2)がウォレットが受信した場合、Tx A1を入力にするTx B1でなく、新しいOutpoint(Tx A2)を使うTx B2を作り署名する必要がある。しかし二重使用がそのウォレットに送られないケースであれば、現状はこの二重使用をリカバリする方法は無い。
OP_CHECKBLOCKATHEIGHT
を追加することで、ウォレットはTX A2が含まれたブロックを検知した場合、Tx B2を作成しこのリスクを排除できる。
ブロックチェーンが分岐した際のリプレイ攻撃への対処
ハードフォーク等によりブロックチェーンが分岐した場合、どちらかのチェーンで有効なUTXOを送金するトランザクションをブロードキャストした際、別のチェーンでも同じ内容の送金するトランザクションが発生する(リプレイ攻撃)ようなことが無い仕組みが望ましい。
こういったリプレイ攻撃が起きないことは、分岐したチェーンのどちらかにしか存在しないブロック(ブロックハッシュ)を選択し、OP_CHECKBLOCKATHEIGHT
を使ってそのブロックがあるチェーンでしか使用できないようUTXOを固定することで保証できる。
ウォレットのベストプラクティス
チェーンが再編成された際に不要なコンフリクトが発生しないよう、ウォレットはラスト100ブロックを指定しないこと。やむを得ず直近のブロックを使う場合は、ネットワークを注意深く監視し、フォークが発生しブロックハッシュが異なるブロックが正になったら、新しいブロックハッシュを使ってトランザクションを再作成する必要がある。そのためセキュリティポリシーに競合しない限り、ウォレットは固定したブロックのconfirmationが少なくとも100になるまで、トランザクションの再作成に必要な秘密鍵をメモリ上に保持する必要がある。
通常の使用では、ウォレットはParamBlockHashを16バイトで指定する必要がる。
論拠
トランザクションのロックタイムとどう違うの?
- ロックタイムはトランザクションが有効になるまでの時間orブロック高を指定する。一方
OP_CHECKBLOCKATHEIGHT
は特定のブロックのブロックハッシュを指定する。
なぜブロック高は相対的なものでなく絶対的な値なの?
- 相対的なブロック高は、N+1ではなくNで有効なトランザクションを作成することができる。これは、Bitcoinで慎重に回避され、指定したブロックが再編成されると、悪意の無いトランザクションは後のブロックを簡単に再確認できる。
どうして52596より前のブロックが検証されないの?
- 全てのブロックヘッダを永遠に保持し続けないといけないような状況を回避するため。52596ブロックのブロックヘッダのサイズは4MB。
- より深いブロックを指定したい場合は、そのブロックに依存するより最近のブロックを指定すればいい。
- 関心のある全てのブロックチェーンで共通のUTXOを二重使用する時間は1年あれば充分。
- より深くチェックするようにするのはソフトフォークでできるけど、より浅くしたい場合はハードフォークになる。
ParamBlockHashが完全なブロックハッシュでなくブロックハッシュの一部なのはなぜ?
- チェーンの分割で、リプレイ攻撃を避けるためには数バイトチェックするので充分。
- 完全なブロックハッシュを使うのに比べてブロックチェーンのディスクスペースの節約になる。
- (
OP_LESSTHAN
)のようなopcodeと1バイトと組み合わせて使用することで、オンチェーンの gambling logicを有効にできる。
ParamBlockHashの先頭が0の場合はどうなる?
- 先頭が0の場合は、実際のブロックハッシュと比較する必要がある。
- 余分なスペースが問題にならないよう、先頭の0が充分な精度で必要になることはない。
- 全てのブロックハッシュは29バイトより小さいので、ParamBlockHashは28バイトより大きくなることはない。
前のブロックと同じように最近のブロックをチェックするのはなぜ安全なのか?
後方互換性
OP_NOP5
はこのような将来の拡張のため、全てのマイナー、ポリシーによって禁止されなければならず、そのため古いマイナーは新しいルールの下で無効なブロックを作ることはない。しかし攻撃の一部としてそのような無効ブロックを受け入れることがないよう、アップグレードする必要がある。
古いノードは、同じ拡張性の理由からこのopcodeを使用したトランザクションをリレーしない可能性もあるが、ルールをブロックの外側のコンテキストで検証できないため重要なことではない。
参照実装
https://github.com/bitcoin/bitcoin/compare/master…luke-jr:cbah
まとめ&所感
- 新しく
OP_CHECKBLOCKATHEIGHT
というopcodeが追加される。 OP_CHECKBLOCKATHEIGHT
はスタック上にブロック高(ParamHeight)とブロックハッシュの一部(ParamBlockHash)という2つ要素を引数として取る。OP_CHECKBLOCKATHEIGHT
は、指定されたブロック高(ParamHeight)のブロックハッシュの後半がParamBlockHashと一致するか評価し、一致しない場合、スクリプトの評価は失敗する。- チェーンが分岐すると分岐以降、2つのチェーンで同じブロック高のブロックハッシュが全く同じになることは基本的にないので、
OP_CHECKBLOCKATHEIGHT
でブロックハッシュを評価することで、必ずどちらかのチェーンでしか利用できないUTXOを作ることができるようになると。 - スクリプトの評価の際に過去のブロックのデータ参照にいくようなスクリプト組むのありなのかー。
- SPVノードもブロックヘッダはダウンロードしてるから、フルノードと同様チェックは可能か。
- 二重使用からの復元が回避できてる理由と、後方互換性のリレーしない問題の大丈夫な根拠がイマイチよく分からない。