Develop with pleasure!

福岡でCloudとかBlockchainとか。

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

Tapscript上で限定的に署名済みトランザクションのインプットの参照先をリバインド可能にする提案 bip-anyprevout

bip-taprootbip-tapscriptが定義されたが、これに関連してLightning Networkのeltooを実現するための提案も出てきてるので見てみる↓

https://github.com/ajtowns/bips/blob/bip-anyprevout/bip-anyprevout.mediawiki

eltooは、LNにおいて旧状態がブロードキャストされた際に、既存のPoon-Dryja形式のペナルティ型ではなく、チャネルの最新状態を適用するタイプのペイメントチャネルの提案。eltooの仕組みについては以前GBECで解説した動画が分かりやすいと思う↓。

goblockchain.network

このeltooを実現するためには、トランザクションの署名時にトランザクションダイジェストがインプットが参照する前のUTXOのOutPointへのコミットをしないようにするSIGHASH_NOINPUTの導入が前提として必要で、これはBIP-118として提案されている↓

techmedia-think.hatenablog.com

この未導入のSIGHASH_NOINPUTの提案を、以下のように変更した新しいフラグSIGHASH_ANYPREVOUTおよびSIGHASH_ANYPREVOUTANYSCRIPTの導入がbip-anyprevoutの提案内容。

  • SIGHASH_NOINPUTフラグはSegwitのversion 1以降で有効になるフラグであったが、bip-anyprevoutは、bip-schnorrおよびbip-taproot、bip-tapscriptの導入後に適用される。その際、ANYPREVOUTが適用できる公開鍵は限定され、bip-tapscriptの現時点では未定義の公開鍵タイプを導入することで適用される。この時の公開鍵タイプは0x000x01
  • NOINPUTフラグはスタック上の任意の公開鍵の署名検証に対して利用可能であるが、ANYPREVOUTはtaproot利用時にtapscripを利用した署名検証にのみ利用可能ので、tapscriptではなくtaprootの内部キーを使ったケースでは使用できず、利用シーンが限定される。
  • NOINPUTは無条件にscriptCodeにもコミットしない形になっていたが、bip-anyprevoutの場合SIGHASH_ANYPREVOUTANYSCRIPTは同様の振る舞いをするが、SIGHASH_ANYPREVOUTは使用するscriptPubkeyとtapscriptにコミットする。
  • ANYPREVOUT利用時にはChaperone署名を適用する=別途ANYPREVOUT以外にインプットの参照先にコミットする署名検証がされることを保証する必要がある。つまり2-of-2のようなマルチパーティ間の署名はANYPREVOUTを使用して、それだとOutPointへの参照がコミットされないので、1-of-2のような署名をもう1つ用意してそっちでANYPREVOUTでないOutPointへコミットする署名を要求する。
所感

実際、bip-anyprevoutが導入されるか、また別の提案が出てくるかはまだ分からないけど、

  • Chaperone署名あたりは署名が余分にもう1つ必要になり複雑かつサイズ増になる気がするんだけど、そこまでの保護が必要か?
  • インプットの参照先をダイナミックに変更できるようになるとeltooみたいないユースケースが可能になる反面、Segwitで解消されたtxidのmalleabilityがまた発生するようになるのでよく注意してプロトコル設計する必要がある。
  • まだ慣れてないかつデプロイされてない&bip-tapscriptの仕様上の話になるけど、tapscript上に未定義の公開鍵タイプを使って拡張するの、新しいスクリプトルールを適用できるという意味で拡張の選択肢は広がるものの、結構仕組みとして複雑な構造にシフトしてるように思える。

あたりが気になるところ。

以下、bip-anyprevoutの内容。

概要

このBIPではtapscript(bip-tapscript)トランザクションのための新しい公開鍵タイプについて定義する。これらの新しいタイプの公開鍵、使用するアウトプットについてコミットしなくても良いようになる。アウトプットが互換性のあるスクリプトである場合、これによりトランザクションを異なるUTXOにバインドすることが可能になる。

動機

オフチェーンプロトコルは、オンチェーンで決済する最終状態を再調整するためBitcoinネットワークにブロードキャストされていないトランザクションを利用する。多くの場合、オンチェーン上のトランザクションに対して、別のトランザクション形式で所定の反応を返すのが望ましい。オンチェーンで確認できる可能性がある様々な異なるトランザクションに対して同じ反応が望まれることがよくあるが、アプリケーションは固有の応答トランザクションを作成する必要がある。応答トランザクションのインプットの署名は対応するトランザクションに正確にコミットしているため、そのようなトランザクションを作るためには署名用の秘密鍵を必要とする。

この提案では、署名の作成と検証に使われるトランザクションダイジェストアルゴリズムの振る舞いを、前のアウトプット(オプションでwitness script)へのコミットメントを除外することで修正する新しい公開鍵タイプを導入する。このコミットメントの削除は、署名済みのトランザクションのインプットの参照先を、同じ鍵による認可を必要とする同じコインの量を持つ別のアウトプットへ動的に再バインドすることを可能にする。

動的な再バインドは別の公開鍵タイプを使うことでオプトインされるもので、異なる公開鍵を使ったり、署名内で使用するスクリプトにコミットすること、UTXO間で異なる金額を使用すること、使用するトランザクション内で異なるnSequenceの値を使用すること、codeseparator opcodeを使ってスクリプトの位置にコミットすることなどで、さらに制限される。

仕様

