Develop with pleasure!

福岡でCloudとかBlockchainとか。

BitVM 2

Bitcoin上で任意のプログラムの検証を可能にするBitVMについて↓

techmedia-think.hatenablog.com

この提案では、チャレンジ&レスポンスを行えるのはコントラクトに参加する二者に限定されていたけど、これを

  • マルチパーティ構成に拡張し、
  • さらに一連のチャレンジ&レスポンスのトランザクションチェーンを2ラウンドに短縮する

BitVM 2という新たな提案が公開された↓のでざっくり見てみる。

BitVM 2: Permissionless Verification on Bitcoin | BitVM

BitVM 2

BitVMでは任意の計算ロジックを回路に変換し、回路の各NANDゲートをそれぞれTapleafにエンコードし、チャレンジ&レスポンスで、そのゲートを1つずつ実行していく構成になっていたが、BitVM 2ではその構成が大きく変わっている。Tapleafのスクリプトを任意の回路の各NANDゲートではなくSNARK検証器となり、単一のゲートに対するチャレンジ&レスポンスではなくSNARK検証器のサブ関数に対するチャレンジ&レスポンスになった。

検証器のプログラムをfとし、入力(プルーフ)をx、そして出力をyとした場合、f(x) = yを検証できれば、オペレーターが正しい計算を行っていることをSNARK検証器を使って検証することができる。

ただ、単純にGroth16などの検証器を実装しようとするとスクリプトが20MBくらいになり、(ブロックサイズが最大でも4MBの)現在のBitcoinではブロックに格納できるサイズのトランザクションではなくなる。

そこで、ランポート署名を利用してf(x) = yを複数のステップに分割する*1。ステップの数を42とした場合、

 f1(x)  = z1
 f2(z1) = z2
 f3(z2) = z3
 ...
 f42(z41) = z42
 f43(z42) = y

のように分割する。するとfの計算をブロックをまたいで43個のトランザクションのシーケンスにする。このアプローチであれば、現状のBitcoinのルールでも一応は動作する。ただ、オンチェーンフットプリントは巨大なままだし、複数のトランザクションに渡って状態遷移をするというオーバーヘッドも発生してしまう。つまり、検証器のプログラムをオンチェーンでそのまま実行するというのは好ましくない。

そこで証明者はSNARK検証器の実行をそのままするのではなく、プルーフxと出力yと各中間状態(z1, z2, ..., z42)にコミットする。たとえば↑のf1〜f43の34個のスクリプトをリーフとしたTaptreeを構成する。そして証明者がコミットしたいずれかの中間状態 {z_i}において {f_i(z_{i-1}) == z_i}が満たされない場合は、ペナルティを与えるようにする。こうすると検証者がFraud Proofにより反証する際に必要なのは、いずれかの {f_i}の不正を証明するだけになる。

これが実際にどうブリッジに適用されるのか見ていく。

パーミッションレスブリッジ

BitVM 2の主な用途は、Bitcoinとロールアップやサイドチェーン間でのブリッジで、そのトラストを最小限に抑えるというもの。Liquidなどのサイドチェーンを利用する場合、現状だとチェーン間の資金の移動時にフェデレーションを信頼する必要があるが、BitVMを利用するとこのトラストを1-of-nまで削減できるということらしい。

https://bitvm.org/permissionless_bridge_v2.png

Peg Inトランザクション

BitVMにコインをデポジットするトランザクションで、ユーザー(アリス)の資金はn-of-nのマルチシグに送金される。ただし、ユーザーがこのトランザクションに署名する前に、Take 1、Take 2、Kick Off、Challenge、Assert、Burnトランザクションが作成され、それぞれのトランザクションについてn-of-nの鍵の所有者とオペレーターが事前署名する。

Kick Offトランザクション

Kick Offトランザクションは、このペグインによるBitVMの状態にコミットする(オペレーターが計算によって生成された出力値yにコミットする)トランザクション。このトランザクションの構成は、

  • インプット:
    • オペレーターの資金が原資でペグインの状態yにコミットする
  • アウトプット:
    • タイムロックされたアウトプット(図では2週間)
    • ネクターアウトプットA:検証者がチャレンジする際に使用する
    • ネクターアウトプットB:インプットの資金をn-of-nのマルチシグに送金し、オペレーターがチャレンジに対する応答として使用するか、応答しない場合は焼却される。
Take 1トランザクション

オペレーターがKick Offトランザクションでコミットした状態に対して、タイムロック期間を過ぎても誰もチャレンジしない場合、その3つのアウトプットとPeg Inトランザクションの資金をインプットにし、それをすべてオペレーターに預け入れるトランザクション

