少し前に、BitcoinでVaultを実現するために2つのopcodeを導入するソフトフォークの提案がBIP-345として登録された↓
https://github.com/bitcoin/bips/blob/master/bip-0345.mediawiki
Vaultとは?
Vault(金庫)は、その名前から分かるようにビットコインを安全に保管するための仕組み。
Bitcoinのような暗号通貨の場合、秘密鍵が漏洩/盗難にあうと、それは資金の損失につながる。金額が大きくなるほど、秘密鍵漏洩のリスクが大きくなる。自分が知らない間に盗まれた秘密鍵によって資金が勝手に使われることがないように、より堅牢な資金保護の仕組みを提供するのがVaultの目的。
flowchart LR A[UTXOをVaultへデポジット] -.-> C[保管されたコインを事前に指定したリカバリーパスに移動] A ---> B[引き出しをトリガー] B -.-> C B --タイムロック期限後--> D[引き出しの完了]
具体的には、資金を以下の機能を持つVault(スクリプト)に預ける:
つまり、攻撃者に秘密鍵を奪われ、攻撃者がその資金を引き出そうとしても、そこには遅延時間が設けられ、それに気づいた所有者がその間に資金を取り戻せるようにしようというもの。
このようなVaultの提案は、
- 2016年に提案されたCovenantsのユースケースの1つとして紹介され(ブログ記事)
- 同じCovenantsの文脈でElementsの
OP_CHECKSIGFROMSTACK
とOP_CAT
で実現する方法(ブログ記事)や*2、 - 現在のコンセンサスルールでも実現可能な方法として、使い捨ての一時鍵で事前署名する方法(GBEC解説動画)*3
などがある。
BIP-345 Vault
BIP-345の提案はこれまでの汎用的なCovenantsを使用したVaultの構成ではなく、Vault専用の2つの新しいopcodeをソフトフォークで導入し遅延時間とリカバリーを可能にする特殊なCovenantsを可能にしてVaultを構成できるようにしようというもの。2つの新しいopcodeは、いずれも、Tapscriptで予約されているOP_SUCCESS
系のopcodeを再定義することで導入される(witness version 1=Taprootを使用した構成になる)。
上記のVaultの操作をサポートするのに、以下の4種類のトランザクションが登場する:
- Vaultトランザクション
資金をVaultにデポジットするトランザクション - トリガートランザクション
資金をVaultから引き出すためのトリガーとなるトランザクション - 引き出し(Withdraw)トランザクション
トリガーされた引き出しを完了するためのトランザクション - リカバリートランザクション
引き出しが完了する前に資金を事前に指定したリカバリーパスに送信するトランザクション
Vaultへのデポジット
VaultトランザクションでVaultに資金をデポジットするには、少なくとも2つのリーフ(トリガー用、リカバリー用)を持つTaptreeを使って以下のようなTaprootのscriptPubkeyを構成する。
graph BT A[トリガーリーフ] --> B[Taptree] C[リカバリーリーフ] --> B D[Internal key] --> E[Vault P2TR] B --> E
このP2TRアドレスに送られたコインを使用する方法は、各リーフを使ってアンロックする方法と、内部鍵(Internal key)を使ってSchnorr署名を作成する方法*4の3通り。2つのリーフのスクリプトのは以下のような構成になる:
トリガーリーフ
トリガーリーフは新しく導入されるOP_VAULT
opcodeを使った以下のようなスクリプトになる。OP_VAULT
は、OP_SUCCESS187
(0xbc
)を再定義する形で導入される。
[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT
trigger-auth
の部分は、この条件を使ってVaultからの引き出しをトリガーするための認可条件で、ウォレットの設計者が任意の条件を設定できる。例えばある公開鍵の秘密鍵を持つものに対して認可する場合は、trigger-auth
は以下のスクリプトになる。
<trigger-auth-pubkey> OP_CHECKSIGVERIFY
つまり、公開鍵trigger-auth-pubkey
対して有効な署名を作れる人のみがこれをトリガーできる。
残りの部分は、OP_VAULT
によって評価されるものなので、OP_VAULT
の挙動と合わせてみていく。
OP_VAULT
はスタックに以下の項目がある前提で実行される:
<leaf-update-script-body> <push-count> [ <push-count> leaf-update script 用のデータ項目の数 ] <trigger-vout-idx> <revault-vout-idx> <revault-amount>
leaf-update-script-body
は、OP_VAULT
によって更新されるリーフスクリプトの断片。サンプルとして以下のスクリプトが掲載されている。
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY
OP_CHECKTEMPLATEVERIFY
は未導入なので、導入されていない場合は以下のようなスクリプトになる。
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG
いずれにせよ、相対的なタイムロック(OP_CSV
)が付与され、これが引き出しのトリガーに対して遅延時間を設定する。
OP_VAULT
の動作は↓
- スタックから
leaf-update-script-body
をポップし、 - スタックから
push-count
をポップする。 push-count
個分の要素をスタックからポップする。- 3の要素をプレフィックスとして1のスクリプトと結合し、
leaf-update-script
を完成させる。 - スタックから、
trigger-vout-idx
、revault-vout-idx
、revault-amount
をポップする。 - スクリプトのsigopsバジェットを60デクリメント*5
trigger-vout-idx
がwitness v1 program(Taproot)かチェック- 現在評価中のリーフスクリプトを4の
leaf-update-script
と置き換えてTaptreeを更新し、そから導出したscriptPubkey(P2TR)と、triggerOut
のscriptPubkeyが合致するかチェック revaultOut
のscriptPubkeyが現在評価中のscriptPubkey(P2TR)と合致するかチェックtriggerOut
の金額とrevaultOut
の金額が現在評価中のインプットの金額以上であることをチェック- インプットの金額から
revaultOut
の金額を差し引いた金額がtriggerOut
の金額になっていることをチェック - すべての検証をパスしたらスタックにTrueをプッシュ
というもの。つまり、OP_VAULT
は、
- Taptree内のトリガーリーフのスクリプトを、
leaf-update-script-body
とpush-count
個分のデータで構成されるスクリプトに更新した新しいP2TRスクリプトがトランザクションアウトプットに作られるように強制する。 - Vaultから資金を引き出す際に、全額引き出すのでなく、一部だけ引き出せるように、残しておく資金は元のVaultと同じ宛先に移動するRevault機能をサポートしている。
- 全額引き出す場合は
revault-vout-idx
に-1
を設定し、revault-amount
は0
- 全額引き出す場合は
トリガートランザクション
先程のトリガーリーフのスクリプト↓
[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT
について、leaf-update-script-body
をOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY
とした場合、トリガートランザクションでこの条件をアンロックするためにwitnessとして提供される項目をスタックに入れると↓
<Taprootのcontrol block(トリガーリーフへのマークルパスを含む)> <トリガーリーフスクリプトのペイロード> <trigger-auth-pubkey-signature> <target-CTV-hash> <trigger-vout-idx> <revault-vout-idx> (Revaultしない場合は-1) <revault-amount>
となり、このスタックに対してTaprootのscript-pathの検証が行われ、トリガーリーフの認可条件(trigger-auth
)が実行され、その後OP_VAULT
を実行する際のスタックの状態は↓
<leaf-update-script-body> 2 <遅延時間> <target-CTV-hash> <trigger-vout-idx> <revault-vout-idx> <revault-amount>
ここでは、push-count = 2
であるため、遅延時間
とtarget-CTV-hash
がスタックからポップされ、それをleaf-update-script-body
と結合したスクリプトが更新用のスクリプトになり、triggerOutのP2TRの構成要素になる。
leaf-update-script-body
がOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG
の場合は、target-CTV-hash
の代わりに公開鍵をwitnessで提供する形になると思われる。
トリガートランザクションでは、リカバリーリーフと、↑で更新された遅延付きスクリプト(leaf-update-script
)で構成されるP2TR宛にVaultの資金が移動されるので、遅延期間が終われば、leaf-update-script
を満たせす引き出し用のトランザクションを完成させ、資金の引き出しが完了する。
target-CTV-hash
や、Revaultの情報はトランザクションのwitnessで提供されるため、Vaultから資金を引き出す際に、引き出し先や金額を決めればいいようになっている。これまでのCovenantsではこれらはVaultにデポジットする段階で決めておく必要があった。
リカバリーリーフ
リカバリー用のリカバリーリーフは新しく導入されるOP_VAULT_RECOVER
opcodeを使った以下のようなスクリプトになる。OP_VAULT_RECOVER
は、OP_SUCCESS188
(0xbd
)を再定義する形で導入される。
[recovery auth] <recovery-sPK-hash> OP_VAULT_RECOVER
recovery auth
はオプションで、この条件でリカバリーをするための認可条件で、trigger-auth
と同様ウォレットの設計者が任意の条件を設定できる。
OP_VAULT_RECOVER
はスタックに以下の項目がある前提で実行される:
<recovery-sPK-hash> <recovery-vout-idx>
recovery-sPK-hash
は元々リーフスクリプト内にあるデータで、32バイトのハッシュ値。
OP_VAULT_RECOVER
の動作は↓
- スタックから
recovery-sPK-hash
をポップし、32バイトかチェック - スタックから
recovery-vout-idx
をポップする。- トランザクション内の該当するインデックスのアウトプットを
recoverOut
と呼ぶ。
- トランザクション内の該当するインデックスのアウトプットを
recoverOut
のscriptPubkeyについてタグ付きハッシュtagged_hash("VaultRecoverySPK", recoveryOut.scriptPubKey)
を計算し、その値がrecovery-sPK-hash
と一致するかチェックrecoverOut
の金額がインプットの金額以上であるかをチェック- すべての検証をパスしたらスタックにTrueをプッシュ
というもの。つまり、OP_VAULT_RECOVER
は、リーフスクリプトでコミットされている宛先(recovery-sPK
)にVaultのコインが全額送られることを強制する。
RBFのシグナリング
また、コンセンサスルールではないけどBitcoin Coreのポリシーとして、Pinning攻撃を防ぐため、リカバリーリーフを使用するリカバリートランザクションは、RBFによるトランザクションの置換可能性をシグナリングする必要がある。
具体的には、リカバリートランザクションのnVersion
が3ではない場合*6、OP_VAULT_RECOVER
インプットのnSequence
は0xffffffff - 1
未満でなければならない。
OP_CTVとの関係
↑のトリガーリーフ内のleaf-update-script-body
では、BIP-119のOP_CHECKTEMPLATEVERIFY
(Covenants)を使って、引き出し先を指定する方法が推奨されている。また、このソフトフォークの展開も、BIP-119と同時であることが望ましいとされている。
BIP-345は厳密にBIP-119に依存しているわけではないものの、OP_VAULT
による指定されたTapleafの更新とOP_VAULT_RECOVER
自体には、遅延時間の設定や送付先のアドレスの強制は機能として含まれない(外だしされている)ので、OP_CSV
による遅延やOP_CTV
によって最終的な宛先を強制する仕組みとの組み合わせが必要になる。
ただ、トリガートランザクションで指定するtarget-CTV-hash
で、引き出し先をOP_CTV
でコミットしたいケースってどんなケースなんだろう?
*1:よりセキュアに管理さているオフラインの鍵を利用するなど
*2:安全な一時鍵の削除や、金額と引き出しのパターンに事前コミットする必要があるなど、いくつかの制約がある
*3:金額、宛先手数料の管理はすべて事前に決めたものになる
*4:内部鍵はリカバリーパスで使用する鍵と同等のセキュリティが必要になる。内部鍵によりこのP2TRがVaultであったことが開示されなくなるというメリットはあるもの、セキュリティ上の懸念がある場合は内部鍵を使用不可能なNUMSポイントにするのも可。
*5:8のtrrigerOutの有効性チェックの際に、TaprootのControl blockの長さに比例する楕円曲線のスカラー乗算とハッシュ計算の実行が必要になるため、そのコストをsigopsのカウントに加味する