このBIPは、先頭バイトが0x000x01である公開鍵のbip-tapscriptの署名opcode(CHECKSIGCHECKSIGVERIFYおよびCHECKSIGADD)の動作を変更する。これらの鍵はbip-anyprevout keyと呼ばれる。

署名opcodeのルール

署名opcodeのbip-tapscriptのルールを次のように変更する。未知の公開鍵タイプのリストから先頭バイト0x000x01の鍵を削除し、未知の公開鍵タイプを処理する前に以下のルールを追加する。

  • 公開鍵の先頭バイトが0x00もしくは0x01の場合それはbip-anyprevout公開鍵とみなされる。
    • 公開鍵が33バイトではなく、単一バイト0x01でもない場合、スクリプトは失敗しすぐに終了しなければならない。
    • 署名が空のベクターでない場合、署名は、以下に定義されている公開鍵および許容されるhash_type、修正されたトランザクションダイジェストを使ってbip-tapscriptの署名検証ルルールに従って検証される。

公開鍵

bip-schnorrの公開鍵は0x02もしくは0x03で始まる33バイト列として定義されているため、(0x00もしくは0x01で始まる)bip-anyprevoutの公開鍵は署名検証に使う前に変換する必要がある。変換の手順は以下のとおり:

  • bip-anyprevoutの公開鍵が単一バイト0x01であった場合、bip-taprootの署名検証ルールに使われる公開鍵はtaprootの内部キーになる(つまりbip-taprootの表記でいうとbytes(P))。
  • bip-anyprevoutの公開鍵が33バイトの場合、検証に使われる公開鍵の先頭バイトが0x02もしくは0x03になるように先頭バイトのbit-1をセットすることで、bip-schnorr互換の公開鍵に変換される。残りの32バイトはスタック上のbip-anyprevout公開鍵と一致する。

hash_type

bip-taprootで許可されているhash_typeの値に加えて、bip-anyprevout公開鍵では、値0x41, 0x42, 0x43, 0xc1, 0xc2,0xc3が有効になる。hash_typeのビット6と7を使って以下の定数を定義する。

  • 0x00 SIGHASH_ALLINPUT
  • 0x80 SIGHASH_ANYONECANPAY
  • 0x40 SIGHASH_ANYPREVOUT``
  • 0xc0 SIGHASH_ANYPREVOUTANYSCRIPT``

そして、例えばhash_type & 0xc0 == SIGHASH_ANYPREVOUTの場合、SIGHASH_ANYPREVOUTがセットされていると言える。

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

署名opcodeの署名検証のメッセージとして、bip-anyprevout公開鍵のトランザクションダイジェストは、bip-tapscriptと以下が異なる。

  • 全てのケースにおいて、key_version0x02の代わりに定数値0x00が使われる。
  • SIGHASH_ANYPREVOUTがセットされている場合、outpointがダイジェストに含まれていないことを除いてSIGHASH_ANYONECANPAYがセットされた場合と同じように計算される。
  • SIGHASH_ANYPREVOUTANYSCRIPTがセットされている場合、outpointscriptPubKeyおよびtapleaf_hashがダイジェストに含まれていないことを除いてSIGHASH_ANYONECANPAYがセットされた場合と同じように計算される。

SIGHASH_ANYPREVOUTがセットされている場合、トランザクションダイジェストへのバイト単位の入力は、対応するSIGHASH_ANYONECANPAYトランザクションダイジェストより36バイト短くなる(outpointのサイズ分)。SIGHASH_ANYPREVOUTANYSCRIPTがセットされている場合、トランザクションダイジェストへの入力は、SIGHASH_ANYPREVOUTがセットされている場合と比べてさらに68もしくは56バイト短くなる(tapleaf_hashの32バイト分と、scriptPubKeyの36バイトもしくは24バイト分)。

Chaperone署名

ANYPREVOUT署名が使われる場合、固定のprevout署名(つまり非ANYPREVOUTの署名)で保護されなければならない。

これを実現するため、スクリプトの実行開始時に3つのフラグv0_anyprevoutv0_fixedprevoutおよびv2_fixedprevoutが導入され、最初はすべてfalseがセットされている。これらは以下の場合にtrueになる。

  • 公開鍵の先頭バイトが0x02もしくは0x03で、nullでない署名の署名opcodeが成功した場合、フラグv2_fixedprevoutにtrueがセットされる。
  • bip-anyprevout公開鍵とnullでない署名の署名opcodeが成功した場合:
    • SIGHASH_ANYPREVOUTもしくはSIGHASH_ANYPREVOUTANYSCRIPTがセットされている場合、フラグv0_anyprevoutにtrueがセットされる。
    • それ以外の場合(SIGHASH_ALLINPUTもしくはSIGHASH_ANYONECANPAYがセットされている場合)、フラグv0_fixedprevoutにtrueがセットされる。

スクリプトの実行終了時に、v0_anyprevoutにtrueがセットされていても、v2_fixedprevoutv0_fixedprevoutがfalseのままの場合、スクリプトは失敗しなければならない。

安全性

署名のリプレイ

SIGHASH_ALLINPUTおよびSIGHASH_ANYONECANPAYの署名と比べて、SIGHASH_ANYPREVOUTおよびSIGHASH_ANYPREVOUTANYSCRIPTは、同じ署名が違うトランザクションで再利用される署名リプレイの可能性をもたらす。