もし誰かがチャレンジを行うと、コネクターアウトプットAがChallengeトランザクションで使用されるため、それに伴いTake 1トランザクションは無効になる。

Challengeトランザクション

オペレーターが主張する値yについて、コネクターアウトプットAを使ってチャレンジを行うトリガーとなるトランザクション。Challengeトランザクションは予めオペレーターによって作成されており、その構成は、

となっており、SIGHASH_SINGLE|ANYONECANPAYで署名されている(つまり誰でも他のインプットを追加できる状態)。この段階で保証金に足りる金額はコネクターアウトプットAには無いので、チャレンジするユーザーが自分のUTXOをインプットに追加し、保証金を支払えるようトランザクションを完成させる必要がある。つまり検証者がお金を払ってチャレンジする。

Assertトランザクション

Assertトランザクションは、検証者のチャレンジに対するオペレーターによるレスポンス。Challengeトランザクションが承認されるとTake 1トランザクションはもう使えないので、オペレーターはAssertトランザクションでレスポンスする必要がある。タイムロック期間内(図では4週間)にレスポンスしない場合はBurnトランザクションが実行される。

トランザクションの構成は、

  • インプット:
  • アウトプット:
    • タイムロックされた(図では2週間)オペレーター宛のアウトプット*2
    • ネクターアウトプットC:検証者によって不正が証明されるか、証明されずにオペレーターが入手する際に使用される。
Take 2トランザクション

検証者がAssertトランザクションのオペレーターのレスポンスに不正を証明しない場合は、タイムロック後にPeg Inトランザクションの資金とAssertトランザクションのアウトプットをインプットにし、それをすべてオペレーターに預け入れるトランザクション。つまり、チャレンジ&レスポンスを経由して、ペグインされる場合に使用されるトランザクション

Disproveトランザクション

Assertトランザクションでコミットされた中間状態のいずれか( {z_i})の誤りを検証者が指摘できた場合に、検証者がコネクターアウトプットBの資金を没収するトランザクション

Burnトランザクション

Challengeトランザクションによりチャレンジがトリガーされたにも関わらず、オペレーターが何も応答しない場合、コネクターアウトプットBの資金を焼却するトランザクション*3

1-of-nトラスト

最後に制限としても挙げられているけど、これをワークさせるにはn人の内少なくとも1人は正直に動作するオペレーターが必要になる。

ただ、以下の疑問が残る。

  • n人の内1人でも悪意あるメンバーがいると、Assertトランザクションで偽の中間状態にコミットできてしまうのでは?その場合、失われるのはオペレーターの資金になって自分を攻撃するようなものかもしれないけど、自分がVerifierになってしまえばDisproveトランザクションでその資金を手に入れられるのでは?
  • Disprove / Burnになった場合、ペグインの資金はn-of-nにロックされたままになってしまうので、Peg Inトランザクションのアウトプットにはタイムロック条件が必要なのでは?

参考

↑のBitVM 2のサイトは簡単にしか書かれていないので、Bitcoin Magazineの記事が参考になる↓

https://bitcoinmagazine.com/technical/bitvm-2-opening-up-the-playing-field

*1:ここでランポート署名と呼んでいるのは、この辺りのスライド見たところ、実際には2つのハッシュロックを使ったbit commitmentのことだと思われる。

*2:2つめのオペレーター側の条件にタイムロック付けたら、このアウトプット要らないように思えるけど、どうなんだろう?

*3:図で出てくるVerifier Yはどういう位置づけなんだろう?

Bitcoinのブロック1,983,702 問題

Bitcoin Optechのニュースレター#296で、コンセンサスクリーンアップの提案が再検討されているという話の一部に、ブロック1,983,702問題が取り上げられていたので、その内容について。

BitcoinのUTXOは、そのアウトプットが含まれるトランザクションのTXIDとトランザクション内のそのアウトプットのインデックスで識別される。そのためチェーン上でUTXOを一意に識別できるようになっている必要があるけど、TXIDが同じ値になるような重トランザクションが作成されるとUTXOの一意な識別ができなくなってしまう。

TXIDの重複

TXIDはトランザクションデータから生成されるハッシュ値であるため、通常の支払いに使用するトランザクションのTXIDが重複するようなことは確率的にほぼないけど、コインベーストランザクションについては(インプットが参照する)親トランザクションが存在しないため、同じTXIDを持つトランザクションを作るのは簡単になる。

過去、実際にTXIDが重複するコインベーストランザクションが作成されている↓

TXIDが重複したUTXOはどうなる?

