Develop with pleasure!

福岡でCloudとかBlockchainとか。

TaprootのアウトプットのスクリプトTapscriptに適用される新しいスクリプトルールを定義したBIPドラフトbip-tapscript

TaprootのBIPドラフトについて書いた↓

techmedia-think.hatenablog.com

ので、続いてそのTaprootのアウトプットで使用されるスクリプト = Tapscriptに適用されるスクリプトの新しいルールの提案「bip-tapscript」↓について見てみる。

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

主な変更点は

  • OP_CHECKSIGOP_CHECKSIGVERIFYがECDSAの代わりにSchnorr署名の検証をするようになる。
  • OP_CHECKMULTISIGOP_CHECKMULTISIGVERIFYは無効化され、新しいopcode OP_CHECKSIGADDを使ったマルチシグポリシーを追加。
  • 現在未使用のopcode(80, 98, 126-129, 131-134, 137-138, 141-142, 149-153, 187-254)にOP_SUCCESS80...OP_SUCCESS254を割り当て、OP_SUCCESS系のopcodeを追加する。OP_SUCCESSが実行されるとスクリプトはその時点で成功する。これは将来のソフトフォークで新しいopcodeを割り当てるのに使われる。従来OP_NOP系のopcodeが拡張用に用意されてたけど、これだとスタックから要素をプッシュができないなどの制約があるので、より柔軟にopcodeを割り当て可能なOP_SUCCESSを利用可能にすると。
  • 署名の際に用いるメッセージであるトランザクションダイジェストの計算方法の変更
  • 署名opcodeの制限がsigopから、トランザクションインプット単位のwitnessのweightベースの制限に変更。
  • MINIMALIFルールがコンセンサスとして適用。MINIMALIFルールというのは、OP_IFOP_NOTIFがスタック要素からTrue/Falseを判定するが、その際のmalleabilityを排除するため、その要素は空ベクター0x01のどちらかでなければならないというルール。

以下、BIPドラフトの内容。

イントロダクション

概要

bip-taprootの下での最初のスクリプトシステムのセマンティクスを規定している。

動機

bip-taprootはスクリプトの構造のみを改善することを提案しているが、その目標のいくつかはスクリプト言語自体の中の特定のopcodeのセマンティクスと両立しない。これらを別々の改善で扱うことは可能だが、bip-taproot自体と同時に対応されない限り、それらの影響は保証されない。

具体的には、目標はSchnorr署名、バッチ検証、およびsignature hashの改善を、スクリプトシステムを使用するコインの使用にも利用できるようにすることだ。

設計

これらの目標を達成するために、署名opcode、OP_CHECKSIGおよびOP_CHECKSIGVERIFYは、taprootのトランザクションダイジェストをベースにした新しいトランザクションダイジェストを使用し、bip-schnorrで定義されたSchnorr署名を検証するよう変更される。tapscriptのトランザクションダイジェストはOP_CODESEPARATORの処理を単純化し、より効率的にする。

非効率な、OP_CHECKMULTISIGおよびOP_CHECKMULTISIGVERIFY opcodeは無効になる。代わりに新しいopcode OP_CHECKSIGADDが導入され、バッチ検証可能な方法で同じマルチシグポリシーを作成できるようになる。Tapscriptは、トランザクション weightを使用した複雑なやりとりを修正するための新しいよりシンプルな署名opcodeの制限を使用する。さらに潜在的なmalleability ベクトルはMINIMALIFを必要とすることで排除する。

Tapscriptは、例えば新しいhash_typesや署名アルゴリズムを追加するために、未知のキータイプを定義することでソフトフォークを介してアップグレードできる。さらに新しいTapscript OP_SUCCESS opcodeを使用すると、OP_NOPを使用するよりもクリーンに新しいopcodeを導入できる。

仕様

以下のルールは、以下のすべての条件があてはまるトランザクションインプットを検証する場合にのみ適用される。

  • 使用するアウトプットはsegregated witnessのアウトプットである(すなわち、scriptPubKeyもしくはBIP 16のredeem scriptがBIP 141で定義されているようなwitness programである)。
  • そのアウトプットはbip-taprootで定義されているtaprootアウトプットである(つまり、witness version 1で、witness programは33バイト、そしてその先頭バイトは0x00もしくは0x01である)。
  • bip-taprootで定義されたスクリプトパスを使用している(つまり、witnessスタックからオプションのannexを削除後、2つ以上のスタック要素が残ること)。
  • リーフバージョンは0x00(オプションのannexを削除後、最後のwitness要素の先頭バイトが0xc0もしくは0xc1である)、つまりTapscriptの使用としてマークされている。