SIGHASH_ALLINPUTSIGHASH_ANYONECANPAYの署名はどちらも1つ以上のインプットにコミットすることで署名のリプレイを防ぐため、同じインプットを複数回使用できる場合のみ署名のリプレイが可能になるが、BIP 30およびBIP 34以降のBitcoinブロックチェーンでは不可能だ。SIGHASH_ANYPREVOUTによる署名のリプレイは、同じscriptPubKeyと同じコインの量を持つ異なるUTXOに対して可能だが、SIGHASH_ANYPREVOUTANYSCRIPTによる署名のリプレイは同じコインの量を持つ異なるUTXOに対して可能で、潜在的スクリプトの1つで同じbip-anyprevout公開鍵を再利用する。

その結果、署名のリプレイが資金の消失や他の望ましくないケースの原因になる場合、プロトコルの設計者とウォレットはANYPREVOUT署名を使用する際、同じアドレスを再利用しないことを保証しなければならず、SIGHASH_ANYPREVOUTANYSCRIPTを使って署名する場合はスクリプトでbip-anyprevout公開鍵を再利用しないことを保証しなければならない。

Malleability

SIGHASH_ANYPREVOUTまたはSIGHASH_ANYPREVOUTANYSCRIPTを使うと追加のmalleabilityが導入される可能性がある。

特に、ANYPREVOUT署名のみを使って承認されたトランザクションは、署名を満たす代替インプットを提供できる人すべてに対しmalleabilityがある。この方法で変更されたインプットは、同じ受信者に対して支払いをする有効なトランザクションであるがtxidが異なるという新しいトランザクションを生成する。インプットへの変更によって、(一部のインプットが共有されたままの場合)これは元のトランザクションと競合する可能性があり、または(そうでない場合)受信者への二重支払いになる可能性がある。

さらに、(稀な失敗のケースとしてeltooで想定されているように)同じscriptPubkeyとコインの量を使い、ANYPREVOUT署名のみで承認されるトランザクションのチェーンでは、特に中間のトランザクションを省略することで、第三者秘密鍵にアクセスすることなく、トランザクション(およびそのtxid)を細工する可能性がある。

この形式の細工は、ANYPREVOUT署名を使用する子トランザクションでも対処できる。親トランザクションに細工された場合、その子はインプットとして細工された新しいtxidを参照するように単純に調整でき、ANYPREVOUT署名は有効のままだ。

ただし、SIGHASH_ALLINPUTもしくはSIGHASH_ANYONECANPAYの署名によって承認された子トランザクションでは、そのインプットがこの方法で細工された場合、新しい署名が必要になる。このリスクは、ANYPREVOUTで承認されたUTXOをSIGHASH_ALLINPUTもしくはSIGHASH_ANYONECANPAYで使用する前に、BIP 68/112の相対的タイムロックを使うことで、いくらか軽減できる。相対的なタイムロックは、インプットに十分承認されていることを保証し、大規模なブロックの再編成が起きた場合のみそれらを置き換えることができる。このアプローチには欠点があることに注意すること:相対的なタイムロックはchild-pays-for-parentによる手数料のバンプを妨げ、タイムロックが期限切れになるまで資金を一時的に使用不可能にするという明らかな欠点がある。

Chaperone署名の効果

Chaperone署名の導入は2つの方法で分析できる。

まず、ANYPREVOUT署名とChaperone署名によって署名されたトランザクションの安全性は、Chaperone署名のみで署名されたトランザクションよりも悪くない。これにより、アドレスの秘密鍵が共有されている場合に、トランザクションが既に可能であったよりも悪いセキュリティ特性を持つことがなくなる。二重使用や二重支払いは、秘密鍵の所有者が複数の署名をする場合にのみ可能であり、秘密鍵の所有者が細工されたトランザクションに署名することを選択した場合にのみトランザクションは細工される。Chaperone署名の秘密鍵がマルチシグアドレス用の既存の秘密鍵よりも基本的に安全性の低い方法で共有されていない場合、ANYPREVOUT署名によって承認された支払いを受け取るBitcoinユーザーは他の支払いと比べて追加のセキュリティ対策を講じる必要は無い。

第2に、ANYPREVOUT署名とChaperone署名を使ってトランザクションに署名をする効率は、ANYPREVOUT署名のみを使った場合よりもそれほど悪くはない。特に、ANYPREVOUT署名の秘密鍵は、トランザクションを発行することを承認されたすべての人の間で共有される鍵を介して1-of-Nのマルチシグ要件として扱うことができる。トランザクション公開される準備ができるまでに全てのインプットが既知でなければならないので、Chaperone署名は生成できる。これにより、トランザクション自体とトランザクションを公開するノードの両方に追加のオーバーヘッドが発生することに注意すること。また、トランザクションの複数の潜在的な発行者がそれぞれ異なるChaperone署名を生成する可能性があるため、witnessのmalleabilityに関する別のベクトルも導入される。これにより同じtxidにも関わらず、書く署名に対して異なるwtxidが生成される。これは(64バイトの署名ではなく65バイトの署名を使うことにより)手数料レートを僅かに変更する可能性があり、(ブロックはある署名を持っているが、そのブロックがリレーされたノードのメモリプールには異なる署名がある)ブロックリレーのパフォーマンスを低下させる可能性がある。

そのため、プロトコルの設計者は、Chaperone署名に対して既知の秘密鍵を使用せず、安全な方法でこれらの秘密鍵を生成し、その秘密鍵の配布を制限する必要がある。さらに署名者は、bip-schnorrで定義されているように、Chaperone署名を生成するために安全で決定論的な方法を使用する必要がある。