これは重複するトランザクションが作成されたタイミングによって挙動が変わる。

  • 重複するトランザクションを作成する前に、先に作成したUTXOを使用していた場合、重複トランザクションのUTXOも使用可能。
  • 重複トランザクションが作成された段階で、先に作成したUTXOも未使用であった場合、既存のコインベースのUTXOが上書きされ1つがUTXOとして認識されなくなる。

↑の実際の重複では、いずれも後者のケースなので2つのコインベースのマイニング報酬(100 BTC)がGoxしたことになる。

そして前者のケースはまた別の問題が発生する。同じTXIDのUTXOのコインを使用するトランザクションは、まったく同じものが後者のトランザクションでも使用できるので、両者の子トランザクションにおいてTXIDの重複を再度発生させることができる。さらにそのトランザクションに複数のアウトプットがある場合、大量の重複トランザクションの再生産が可能になってしまう。

重複に対する対応

↑が発生したことから

BIP-30

まず、TXIDの重複を禁止するBIP-30が導入された。タイムスタンプが2012 年 3 月 15 日 00:00 UTC以降のブロックに対して、チェーン内の未使用なUTXOと同じTXIDを持つトランザクションをブロックに含めることはできないというコンセンサスルールを追加した。*1

BIP-34

その後、BIP-34の導入により、コインベーストランザクションのscriptSigの先頭にそのブロック高をセットするというコンセンサスルールが追加され、ブロック227836から適用されるようになった。

ブロック1,983,702問題

BIP-34のルールの導入により、同一のTXIDを持つコインベーストランザクションを作成することが困難になったため、BIP-30のチェックで発生するDBアクセスを考慮し、Bitcoin Core 0.12.0からBIP-30のチェックがスキップされるようになった↓

github.com

しかし、その後BIP-34の欠陥が見つかる。BIP-34適用前のブロックにおいて、将来のブロックのブロック高がscriptSig内にエンコードされているブロックが複数存在することが発覚した。その最初のブロックがブロック高1,983,702のブロック(20年以上先のブロック)。

問題のコインベースは、ブロック164,384に存在する↓

https://blockstream.info/tx/3aa03753fc238b38f4db9e5889362f3a6f0900d602baa8c9a7e789cb13ca1462?expand

scriptSigの先頭にd6441eとある。このリトルエンディアンの値が1,983,702。

そのため、ブロック高1,983,702でTXIDが重複するコインベーストランザクションが作成される可能性がある。ただ、ブロック164,384のコインベーストランザクションと同じトランザクションを作成するには、

  • コインベーストランザクションで、170BTC近くを手数料として収集する必要がある。
  • マイナーはブロック164,384のコインベースと全く同じ金額を同じ宛先に送付する必要がある
  • Segwitトランザクションはマイニングしてはならない。

といったかなり高いハードルがあり、現実的には無理だろう。

適用済みの対策

しかし、実際に作れてしまうとBIP-30に違反するブロックを作成してしまうことになるため、Bitcoin Core 0.17.0でブロック高1,983,702以降では再度BIP-30のチェックを実行する変更が入っている↓

github.com

今後の対応

ブロック高1,983,702以降では再度BIP-30チェックが実行されるようになっているものの、BIP-30チェックによるオーバーヘッドは減らしたい。ということで、BIP-34を改良してコインベーストランザクションに追加のデータを含めるようなコンセンサスを追加しようというのが、冒頭のコンセンサスクリーンアップの提案の中の1つ。

参考

*1:その後、このタイムスタンプからだいぶ経過したところで、↑の違反ケースを除くすべてのブロックにこのルールが適用された

ASMapファイルを作成してみる

Bitcoin Coreはv0.20.0からASMapを利用したアウトバウンドピアの選択をサポートしている↓

Bitcoin Core 0.20.0でASマップを使ったピア選択を有効にする - Develop with pleasure!

このASMapを作成して配布するプロセスについて最近投稿されていたので↓

delvingbitcoin.org

Fabian Jahrが作成したツール↓を使ってASMapファイルを作成してみた。

https://github.com/fjahr/kartograf

Kartograf

セットアップに必要なので、パッケージマネージャーNixをインストールして、~/.config/nix/nix.confに以下を設定して、Nix flakesを有効にしておく。

experimental-features = nix-command flakes

↑のリポジトリをcloneして、

$ nix develop

を実行する。

ASMapの作成

RPKIを使用して、ASNへのIPプレフィックスのマップを作成する。RPKI(Resource Public Key Infrastructure)は、IPアドレスやAS番号などの資源が分配された際にその所有を証明する認証基盤。JPNICのブログで詳しく解説されてる。

