Develop with pleasure!

福岡でCloudとかBlockchainとか。

マークルツリーを利用したハッシュベースのアキュムレータ「Utreexo」

BitcoinのUTXOセットを管理するにあたって、そのストレージ要件を大幅に削減すると期待されているアキュムレータ。昨年のScaling Bitcoinでは、スタンフォード大学のBenedikt BünzがRSAを利用したアキュムレータを紹介した↓

techmedia-think.hatenablog.com

↑はその性質上Trusted Setupを必要とするという課題があり、Trusted Setupを回避するためにClass Groupを使用するという提案もあるがまだ新しい暗号プリミティブであるという課題はある。

そんな中、今回Lightning Networkのホワイトペーパーの共著者でもあるThaddeus Dryjaがマークルツリーを利用したハッシュベースのアキュムレータ「Utreexo」を発表した↓

https://eprint.iacr.org/2019/611.pdf

アキュムレータの構造

Utreexoは、要素のセットを完全二分木のフォレスト(森=複数の完全二分木が存在する)で管理する。

例えば要素の数が3つある場合、以下の2つのツリーが構成される。

f:id:techmedia-think:20190614110058j:plain

Aは1,2の要素から生成された親ノードのハッシュ値。この場合、アキュムレータは各ツリーのルートであるAと3の値を管理できれば良い。

要素の追加

さらに要素4を追加すると、フォレストは以下のような1つのツリーになる。

f:id:techmedia-think:20190614110110j:plain

この場合、アキュムレータはルートCの値だけ管理すれば良い。

さらに要素5を追加すると、2つのツリーになり、アキュムレータはCと5を管理する。

f:id:techmedia-think:20190614110133j:plain

このような形で、アキュムレータは要素を完全二分木になるよう追加していく。新しく要素を追加する際も、各ツリーのルートハッシュだけ知っていれば新しいフォレストを構成できる。

この完全2分木の構造には効率的な特性がある。要素の数=リーフの数さえ分かれば、どのようなフォレストが構成されるかはリーフの数をバイナリ表現にすると明白になる例えば上記の要素の数5を例にすると、そのバイナリ表現は101となる。フォレースと内のツリーの数はこのバイナリ表現の1が立っているビットの数と等しく、それらの木の高さはbit 1が立っている位置から分かる。例えば、101であれば、そこから高さ1と高さ3のツリーが2つあることが分かる。

要素の包含証明

要素の追加については↑のようにフォレストの各ツリーのルートハッシュを管理すればいいことが分かったので、続いて要素の証明について。

要素がアキュムレータ内に存在するかどうかは包含証明(inclusion proof)を提供することで証明する。

この包含証明は

  • リーフの位置
  • ハッシュのリスト

で構成される。ハッシュのリストには、その要素が存在するツリーにおいて、その要素の兄弟のハッシュと、さらにそこからルートのハッシュを計算するのに必要な中間ノードのハッシュが含まれる。

アキュムレータと包含証明が与えられると、アキュムレータは要素の総数からフォレストの構成を知っているので、包含証明内のリーフの位置から、対象の要素がフォレスト内のどのツリーに属するものか判断する。ツリーが特定できると後は、包含証明のハッシュのリストと、アキュムレータが管理しているそのツリーのルートハッシュを使って、要素がアキュムレータ内に存在するか検証する。

例えば↑の5の要素があるツリーにおいて、要素3の包含証明は、

f:id:techmedia-think:20190614150608j:plain

3の位置と、要素3と、その兄弟である4のハッシュと、さらにルートを計算するのに必要なAのハッシュとなる。これらの包含証明からルートハッシュを計算し、それがアキュムレータの管理するハッシュと等しければ要素がアキュムレータ内に存在することが分かるという仕組みだ。この辺のマークルツリーの復元とルートハッシュの計算は、現在のBloom Filterを利用したSPVクライアントがブロック内のトランザクションの有無を検証する手法と基本的に同じだ。

要素の削除

削除する際は、アキュムレータにその要素の包含証明(inclusion proof)が与えられ、該当する要素がアキュムレータ内に存在するか検証し、存在する場合要素を削除する。Bitcoinでは、あるトランザクションでUTXOが消費された場合にそのUTXOの包含証明が添付され、アキュムレータに対して要素の存在検証が終わったのち、アキュムレータから要素を削除する。このようなケースは基本的に新しいブロックを受信した際に行われ、その際には大量の新規UTXOの追加と大量の既存UTXOの削除が行われる。このため1つ1つの要素をアキュムレータから削除し都度アキュムレータを更新するよりも、削除対象の要素のセットを一括で処理してしまう方が必要なハッシュ計算の回数が少なくて済む。

削除では以下のTwin、Swap、Root、Climbという4つのステップ順番に繰り返す。

Twin

まず最初に処理されるのがTwinで、左右の兄弟両方が削除される場合の削除を指す。

f:id:techmedia-think:20190614153206p:plain

例えば↑の場合、0と1が削除対象のリストに入っている要素。Twinで削除されると更にその親の要素4が削除リストに加えられる。

Swap

f:id:techmedia-think:20190614153425p:plain

↑のように要素1, 2が削除対象な場合、要素3の要素1があった場所に移動する。そして親の5は次の行を処理する際の削除リストに追加される。

この時ノードは要素3のハッシュ値について知っている必要があるが、ツリーのルート値しか知らないアキュムレータにはそれが分かるのか?と疑問に思うかもしれないが、要素3のハッシュ値は要素2を削除する際の包含証明で与えられているのでそれを利用する。

Root

TwinやSwapが起きるのはその行に対して偶数数の削除が発生する場合だ。削除の要素数が偶数の場合Rootステップはスキップされる。逆に要素の削除数が奇数の場合、Twin、Swapフェーズ終了後1つの削除要素が残った状態になり、この削除要素はRootステップで処理される。

Rootステップでは、処理する行にルートが存在する場合、ルートを削除の位置に移動する。

f:id:techmedia-think:20190614155323p:plain

↑の場合、要素3が削除されているが同じ行にルート要素4があるので要素4を3の位置に移動して終了。

その行にルートが存在しない場合は、削除対象の兄弟をその高さのルート位置に移動し、フォレスト内に新しいツリーを作成する。

f:id:techmedia-think:20190614155528p:plain

↑の場合、要素2は高さ1のツリーのルートに昇格する。2があった位置は削除対象になる。そのため2,3の削除がTwinに該当するので、その親の9も次の行での削除対象としてマークされる。

Climb

Rootステップが終了すると、Climbステップではフォレストのレベル間を移行する。各ステップを適用し移動後の親の再計算や削除が終了すると、ツリーの1つ上のレベルに上がってまたTwinから始める。これをフォレストの頂点に達するまで続ける。

Bitcoinへの適用