プライバシーに関する考慮事項

ANYPREVOUT署名は実際にはめったに使用されないことが予想される。プロトコルおよびウォレットの設計者は、トランザクションweightが低いという効率上の理由だけでなく、第三者が他のプロトコルトランザクションとこのトランザクションを区別できないようにするためにも、できる限りTaprootのキーパスを使うようにする必要がある。

そのため、ANYPREVOUTを使ったトランザクションは、協調が不可能であったことを潜在的に含むトランザクションに関する情報または、(スクリプトの詳細により)使用しているプロトコル、ソフトウェアに関する情報が明らかになる。

プライバシーを最大限にするため、プロトコルの設計者は少なくとも1つのANYPREVOUT署名を使って使われるコインのスクリプト内でbip-anyprevout公開鍵のみを使用し、ANYPREVOUT署名なしで承認可能な使用にはキーパスもしくはマークルツリー内の代替スクリプトを使用することを推奨する。この推奨事項に従うと、追加のスクリプトブランチが必要になる場合がある。つまり推奨事項を無視すると、状況によってはコストとプライバシーの間のトレードオフが向上する可能性がある。

論拠

  1. どうして新しい公開鍵タイプを作るのか? tapscriptの新しい公開鍵タイプは、bip-tapscriptで指定されている未知の公開鍵タイプに対して新しいルールを指定することでソフトフォークで導入できる。これは既存の署名opcodeに制限を加えるだけで良いためだ。可能な代替アプローチは、新しいscript opcodeを定義すること、異なるtaprootのleaf versionを使用すること、もしくはbip-taprootで定義されるものとは異なるsegwitアウトプットを使用することくらいだ。しかし、これらのアプローチはすべてより複雑で、これらのアプローチが提供する追加の柔軟性が実際に必要とされる他のアップグレードのために予約される。今回のケースでは、新しいトランザクションダイジェストを定義するが、同じ楕円曲線と署名アルゴリズム(つまりsecp256k1とbip-schnorr)を維持する。
  2. どうしてwitness scriptへコミットするのか(しないのか) eltooのペーパーは、witness scriptへコミットすることが必ずしも適切でない例を掲載している。署名を非対称に作成するためスクリプトトランザクションnLocktimeを使用する。そのため、より早い番号の署名を持つトランザクションは、より後の番号の署名を持つトランザクションで使用できるが、後の番号の署名を持つトランザクションを、それより早い番号の署名を持つトランザクションで使用することはできない。結果として、さらに後の3番めの単一の署名を持つ後のトランザクションは、前の2つのトランザクションが例え異なるtaprscriptを持っていたとしても、そのトランザクションを使用できなければならない。一方、これらのケースでは、スクリプトにコミットするオプションがある正当な理由もある:各トランザクションには新しいスクリプトがあるため、スクリプトへコミットするとこれらのトランザクションの1つに正確に適用される署名を作成できる。eltooの場合、これにより、以前の任意の更新トランザクションに適用できる更新トランザクションの署名と、対応する更新トランザクションにのみ適用される決済トランザクションの署名が可能になり、両方に同じ鍵を使用する。結果、スクリプトをよりコンパクトにできる。
  3. キーパスの使用はどうなる? この提案は、スクリプトパスを介したANYPREVOUTの使用のみをサポートし、キーパスを使った使用へのANYPREVOUTの使用はサポートしていない。これには3つの理由がある:最初に、キーパスの使用をサポートしないことで、bip-taprootおよびbip-tapscriptに含まれるコアな変更から独立させることができる。第二に、キーパスでChaperone署名を要求するのは不可能であるということ。第三に、アドレスがANYPREVOUTサポートをオプトインもしくはオプトアウトできるようにすると、使用される前に見分けがつかない。
  4. 0x00と0x01の使用 プレフィックスには125個のフリーペア(トータル128で、0x02/0x03は既に使われ、0x04および0x060x07は使用不可)があるので、最初に利用可能なものを選択した。これによりtaprootの内部キーの1バイトのプッシュにOP_1を使用することもできる。(1バイトのpush opcodeは、0x08/0x09, 0x0a/0x0b, 0x0c/0x0d, 0x0e/0x0f,0x10, OP_8 - OP_16, 0x81,OP_1NEGATE`にも使用可能)。
  5. taprootの内部キー taprootの内部キーを使って署名することは、taprootのキーパスを使って署名することを意味するが、SIGHASH_ANYPREVOUTANYSCRIPT署名の場合、これはtaprootアウトプットの鍵もしくはscriptPubkey計算する、もしくは全てのスクリプトを知る前に行うことができる。このキーのショートカットとして単一バイトの0x01を使用すると、単一バイトのOP_1を介してプッシュできるので、スクリプトエンコードするのが非常に効率的になる。
  6. なぜkey_versionを変更するのか? key_versionを変更することで、同じ秘密鍵を使ってbip-tapscriptの鍵とbip-anyprevout鍵の両方を生成した場合、 bip-tapscriptの鍵の署名はbip-anyprevoutの鍵の署名に対して有効ではなくなる(その逆もしかり)。
  7. Chaperone署名は必要? 署名のリプレイや、追加のmalleabilityのリスクが単にウォレットや2ndレイヤーのプロトコルレベルではなく、コンセンサスレイヤーで実際の影響を与えるか、あるいは防止が必要かどうかについてはいくつかの論争がある*1。この提案が採用する設計の哲学は、コンセンサスの変更は、物事を危険にする証拠が不十分であるという否定的な議論ではなく、物事が安全でなくなることはないという肯定的な議論と一緒に検討すべきだ。Chaperone署名を追加しない限り、ANYPREVOUTの安全性に対する肯定的な議論はない。実際には、ANYPREVOUT署名のみでも安全かもしれないが、それに対して理論的な証明も、実際にそれを実証する大規模な経験もない。安全性のセクションの議論は、ChaperoneANYPREVOUT署名の安全性が現在の署名と実質的に同等であることを示すのを目的にしている。つまり物事がそれほぞ安全でないことを示している。
  8. 常に固定のprevoutを必要とする署名にしないのはなぜ? 別のアプローチは、ANYPREVOUT署名が使われているかどうかに関わらず、常に固定のprevout署名を要求するというもので、つまりスクリプトの実行終了時にv2_fixedprevoutがtrueになり、v0_anyprevoutv0_fixedprevoutをまったく追跡しないというものだ。これにより強い保証が提供される。トランザクションを検証するどんなノードもそれが認識する方法でインプットが署名されていることを確認するか、(将来の署名アップグレードが使われいるなら)トランザクションを完全に妨げられないと考えるだろう(アップグレードを有効にするOP_SUCCESSxの存在があるため)。欠点は、このような将来の変更にはbip-tapscript署名を伴う必要があるため、未知の公開鍵タイプを使って新しいトランザクションダイジェスト、もしくは潜在的に新しい署名アルゴリズム(別の楕円曲線への変更など)を導入する能力を大幅に制限する点だ。対照的に、Chaperone署名を必要とするかどうか選択するのは、新しい公開鍵タイプもしくは安全でないことが知られている署名を導入する場合のみである。つまり何か新しいものを導入する際に、我々が安全だと確認しているなら、不要なChaperone署名を要求することは強制されない。

展開

これは、bip-scnorr、bip-taproot、bip-tapscriptの展開と同時もしくはその後にソフトフォークとして展開できる。

後方互換

ソフトフォークとして、古いソフトウェアは変更なく動作し続ける。bip-taprootをサポートするためにアップグレードしていないノードは全てのtaprootのwitness programを誰でも使用可能なスクリプトとして認識し、bip-taprootおよびbip-tapscriptをサポートするためにアップグレードしたがこのBIPには対応していないノードは単にbip-anyprevout公開鍵に対して空でない署名を有効なものとして扱う。ただし、bip-tapscript公開鍵に対するものである場合は、Chaperone署名を検証する。そのため、新しい公開鍵タイプの署名を完全に検証するためにはアップグレードすることを強く推奨する。

アップグレードされていないウォレットは、segwit version 0 programや従来のpay-to-pubkey-hashを使って、アップグレードされていないウォレットおよびアップグレードされたウォレットからBitcoinを送受信できる。実装によってはアップグレードされていないウォレットも、BIP 173 のBech32アドレスの送金をサポートしている場合、segwit version 1プログラムへの送金が可能である。アップグレードされていないウォレットはBIP 16のP2SHでネストされているsegwit version 1 programを使って、アップグレードされたウォレットにBitcoinを送金できる。

BIP 118との違い

segwit v0ではなくTaprootに基づいていることを除けば、BIP 118の主なセマンティクスの違いは以下のとおり:

  • BIP 118 NOINPUT署名は、scriptPubkeyもしくはredeem/witness scriptのいずれかでアウトプットの使用条件にコミットしない。この提案は、SIGHASH_ANYPREVOUTANYSCRIPTが使用される場合は同じ振る舞いをするが、SIGHASH_ANYPREVOUTが使用される場合は、scriptPubkeyとtapscriptにコミットする。
  • スクリプト内のOP_CODESEPARATORは、この提案のSIGHASH_ANYPREVOUTおよびSIGHASH_ANYPREVOUTANYSCRIPTの両方の署名に影響するが、BIP 118のNOINPUT署名には影響しない。
  • BIP 118は、(展開がBIP 141のP2WPKHおよびP2WSHと同様の方法で具体化されている仮定した場合)直接的な公開鍵の使用に対して有効に機能するはずだが、この提案では、tapscriptを介した署名にのみ適用され、キーパスを使った直接的な使用には適用されない。
  • この提案ではChaperone署名を必要とするとが、BIP 118では必要としない。
  • この提案では、NOINPUTではなくANYPREVOUTという名称を使用する。これは前のアウトプットのコインの量は署名に使われる、つまり前のアウトプットのvalueやインプットのnSequenceの値および(オプションで)使用条件など、インプットのいくつかはまだコミットされるためである。

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

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

techmedia-think.hatenablog.com

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

https://github.com/bitcoin/bips/blob/master/bip-0342.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ドラフトの内容(追記:2020.03.19時点の内容で以下更新)。

イントロダクション

概要

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

動機

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

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

設計

これらの目標を達成するために、署名opcode、OP_CHECKSIGおよびOP_CHECKSIGVERIFYは、BIP340で指定されたSchnorr署名を検証し、BIP-341の共通メッセージ計算に基づく署名メッセージアルゴリズムを使用する。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 141で定義されているようなwitness programを含む)。
  • そのアウトプットはBIP-341で定義されているtaprootアウトプットである(つまり、witness version 1で、witness programは32バイト。ただしP2SHでラップされていないこと)。
  • BIP-341で定義されたスクリプトパスを使用している(つまり、witnessスタックからオプションのannexを削除後、2つ以上のスタック要素が残ること)。
  • リーフバージョンは0xc0(オプションのannexを削除後、最後のwitness要素の先頭バイトが0xc0もしくは0xc1である)、つまりTapscriptの使用としてマークされている。

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

  1. インプットがBIP 141もしくはBIP 341に対して無効な場合、失敗する。
  2. BIP 341で定義されているスクリプト(つまりオプションの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 341で定義されている初期スタックが(つまり、オプションのannexおよび最後から2つのスタック要素を両方削除した後のwitnessスタックのサイズ)、リソース制限(スタックサイズ、スタック内の要素のサイズ、「リソース制限参照」)に違反する場合、失敗する。このチェックはOP_SUCCESSxを使ってバイパスできることに注意すること。
  4. tapscriptは、初期スタックをインプットとして次のセクションのルールに従って実行される。
    1. 何らかの理由で実行に失敗した場合、失敗する。
    2. 実行した結果、スタック上の要素が正確に1つで、それがCastToBool()でtrueとして評価される要素以外が発生した場合、失敗する。
  5. 失敗すること無くこのステップに到達すると、検証は成功となる。

スクリプトの実行

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

  • 無効化されたスクリプトopcode 次のscript opcodeはtapscriptでは無効になっている:OP_CHECKMULTISIGおよびOP_CHECKMULTISIGVERIFY。無効化されたopcodeは、実行されるとすぐに終了し、未実行ブランチで見つかっても無視するという点でOP_RETURNと同じように動作する。
  • コンセンサスによる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の場合、スクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵のサイズが32バイトの場合、BIP 340の公開鍵とみなされる。
    • 署名が空のベクターでない場合、公開鍵に対して、検証される(以下のセクション参照)。検証に失敗するとスクリプトは失敗し直ちに終了しなければならない。
  • 公開鍵のサイズが0でも32バイトでもない場合、公開鍵は未知の公開鍵タイプであり、実際の署名検証は適用されない。署名opcodeのスクリプト実行中、署名検証が成功したとみなされることを除き、既知の公開鍵タイプと同じように動作する。
  • 公開鍵のタイプに関係なく、この手順の前にスクリプトが失敗し終了しなかった場合は:
    • 署名が空のベクターの場合
      • OP_CHECKSIGVERIFYの場合、スクリプトは失敗し直ちに終了しなければならない。
      • OP_CHECKSIGの場合、空のベクターがスタックにプッシュされ、次のopcodeへ実行が続く。
      • OP_CHECKSIGADDの場合、値nCScriptNumがスタックにプッシュされ、次のopcopdeへ実行が続く。
    • 署名が空のベクターでなく、sigopsバジェットにカウントされる
      • OP_CHECKSIGVERIFYの場合、スタックのこれ以上の変更なく実行は継続する。
      • OP_CHECKSIGの場合、1バイトの値0x01がスタック上にプッシュされる。
      • OP_CHECKSIGADDの場合、値n + 1CScriptNumがスタックにプッシュされる。

署名検証

公開鍵pで署名sigを検証するには、

  • 次のデータの連結で構成されるtapscriptメッセージ拡張extを計算する。
    • tapleaf_hash(32):BIP 341で定義されているtapleaf hash
    • key_version(1):tapscript内の署名opcode実行における公開鍵の現在のバージョンを表す定数値0x00。
    • codesep_pos(4):現在実行中の署名opcodeの前に最後に実行されたOP_CODESEPARATORの位置でリトルエンディアンで表現(OP_CODESEPARATORが実行されていない場合は0xffffffff)。スクリプト内の最初のopcodeの位置は0。マルチバイトのpush opcodeはプッシュされるデータのサイズに関係なく1 opcodeとしてカウントされる。
  • sigの長さが64バイトの場合、Verify(p, hashTapSigHash(0x00 || SigMsg(0x00, 1) || ext), sig)の結果を返す。このVerifyはBIP 340に定義されている。
  • sigの長さが65バイトの場合、sig[64] ≠ 0x00で、Verify(p, hashTapSighash(0x00 || SigMsg(sig[64], 1) || ext), sig[0:64])の結果を変えす。
  • それ以外の場合、失敗。

要約すると署名検証のセマンティクスは以下の除いてBIP 340と同じだ。

  1. 署名メッセージにはtapscript固有のデータkey_versionが含まれる。
  2. 署名メッセージは、scriptCodeではなくleaf versionとスクリプトを含むtapleaf_hashを介して実行されたスクリプトにコミットする。これはこのコミットメントがOP_CODESEPARATORの影響を受けないことを意味する。
  3. 署名メッセージには、最後に実行されたOP_CODESEPARATORのopcode位置が含まれる。

リソース制限

多数のopcodeのセマンティクスを変更することに加えて、リソース制限にもいくつかの変更がある。

  • スクリプトサイズの制限:最大スクリプトサイズ10000バイトは適用されない。サイズはブロックのweight制限によってのみ暗黙的に制限される。
  • 非プッシュopcodeの制限スクリプトあたり201の非プッシュopcodeの最大数制限は適用されない。
  • Sigops制限:tapscript内のsigopsはブロック全体の制限である80000にはカウントされない。代わりに、スクリプト毎のsigopsバジェットがある。バジェットは50 + (CCompactSizeプレフィックスを含む)トランザクションインプットwitnessの合計シリアライズサイズ。空でない署名を使って署名opcode(OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD)を実行するとバジェットが50減る。バジェットが0未満になるとスクリイプとはすぐに失敗する。未知の公開鍵タイプと空で無い署名を持つ署名opcodeもカウントされる。
  • スタックとアルトスタックのカウント制限:全てのopcode実行後のスタックとアルトスタックの要素の最大数が1000の既存の制限は残り、初期スタックのサイズにも適用されるよう拡張される。
  • スタック要素サイズの制限:初期スタックとプッシュopcodeの両方で、スタック要素あたり最大520バイトの制限は残る。

論拠

  1. OP_SUCCESSxOP_SUCCESSxスクリプトシステムのアップグレードのための仕組み。ソフトフォークによって意味が定義される前にOP_SUCCESSxを使用することは安全ではなく、資金消失に繋がる。スクリプトOP_SUCCESSxを含めると無条件にコインが渡される。さまざまなエッジケースを指定する際の難しさを回避するため、スクリプト実行ルールに先行する。例えば、入力スタックが1000要素を超えるスクリプト内のOP_SUCCESSx、もしくは署名opcodeが多すぎた後のOP_SUCCESSxOP_ENDIFが内条件付きスクリプトなど。スクリプト内のどこかにOP_SUCCESSxが存在するだけで、このようなケースのパスが保証される。OP_SUCCESSxは初期のBitcoin(v0.1からv0.3.5まで)のOP_RETURNに似ている。元々のOP_RETURNはスクリプトの実行を即座に終了し、終了した時点で一番上のスタック要素に基づいて成功、失敗を返す。これは、scriptSigにOP_RETURNを配置することで無条件に第三者が窃盗できてしまうので、元々のBitcoinプロトコルの主な設計上の欠陥の1つだった。OP_SUCCESSxスクリプトの一部であり(したがって、taprootアウトプットによってコミットされる)コインの所有者の同意を意味し、第三者が検証プロセスにOP_SUCCESSxを挿入することはできないため、これは本提案では懸念事項にならない。OP_SUCCESSxはさまざまなアップグレードの可能性に使用できる。
    • OP_SUCCESSxは、ソフトフォークを介して機能的なopcodeに変換できる。スタックへのread-onlyアクセスのみを持つOP_NOP系のopcodeとは異なり、OP_SUCCESSxはスタックに書き込むこともできる。OP_SUCCESSxを含むスクリプトに対するルール変更は、有効なスクリプトのみを無効なスクリプトに変えることができ、これはソフトフォークで常に達成可能だ。
    • OP_SUCCESSxは初期スタックやプッシュopcodeのサイズチェックに先行するため、520バイトを超えるスタック要素を必要とするOP_SUCCESSxから派生したopcodeは、ソフトフォークの制限を引き上げる可能性がある。
    • OP_SUCCESSxは、既存のopcodeの動作を再定義し、新しいopcodeと連携できるようにすることもできる。例えば、OP_SUCCESSxから派生したopcodeが64bit整数と機能する場合、同じスクリプト内の算術opcodeでも同じことが可能になる。
    • OP_SUCCESSxにより解析不能スクリプトが渡される可能性があるため、マルチバイトopcodeを導入したり、特定のOP_SUCCESSxopcodeをプレフィックスとして使用してまったく新しいスクリプト言語を導入することができたりする。
  2. OP_CHECKMULTISIGとOP_CHECKMULTISIGVERIFYが無効になり、OP_SUCCESSxにならないのはなぜ?:これはTaprootで誤ってOP_CHECKMULTISIGを使い続けている人がすぐに問題に気付くようにするための予防策だ。また、コンテキスト依存になるスクリプトの逆アセンブラの複雑さを回避する。
  3. MINIMALIFをコンセンサスにするのはなぜ? こうすることで、スタックからブランチ情報を取得するmalleabilityの無いコードを書くのがかなり簡単になる。
  4. OP_CHECKSIGADD このopcodeはバッチ検証とは互換性のないOP_CHECKMULTISIGのようなopcodeが無くなるのをカバーするために追加されている。OP_CHECKSIGADDは機能的にはOP_ROT OP_SWAP OP_CHECKSIG OP_ADDと同等だが1バイトしか必要ない。OP_ADDCScriptNum関連の振る舞いはすべてOP_CHECKSIGADDにも適用できる。
  5. CHECKMULTISIGの代替:TaprootおよびTapscriptを使って閾値署名k-of-nポリシーを実装する方法は複数ある。
    • 単一のOP_CHECKSIGADDベースのスクリプト:witnessに0 <signature_1> ... <signature_m>を持つCHECKMULTISIGスクリプトm <pubkey_1> ... <pubkey_n> n CHECKMULTISIGは、witnessに<w_n> ... <w_1>を持つ<pubkey_1> CHECKSIG <pubkey_2> CHECKSIGADD ... <pubkey_n> CHECKSIGADD m NUMEQUALスクリプトに書き換えることができる。各witness要素w_ipubkey_iに対応する署名から空のベクトルのいずれか。NUMEQUALNUMEQUALVERIFYに置き換えることで、CHECKMULTISIGVERIFYスクリプトと同様に変換できる。このアプローチには、既存のOP_CHECKMULTISIGベーススクリプトと非常によく似た特性がある。
    • 全ての組み合わせのk-of-kスクリプトを使用する:k-of-nポリシーは、スクリプトをマークルツリーのいくつかのリーフに分割し、それぞれが<pubkey_1> CHECKSIGVERIFY ... <pubkey_(n-1)> CHECKSIGVERIFY <pubkey_n> CHECKSIGを使ってk-of-kポリシーを実装することで実装できる。これは参加する公開鍵のみが公開されるため、以前のアプローチよりもプライバシー上の理由で望ましい場合がある。またkが小さい値の方が費用対効果が高い。さらにここでの署名は、ブランチにコミットする。つまり署名者は他の署名者が参加することを認識するか、ツリーのリーフ毎に署名を作成する必要がある。
    • 全ての組み合わせに集約公開鍵を使用する:全てのリーフがk個の公開鍵で構成されるツリーを作成する代わりに、MuSigを使って各リーフはk個の鍵の単一の集約鍵を含むツリーを構築できる。このアプローチははるかに効率的だが、(単一の)署名を共同で生成するため3ラウンドの対話型署名プロトコルを必要とする。
    • ネイティブのSchnorr閾値署名:マルチシグポリシーは検証可能な秘密分散法を使って閾値署名で実現することもできる。これにより単一の鍵の支払いと区別できない出力と入力が得られるが、送信先のアドレスを決める前に対話型プロトコル(および関連するバックアップ手順)が必要になる。
  6. 未知の公開鍵タイプにより、ソフトフォークを介して新しい署名検証ルールを追加できる。ソフトフォークはスクリプをパスするか失敗させすぐに終了させる実際の署名検証を追加できる。この方法で新しいSIGHASHモードを追加できる他、NOINPUTタグ付き公開鍵や署名検証のためにtaprootの内部キーに置き換え可能な効果鍵定数を追加できる。
  7. 署名メッセージがkey_versionにコミットするのはなぜ?これは未知の公開鍵タイプを定義する将来の拡張のためで、署名をある鍵タイプから別の鍵タイプに移動できないようにする。
  8. 署名メッセージに最後に実行されたOP_CODESEPARATORの位置が含まれるのはなぜ?これにより引き続き、OP_CODESEPARATORを使って、スクリプトの実行パスに署名できる。codeseparator_positionはハッシュへの最後の入力であるため、SHA256 midstateは単一のスクリプト内の複数のOP_CODESEPARATORに対して効率的にキャッシュ可能。対照的にBIP143のOP_CODESEPARATOR処理は最後に実行されたOP_CODESEPARATOR以降からのみ実行されたスクリプトにコミットするため、スクリプトの不必要な再ハッシュが必要になる。既知のOP_CODESEPARATORユースケースは、2つのコードブランチ間で最初のプッシュを共有することで2つめの公開鍵のプッシュを保存する場合、各ブランチを異なるtaprootリーフに移動することでより安価に表現出来る可能性が高いことに注意。
  9. スクリプトのサイズ制限が不要になったのはなぜ?scriptCodeがsighashに直接含まれないため、署名チェックに使われるCPU時間は実行されるスクリプトのサイズに比例しなくなる。
  10. opcode数の制限が不要になったのはなぜ?opcodeの制限は、実行中にデータ構造が無制限に大きくなるのを防ぐことができる範囲でのみ役立つ(メモリ使用量と、それらの構造のサイズに比例して大きくなる可能性があるため)。スタックとアルトスタックのサイズは既に独立して制限されている。ここで実装されているようにOP_IF、OP_NOTIF、OP_ELSE、およびOP_ENDIFに対してO(1)ロジックを使用することにより、他の唯一のインスタンスも回避できる。
  11. tapscriptのsigops制限:署名opcodeの制限は、過度に多くの署名操作のために検証が遅いスクリプトから保護するためのもの。tapscriptでは、署名opcodeの数はBIP 141または従来のsigop制限にカウントはされない。古いsigop制限により、ブロック構築におけるトランザクション選択は、weightに加えて2つめの制約であるため、不必要に難しくなる。代わりにtapscript署名opcodeの数は、witnessのweightによってのみ制限される。さらに、制限はブロックでっはなくトランザクションインプットに適用され、実際に実行された署名opcodeのみがカウントされる。tapscriptの実行により、50 witness weight毎に1つの署名opcodeと1つの無料の署名opcodeが許可される。
  12. sigops制限のパラメータ選択:通常のwitnessはそのweightが公開鍵と(SIGHASH_ALLの)署名のペアで構成されそれぞれ33 + 65 weightの単位であるため、制限の影響を受けない。これは署名のweightが65か66であるため、スクリプト内で公開鍵が再利用される場合にも当てはまる。ただし、この制限により追加のweightが必要になるため、署名(および公開鍵)が重複する異常なスクリプトの手数料は高くなる。sigop係数50あたりのweightはBIP141のブロック制限の比率(4 MB weightを80,000sigopsで割った値)に対応する。トランザクションインプットの非witness部分のweightに対応するため、制限によって許可される無料の署名opcodeが存在する。
  13. なぜ署名opcodeのみが場ジェッドにカウントされ、例えばハッシュopcodeなどの高価な操作はカウントされないのか?署名チェックopcode最大密度で構成されるスクリプトの検証のためのwitnessバイト辺りのCPUコストは、ハッシュopcodeやOP_ROLLを含む他のopcodeが詰め込まれたスクリプトのコストに非常に近いことが分かる。ただし、この構造は非常に柔軟で、CHECKSIGFROMSTACKなどの新しい署名opcodeを追加して、ソフトフォークを介して制限にカウントできる。将来、通常のスクリプトコストを変更する新しいopcodeが導入されたとしても、witnessに無意味なデータを詰め込む必要はない。代わりに、taproot annexを利用して、実際のwitnessのサイズを増やすこと無くwitnessにweightを追加できる。