$ ./run map

このmapコマンドを実行すると、まず最初に. 5箇所の地域インターネットレジストリ(RIR)からTAL(Trust Anchor Locator)ファイルを取得する。取得したファイルは<cloneしたリポジトリのパス>/data/<実行時のタイムスタンプ>/rpki/tals/ディレクトリ以下にある。

各TALファイルにはルート認証局(各RIR)の公開鍵と証明書のURLが記載されている。例えばAPNICのTALファイルは↓

https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal

https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
0wIDAQAB

最初のURLが、ルート証明書のURLで、最後のBase64エンコードされた文字列がルート認証局の公開鍵。

次に、ダウンロードしたTALファイルを指定してrpki-clientが実行され、RPKIデータの検証が行われる。この時、ルート証明書から証明書チェーンを辿り、各認証局が発行した証明書やROA(Route Origin Authorization)ファイルが<cloneしたリポジトリのパス>/data/<実行時のタイムスタンプ>/rpki/cache/ディレクトリ以下にダウンロードされる(現状約1.7GB)。

ROAの検証が終わるとIPアドレスとAS番号のマッピングファイルが、<cloneしたリポジトリのパス>/out/<実行時のタイムスタンプ>/rpki/rpki_final.txtとして出力される。

-irrオプション

-irrオプションを付けてmapコマンドを実行すると、上記に加えて、インターネット上の経路に関するデータベースであるIRR(Internet Routing Registry)のデータも使用される。Kartografのデータの取得先はこちら

-rvオプション

-rvオプションを付けてmapコマンドを実行すると、上記に加えて、世界中から集められたBGP経路情報を提供するオレゴン大学のRouteViewsプロジェクトのデータも使用される。

AMD® Ryzen 9 5950x 16-core、メモリ64GBの環境で -irr-rvオプション付けて実行すると約4時間かかった(そしてマシンリソースがっつり持ってかれる)。

最終的にマージされたASMapファイルが<cloneしたリポジトリのパス>/out/<実行時のタイムスタンプ>/final_result.txtというファイルに出力される。約30MB。

バイナリ変換

↑で生成したASMapはテキスト形式でIPアドレスとAS番号がマッピングされたデータになるけど、Bitcoin Coreに-asmapオプションで渡す場合にはバイナリデータに変換する必要がある。

この変換を行うツールをBitcoin Coreに導入するためのPRが作られてる↓

github.com

PRにある、asmap.pyasmap-tool.pyを使えば、テキストファイルとバイナリファイルの変換をそれぞれ行える。

# テキスト形式からバイナリ形式に変換
$ python3 asmap-tool.py encode <ASMapのテキストファイルのパス> <変換先のファイルパス>

# バイナリ形式からテキスト形式に変換
$ python3 asmap-tool.py decode <ASMapのバイナリファイルのパス> <変換先のファイルパス>

バイナリ形式に変換できたら、以前の記事↑のように、-asmapでASMapを指定してBitcoin Coreを起動すればいい。

Vault用のopcodeの導入を提案するBIP-345

少し前に、BitcoinでVaultを実現するために2つのopcodeを導入するソフトフォークの提案がBIP-345として登録された↓

https://github.com/bitcoin/bips/blob/master/bip-0345.mediawiki

Vaultとは?

Vault(金庫)は、その名前から分かるようにビットコインを安全に保管するための仕組み。

Bitcoinのような暗号通貨の場合、秘密鍵が漏洩/盗難にあうと、それは資金の損失につながる。金額が大きくなるほど、秘密鍵漏洩のリスクが大きくなる。自分が知らない間に盗まれた秘密鍵によって資金が勝手に使われることがないように、より堅牢な資金保護の仕組みを提供するのがVaultの目的。

flowchart LR
  A[UTXOをVaultへデポジット] -.-> C[保管されたコインを事前に指定したリカバリーパスに移動]
  A ---> B[引き出しをトリガー] 
  B -.-> C
  B --タイムロック期限後--> D[引き出しの完了]

具体的には、資金を以下の機能を持つVault(スクリプト)に預ける:

  • 預けた資金を引き出す場合、引き出しを申請してから資金を実際に手に入れるまで一定の待機期間がかかる。
  • 待機期間の間であれば、申請した引き出しを取りやめ資金をリカバリーすることができる*1

つまり、攻撃者に秘密鍵を奪われ、攻撃者がその資金を引き出そうとしても、そこには遅延時間が設けられ、それに気づいた所有者がその間に資金を取り戻せるようにしようというもの。