UTXOモデルを採用しているBitcoinでは、トランザクションには、インプットとアウトプットがあり、インプットは以前に作られたアウトプットを参照し使用する。この設計では、データ・セットに対する唯一の操作は作成、読み取り、削除で、記録後の要素のセットには削除以外に変更を加えることはできない。新しいトランザクションによって既存のUTXOが消費され新しいUTXOが誕生する。Bitcoinのフルノードはこの状態変化をすべて検証している。IBDと呼ばれる初期同期プロセスでは、ユーザーは200GBを超える履歴をダウンロードし、何億ものデジタル署名を検証する必要がある。そして最終状態においてそのUTXOセットは4GB近くになる。

現在のBitcoinのフルノードはUTXOセットをディスク上のデータベースに保存している(levelDB)。このUTXOは作成されてから使用される直前までの期間、システムに影響を与えずデータベースエントリーにアクセスされることもなく、それが使用される場合のみディスクから読み取られる。

そのため、消費されるまでアクセスされないUTXOを保存しなくて済む設計でにできると、フルノードを運用するハードウェア要件が緩くなる。現在でもRaspberry Piなんかの小型デバイスでフルノードを動かすことはできるが、基本的にUTXOセットにはサイズ制限が無いので、将来的に利用者の増加に伴いUTXOセットの膨張が進むかもしれない。このためUTXOの管理をアキュムレータ化すると、長期的なスケーラビリティに貢献することになり、またディスクの読み書きが最小限に抑えられることでIBDの同期時間の向上にもなる。

ブリッジノード

ただ、Bitcoinはもともとアキュムレータネイティブな設計ではなく、既に稼働中のBitcoinノードが多数ある状況で、当然そのノードもこのアキュムレータを実装している訳ではない。アキュムレータに対応したノードは、トランザクションを検証する際に別途UTXOの包含証明を添付する必要がある。アキュムレータに対象したノードであれば自身が所有するUTXOの包含証明を提供することができるが、他のノードはそれを要求しないだろうし、他のノードが伝播したトランザクションには包含証明が含まれていないのでアキュムレータ対応ノードはそれを検証できないといったことになる。

このため、現在稼働中のフルノードとアキュムレータに対応したノードが同時に稼働するためにはネットワークにブリッジノードが必要になる。ブリッジノードというのはアキュムレータ内の全てのUTXOに対するプルーフを持っているノードだ(Utreexoの場合マークルフォレスト全体を常に持っているノード)。

f:id:techmedia-think:20190614161908p:plain

ブリッジノードは上記のように現状のフルノードとアキュムレータに対応したCompact State Nodeとのブリッジとして機能する。フルノードは以前と同様に動作し、トランザクションとブロックをお互いに伝播する。ブリッジノードはフルノードで、マークルフォレスト全体も格納し、Compact State Nodeに包含証明を提供する。Compact State Nodeは包含証明を省略することでトランザクションメッセージをフルノードに送信できるが、証明を提供できないフルノードから直接トランザクションを受け取ることはできない。

RSAアキュムレータと違ってTrusted Setupは必要ない。ただ、アキュムレータがフォレストのツリーのルートハッシュで構成されるので、ツリーの数の増減に伴いアキュムレーターのサイズも増減するので固定サイズにはならない。

Bitcoinネットワークの安全性を向上させるための新しいトランザクションリレープロトコル「Erlay」

先日、Gleb Naumenko, Pieter Wuille, Gregory Maxwell, Sasha Fedorova, Ivan Beschastnikhらが発表したBitcoinの新しいトランザクションリレープロトコル「Erlay」について論文読んでみた↓

https://arxiv.org/pdf/1905.10518.pdf

現在のトランザクションリレープロトコル

現在、Bitcoinトランザクションリレーは以下のプロセスで行われている。

f:id:techmedia-think:20190611105900p:plain
現在のBitcoinトランザクションリレーフロー

  1. ノード(Peer 1)が接続中のピアからトランザクションを受信する。
  2. Peer 1はそのトランザクションについて、そのトランザクションのハッシュをINVメッセージを使って、接続中の(まだそのトランザクションのハッシュをINVで送っていない)他のピアにトランザクションをアナウンスする。
  3. INVメッセージを受け取ったノード(Peer 2)は、そのハッシュのトランザクションについて自身が知らない場合、送信元のPeer 1にGETDATAメッセージを送信しそのトランザクションを要求する。
  4. GETDATAメッセージを受信したノードは(Peer 1)は、該当するトランザクション情報をGETDATAの送信元(Peer 2)にTXメッセージで送信する。

新しいトランザクションを受信したピアが、そのトランザクション情報を自身が接続している全てのピアに一斉に送信する(正確にはピア毎にランダムな遅延時間がある)このような方式をフラッディング方式と呼ぶ。なお、Bitcoinネットワークには、以下の2種類のノードが存在する。

  • パブリックノード
    8つのアウトバウンド接続とデフォルトで最大125のインバウンド接続(インバウンド接続の最大値は1000まで設定可能)を持つノード(一般的なフルノード)
  • プライベートノード
    8つのアウトバウンド接続を持つが、インバウンド接続は持たないノード(トランザクションを検証できいない軽量ノードとかはこっち)

このフラッディング方式は非常に単純でトランザクションの存在をBitcoinネットワーク全体に素早く伝播するという点では効果的だ。だが、効率的ではない。ノードが接続中のインバウンド、アウトバウンドピア全てに配信するため、各ピアが既に自身が知っているトランザクションハッシュ値INVメッセージで受け取ることが多い。Bitcoinの場合トランザクションハッシュ値は32バイトなので、1つのトランザクションのリレーについて、32バイト×冗長なメッセージの個数分、ネットワークの帯域幅を無駄に使用していることになる。

論文で言及されている分析結果では、トランザクションのアナウンスに使われている帯域幅の88%が冗長で、それは使用される全帯域幅の44%を占めるとされている。

一方、Bitcoinのネットワークを堅牢にするためには、各ノードが繋がるノードの数を増やす必要がある。ネットワークの接続性が高いほどネットワーク・セキュリティも向上する。しかし、現状のリレープロトコルのままアウトバウンドの接続数を増やすと、帯域幅の使用量もリニアに増えてしまう。

Erlayプロトコル

↑のようなリレープロトコルの冗長性を軽減し、接続数に対してスケール可能な新しいリレープロトコルがErlayだ。Erlayではトランザクションのリレーを以下の2段階(ファンアウト、reconciliation)で行う。

ファンアウトフェーズ

フラッディング方式は、情報素早くネットワーク全体に伝播するという意味では効果的なので、これを限定的に利用する=ファンアウトフラッディング。このフェーズでは、パブリックノードがアウトバウンド接続のみを使って*18つのピアに対し新しいトランザクションの存在をフラッディングする。パブリックノードのアウトバウンド接続のリンクを使ってトランザクションを受け取るのはインバウンド接続を許可している同じくパブリックノードであるため、このフラッディングではパブリックノード間にトランザクションを迅速に伝播していくことになる。

