Develop with pleasure!

福岡でCloudとかBlockchainとか。

Vault用のopcodeの導入を提案するBIP-345

少し前に、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(スクリプト)に預ける:

  • 預けた資金を引き出す場合、引き出しを申請してから資金を実際に手に入れるまで一定の待機期間がかかる。
  • 待機期間の間であれば、申請した引き出しを取りやめ資金をリカバリーすることができる*1

つまり、攻撃者に秘密鍵を奪われ、攻撃者がその資金を引き出そうとしても、そこには遅延時間が設けられ、それに気づいた所有者がその間に資金を取り戻せるようにしようというもの。

このようなVaultの提案は、

などがある。

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に資金をデポジットするには、少なくとも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_SUCCESS1870xbc)を再定義する形で導入される。

[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の動作は↓

  1. スタックからleaf-update-script-bodyをポップし、
  2. スタックからpush-countをポップする。
  3. push-count個分の要素をスタックからポップする。
  4. 3の要素をプレフィックスとして1のスクリプトと結合し、leaf-update-scriptを完成させる。
  5. スタックから、trigger-vout-idxrevault-vout-idxrevault-amountをポップする。
  6. スクリプトのsigopsバジェットを60デクリメント*5
  7. trigger-vout-idxがwitness v1 program(Taproot)かチェック
  8. 現在評価中のリーフスクリプトを4のleaf-update-scriptと置き換えてTaptreeを更新し、そから導出したscriptPubkey(P2TR)と、triggerOutのscriptPubkeyが合致するかチェック
  9. revaultOutのscriptPubkeyが現在評価中のscriptPubkey(P2TR)と合致するかチェック
  10. triggerOutの金額とrevaultOutの金額が現在評価中のインプットの金額以上であることをチェック
  11. インプットの金額からrevaultOutの金額を差し引いた金額がtriggerOutの金額になっていることをチェック
  12. すべての検証をパスしたらスタックにTrueをプッシュ

というもの。つまり、OP_VAULTは、

  • Taptree内のトリガーリーフのスクリプトを、leaf-update-script-bodypush-count個分のデータで構成されるスクリプトに更新した新しいP2TRスクリプトトランザクションアウトプットに作られるように強制する。
    • これにより、Vault作成時の条件の内部鍵とリカバリーリーフは同じまま、トリガーリーフのみが遅延時間を設けたスクリプトに更新された宛先宛にVaultの資金を移動する。
    • リカバリーリーフは同じままなので、移動した資金はリカバリー条件を使っていつでも回収できる。
  • Vaultから資金を引き出す際に、全額引き出すのでなく、一部だけ引き出せるように、残しておく資金は元のVaultと同じ宛先に移動するRevault機能をサポートしている。
    • 全額引き出す場合はrevault-vout-idx-1を設定し、revault-amountは0
トリガートランザクション

先程のトリガーリーフのスクリプト

[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT

について、leaf-update-script-bodyOP_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-bodyOP_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_SUCCESS1880xbd)を再定義する形で導入される。

[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の動作は↓

  1. スタックからrecovery-sPK-hashをポップし、32バイトかチェック
  2. スタックからrecovery-vout-idxをポップする。
  3. recoverOutのscriptPubkeyについてタグ付きハッシュtagged_hash("VaultRecoverySPK", recoveryOut.scriptPubKey)を計算し、その値がrecovery-sPK-hashと一致するかチェック
  4. recoverOutの金額がインプットの金額以上であるかをチェック
  5. すべての検証をパスしたらスタックにTrueをプッシュ

というもの。つまり、OP_VAULT_RECOVERは、リーフスクリプトでコミットされている宛先(recovery-sPK)にVaultのコインが全額送られることを強制する。

RBFのシグナリング

また、コンセンサスルールではないけどBitcoin Coreのポリシーとして、Pinning攻撃を防ぐため、リカバリーリーフを使用するリカバリトランザクションは、RBFによるトランザクションの置換可能性をシグナリングする必要がある。

具体的には、リカバリトランザクションnVersionが3ではない場合*6OP_VAULT_RECOVERインプットのnSequence0xffffffff - 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のカウントに加味する

*6:v3トランザクションリレーが導入されると、バージョン3のトランザクションはすべて置換可能性のシグナルになるため。