このようなVaultの提案は、

などがある。

BIP-345 Vault

BIP-345の提案はこれまでの汎用的なCovenantsを使用したVaultの構成ではなく、Vault専用の2つの新しいopcodeをソフトフォークで導入し遅延時間とリカバリーを可能にする特殊なCovenantsを可能にしてVaultを構成できるようにしようというもの。2つの新しいopcodeは、いずれも、Tapscriptで予約されているOP_SUCCESS系のopcodeを再定義することで導入される(witness version 1=Taprootを使用した構成になる)。

上記のVaultの操作をサポートするのに、以下の4種類のトランザクションが登場する:

Vaultへのデポジット

VaultトランザクションでVaultに資金をデポジットするには、少なくとも2つのリーフ(トリガー用、リカバリー用)を持つTaptreeを使って以下のようなTaprootのscriptPubkeyを構成する。

graph BT
  A[トリガーリーフ] --> B[Taptree]
  C[リカバリーリーフ] --> B
  D[Internal key] --> E[Vault P2TR]
  B --> E

このP2TRアドレスに送られたコインを使用する方法は、各リーフを使ってアンロックする方法と、内部鍵(Internal key)を使ってSchnorr署名を作成する方法*4の3通り。2つのリーフのスクリプトのは以下のような構成になる:

トリガーリーフ

トリガーリーフは新しく導入されるOP_VAULT opcodeを使った以下のようなスクリプトになる。OP_VAULTは、OP_SUCCESS1870xbc)を再定義する形で導入される。

[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT

trigger-authの部分は、この条件を使ってVaultからの引き出しをトリガーするための認可条件で、ウォレットの設計者が任意の条件を設定できる。例えばある公開鍵の秘密鍵を持つものに対して認可する場合は、trigger-authは以下のスクリプトになる。

<trigger-auth-pubkey> OP_CHECKSIGVERIFY

つまり、公開鍵trigger-auth-pubkey対して有効な署名を作れる人のみがこれをトリガーできる。

残りの部分は、OP_VAULTによって評価されるものなので、OP_VAULTの挙動と合わせてみていく。

OP_VAULTはスタックに以下の項目がある前提で実行される:

<leaf-update-script-body>
<push-count>
[ <push-count> leaf-update script 用のデータ項目の数 ]
<trigger-vout-idx> 
<revault-vout-idx>
<revault-amount>

leaf-update-script-bodyは、OP_VAULTによって更新されるリーフスクリプトの断片。サンプルとして以下のスクリプトが掲載されている。

OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY

OP_CHECKTEMPLATEVERIFYは未導入なので、導入されていない場合は以下のようなスクリプトになる。

OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG

いずれにせよ、相対的なタイムロック(OP_CSV)が付与され、これが引き出しのトリガーに対して遅延時間を設定する。

OP_VAULTの動作は↓

  1. スタックからleaf-update-script-bodyをポップし、
  2. スタックからpush-countをポップする。
  3. push-count個分の要素をスタックからポップする。
  4. 3の要素をプレフィックスとして1のスクリプトと結合し、leaf-update-scriptを完成させる。
  5. スタックから、trigger-vout-idxrevault-vout-idxrevault-amountをポップする。
  6. スクリプトのsigopsバジェットを60デクリメント*5
  7. trigger-vout-idxがwitness v1 program(Taproot)かチェック
  8. 現在評価中のリーフスクリプトを4のleaf-update-scriptと置き換えてTaptreeを更新し、そから導出したscriptPubkey(P2TR)と、triggerOutのscriptPubkeyが合致するかチェック
  9. revaultOutのscriptPubkeyが現在評価中のscriptPubkey(P2TR)と合致するかチェック
  10. triggerOutの金額とrevaultOutの金額が現在評価中のインプットの金額以上であることをチェック
  11. インプットの金額からrevaultOutの金額を差し引いた金額がtriggerOutの金額になっていることをチェック
  12. すべての検証をパスしたらスタックにTrueをプッシュ

というもの。つまり、OP_VAULTは、

  • Taptree内のトリガーリーフのスクリプトを、leaf-update-script-bodypush-count個分のデータで構成されるスクリプトに更新した新しいP2TRスクリプトトランザクションアウトプットに作られるように強制する。
    • これにより、Vault作成時の条件の内部鍵とリカバリーリーフは同じまま、トリガーリーフのみが遅延時間を設けたスクリプトに更新された宛先宛にVaultの資金を移動する。
    • リカバリーリーフは同じままなので、移動した資金はリカバリー条件を使っていつでも回収できる。
  • Vaultから資金を引き出す際に、全額引き出すのでなく、一部だけ引き出せるように、残しておく資金は元のVaultと同じ宛先に移動するRevault機能をサポートしている。
    • 全額引き出す場合はrevault-vout-idx-1を設定し、revault-amountは0
トリガートランザクション

先程のトリガーリーフのスクリプト

[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT

について、leaf-update-script-bodyOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFYとした場合、トリガートランザクションでこの条件をアンロックするためにwitnessとして提供される項目をスタックに入れると↓

<Taprootのcontrol block(トリガーリーフへのマークルパスを含む)>
<トリガーリーフスクリプトのペイロード>
<trigger-auth-pubkey-signature>
<target-CTV-hash>
<trigger-vout-idx>
<revault-vout-idx> (Revaultしない場合は-1)
<revault-amount>

となり、このスタックに対してTaprootのscript-pathの検証が行われ、トリガーリーフの認可条件(trigger-auth)が実行され、その後OP_VAULTを実行する際のスタックの状態は↓

<leaf-update-script-body>
2
<遅延時間>
<target-CTV-hash>
<trigger-vout-idx>
<revault-vout-idx>
<revault-amount>

ここでは、push-count = 2であるため、遅延時間target-CTV-hashがスタックからポップされ、それをleaf-update-script-bodyと結合したスクリプトが更新用のスクリプトになり、triggerOutのP2TRの構成要素になる。

leaf-update-script-bodyOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIGの場合は、target-CTV-hashの代わりに公開鍵をwitnessで提供する形になると思われる。

トリガートランザクションでは、リカバリーリーフと、↑で更新された遅延付きスクリプトleaf-update-script)で構成されるP2TR宛にVaultの資金が移動されるので、遅延期間が終われば、leaf-update-scriptを満たせす引き出し用のトランザクションを完成させ、資金の引き出しが完了する。

target-CTV-hashや、Revaultの情報はトランザクションのwitnessで提供されるため、Vaultから資金を引き出す際に、引き出し先や金額を決めればいいようになっている。これまでのCovenantsではこれらはVaultにデポジットする段階で決めておく必要があった。

リカバリーリーフ

リカバリー用のリカバリーリーフは新しく導入されるOP_VAULT_RECOVER opcodeを使った以下のようなスクリプトになる。OP_VAULT_RECOVERは、OP_SUCCESS1880xbd)を再定義する形で導入される。