この時、現状8つであるアウトバウンド接続のピアの数を将来的に増やしたとしても、ファンアウトフェーズでフラッディングするピアの数は8つのままとする。そうすることで8つ以上アウトバウンド接続を増やしてもトランザクションの伝播コストはそれに比例して増えることはない。

set reconciliationフェーズ

↑のファンアウトフェーズだけではトランザクションはネットワークの全体には行き渡らない。特にプライベートには行き渡らない。そこで続けてset reconciliationフェーズを実行する。

set reconciliationフェーズでは、P2Pネットワーク内のノードはローカルの状態と接続しているピアの状態を定期的に比較し、必要な情報(状態の差分)のみを送信/要求する。つまり各ノードは自分が知っているトランザクションセット(状態)を累積し、あるタイミングで接続先のピアとトランザクションセット(状態)の差を計算し、自身に不足しているトランザクションのみを要求する。

この二者間の状態セットの差分の計算と不足データの復元を効率的に行うためのライブラリが以前公開された、エラー訂正符号に基づいた帯域幅効率の高いMinisketchだ↓

GitHub - sipa/minisketch: Minisketch: an optimized library for BCH-based set reconciliation

Erlayを実装したピアは、Minisketchを利用してローカルの状態セットのSketchを計算し、set reconciliationを実行する。ここで計算するSketchは以下の2つの特性を持つ。

  • Sketcheは事前定義された容量を持ち、セット内の要素数がその容量を超えなければ、sketchをデコードすることでsketchからセット全体を復元することが常に可能である。容量cで、bビットの要素のsketchはbcビットで保存できる。
  • 2つのセットの対称差のsketchは、2つのセットのsketchのビット表現をXORすることで得られる。

具体的には、次のプロセスで二者間のトランザクションセットを同期する。アリスとボブがそれぞれ要素のセットを持っているとする。両者のセットは大部分が同じであるが、完全に同じではない。この場合両者は自分に不足している要素を学習するために以下のプロセスを実行する。なお、実際に両者が持つセットの要素というのはトランザクションの短い識別子(短縮txid)で構成される。

  1. アリスとボブは共にローカルでそれぞれのセットのsketchを計算する。
  2. アリスは自分のsketchをボブに送る。
  3. ボブは2つのsketchを結合し、対称差のsketchを入手する。
  4. ボブは対称差のsketchから要素の復元を試みる。
  5. ボブはアリスにアリスが持っていない要素を送り、ボブが持っていない要素をアリスに要求する。

このプロセスはセットの差のサイズ(アリスが持っているがボブは持っていない要素+ボブが持っているがアリスは持っていない要素)がアリスが送ったsketchのサイズを超えない場合は常に成功する。そうでない場合、手順は失敗する可能性がかなり高い。

この手順の重要な特性は、実際のセットサイズに関係なく機能することで、セットの差のサイズだけが重要になる。

上記のようにファンアウトフェーズで伝播されなかったトランザクションは、その後のset reconciliationフェーズでノード間のトランザクションのセットの差異を効率的に解消することでネットワーク全般にトランザクションがいきわたること保証する。プライベートノードは基本的にそのアウトバウンド接続先であるパブリックノードとset reconciliationを実行することでトランザクションを同期する。

f:id:techmedia-think:20190611152535p:plain
フラッディングとset reconciliationを組み合わせたトランザクションリレーフロー

Sketchのデコードに失敗した場合

set reconciliationは二者間のセットの差分の上限が予測可能であるという仮定に依存している。つまり、実際の差分が見積もりよりも大きい場合、Sketchのデコードに失敗する。この場合ノードは二分法を使って再度set reconciliationを試みる。これでも失敗する場合、従来のフラッディングによりトランザクションを通知する。

set reconciliationの実行スケジュール

各ノードは1秒ごとに1つのアウトバウンドピアとreconciliationを開始する。この設定で現在のプライベートノードとパブリックノードの比率では、各パブリックノードが1秒間に約8回のreconciliationを実行する。現在の最大Bitcoinネットワークトランザクションレートが7トランザクション/秒であると仮定すると、このプロトコルの平均差分セットのサイズは7要素。毎秒1回新しいピアに対して繰り返され、ファンアウトアナウンスを介して受信しなかったトランザクションを迅速に受信できる。

拡散遅延

従来のBitcoinのフラッディング方式ではトランザクションのアナウンスにランダムな遅延を入れることで、タイミング攻撃および飛行中の衝突*2を軽減している。Erlayにおいては、各フェーズの拡散遅延は以下のようになる。

ファンアウトの拡散遅延

ファンアウトフラッディングはアウトバウンド接続のピアに対してのみ行われるため、タイミング攻撃および飛行中の衝突の心配がないため、レイテンシーを減らすこともあり、より小さな拡散インターバルは1秒。

set reconciliationの拡散遅延

set reconciliationフェーズではタイミング攻撃の可能性が考えられることから、それをコスト高にするため、各ピアにreconciliation要求に応える前に小さなランダムな遅延(平均1秒であるポイズン分布の確率変数)を設けるのを強制する。

Erlay導入の効果予測

限定されたフラッディング方式の利用と断続的なreconciliationを組み合わせた代替プロトコルであるErlayの論文で紹介されているシミュレーション結果(60000ノードのシミュレーションネットワークと、複数のデータセンターにまたがって国際的に広がる100ノードのライブセットの両方を使ってパフォーマンスを分析している)では、Erlayが新しいトランザクションの存在をアナウンスするのに使われる帯域幅の84%を削減するという結果が出ている。この帯域幅の削減効果は大きく、将来的にBitcoinネットワークの接続性を向上させ、ネットワーク・セキュリティを向上させるために重要な改善と言える。

ただ、トランザクションがネットワーク全体に伝播するまでにかかる時間は約2.6秒長くなるとされている。この点については、トランザクションの承認は10分に1回であるという側面もあることからトレードオフとしては問題の無い範囲のように思える。

特に異議がなければ今後Bitcoin Coreに実装されていくことになるだろう。P2Pネットワークのような分散環境を堅牢にしていくための地道な取り組みを行っていくのがBitcoinらしくて良い!

*1:インバウンド接続ではなくアウトバウンド接続を利用するのは、タイミング攻撃を防ぐためでもある。

*2:同じタイミングで接続中のノードがそれぞれ同じトランザクションを相手に送り合う

Grinでオフチェーン決済するためのPayment Channelプロトコル「Elder Channel」

Mimblewimbleを実装したGrinにはBitcoinのようなスクリプト機能は存在しない。コインの所有権は、秘密鍵の役割をするPedersen CommitmentのBlinding Factorの値を知っているかどうかで、UTXOを使用する際はその値を使った電子署名が求められる。Mimblewimble/Grinにおけるコインとその使用方法については、以前GBECで解説動画を作ったので↓参照。

goblockchain.network

そんなスクリプト機能の無いGrinでオフチェーン決済をするためのPayment Channelプロトコルが今回提案されているElder Channel↓

https://gist.github.com/antiochp/e54fece52dc408d738bf434a14680988