そのようなインプットの検証は、指定された順序で以下の手順を実行することと同等である必要がある。

  1. インプットがBIP 16, BIP 141もしくはbip-taprootに対して無効な場合、失敗する。
  2. bip-taprootで定義されているスクリプト(つまりオプションのannexを削除後の最後から2番めのwitnessスタック要素)は、tapscriptと呼ばれ、1つずopcodeにデコードされる。
    1. opcode番号80, 98, 126-129, 131-134, 137-138, 141-142, 149-153, 187-254が検出された場合、検証は成功する(以下のルールはいずれも適用されない)。tapscriptの後のバイトがそれ以外でデコードに失敗したとしても、これは適用される。これらのopcodeはOP_SUCCESS80...OP_SUCCESS254に名前が変更され、まとめてOP_SUCCESSxと呼ばれる。
    2. 任意のpush opcodeのデコードに失敗すると、tapscriptの終わりを超えて広がるため、失敗する。
  3. bip-taprootで定義されている初期スタック内の任意の要素のサイズが(つまり、オプションのannexおよび最後から2つのスタック要素を両方削除した後のwitnessスタックのサイズ)520バイトを超える場合、失敗する。
  4. tapscriptが10000バイトを超える場合、失敗する。
  5. tapscriptは、初期スタックをインプットとして次のセクションのルールに従って実行される。
    1. 何らかの理由で実行に失敗した場合(201回の非push opcodeの制限を含む)、失敗する。
    2. 実行した結果、スタック上の要素が正確に1つで、それがCastToBool()でtrueとして評価される要素以外が発生した場合、失敗する。
  6. 失敗すること無くこのステップに到達すると、検証は成功となる。

スクリプトの実行

tapscriptの実行ルールは、BIP 65やBIP 112で定義されているOP_CHECKLOCKTIMEVERIFYOP_CHECKSEQUENCEVERIFY opcodeを含めBIP 141に準拠したP2WSHがベースになっているが、以下の点が変更されている。

  • 無効化されたスクリプトopcode 次のscript opcodeはtapscriptでは無効になっている:OP_CHECKMULTISIGおよびOP_CHECKMULTISIGVERIFY。無効化されたopcodeは、実行されるとすぐに終了し、未実行ブランチで見つかっても無視するという点でOP_RETURNと同じように動作する。無視されていても、201回の非push opcodeの制限にカウントされる。
  • コンセンサスによるMINIMALIFの強制 MINIMALIFルールは、P2WSHの標準ルールに過ぎないが、tapscriptではコンセンサスとして適用される。つまり、OP_IFおよびOP_NOTIF opcodeへの入力引数は、厳密に0(空のベクター)または1(値1の1バイトのベクター)でなければならないことを意味する。
  • OP_SUCCESSx opcode 上記のようにいくつかのopcodeがOP_SUCCESSxという名前に変更され、無条件にスクリプトを有効とするようになる。
  • 署名opcode OP_CHECKSIGおよびOP_CHECKSIGVERIFYは、ECDSAに代わってSchnorr署名(bip-schnorr参照)で動作するように変更され、新しいopcode OP_CHECKSIGADDが追加された。
    • opcode 186 (0xba)がOP_CHECKSIGADDという名前になる。

署名opcodeのルール