[recovery auth] <recovery-sPK-hash> OP_VAULT_RECOVER

recovery authはオプションで、この条件でリカバリーをするための認可条件で、trigger-authと同様ウォレットの設計者が任意の条件を設定できる。

OP_VAULT_RECOVERはスタックに以下の項目がある前提で実行される:

<recovery-sPK-hash>
<recovery-vout-idx>

recovery-sPK-hashは元々リーフスクリプト内にあるデータで、32バイトのハッシュ値

OP_VAULT_RECOVERの動作は↓

  1. スタックからrecovery-sPK-hashをポップし、32バイトかチェック
  2. スタックからrecovery-vout-idxをポップする。
  3. recoverOutのscriptPubkeyについてタグ付きハッシュtagged_hash("VaultRecoverySPK", recoveryOut.scriptPubKey)を計算し、その値がrecovery-sPK-hashと一致するかチェック
  4. recoverOutの金額がインプットの金額以上であるかをチェック
  5. すべての検証をパスしたらスタックにTrueをプッシュ

というもの。つまり、OP_VAULT_RECOVERは、リーフスクリプトでコミットされている宛先(recovery-sPK)にVaultのコインが全額送られることを強制する。

RBFのシグナリング

また、コンセンサスルールではないけどBitcoin Coreのポリシーとして、Pinning攻撃を防ぐため、リカバリーリーフを使用するリカバリトランザクションは、RBFによるトランザクションの置換可能性をシグナリングする必要がある。

具体的には、リカバリトランザクションnVersionが3ではない場合*6OP_VAULT_RECOVERインプットのnSequence0xffffffff - 1未満でなければならない。

OP_CTVとの関係

↑のトリガーリーフ内のleaf-update-script-bodyでは、BIP-119のOP_CHECKTEMPLATEVERIFY(Covenants)を使って、引き出し先を指定する方法が推奨されている。また、このソフトフォークの展開も、BIP-119と同時であることが望ましいとされている。

BIP-345は厳密にBIP-119に依存しているわけではないものの、OP_VAULTによる指定されたTapleafの更新とOP_VAULT_RECOVER自体には、遅延時間の設定や送付先のアドレスの強制は機能として含まれない(外だしされている)ので、OP_CSVによる遅延やOP_CTVによって最終的な宛先を強制する仕組みとの組み合わせが必要になる。