Payment Channelを構成するのに必要な要素は、主に2つ↓

  • マルチシグ
  • チャネルの旧状態のトランザクションがブロードキャストされた際の対処方法

Grinの場合これをスクリプトレスで実現する必要がある。この内、マルチシグについては、Schnorr署名の公開鍵の集約特性を利用して実現できる。詳細は↓

techmedia-think.hatenablog.com

そして、旧状態のトランザクションがブロードキャストへの対処は相対的なタイムロックの仕組み(↑で解説)と、MimblewimbleならではのPedersen Commitmentの特性を利用する。現在のLightning Network(Joseph Poon & Tadge Dryjaモデル)は、旧状態をブロードキャストした場合、不正をしたユーザーが資金を全て没収されるペナルティモデルを採用しているが、Elder Channelは最新の状態を適用させるeltooモデルに近い。

Elder Channelのプロトコルは以下のようになる。

チャネルのセットアップ

アリスとボブはそれぞれ自身が所有するコミットメントを持ち寄り、それをマルチシグのコミットメントにロックするFunding Txを作成する。

この時、相手が応答不能になって資金を取り戻せないといった状況が起きないように、Close TxとSettle Txをそれぞれ作成する。

  • Close TxのインプットはFunding Txのマルチシグアウトプットで、アウトプットは、同額をそのまま持つマルチシグのコミットメント。
  • Settle TxのインプットはClose Txのマルチシグアウトプットで、アウトプットはその時のアリスとボブの残高を表す2つのコミットメント。

そして、Settle Txのカーネルにだけ、相対的なタイムロックが設定される。これはSettle TxのインプットであるClose Txがブロックに承認されて1440ブロック経過するまでSettle Txをブロードキャストできないという相対的なものだ。

それぞれ作成したClose TxとSettle Txに署名したら、Funding Txに署名しブロードキャストする。Funding Txが承認されるとチャネルがオープンする。

この時、Close Txのみがブロードキャストされた状態のまま資金がロックされることが内容、お互い相手のSettle Txを持つ。

チャネルの更新

オフチェーンで決済する際はチャネルを更新する。チャネルの更新は新しいClose TxとSettle Txを作成することを意味し、送金額に応じて新しいSettle Txの各自の残高を調整する。

そして、ここで不正対策を行う。古い状態のCloset TxやSettle Txがブロードキャストされるとコインが盗まれてはまずいので、お互いにRevoke Txというのを作る。このRevoke Txは前の状態のClose Txのアウトプットをインプットとし、そのアウトプットはチャネルの実体であるFunding Txのコミットメントと同じ値を再利用する。

こうすることで、古い状態のClose Txがブロードキャストされると、Settle Txのタイムアウトの期間内に、Revoke Txをブロードキャストすることで、古いClose Txの効果を取り消す。そして、最新のClose Txをブロードキャストし、その後タイムロックの期間を待って、Settle Txでチャネルをクローズする。最新の状態にはRevoke Txは存在しないので、最新のClose Txが取り消されることはない。

この仕組みにより、不正が行われた場合にそれを取り消しチャネルの最新残高の反映を可能にする。

チャネルのクローズ

最新のClose Tx、Settle Txをブロードキャストする以外に、両者が協力してチャネルを閉じるMut Txを作成する。このトランザクションのインプットはFunding Txのアウトプットで、アウトプットはそれぞれの最新のチャネル残高を反映したコミットメントになる。

大まかに↑のフローを図にすると以下のようになる。

f:id:techmedia-think:20190531184809j:plain
Elder Channelの構築フロー

注意点

トランザクションカットスルー

個々のRevoke Txが分かると、永遠にClose & Revokeを繰り返し資金をロックする攻撃が可能になるので、Revoke Txとその後の最新のClose Txは個別にブロードキャストするのではなく、トランザクションカットスルーをして1つのトランザクションにしてブロードキャストする必要がある。カットスルーする分、承認時間も短縮され手数料も安くなる。

手数料

手数料について↑では触れてないが、それぞれトランザクションをブロードキャストする際に、手数料用のトランザクションと集約してブロードキャストする必要がある。

コミットメントの重複

あと1番大事なのが、一見これで正常に動作するように思えるが、Elder Chanelは各UTXO=コミットメントの重複(同じコミットメントの値の再利用)を許可することが前提となっている。これは現在のGrin/Mimblewimbleの設計には当てはまらないので、改修が必要になる。

所感

Mimblewimbleプロトコルを採用したペイメントチャネルのプロトコルということで結構面白い。キーになってるのはRevoke Txでコインをチャネルに送り返せば元々作ってたClose Txが再利用できるという点。これはUTXOが単純なPedersen Commitmentでその特徴を上手く利用してると思う(よく思いつくなー)。

後は、LNにするにあたってHTLCとかルーティングとかの課題が残る。特に金額がコミットメントによって秘匿されている状態でルーティングどうするのか気になるところ。

c-lightningのPluginの作り方

c-lightning 0.7から任意の言語でPluginを書けるようになった↓

blockstream.com

現在、CPythonGolangJavaでPluginを書くためのライブラリが提供されている。

c-lightningのPlugin

Pluginを利用することでc-lightningが提供する機能を拡張することができ、そのPluginは任意の言語で書くことができる。これはc-lightningが標準入力と標準出力を介してPluginと連携しているためで、Pluginは標準入力でc-lightningからイベントの通知や、メソッドの要求を待ち受け、Pluginはその応答を標準出力を介して送信する。この標準入力と標準出力で連携する際のデータフォーマットはJSON-RPCv2。

Pluginを作ることで以下のようなことが可能になる。

  • コマンドラインオプションパススルー
    lightningdを介して公開される独自のコマンドラインオプションを登録できる。
  • JSON-RPCコマンドパススルー
    独自のコマンドをJSON-RPCインターフェースに追加することができる。
  • イベント通知
    lightningdからイベントのプッシュ通知を受け取れる。
  • フック
    lightningdの内部イベントの通知を受け取り、その動作を変更したり、カスタム動作を追加したりすることができる。

Pluginの登録方法

Pluginはlightningd起動時に--plugin=オプションで登録できる。複数のPluginを登録したい場合は、Pluginの数分--plugin=オプションを指定する。

$ lightningd --plugin=<Pluginファイルのパス>

なお、Pluginのファイルは実行可能ファイル(実行権限のあるファイル)である必要がある。実行権限が付いてないと、lightningd起動時にPermission deniedで怒られる。

lightningdJSON-RPCリクエストをプラグインの標準入力に書き込み、標準出力からの返信を読み取る。Pluginを初期化する際は以下の2つのRPCメソッドが必要になるため、Pluginを作る際は必ず以下のgetmanifestinitのRPCメソッドを作る必要がある。

実際、Pluginを指定してlightningdを起動すると、Pluginの標準入力に以下のようなgetmanifestを要求するリクエストが飛んでくる(↓ログ形式で表記してるけど、データの実態はINFO -- : 以降)。