以下のルールがOP_CHECKSIGおよびOP_CHECKSIGVERIFYOP_CHECKSIGADDに適用される。

  • OP_CHECKSIGVERIFYおよびOP_CHECKSIGの場合、公開鍵(1番上の要素)と署名(上から2番めの要素)がスタックからポップされる。
    • スタックの要素が2要素未満の場合、スクリプトは失敗し直ちに終了しなければならない。
  • OP_CHECKSIGADDの場合、公開鍵(1番上の要素)とCScriptNum n(上から2番めの要素)および署名(上から3番めの要素)がスタックからポップされる。
    • スタックの要素が3要素未満の場合、スクリプトは失敗し直ちに終了しなければならない。
    • nが4バイトより大きい場合、スクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵のサイズが0の場合、スクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵の先頭バイトが0x040x06および0x07の場合、公開鍵のサイズに関係なくスクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵の先頭バイトが0x02もしくは0x03の場合、bip-schnorrで定義されているように公開鍵とみなされる。
    • 公開鍵が33バイトでない場合、スクリプトは失敗し直ちに終了しなければならない。
    • 署名が空のベクターでない場合、公開鍵およびメッセージであるtapscriptトランザクションダイジェスト(以下に定義)に対して、bip-taprootの署名検証ルールに従って検証される。検証に失敗するとスクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵の先頭バイトが0x020x030x040x060x07のいずれでもない場合、公開鍵は未知の公開鍵タイプであり、実際の署名検証は適用されない。署名opcodeのスクリプト実行中、署名検証が成功したとみなされることを除き、既知の公開鍵タイプと同じように動作する。
  • 公開鍵のタイプに関係なく、この手順の前にスクリプトが失敗し終了しなかった場合は:
    • 署名が空のベクターの場合
      • OP_CHECKSIGVERIFYの場合、スクリプトは失敗し直ちに終了しなければならない。
      • OP_CHECKSIGの場合、空のベクターがスタックにプッシュされ、次のopcodeへ実行が続く。
      • OP_CHECKSIGADDの場合、値nCScriptNumがスタックにプッシュされ、次のopcopdeへ実行が続く。
    • 署名が空のベクターでなく、sigops_passedカウンターがインクリメントされる場合
      • OP_CHECKSIGVERIFYの場合、スタックのこれ以上の変更なく実行は継続する。
      • OP_CHECKSIGの場合、1バイトの値0x01がスタック上にプッシュされる。
      • OP_CHECKSIGADDの場合、値n + 1CScriptNumがスタックにプッシュされる。

これらのopcodeは201回の非push opcode制限にカウントされる。

トランザクションダイジェスト

署名opcodeの署名検証のためのメッセージとして、トランザクションダイジェストは以下を除いてbip-taprootと同じ定義である。

1バイトのspend_typeはビット2で指定される異なる値を持つ:

  • 使用されるscriptPubKeyが(native segwitではなく)P2SHである場合、ビット0がセットされる。
  • (元のwitnessスタックにはすくなくとも2つのwitness要素があり、最後の要素の先頭バイトが0x50である)annexが存在する場合、ビット1がセットされる。
  • ビット2ガセットされる。
  • 他のビットはセットされない。

 {hash_{TapSighash}}関数への入力の最後に追加される追加のデータ:

  • tapleaf_hash(32): bip-taprootで定義されるtapleafハッシュ
  • key_version(1): tapscript署名opcode実行において、現在の公開鍵のバージョンを表す定数値0x02
  • codeseparator_position(2): 現在実行されている署名opcodeの前に最後に実行されたOP_CODESEPARATORのopcodeの位置のリトルエンディアン表記(実行されていない場合は、0xffff)。スクリプト内の最初のopocdeの位置は0。マルチバイトpush opcodeはプッシュされるデータのサイズに関係なく1つのopcodeとしてカウントされる。

ハッシュされる総バイト数は最大244。

要約すると、BIP 143sighash typeのセマンティクスは以下を除いて変更されていない。

  1. 例外はbip-taprootに記載されている。
  2. ダイジェストはtaproot固有のデータkey_versionをコミットする。
  3. ダイジェストは、scriptCodeの代わりにleaf versionとスクリプトを含むtapleaf_hashを介して実行されるスクリプトにコミットする。
  4. ダイジェストは、最後に実行されたOP_CODESEPARATORの位置にコミットする。

署名opcodeの制限

201回の非push opcodeの制限に加えて、署名opcodeの使用はさらに制限を受ける。

  • input_witness_weightが、特定のトランザクションインプットに関連付けられているシリアライズされたインプットのwitnessのサイズとして定義される。BIP 141で定義されているようにシリアライズされたインプットのwitnessは要素数および各要素のサイズ、各要素の内容を示すCCompactSizeタグが含まれている。input_witness_weightは、CCompactSizeタグと要素の内容の合計サイズである。
  • sigops_passedが、正常に実行された署名opcodeの合計数として定義される。これらの署名サイズはゼロ以外で、失敗してスクリプトを終了させるものではない。誤解を避けるため、未知のタイプの公開鍵とゼロ以外のサイズの署名を署名opcodeに渡してもsigops_passedにカウントされる。
  • 50 * (sigops_passed - 1)input_witness_weightより大きい場合、スクリプトは失敗し直ちに終了しなければならない。