ただ、トリガートランザクションで指定するtarget-CTV-hashで、引き出し先をOP_CTVでコミットしたいケースってどんなケースなんだろう?

*1:よりセキュアに管理さているオフラインの鍵を利用するなど

*2:安全な一時鍵の削除や、金額と引き出しのパターンに事前コミットする必要があるなど、いくつかの制約がある

*3:金額、宛先手数料の管理はすべて事前に決めたものになる

*4:内部鍵はリカバリーパスで使用する鍵と同等のセキュリティが必要になる。内部鍵によりこのP2TRがVaultであったことが開示されなくなるというメリットはあるもの、セキュリティ上の懸念がある場合は内部鍵を使用不可能なNUMSポイントにするのも可。

*5:8のtrrigerOutの有効性チェックの際に、TaprootのControl blockの長さに比例する楕円曲線スカラー乗算とハッシュ計算の実行が必要になるため、そのコストをsigopsのカウントに加味する

*6:v3トランザクションリレーが導入されると、バージョン3のトランザクションはすべて置換可能性のシグナルになるため。

Bitcoin Coreに実装された変異ブロックの早期検証

最近Bitcoin Coreにマージされた変異ブロックに対する早期検証ロジック↓

github.com

変異ブロックとは?

変異ブロック(Muted Block)とは、新しく生成された有効なブロックの一部を変更した無効なブロックではあるけど、ハッシュ値が元のブロックと同じブロック。こういった変異ブロックを作成する方法は、いくつかある。

1.マークルツリーの曖昧さを利用する

Bitcoinでブロックを作成する際、ブロック内の全トランザクションのハッシュをリーフノードとしたマークルツリーを構成し、そのルートハッシュをブロックヘッダーにセットするようになっている。

ただ、このトランザクションのコミット方法とルートハッシュの計算方法には、同じマークルルートを導出可能なハッシュのリストを簡単に構築できるという欠陥がある。トランザクションの数が奇数個の場合、不足する最後の要素はその前の要素のハッシュをコピーして偶数にしてツリーの親ノードを計算するようになっている。

例えば、リーフノードの値がa, b, cの3つのリストにコミットするマークルツリーのルートハッシュと、リーフノードがa, b, c, cの4つのリストにコミットするマークルツリーのルートハッシュは同じ値になる。

つまり、トランザクションが奇数個のブロックについて、最後のトランザクションをコピーしてそれを追加したブロックを作れば無効な変異ブロックを作成することができる。最後に追加したトランザクションは1つ前のトランザクションと同じであるため(同じUTXOを二重使用しようとするトランザクションなので)コンセンサスルール上無効なトランザクションになる。

ちなみに、こうった曖昧さの問題もあり、Taprootのスクリプトツリーを構成する際は、タグ付きのハッシュを利用するようになっている。

2.トランザクションのwitnessデータを変更する

Bitcoinのブロックヘッダーは上記のようにトランザクションのデータから作成される値にコミットするようになっているけど、この時segwit系のトランザクションのwitnessデータは、このハッシュには含まれていない。ただ、ブロックヘッダーにはコミットされないけど、代わりにブロック内のコインベーストランザクション内にコミットされる。witnessを含むトランザクションのハッシュについて、上記と同様にマークルツリーを構成し、そのルートハッシュをコインベーストランザクションのアウトプットにセットする必要がある。

ただ、ブロックヘッダーにはコミットされないので、ブロック内のsegwitトランザクションのwitnessデータを変更し(無効なものにし)ても、ブロックヘッダーのハッシュ値は変わらないため、これを利用して無効な変異ブロックを作成することができる。

3.2つのトランザクションハッシュ値と合致する無効なトランザクションを作成する

方法は1と似ているけど少しだけ複雑。2つの有効なトランザクションのハッシュ(それぞれ32バイト)を連結した値が(無効だけど)トランザクションとしてパースできるような値になるような、2つのトランザクションを生成する。

たとえば、2つのトランザクションTx1とTx2を持つブロックを考える。この場合のマークルルートは、以下のようになる。

graph BT;
    A[Tx1] --DSHA256--> B["H(Tx1)"]
    C[Tx2] --DSHA256--> D["H(Tx2)"]
    B --> E["Root Hash<br>DSHA256(H(Tx1) || H(Tx2))"]
    D --> E

ここで、H(Tx1) || H(Tx2))の値がトランザクションとしてパース可能なデータである場合(Tx3 = H(Tx1) || H(Tx2)))、Tx3のみを含むブロックのマークルルートは、