INFO -- :  {
INFO -- :   "jsonrpc": "2.0",
INFO -- :   "id": 1,
INFO -- :   "method": "getmanifest",
INFO -- :   "params": {
INFO -- :   }
INFO -- : }
INFO -- :

Pluginは、改行コードが2つ続いたら、それまでのデータをJSONとしてパースして処理すればいい。なお、標準出力はlightningdへの応答になるので、Plugin内でログを出力したい場合は、標準出力以外(ファイルなど)に出力する必要がある。

getmanifest

全てのPluginに必要なメソッドで起動時にパラメータ無しで呼び出される。getmanifestメソッドは以下のような内容を返す。

{
    "options": [
        {
            "name": "greeting",
            "type": "string",
            "default": "World",
            "description": "What name should I call you?"
        }
    ],
    "rpcmethods": [
        {
            "name": "hello",
            "usage": "[name]",
            "description": "Returns a personalized greeting for {greeting} (set via options)."
        },
        {
            "name": "gettime",
            "usage": "",
            "description": "Returns the current time in {timezone}",
            "long_description": "Returns the current time in the timezone that is given as the only parameter.\nThis description may be quite long and is allowed to span multiple lines."
        }
    ],
    "subscriptions": [
        "connect",
        "disconnect"
    ]
}

optionlightningdが受け入れるコマンドラインオプションのリストに追加される。上記の例では、デフォルト値がWorldで指定された定義内容の--greetingオプションが追加される。現在サポートしているのは文字列オプションのみ。

rpcmethodslightningdの組み込みコマンドのようにlightningdJSON-RPC over Unix-Socketインターフェースを介して公開されるメソッドになる。JSON-RPC呼び出しに与えられた任意の引数は、すべて逐次渡される。name、usage、descriptionフィールドは必須だが、long_descriptionは省略可能。usageでは、[]でオプションのパラメータ名を囲む必要がある。

Pluginはrpcmethodの名称が以前に登録されているのと同じでない限り任意の名称を自由に登録できる。これにはgetinfohelpなどの組み込みメソッドも含まれる。名称が競合する場合lightningdはエラーを出力して終了する。

init

getmanifestのレスポンスを返すと、続いてlightningdが受け入れるコマンドラインオプションのリストと、稼働しているlightningdのホームディレクトリとRPCのソケットファイルの情報を含むJSONオブジェクトが送られて来て、lightningdJSON-RPCコマンドを受け取る準備ができたことをPluginに通知するのにinitメソッドが要求される。

{
  "jsonrpc"=>"2.0", 
  "id"=>3, 
  "method"=>"init", 
  "params"=>{
    "options"=>{}, 
    "configuration"=>{
      "lightning-dir"=>"/home/azuchi/.lightning", 
      "rpc-file"=>"lightning-rpc"
    }
  }
}

Pluginはinit呼び出しに応じなければならないが、応答するかどうかは任意で、応答しても現在はlightningdによって破棄される。

イベント通知

上記のRPCメソッドに加えて、Pluginはlightningdのイベントを購読することができる。↑のgetmanifestのレスポンスのsubscriptionsで、購読するイベントを指定する。指定したイベントがlightningdで発生すると、lightningdJSON-RPCを使ってPluginに通知をプッシュする。この時、上記のRPCと違って、JSONにidパラメータは含まれない(通知であり、レスポンスを受け取る必要がないので)。

ただ、現状のlightningdで通知されるイベントは以下の2つのみっぽい。

  • connect
    ピアへの新しい接続が確立された際に通知されるイベントで、接続先ピアのidaddressが通知される。
{
    "id": "02f6725f9c1c40333b67faea92fd211c183050f28df32cac3f9d69685fe9665432",
    "address": "1.2.3.4"
}
  • disconnect
    ピアへの接続が切断された際に通知するイベントで、切断したピアのidが通知される。
{
    "id": "02f6725f9c1c40333b67faea92fd211c183050f28df32cac3f9d69685fe9665432"
}

もうちょっと、通知されるイベント増えないもんかねー。

Hook

Hookを使うと、c-lightningのコードを変更することなく、Pluginでlightningdの動作をカスタマイズできる。このHookとイベントの通知は似ているが、以下の点が異なる。

  • 通知は非同期で、lightningdは通知を送信するが、Pluginがその通知を処理するのを待つことはない。一方Hookは動機的で、Pluginからの応答があるまでlightningdはイベントの処理を完了できない。
  • 通知を受け取るPluginは何個でも登録できるけど、Hookに登録できるPluginは1つだけ(複数のPluginからHookのコールバックで矛盾する結果が返ってくると処理できないため)。

Hookはlightningdの動作を変更できるので、有効なレスポンスをlightningdに返すよう注意して実装する必要がある。

Hookの種類

現在対応しているHookは以下のとおり。

peer_connected

ピアが接続しハンドシェイクが正常に完了した際に呼び出されるHook。接続したピアと既にチャネルを開いている場合は、以下の情報が返ってくる。

{
  "peer": {
    "id": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
    "addr": "34.239.230.56:9735",
    "globalfeatures": "",
    "localfeatures": ""
  }
}

Pluginからlightningdへの応答は、resultメンバーとしてdisconnectcontinueを返さなければならない。disconnecterror_messageメンバーがある場合、その情報は切断前にピアに送信される。

db_write

変更がデータベースにコミットされる直前に呼び出されるHook。このHookの利用にあたっては、以下のような厳しめの制約がある。

  1. このHookに登録するPluginは応答としてDB操作を引き起こす可能性があるもの(ロギング以外の)を実行してはならない。
  2. このHookを登録しているPluginや他のHookやコマンドに登録してはならない。それらが混在していると↑のルールを破る可能性があるため。
  3. HookはPluginが初期化される前に呼ばれる。

lightningdから以下のようなデータが通知される。

{
  "writes": [ "PRAGMA foreign_keys = ON" ]
}

応答はtrueで、それ以外の場合、データベースへのコミットはされずlightningdはエラーになる。

invoice_payment

まだ支払いがされていないinvoiceに対する有効な支払いが届いた際に呼び出されるHook。

{
  "payment": {
    "label": "unique-label-for-invoice",
    "preimage": "0000000000000000000000000000000000000000000000000000000000000000",
    "msat": "10000msat"
  }
}

応答は、BOLT 4で定義されているゼロでないfailure_codeを返すか、支払いを受け入れる場合は、空のオブジェクトを返す。

openchannel

リモートピアからチャネルオープンの要求が来て、基本的なチェックをパスした際に呼ばれるHookで、lightningdからは以下の情報が送られてくる。

{
  "openchannel": {
    "id": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
    "funding_satoshis": "100000000msat",
    "push_msat": "0msat",
    "dust_limit_satoshis": "546000msat",
    "max_htlc_value_in_flight_msat": "18446744073709551615msat",
    "channel_reserve_satoshis": "1000000msat",
    "htlc_minimum_msat": "0msat",
    "feerate_per_kw": 7500,
    "to_self_delay": 5,
    "max_accepted_htlcs": 483,
    "channel_flags": 1
  }
}