このルールは、従来のP2WSHスクリプトにのみ適用されるsigopsの制限と同様にtapscriptのワーストケースの検証コストを制限する。

論拠

  1. どうして0xcが選択されたのか? bip-taprootのガイドラインにしたがって、2つの最上位ビットをセットした値を選択することで、使用するUTXOにアクセスすることなく使用する量を識別することができる。
  2. OP_SUCCESSx OP_SUCCESSxスクリプトシステムをアップグレードするための仕組みだ。ソフトフォークによって意味が定義される前にOP_SUCCESSxを使うことは、安全ではなく、資金の損失に繋がる。スクリプト内にOP_SUCCESSxを含めると、無条件にコインを渡すことができる。これはさまざまなエッジケースを定義する際の問題を回避するためにスクリプトの実行ルールより優先される。例えば、OP_SUCCESSxが202回目のopcodeになったり、OP_SUCCESSxの後にたくさんの署名opcodeがあったり、OP_ENDIFを書いた条件付きスクリプトであっても。スクリプトのどこかにOP_SUCCESSxがあるだけで、そのようなすべてのケースでパスすることが保証される。OP_SUCCESSxは非常に初期バージョン(v0.1〜v0.3.5まで)のOP_RETURNに似ている。オリジナルのOP_RETURNスクリプトの実行をすぐに終了し、終了時のトップスタックの要素に基づいて成功か失敗かを返していた。これだとscriptSigにOP_RETURNを入れることで無条件に第三者による窃盗が可能であったため、オリジナルのBitcoinプロトコルの大きな設計上の欠陥の1つだった。OP_SUCCESSxスクリプトの一部であり(したがってtaprootアウトプットにコミットしている)、コイン所有者の同意を暗示しているため、第三者が検証プロセスにOP_SUCCESSxを挿入することは不可能であるため、これは本提案では問題にならない。OP_SUCCESSxはさまざまなアップグレードの可能性に使用できる。
    • OP_SUCCESSxはソフトフォークで機能的なopcodeに変換することができる。スタックへの読み取り専用アクセスしかできないOP_NOPx派生opcodeとは違って、OP_SUCCESSxはスタックへの書き込みも可能だ。OP_SUCCESSxを含むスクリプトの変更は、有効なスクリプトが無効なスクリプトになるだけで、これはソフトフォークで常に実現できる。
    • OP_SUCCESSxは初期スタックとpush opcodeのサイズチェックに先行するので、OP_SUCCESSx派生opcodeはソフトフォークで520バイトを超えるスタック要素を可能にするかもしれない。
    • また、OP_SUCCESSxは既存のopcodeを再定義し、新しいopcodeとの連携を可能にする。例えば、OP_SUCCESSx派生opcodeが64ビット整数で機能する場合、同じスクリプト内の既存の算術opcodeもでも同じことが可能になる。
    • OP_SUCCESSxはパースできない可能性があるスクリプトもパスさせるので、マルチバイトopocdeの導入や、OP_SUCCESSxプレフィックスにした完全に新しいスクリプト言語を導入するのに使うことができる。
  3. MINIMALIFをコンセンサスにするのはなぜ? こうすることで、スタックからブランチ情報を取得するmalleabilityの無いコードを書くのがかなり簡単になる。
  4. OP_CHECKSIGADD このopcodeはバッチ検証とは互換性のないOP_CHECKMULTISIGのようなopcodeが無くなるのをカバーするために追加されている。OP_CHECKSIGADDは機能的にはOP_ROT OP_SWAP OP_CHECKSIG OP_ADDと同等だが、201回の非push opcode制限に対して1つのopcodeとしてカウントされる。OP_ADDCScriptNum関連の振る舞いはすべてOP_CHECKSIGADDにも適用できる。
  5. CHECKMULTISIGとCHECKSIGの比較 witnessとして0 <signature_1> ... <signature_m>を持つ、CHECKMULTISIGスクリプトm <pubkey_1> ... <pubkey_n> n CHECKMULTISIGは、witness <w_1> ... <w_n>を持つスクリプト<pubkey_1> CHECKSIG ... <pubkey_n> CHECKSIGADD m NUMEQUALに書き換えることができる。各witness要素w_iは同じインデックスの公開鍵に対応する署名か空のベクターのいずれかである。同様にCHECKMULTISIGVERIFYスクリプトは、NUMEQUALNUMEQUALVERIFYに置き換えることでbip-tapscriptに変換できる。別の方法として、スクリプトをマークルツリーのいくつかのリーフに分割し、それぞれが<pubkey_1> CHECKSIGVERIFY ... <pubkey_(n-1)> CHECKSIGVERIFY <pubkey_n> CHECKSIGを使ってm-of-mポリシーを実装することで、m-of-nマルチシグポリシーを実装できる。この設定で参加者が署名しながら対話的にコラボレーションできる場合、マルチシグポリシーはm-of-mではMuSigを使い、m-of-nでは検証可能な秘密共有使って閾値署名を実現できる。
  6. 未知のタイプの公開鍵はソフトフォークを介して新しい署名検証ルールの追加を可能にする。ソフトフォークは、スクリプトをパスするか失敗してすぐに終了させる実際の署名検証を追加できる。この方法で、NOINPUTタグ付き公開鍵と署名検証用のtaprootの内部キーによって置き換えられる公開鍵定数と同様に新しいSIGHASHモードを追加できる。
  7. signature hash用にハッシュされたバイト数は?  {hash_{TapSighash}}への入力の合計サイズは(最初の64バイトのハッシュタグを除く)、次の式を使って計算できる。212 - is_anyonecanpay * 50 - is_none * 32 - is_p2sh_spending * 12 + has_annex * 32
  8. トランザクションダイジェストがkey_versionにコミットするのはなぜ? これは未知の公開鍵タイプを定義する将来の拡張のためのもので、署名をある鍵タイプから別のタイプに移動できないようにするため。この値は、Y座標の偶奇のようなフラグをマスキングした後の公開鍵の先頭バイトと等しくなるよう設定されることを意図している。
  9. トランザクションダイジェストが最後に実行されたOP_CODESEPARATORの位置にコミットするのはなぜ? これによりOP_CODESEPARATORを引き続き使用してスクリプトの実行パスに署名できる。codeseparator_positionがダイジェストへの最後の入力なので、SHA256のmidstateが単一のスクリプトで複数のOP_CODESEPARATORに対して効率的にキャッシュできる。対照的に、BIP 143のOP_CODESEPARATORのハンドリングは最後に実行されたOP_CODESEPARATOR以降から実行されたスクリプトにコミットするため、スクリプトの不必要な再ハッシュが必要になる。2つのコードブランチ間で最初の公開鍵を共有することで、2つ目の公開鍵プッシュをスクリプトに保存するというOP_CODESEPARATORの既知のユースケースは、各ブランチを別々のtaprootリーフに移動することでさらに安価に表現できる。
  10. tapscriptのsigop制限 署名opcodeの制限は、過度に多い署名操作によって検証が遅くなることからスクリプトを保護する。tapscriptでは、署名opcodeの数はBIP141または従来のsigop制限にはカウントされない。従来のsigop制限は、トランザクションweightに加えて2つめの制約であるため、ブロックを構成する際のトランザクションの選択を不必要に難しくする。代わりに、tapscriptの署名opcodeの数はwitnessのweightによって制限される。さらに制限はブロックではなくトランザクションのインプットに適用され、実際に実行された署名opcodeのみがカウントされる。tapscriptの実行では、50 witness weightあたり1つの署名opcodeを許可し、プラス1つの無料の署名opcodeが許可される。tapscriptの署名opcodeの制限により、CHECKSIGFROMSTACKのような新しい署名opcodeを追加し、ソフトフォークを介して制限にカウントすることができる。将来、通常のスクリプトコストを変更する新しいopcodeが導入されたとしても、witnessに無意味なデータを詰め込む必要がある。その場合、実際のwitnessサイズを増やすこと無くwitnessのweightを増やすためにtaproot annexを使用することができる。
  11. sigop制限のパラメータ選択 通常のwitnessはそのweightが公開鍵と(SIGHASH_ALL)署名のペアの34 + 65 weight単位で構成されているため(1 weightのCCompactSizeタグを含む)、制限の影響を受けない。署名のweightだけで65もしくは66 weightの単位になるため、公開鍵がスクリプトで再利用されている場合も同様。ただし、この制限により追加のweightが必要となり、重複した署名(および公開鍵)を持つ異常なスクリプトの手数料が増加する。sigopファクター50あたりのweightはBIP 141のブロックリミットの比率(4MBのweightを80,000 sigopで割ったもの)に相当する。制限によって許可された無料の署名opcodeは、トランザクションインプットの非witness部分のweightを考慮するために存在する。