graph BT;
    A[Tx3] --DSHA256--> B["Root Hash"]

となり、この2つのマークルルートの値は一致し、無効な変異ブロックを作成することができる。

Tx3はTx1とTx2のハッシュの連結値で構成されるため、64バイトのトランザクション。このようなトランザクションを作成する方法については、以下のペーパーで解説されてる↓

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/attachments/20190225/a27d8837/attachment-0001.pdf

Bitcoinトランザクションは、

フィールド サイズ
version 4 byte
インプットのリスト 可変長
アウトプットのリスト 可変長
locktime 4 byte

で構成されている。インプットとアウトプットのリストの先頭には、その個数がCompactSizeエンコードされている。各インプットは、

フィールド サイズ
OutPoint 36 byte
scriptSig 可変長
sequence 4 byte

各アウトプットは、

フィールド サイズ
value 8 byte
scriptPubkey 可変長

という構成になる。scriptSigとscriptPubkeyは、その先頭にCompactSizeエンコードされたそれぞれのサイズが付与される。

64バイトのパース可能なトランザクションになるためには、

  • インプットとアウトプットの数は1つだけ。つまり、それぞれのリストのサイズは1 byteの0x01でエンコードされなければならない。
  • scriptSigとscriptPubkeyの合計サイズが4 byteでなければならない。

という制約がある。scirptSigが空でscriptPubkeyを4 byteとした場合、トランザクションのデータは以下のように制約される(xは任意の値でOK)↓

xxxxxxxx01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx00xxxxxxxx01xxxxxxxxxxxxxxxx04xxxxxxxxxxxxxxxx

Tx1のハッシュが上記の前半32 byte中1 byte衝突し、Tx2のハッシュが後半32 byte中3 byteと衝突するような2つのトランザクションを用意できればいい。このような衝突は、少ない計算量の総当りで見つけることができる。

一方、無効ではない有効なトランザクションを見つけるのは、制約されるデータ長が増加するため計算上実行不可能な作業になる。

CVE-2012-2459

上記のような無効な変異ブロックは当然ながらコンセンサスルールに合致しないのでリジェクトされる。ただ、昔のBitcoin Coreの実装では、このリジェクトしたブロックのハンドリング方法に問題があった(2012年に開示されたCVE-2012-2459)。

新しく生成されたブロックについて、攻撃者が上記のような変異ブロックを生成し、変異ブロックの方を先に受信したノードは、そのブロックを無効なブロックとしてマーキングする。そのノードはそのブロックハッシュを無効なものとしてマーキングしているため、その後再起動するまで、同じブロックハッシュを持つ有効なブロックを再度受け入れなくなり、最長チェーンから分離されてしまう。つまり、エクリプス攻撃の一種が実行されてしまう。

この脆弱性は、変異ブロックについてはそれを拒否しても無効なブロックハッシュとしてキャッシュしないようにすることで修正された。

ただその後、2017年にBitcoin Core v0.13.0に加えられた最適化により、このキャッシュの問題が再度発生し、v0.14.0で修正されるということもあった。

検証ロジック

今回のPR#29412では、BLOCKメッセージでブロックを受信した際、そのブロックを処理する前に、それが変異ブロックかどうかを先に検証するロジックを追加している。

具体的には、validatoin.cppにブロックが変異ブロックかどうかを検証するIsBlockMutated関数が追加され、新しく受信したブロックを処理する前に変異ブロックかどうかチェックし、変異ブロックだった場合はそれを送信したピアのスコアを下げ、その後の処理を行わない。

IsBlockMutated関数は、ブロックに対して以下の検証が行う。

  • ブロック内のトランザクションリストからマークルルートを計算し、ブロックヘッダーのマークルルートと一致するか検証
  • 上記マークルルートの計算中に、ツリー内の兄弟ノードのハッシュ値が同じ値のものがないかチェック(作成方法1のチェック)
  • ブロック内の先頭のトランザクション(コインベーストランザクション)のprevoutが空かチェックする。
    • 空でない場合、ブロック内のトランザクションでサイズが64バイトのものがないかチェックする(作成方法3のチェック)
  • コインベーストランザクションのインプットのwitnessを検証。要素数は1つのみで、そのサイズは32バイトでなければならない。
  • ブロック内のトランザクションのリストからwitnessマークルルートを計算し、コインベーストランザクションのアウトプット内にあるwitness commitmentと一致するか検証(作成方法2のチェック)
  • Segwitアクティベート前のブロックのトランザクションについて、witnessデータが含まれていないこと。