他にもフィールドがある場合があり、各フィールドはBOLT 2のopen_channelメッセージに定義されている

応答には、resultメンバーに、rejectもしくはcontinue文字列を含める必要がある。rejecterror_messageメンバーがある場合、その情報は切断前にピアに送信される。

PluginをRubyで簡単に書く

↑のPluginをRubyで簡単に書けるようライブラリc-lightningrbを作ってみた↓

github.com

このライブラリを使うと以下のようにDSLでRPCやイベント通知、Hookのハンドラをlambdaで記述できる。lambdaで記述したロジックは、クラスのインスタンスメソッドとして定義されるので、インスタンスの各フィールド、メソッドにアクセス可能。

#!/usr/bin/env ruby
require 'lightning'

class HelloPlugin < Lightning::Plugin

  # PRCの定義。RPCの場合は引数やその定義をc-lightning側に渡す必要があるので、それらをdescで定義。
  desc '[name]', 'Returns a personalized greeting for {greeting} (set via options).'
  define_rpc :hello, -> (name) do
    log.info "log = #{log}"
    "hello #{name}"
  end

  # イベント通知用のハンドラ
  subscribe :connect, ->(id, address) do
    log.info "received connect notification. id = #{id}, address = #{address}"
  end

  # Hookのハンドラ
  hook :peer_connected, ->(peer) do
    log.info "peer_connected. peer = #{peer}"
    {result: 'continue'}
  end

end

p = HelloPlugin.new
p.run

pluginオプションでlightningdにpluginを指定する。この時ファイルに実行権限を付けておくこと。

$ lightningd --network=testnet --plugin=<Pluginファイルのパス>

help実行すると、追加したRPCメソッドの情報もでてくる。

$ lightning-cli help
...
hello [name]
    Returns a personalized greeting for {greeting} (set via options)
...

実行すると、ちゃんとPluginが実行されてるのが分かる。

$  lightning-cli hello lightning                                                                               
"hello lightning"

BitcoinへCovenantsの導入を提案するbip-coshv

Bitcoinは通常ロックスクリプトにロックされたコインをアンロックできるアンロックスクリプトを提供できれば、そのコインはどこにでも送金できる。これに対し、あるコインをアンロックした場合、そのコインの送付先を限定する仕組みがCovenantsだ。つまりある特定のスクリプトにロックされたコインは、ある特定のロックスクリプトにしか送信できなくするというもの。このようにコインの送信先を制限することで、例えば、秘密鍵が盗難されても、その秘密鍵の管理下にあるコインは特定の宛先にしか遅れない金庫のような仕組みを作ることができる。

このCovenantsのコンセプトは元々、2016年にミラノで開催されたScaling BitcoinでEmin Gun Sirerが発表したものだ。Eminが提案したのは、CheckOutputVerifyというopcodeを追加してCovenantsを構成するというもの。CheckOutputVerifyは、アウトプットのインデックス、そのアウトプットのコインの量、そのアウトプットscriptPubkeyの3つを引数にとり、そのロックスクリプトを使用しようとしているトランザクションの指定されたインデックスのアウトプットに、指定された量、指定されたscriptPubkeyがセットされているか検証するというアプローチ。詳細は以前書いた↓参照。

techmedia-think.hatenablog.com

これに対し、当時Blockstreamが開発していたElementsでサポートしている任意メッセージの署名検証ができるopcodeOP_CHECKSIGFROMSTACKを使ってCovenantsを実現するという提案もあった。CheckOutputVerifyに比べて直観的なロックではないものの確かにこのアプローチでもコインの送金先を制御できる。(BCHに導入された同様の機能であるOP_CHECKDATASIGを使えばBCHではこの方法は既に利用可能である)詳細は以前書いた↓参照。ただ、これはスクリプトを組むのがとても大変で複雑になるので、オススメはしない。

techmedia-think.hatenablog.com

今回のbip-coshvの提案者であるJeremy Rubinも、2017年のStanford Blockchain Conferenceや2017年2月に日本で開催されたDGLab主催のBC²でCovenantsのさまざまなユースケースや拡張について発表している↓

https://bc-2.jp/archive/season1/materials/0303-JP-StructuringMultiTransactionContracts.pdf

そして、今回Jeremy Rubinによって書かれたCovenantsのBIPドラフトが↓

https://github.com/JeremyRubin/bips/blob/op-checkoutputshashverify/bip-coshv.mediawiki

今回の提案では、CHECKOUTPUTSHASHVERIFYという新しいopcodeをTapscriptのOP_SUCCESS系opcodeの1つに割り当てて導入する。このopcodeを使ったCovenantsの仕組みはEminのCheckOutputVerifyのアプローチと似ている。ただCheckOutputVerifyと違うのは、 CHECKOUTPUTSHASHVERIFYが取る引数は1つのみで、この引数の値はCHECKOUTPUTSHASHVERIFYを含むUTXOを使用する際のトランザクションの全アウトプットをdouble-SHA256したデータである。

... OP_CHECKOUTPUTSHASHVERIFY <トランザクションの全アウトプットのdouble-SHA256ハッシュ> ...

トランザクションアウトプットのデータはコインの量とscripPubkeyで構成されているため、CHECKOUTPUTSHASHVERIFYはコインの送金先(トランザクションアウトプット)の情報を制御することになる。もちろんCHECKOUTPUTSHASHVERIFYはTapscript内で自由に利用できるので、OP_IF分岐を利用して送信先の条件を複数コントロールできる。

そして実装を簡単にするため、まずこの段階でCHECKOUTPUTSHASHVERIFYを使う場合、トランザクションのインプットは1つのみと制限される。また全アウトプットへの事前コミットが必要というのも制約になる。なので、トランザクション手数料なんかも加味した上で送信先トランザクションアウトプットを予め決めておかなければならない。

またBIPにはCovenantsを利用した金庫のユースケース以外にも、ブロックの混雑具合に応じてトランザクションの送金をCHECKOUTPUTSHASHVERIFYを使って束ねる方法が紹介されている。トランザクションを多数の送信先に送らないといけない場合、混雑時にそういったトランザクションを作成すると手数料高になるが、とりあえずCHECKOUTPUTSHASHVERIFYでロックされたアウトプットに送金しておき、混雑が解消されたタイミングでCHECKOUTPUTSHASHVERIFYのアウトプットを多数の送信先に送信するというユースケースで、この場合最初のCHECKOUTPUTSHASHVERIFYで送金先自体は保証されているというのがポイントになる。他にもCoinJoinやChannel Factoryへの適用などのユースケースが挙げられている。

