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
は以下の条件を検証する。
- 次のスクリプトが32バイトの最小限のデータプッシュである。
- トランザクションで使用されるインプットは1つだけ。
- シリアライズされたアウトプットのSHA256doubleハッシュが指定された値と一致する。
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。ただし、そのようなトランザクションに必要な署名無いため、実際のオーバーヘッドは少なくなる。
以下のチャートは、これらのトランザクションと通常のトランザクションおよびバッチトランザクションの構造を比較している。
5%のネットワークの採用と50%のネットワークの採用の場合で、これがmempoolバックログに与える可能性がある影響のシミュレーションを以下に示す。シミュレーション用のコードはこのBIPのサブディレクトリにある。
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や他の仕組みに頼らず手数料を調整する能力を向上させるという点でいいくつかの利点をもたらすだろう。しかし、これらの機能はかなり複雑さを増し、意図しない動作をする余地がある。或いは、CHECKSIGFROMSTACK
やSIGHASH_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