もともと、Covenantsはfungibilityを損ねる可能性があるということで具体的な実装の提案はなかったが、今回Taprooの機能と一緒に組み込むことが、そのリスクの低減になりそうだ。この場合、Taprootのキーパスを利用して、そのような条件がセットされていることを秘匿した状態で条件に合意した送金を可能にするが、その担保は実はCHECKOUTPUTSHASHVERIFYで行われているということが可能になる。

果たして、遂にCovenantsの導入になるか!?

詳細な仕様は、以下BIPドラフトの内容参照↓

Abstract

このBIPはTapscriptバージョン 0に対してアクティブ化される新しいopcode OP_CHECKOUTPUTSHASHVERIFYを提案する。

新しいopcodeには、トランザクション輻輳制御やペイメントチャネルの具体化などの用途がある。これらについては、このBIPの「動機」のセクションで説明している。

概要

Tapscriptの実行中、CHECKOUTPUTSHASHVERIFYはopcodeOP_RESERVED1(0x89)を使用する。

CHECKOUTPUTSHASHVERIFYは以下の条件を検証する。

CHECKOUTPUTSHASHVERIFYの後の操作が、32バイトのデータプッシュではない場合、それは無視される。条件が満たされなければ、実行は失敗する。

動機

Covenantsもしくは鍵の所有権を超えたコインの使用方法に対する制限は、スマートコントラクトを構築するための非常に強力な構成要素だ。しかし、その複雑さとfungibilityのリスクをもたらす可能性を考えると、これまでBitcoinに導入するのを真剣に検討してこなかった。

このBIPの目的は、実用的な機能の限定されたセットを可能にする最小の実行可能なcovenantを導入することにある。例えば、

輻輳制御されたトランザクション

ブロックスペースに対して大きな需要があるタイミングでの支払いは非常に高価になる。CHECKOUTPUTSHASHVERIFYを使うことで、大容量のペイメントプロセッサは、承認のために、それらのすべての支払いを単一のO(1)トランザクションに集約することができる。そして、その後ブロックスペースの需要が減少したタイミングで、支払いをそのUTXOから拡張できる。

CHECKOUTPUTSHASHVERIFYがなくても、これはSchnorr署名を使って実現できる(もしくはマルチパーティスキームが与えられたECDSAでも)。しかし、非対話的に行うのは不可能で、それはアプローチの実行可能性を根本的に制限する。

ユーザーが複数の選択肢を持つように輻輳制御トランザクションを構築するために、CHECKOUTPUTSHASHVERIFYを使って1からNまでの単一のアウトプットへの支払いを保証するか、アウトプットのツリーにコミットするかして、好きなだけ多くの支払いを承認するのを可能にする。さらにTaprootha可変サイズの拡張(あるノードが2、4,8などで拡張する)にコミットできる。これによりトランザクションのオーバーヘッドとすぐに利用可能なブロックスペースの間のトレードオフが可能になる。その場合マークルツリーの検索はO(log(log(N)))の追加のオーバーヘッドとなるが、ブロックの要求に応じてそれをE[O(1)]となるようハフマン符号化することができる。ツリーの各ノードはTaproot署名ベースの使用を優先するようオプトインすることも可能だが、参加者がオフラインまたは悪意ある場合、拡大をより小さなグループで進めることができる。

このアプローチの全体的な(最適化なしの)オーバーヘッドは、各ユーザーO(log(N))トランザクションの観点から見たもので、追加のトランザクションは1つだけで、ネットワークの観点からは2N。ただし、そのようなトランザクションに必要な署名無いため、実際のオーバーヘッドは少なくなる。

以下のチャートは、これらのトランザクションと通常のトランザクションおよびバッチトランザクションの構造を比較している。

https://github.com/JeremyRubin/bips/raw/op-checkoutputshashverify/bip-coshv/states.svg?sanitize=true

5%のネットワークの採用と50%のネットワークの採用の場合で、これがmempoolバックログに与える可能性がある影響のシミュレーションを以下に示す。シミュレーション用のコードはこのBIPのサブディレクトリにある。

https://github.com/JeremyRubin/bips/raw/op-checkoutputshashverify/bip-coshv/five.png

https://github.com/JeremyRubin/bips/raw/op-checkoutputshashverify/bip-coshv/fifty.png

Channel Factories

このユースケースは、支払いの代わりにリーフノードを(おそらく、支払人と受取人の間もしくは、受取人が選択した対象との間の)チャネルとしてセットアップする必要があることを除いて、上記と同じだ。

すべての罰則は相対的なタイムロックで実際に具体化されるので、これらのチャネルは既にセットアップの時間については重要でない。

これにより、この遅延方法を使って送信されたコインの即時流動性が可能になる。

ウォレットの金庫

コールドストレージソリューションにより高い安全性が必要な場合、あるターゲットから別のターゲットへ資金を移動するデフォルトのTapscriptパスが存在する可能性がある。

例えば、コールドウォレットを、1人のカスタマーサポートデスクが追加の承認なく(複数の事前セットされた量を使って)資金の一部を、隔離されたサポートデスクによって操作されるウォレットに移動できるよう設定できる。サポートデスクはその後、いくつかの資金をホットウォレットに発行し、残りを同様の償還の仕組みを使ってコールドストレージに返送することができる。

これはCHECKOUTPUTSHASHVERIFYを使わなくても可能だが、CHECKOUTPUTSHASHVERIFYを使うことで、調整およびオンライン署名者を排除し、サポートデスクが資金を不適切い移動する能力を削減できる。

さらに、そのような全ての設計を相対的なタイムロックと組み合わせて、コンプライアンスおよびリスクデスクに介入する時間を与えることができる。

CoinJoin

この種のアプローチは、トラストレスなCoinJoinをセットアップするのを簡単にする。

すべての参加者は、そのアウトプットのハッシュにコミットする単一のUTXOに同意し、参加者はその後自分が好きなインプットでトラストレスに資金を供給することができる。

続いて、トランザクションが承認される。

必要に応じて、Tapscriptパスは、fungibilityを向上させるために署名ベースの使用によって奪われる可能性があある。

設計

CHECKOUTPUTSHASHVERIFYの目標は、既存のコードベースへの影響を最小限に抑えることだ。将来的には、より複雑になることが分かっているが、安全なユースケースであることが示されているため新しいcovenantタイプが有効になる可能性がある。

重要なのは、これはTapscriptなので、Tapscriptパスを署名で置き換えるために参加者が協同することができることを意図している。他の(チャネル状態などの)依存関係が更新される可能性がある場合、アウトプットは単独で使用されアウトプットのハッシュが正確に一致するという要件は外れる。

以下では、ルールについて1つずつ説明する。

次のスクリプトが32バイトの最小限のデータプッシュである

CHECKOUTPUTSHASHVERIFYは、opcodeの前ではなくopcodeの後のプッシュアイテムを使用する。CHECKOUTPUTSHASHVERIFYがスタックからデータを使うと、どのデータがコミットされるのかスクリプトで構成することが可能になる。データの先読みを使用することで使用時にアウトプットが確実に分かるようにする。

スクリプトプログラマーOP_IF OP_CHECKOUTPUTSHASHVERIFY <outputs 1> OP_ELSE <outputs 2> OP_ENDIF.のように、どのハッシュがチェックされるか条件付けすることができる。ただし、アウトプットをリテラルのハッシュにしておくことで可能性が制限される。

いずれにせよ、TapscriptのAPIを考えると、ユーザーは複数のOP_CHECKOUTPUTSHASHVERIFY操作を含むコードを別々のブランチにコンパイルする可能性が高い。

トランザクションで使用されるインプットは1つだけ

トランザクションで複数のインプットを使用できるようにすると、2つのアウトプットが同じアウトプットのセットへの支払いを要求する可能性があり、その結果意図した支払いの半分が破壊される。複数のインプットを許可する安全な方法はあるが、設計ははるかに複雑で、ユースケースはあまり明確でない。

さらに安定したTXIDが必要とされるペイメントチャネルの構成にとっては、どのインプットを同時に使用できるかという制限は非常に重要だ。

シリアライズされたアウトプットのSHA256doubleハッシュが指定された値と一致する

これは既に計算されているハッシュであるため、余分な検証のオーバーヘッドを回避できる。したがって、OP_CHECKOUTPUTSHASHVERIFYでは追加の検証オーバーヘッドが大幅に増加することはない。

このハッシュをスタックに公開することで、アウトプットの解析が可能になる可能性を心配する必要はない。それらはスクリプトの構築時に既に正確に分かっているため。

設計のトレードオフとリスク

Covenantsは、それがもたらすfungibilityのリスクの可能性から、歴史的に物議を醸してきた - コインがどのように使われるか、使われないかという。

ここに提示されたアプローチでは、Covenantsは以下のように厳しく制限される。全てのCovenantsは、Covenantsの要件よりも優先されるマルチシグベースの鍵でラップされる。さらに、OP_CHECKOUTPUTSHASHVERIFY Covenantsの構造は、その送信先となるアウトプットが正確に分かっている必要がある。したがって、作れるのは有限数のステップで拡大するCovenantsのみで、これは、安全という意味では、到達可能な最終状態ですべてのインプットを作成するトランザクションのセットに相当する。さらに、Covenantsは単一のインプットとしてのみ使用可能なように制限されており、「半分だけ使用」問題を防いでいる。

これらのCovenantsは、その制限のように、いくつかのリスクを負っている。OP_CHECKOUTPUTSHASHVERIFYに提供されるハッシュのプリイメージが分からなかったり、Taprootが未知の秘密鍵の公開鍵で構成されているCovenantsの可能性もある。アドレスが使用可能かどうか知ることと、送信者の任意のアドレスへ支払いする能力とは互換性がない(特にOP_RETURN)。送信者が送金する前に受信者がcovenantsを削除できることを知る必要があるなら、送信者は受取人からチャレンジ文字列の署名を要求するかもしれない。最後のリスクは「転送アドレスコントラクト」の悪用だ。転送アドレスは事前定義された方法で自動的に実行できるスクリプトだ。例えば、ホットウォレットには、相対的なタイムアウト後に自動的にコールドストレージアドレスに移動できるコインがある場合がある。問題は、そのような鍵を再利用するのはとても安全ではないということだ。例えば、1 BTCをコールドストレージに転送するアドレスを作成するとする。1 BTC未満でこのアドレスのアウトプットを作成すると、Taprootの署名パスが使われるまでフリーズすることになる。1 BTC以上がそのアドレスに支払われ、redeemscriptが公に知られている場合、誰でも1 BTCの超過分の資金を大きなマイナー手数料として支払うことができる。再利用可能な鍵をより安全に使用できるようにするために、最大の手数料額にコミットするopcodeや他の制限を後で導入することは可能だ。今のところ、すべてのブランチが希望する支払いと互換性があることが確実でない限り、Taprootキーを再利用しないことが最善だ。この制限とリスクはOP_CHECKOUTPUTSHASHVERIFY固有のものではない。Taprooスクリプトには、複数回使用するのが安全でない論理分岐が多数含まれる可能性がある(ハッシュタイムロック分岐は使用するたびにユニークなハッシュで具体化する必要がある)。

MES16(Eminの提案)で提案されたようなより強力なCovenantsが実装されると、OP_CHECKOUTPUTSHASHVERIFYタイプのCovenantsは不要になるだろう。それらは、 child-pays-for-parentや他の仕組みに頼らず手数料を調整する能力を向上させるという点でいいくつかの利点をもたらすだろう。しかし、これらの機能はかなり複雑さを増し、意図しない動作をする余地がある。或いは、CHECKSIGFROMSTACKSIGHASH_NOINPUTベースのCovenants設計でもCovenantsを実装することは可能だ。SIGHASH_NOINPUTには、Bitcoinへの導入を阻む、追加のリスクがある。CHECKSIGFROMSTACKは使うのがより複雑で、OP_CHECKOUTPUTSHASHVERIFYにはない追加の検証オーバーヘッドがある。実装および分析するためのこのアプローチの単純さおよびユーザーアプリケーションによって実現可能な利点を考慮すると、OP_CHECKOUTPUTSHASHVERIFYアプローチが提案される。

仕様

以下のコードはOP_CHECKOUTPUTSHASHVERIFYを検証するための主なロジック。

case OP_CHECKOUTPUTSHASHVERIFY:
{
    // 有効になる前は検証しない
    if (flags & SCRIPT_VERIFY_OUTPUTS_HASH) {
        CScript::const_iterator lookahead = pc;
        opcodetype argument;
        // 先読みの引数として1opcodeを先読み
        if (!script.GetOp(lookahead, argument, vchPushValue))
            return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
        // 先読みの引数が正確に32バイトの場合、OutputHashをチェック
        // これは後でこのopcodeに異なる意味を追加できるようにするため。
        if (vchPushValue.size() == 32) {
            // 引数は0x20であるべきで(MinimalPush)、その他は失敗する
            if (!CheckMinimalPush(vchPushValue, argument)) {
                return set_error(serror, SCRIPT_ERR_MINIMALDATA);
            }
            // 複数のインプットが許可されている場合、同じOutputsHashVerifyを持つ2つのインプットが意図した金額半分だけしはらうことになる。
            if (!checker.CheckOnlyOneInput()) {
                return set_error(serror, SCRIPT_ERR_OUTPUTSHASHVERIFY);
            }
            // 最後にアウトプットのハッシュが渡された値と一致することを確認する。
            if (!checker.CheckOutputsHash(vchPushValue)) {
                return set_error(serror, SCRIPT_ERR_OUTPUTSHASHVERIFY);
            }
        }
    }
}
break;

展開

展開は、Tapscriptで行われることを意図している。

https://github.com/sipa/bips/blob/bip-schnorr/bip-tapscript.mediawiki

実装

実装とテストは以下で入手可能。

https://github.com/JeremyRubin/bitcoin/tree/congestion-control