Develop with pleasure!

福岡でCloudとかBlockchainとか。

P2TRの鍵導出仕様を定義したBIP-86

Taprootのアクティベートに向けて、単一の鍵でPay to Taproot(P2TR)にコインをロックする際の、鍵の導出仕様がBIP-86として定義された↓

https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki

単一の鍵なので、対象はTaprootのKey-Path使うケースで、鍵の導出仕様はこれまでのBIP-44(P2PKH), 49(P2SH-P2WPKH), 84(P2WPKH)と基本的に同じ。

HDウォレットでTaprootのInternal Keyを導出し、(BIP-341で推奨されているように)Script-PathによるアンロックができないようにそのInternal KeyのハッシュからScript-Path側の鍵を導出して、その2つの鍵を加算して、P2TRのscriptPubkeyを導出する方法になってる。

以下、BIPの意訳。

概要

このドキュメントは、TaprootのInternal Keyとして単一鍵のP2TR(BIP341)のアウトプットに関与する鍵を持つHDウォレットの導出方式を提案する。

動機

単一鍵のP2TRトランザクションを使用する場合に、共通の導出方式があると便利で、HDシードのバックアップのみを持つHDウォレットが単一鍵のTaprootアウトプットをリカバリーできるようになる。現在では、特定のスクリプトタイプに対して固定の導出パスを必要としないソリューションがあるが、多くのソフトウェアやハードウェア署名者は、導出パスやスクリプトの情報がないシードのバックアップを使用している。そのため、実装を容易にするため、BIP49およびBIP84で使用されているのと同じアプローチを主に使っている。

仕様

このBIPは、BIP32のマスター秘密鍵を基に複数の決定論的アドレスを導出するために2つのステップを定義する。

公開鍵の導出

ルートアカウントから公開鍵を導出するために、このBIPは、BIP44, 49, 84で定義されているものと同じアカウント構造を使用するが、purposeスクリプトタイプが異なる。

m / purpose' / coin_type' / account' / change / address_index

purposeパスの階層では86'を使用する。残りの階層はBIP44, 49, 84で定義されているものを使用する。

この導出パスパターンで導出した鍵は、このドキュメントではderived_keyと呼ぶ。

アドレスの導出

BIP341には、「使用条件にScript-Pathが必要ない場合、アウトプットの鍵は、Script-Pathが無い状態ではなく、使用できないScript-Pathにコミットする必要がある。これはアウトプットの鍵の点をQ = P + int(hashTapTweak(bytes(P)))Gとして計算することで実現できる」と記されている。したがって、

internal_key:       lift_x(derived_key)
32_byte_output_key: internal_key + int(HashTapTweak(bytes(internal_key)))G

トランザクションスクリプトとwitnessは、BIP341で定義されているとおり

witness:      <署名>
scriptSig:    (空)
scriptPubKey: 1 <32_byte_output_key>
              (0x5120{32_byte_output_key})

後方互換

このBIPは、設計上、後方互換性はない。互換性のないウォレットは、これらのアカウントを検出せず、ユーザーは何かが間違っていることに気づくだろう。

しかし、このBIPはBIP 44, 49, 84で使用されているのと同じ方法を使用するため、実装は難しくない。

Test Vector

BIP参照

Taprootのビルド、署名作成のサポートをするBuilderを実装してみた

Taprootもロックインされたので、Taproot宛に送金するP2TR(Pay to Taproot)の作成や、そのUTXOを使用する際の署名作成のサポートをするBuilderクラスをbitcoinrbに実装してみた*1

※ 2021/11/15追記:以下のBitcoin::Taproot::SimpleBuilder については一部インターフェースを変更したため、最新のbitcoinrbで動作するコードについてはwiki参照。

Taprootのスクリプトツリーでは、任意の2分木の構成を取ることができ、最近マージされたBitcoin CoreのTaprootBuilderでは、リーフノードのスクリプトや中間ノードをツリーの深さを指定して構成するようになっている。ただ、二分木にならないような指定をするとエラーになり、使う側がある程度ツリー構造を考えてリーフノードや中間ノードを配置する必要がある。任意のツリーを構成できる反面、ツリー構造に特別な要求がない場合は面倒なので、bitcoinrbで今回実装したのはまずシンプルに複数のロック条件から自動的にスクリプトをビルドする(ツリーのカスタマイズはできない)SimpleBuilder

https://github.com/chaintope/bitcoinrb/blob/v0.8.0/lib/bitcoin/taproot/simple_builder.rb

※ Taprootはロックインされたけど、実際にアクティベートされるのは11月頃と想定されるブロック709,632で、それまではmainnetで使用するとコインがマイナーに回収されるので、実験する場合は既にアクティベートされているSignetで。

P2TRのビルド

TaprootのscriptPubkeyはsegwit version 1を使用した

OP_1 <32バイトの公開鍵>

形式で、↑の32バイトの公開鍵は、Key-PathのInternal KeyとScript-Pathのマークルルートから計算した鍵を加算した点になる。この辺りの仕組みについてはGBEC動画参照↓

goblockchain.network

SimpleBuilderで、まずはこのP2TRを作成する。↓の例ではInternal Keyに加えて、3つ公開鍵を作成し、それぞれに対して署名検証をする(OP_CHECKSIG)をする3つのロックスクリプトを持つP2TRを生成している。※ なお、Internal KeyおよびTapscriptで署名検証に用いる公開鍵はすべて32バイトで、これまでの33バイトの圧縮公開鍵とは異なるので要注意。

require 'bitcoin'

include Bitcoin::Opcodes

# Internal Keyの鍵を生成(秘密鍵を指定してるけど、公開鍵だけあればいい)
internal_key = Bitcoin::Key.new(priv_key: '98d2f0b8dfcaa7b29933bc78e8d82cd9d7c7a18ddc128ce2bc9dd143804f36f4')

# 3つのロックスクリプト(秘密鍵を指定してるけど、公開鍵だけあればいい)
key1 = Bitcoin::Key.new(priv_key: 'fd0137b05e26f40f8900697b690e11b2eba8abbd0f53c421148a22646b15f96f')
key2 = Bitcoin::Key.new(priv_key: '3b0ce9ef75031f5a1d6679f017fdd8d77460ecdcac1a24d482e1465e1768e22c')
key3 = Bitcoin::Key.new(priv_key: 'df94bce0533b3ff0c6b8ca16d6d2ce08b01350792cb350146cfaba056d5e4bfa')
script1 = Bitcoin::Script.new << key1.xonly_pubkey << OP_CHECKSIG
script2 = Bitcoin::Script.new << key2.xonly_pubkey << OP_CHECKSIG
script3 = Bitcoin::Script.new << key3.xonly_pubkey << OP_CHECKSIG

# Internal Keyと3つのロックスクリプトを持つP2TRを作成
builder = Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey, script1, script2, script3)
script_pubkey = builder.build
script_pubkey.to_addr
=> 'tb1p9uv58mst47h0r9zd8lm9hjlttcskq4wndxfceh8mjknd92mmflzspnsygf'

さくっとP2TRスクリプトが作れる*2。↑のように3つのロック条件がある場合のスクリプトツリーは、

      N0
   /     \
  N1      C
 /  \
A    B

のようになる。スクリプトが増えれば、ツリーも変化していく↓

4つスクリプトがある場合:
      N0
   /     \
  N1      N2
 /  \    /  \
A    B  C    D

5つスクリプトがある場合:
          N0
       /     \
      N1      E
   /     \
  N2      N3
 /  \    /  \
A    B  C    D

SimpleBuilderの内部的で行っているのは↓

  1. スクリプトのリストからルートハッシュを計算:
    追加されたスクリプトを2つずつのペアにして親ノードを作り、あぶれてペアにならない場合は1つ上の階層に移動するというの繰り返してツリーを構成し、ルートハッシュを計算する。
  2. ルートハッシュとInternal Keyからtweakを生成し、それを秘密鍵として、スクリプトツリーの公開鍵を計算する。
  3. Internal Keyの公開鍵と2の公開鍵を加算してP2TRの公開鍵を計算する。
  4. 3で計算した公開鍵からP2TRのscriptPubkeyを生成する。

P2TR UTXOの使用

↑で作成したP2TRのUTXOを今度は使ってみよう。このUTXOは、Key-PathとScript-Pathのいずれかでアンロックできる。

Key-Pathを使う場合

1つの方法は、Internal Keyを使ってアンロックする方法。この場合、witnessとして、P2TRのscriptPubkey内の(↑の3で計算した)公開鍵に対して有効なSchnorr署名を提供すればいい。SimpleBuilderでサポートするのは、この公開鍵に対応する秘密鍵の導出。

# 署名用の秘密鍵を導出
key = builder.tweak_private_key(internal_key)

SimpleBuilder#tweak_private_keyは、Internal Keyの秘密鍵スクリプトからP2TRの公開鍵に対応する秘密鍵を導出する。

あとは、この秘密鍵を使って送金Txに署名すれば良い。bitcoinrbだと↓な感じで実装する:

# Txを作成
tx = Bitcoin::Tx.new
tx.in << Bitcoin::TxIn.new(out_point: Bitcoin::OutPoint.from_txid('9b5dbbe79a8938b9527b0a5f12c9be695ca1dac4e4267529a228c380c0b232bd', 1))
tx.out << Bitcoin::TxOut.new(value: 90_000, script_pubkey: script_pubkey)

# 署名対象のsighashを計算
prevouts = [Bitcoin::TxOut.new(value: 100_000, script_pubkey: script_pubkey)]
sighash = tx.sighash_for_input(0, sig_version: :taproot, prevouts: prevouts, hash_type: Bitcoin::SIGHASH_TYPE[:default])

# Schnorr署名を計算
sig = key.sign(sighash, algo: :schnorr)

# インプットに署名をセット
tx.in[0].script_witness.stack << sig

# Txペイロード
tx.to_hex

なお、SIGHASH_TYPE: allはデフォルトで省略しなければならないので(省略しないとTaprootではエラーになる)、インプットに署名をセットする際は付与していない。

実際にSignetでKey-Pathを使ってアンロックしたTxが2d881433592e893e0dbb079929198fed1ef23e132927dfb97c3ada6f5598ecf3

Script-Pathを使う場合

もう1つのScript-Pathを使ってアンロックする場合は、witnessとしてアンロックに使用するスクリプトと、Internal keyおよびスクリプトがツリーに含まれていることを証明するプルーフを提供する。またそれに加えて、スクリプトをアンロックするために必要な要素。

プルーフは、P2TRの公開鍵のY座標の偶奇を表すparity bitとInternal key、leaf versionと合わせてControl Blockを構成し、それをwitnessにセットすることになる。

今回は↑のscript2を使ってアンロックする。この場合、プルーフとして必要になるのはscript1script3のハッシュになる。Script-Pathを使ってアンロックするトランザクションの作成方法は↓

# Txの作成
tx = Bitcoin::Tx.new
tx.in << Bitcoin::TxIn.new(out_point: Bitcoin::OutPoint.from_txid('3cad3075b2cd448fdae11a9d3bb60d9b71acf6a279df7933dd6c966f29e0469d', 1))
tx.out << Bitcoin::TxOut.new(value: 90_000, script_pubkey: script_pubkey)

# 署名対象のsighashを計算
prevouts = [Bitcoin::TxOut.new(value: 100_000, script_pubkey: script_pubkey)]
opts = {leaf_hash: builder.leaf_hash(script2)} # script pathではleaf hashにもコミットするためオプションで渡す
sighash = tx.sighash_for_input(0, sig_version: :tapscript, prevouts: prevouts, hash_type: Bitcoin::SIGHASH_TYPE[:default], opts: opts)

# Schnorr署名を計算
sig = key2.sign(sighash, algo: :schnorr)

# witnessにアンロックに必要なデータをセット
## sript2のアンロックアイテム(署名)をセット
tx.in[0].script_witness.stack << sig
## script2をセット
tx.in[0].script_witness.stack << script2.to_payload
## script2がツリーに含まれていることを証明するプルーフを含むControl Blockをセット
tx.in[0].script_witness.stack << builder.control_block(script2)

# Txペイロード
tx.to_hex

SimpleBuilderは、#leaf_hashメソッドで使用するスクリプトleaf hashの提供と、#control_blockメソッドでそのスクリプトスクリプトツリーに含まれていることを証明するためのプルーフを含むControl Blockを生成する。このControl Blockは、以下のデータを連結したもの:

<P2TRの公開鍵のY座標の偶奇を表すparity bit + スクリプトのleaf version> + <Internal Key> + <Proof(ツリーのリーフノードからルートまでの兄弟ノードのハッシュ)>

実際にSignetでScript-Pathを使ってアンロックしたTxが3117d2bb1c9962c9d4c7bc3d31f48a13878d346407323bbb316908f3ea6279ae

と、Key-Path、Script-PathでP2TRを使用するトランザクションを構成するサポート機能ができた。任意の構成のスクリプトツリーを構成するようなBuilderは需要があれば作るかもしれない。

*1:ScriptInterpreterのTaproot対応は既に完了している

*2:スクリプトleaf versionはデフォルトで0xc0がセットされる。引数で指定することも可能。

秘密鍵をオフラインにしたままLN支払いを受け入れ可能にするFast Forwardプロトコル

Lightning Networkでオフチェーン支払いをする場合、(オンチェーンの支払いと違って)チャネル参加者は常にオンラインである必要がある。その際、支払いによる状態の更新で新しいコミットメントトランザクションに署名するため、秘密鍵も同様にオンラインである必要がある。

そんなオンライン要件を緩和する方法が提案された↓

https://lists.linuxfoundation.org/pipermail/lightning-dev/2021-June/003045.html

ベースになってるプロトコルは2019年にZmnSCPxjが提案したネットワーク帯域幅レイテンシーを削減するFast Forwardという仕組み↓

https://lists.linuxfoundation.org/pipermail/lightning-dev/2019-April/001986.html

提案では、チャネル参加者の内、受信者の秘密鍵をオフラインにできる。ただし、

  • 支払いの送信者の秘密鍵はオンラインのまま。これには自分→相手へのルーティングも含まれるので、オフラインにできるのは自分が最終の受信者である場合のみ。
  • 秘密鍵はオフラインにできるが受信者自体はオンラインである必要がある。
  • チャネルを一方的に閉じられた場合は、秘密鍵をオンラインにして署名する必要がある。

Poon-Dryjaスタイルのペイメントチャネル

現在のLightning Networkは、Poon-Dryjaスタイルのペイメントチャネルを採用している。この場合、アリスとボブの二者間でチャネルを開いていると仮定し、両者はそれぞれコミットメントトランザクションを保持している。コミットメントトランザクションは、両者の残高を表す2つのアウトプットを持つ。

1つは自分宛(to_local)、もう1つは相手宛(to_remote)。この2つのアウトプットのコインは、それぞれ以下の条件でロックされている。

  • to_local:以下のいずれかの条件でアンロック可能
    • OP_CSVによるタイムロック期間が過ぎたら、自身の鍵で使用可能
    • 相手がrevocation keyを知っていれば、相手がすぐに入手可能
  • to_remote:相手のP2WPKHアドレス

to_localスクリプトは、具体的には↓のようなスクリプト

OP_IF
    <revocationpubkey> # 旧状態がブロードキャストされた場合のペナルティ用
OP_ELSE
   `to_self_delay`
    OP_CSV
    OP_DROP
    <local_delayedpubkey>
OP_ENDIF
OP_CHECKSIG

支払いをルーティングする際は、これにHTLCアウトプットが追加される。そしてそれぞれ保持するコミットメントトランザクションについて、相手の署名を持っている。

新しく支払いやルーティングをする場合、新しいコミットメントトランザクションを作成し、古いコミットメントトランザクションで使用したシークレットを相手に明かす。この時、コミットメントトランザクションも新しくなっているので、それに合わせて署名も新たに作ることになる。

つまり、支払いをする=チャネルの状態を更新するためにいは、秘密鍵はオンラインである必要がある。

Fast Forwardプロトコル

Fast Forwardプロトコルでは、コミットメントトランザクションの2つのアウトプットを対称的な構成に変更している。具体的には、to_remoteが単純な相手へのP2WPKHではなくto_localと対称的なスクリプトで構成される↓

  • to_local:以下のいずれかの条件でアンロック可能
    • OP_CSVによるタイムロック期間が過ぎたら、自身の鍵で使用可能
    • 相手がrevocation keyを知っていれば、相手がすぐに入手可能
  • to_remote:以下のいずれかの条件でアンロック可能
    • OP_CSVによるタイムロック期間が過ぎたら、相手の鍵で使用可能
    • 自分が相手のremote penalty claim keyを知っていれば、すぐに入手可能

to_localのロックスクリプトは↓

OP_IF
    # ペナルティ・トランザクション用
    <local_revokepubkey> OP_CHECKSIGVERIFY <remote_penaltyclaimpubkey>
OP_ELSE
    `to_self_delay`
    OP_CSV
    OP_DROP
    <local_delayedpubkey>
OP_ENDIF
OP_CHECKSIG

to_remoteのロックスクリプトは↓

OP_IF
    # ペナルティ・トランザクション用
    <local_revokepubkey> OP_CHECKSIGVERIFY <remote_penaltyclaimpubkey>
OP_ELSE
    `to_self_delay`
    OP_CSV
    OP_DROP
    <remote_delayedpubkey>
 OP_ENDIF
OP_CHECKSIG

どちらも、ペナルティトランザクション用のアンロック条件に、revocation keyに加えてremote penalty claim keyによる署名検証が加えられている。これが↓のFast Forward HTLCトランザクションを有効にするための仕掛け。

Fast Forward HTLCトランザクション

支払いをルーティングする際は、コミットメントトランザクションにHTLCアウトプットを追加するのではなく、Fast Forward HTLCトランザクションを新たに作成する。このトランザクションは、コミットメントトランザクションto_local/to_remoteをインプットとし、HTLCの送金用のアウトプット(このスクリプトは既存のHTLCのスクリプトと同じ)と、お釣り用のアウトプットを持つトランザクションになる。お釣り用のアウトプットは、インプットにしたto_local/to_remoteと同じスクリプトで(鍵だけ更新)、これが送信者にとっては最新の自身の残高(mainアウトプット)になる。

図示すると↓のようなトランザクションフローになる。

f:id:techmedia-think:20210611230720p:plain
Fast Forwardプロトコル

Fast Forward HTLCトランザクションで使用するインプットのアンロック条件は、↑のスクリプトの最初のブランチの条件↓

<local_revokepubkey> OP_CHECKSIGVERIFY <remote_penaltyclaimpubkey>

で、これをアンロックするにはローカルの署名とリモートの署名、2つの署名が必要になる。この内、Fast Forward HTLCトランザクションを作成する際は、送信者側が両方のFast Forward HTLCトランザクションの署名を提供する。受信側は、このトランザクションがブロードキャストされるような状況になるまで署名を生成しなくても良い。具体的には、送信者は自身が持つコミットメントトランザクションについては、local_revokepubkeyを使って署名し、相手が持つコミットメントトランザクションについては、remote_penaltyclaimpubkeyを使って署名し、この2つの署名を受信者ノードに送信する。

そしてその後のHTLC支払いは、作成した最新のFast Forward HTLCトランザクションのアウトプットを使用する新しいFast Forward HTLCトランザクションを作ることになる。

コミットメントトランザクションからHTLCアウトプットを分離するこの構成の狙いは、ルーティング支払いにおいて、コミットメントトランザクションの更新を発生させないことにある。コミットメントトランザクションを更新する場合、両者の署名が必要になるため、両者の秘密鍵は共におンラインでなければならない。

ペイメントチャネルを↑のような構成にすることで、最終受信者の秘密鍵がオフラインでも支払いが受領できるようにしている。ただ、ペナルティトランザクションを作る際は当然秘密鍵が必要であるし、その署名をする期間はto_self_delayよりも速くなければいけない。また、↑のようにHTCLトランザクションのチェーンが構成されるので、一方的なクローズが発生した場合のオンチェーン手数料は既存のプロトコルより増加する。

BIP32鍵導出パスのパステンプレートを定義したBIP-88

Bitcoin秘密鍵やアドレスは決定性ウォレットによって導出されるのが主流だが、シードからどんなアドレスを導出するか(どんなBIP32導出パスを使うか)は、スクリプトタイプによっていくつかのBIPが定義されており、そのどれが使われているか、もしくは独自のカスタム仕様になっているかは、使用するウォレットによって異なる。つい最近も、マルチシグ用のパス仕様の提案がされたばかり↓

techmedia-think.hatenablog.com

そのため、あるウォレットから別のウォレットに移行するための、シードをインポートしたとしても、同じアドレスを導出するとは限らない。

そこで、多々ある導出パスの仕様がある中、各ウォレットがそのどれを使っているのか、またはカスタム仕様がある場合、どういった制約があるのかを表現できるフォーマットの標準を定めようというのが最近提案されたBIP-88↓

https://github.com/bitcoin/bips/blob/master/bip-0088.mediawiki

m/{44,49,84}'/0'/0'/{0-1}/{0-50000}{0-2,33,123}/*のようなフォーマットで、各ソフトウェアが使用するパスおよびその制限(ある階層以下では何個以上の鍵を導出してはならないなど)を表現する。

この仕様とは別に、output descriptorもパスを表現する方法もあり、こないだ書いたBSMSの提案ではdescriptorテンプレートとして制限の置き換えが可能な/**の表記の提案もある↓

techmedia-think.hatenablog.com

ので、今後の標準化とどう採用が進むかはまだ柔らかそう。

以下、BIPの意訳。

概要

このドキュメントでは、BIP32導出パスに課すことができる制約を指定するテンプレートの表現形式を説明する。

テンプレートで指定された制約により、制約にマッチした有効なパスと、制約を超えた無効なパスを簡単に識別できる。

動機

BIP32の導出パスフォーマットは普遍的なもので、BIP43を始めBIP44、45、49、84などで多くの導出スキームが提案された。また、フォーマットの柔軟性により、業界の参加者は、一般的には必ずしも有用ではないが、特定の目的にあったカスタム導出スキームを実装することができた。

導出スキームに既存のBIPを使用する場合でも、その使用法はウォレット間で一様ではない。これは、ソフトウェアベンダーが導出パスを決定する際に、考慮事項や優先順位が異なる可能性があるためだ。これにより、ユーザーが以前使用していたウォレットとは異なるアドレスを導出するウォレットを使ってコインにアクセスしようとすると、問題に直面する可能性がある。

既知の解決策

この問題は、異なるウォレットで使用されているパスを追跡する専用のウェブサイト(walletsrecovery.org)が作られるほど、一般的なものだ。

この記事を書いている時点で、このウェブサイトは、複数の導出パスを簡潔に記述するために独自のフォーマットを使用している。著者が知る限り、このBIPが登場する前にパステンプレートを記述するために公に使われていた唯一のフォーマットだった。このフォーマットは、ウェブサイトのメインページ以外、どこにも明記されていなかった。このフォーマットでは代替導出インデックス(例:m/|44'|49'|84'/0'/0')や代替パス全体(m/44'/0'/0'|m/44'/1'/0')を示すのに|が使われている。

これは、ソフトウェアで処理するためのテンプレートフォーマットとして宣言されたものではなく、説明のためだけのアドホックなフォーマットのようだ。このようなアドホックなフォーマットに対し、本BIPえは、ソフトウェアによる曖昧さのないパースと、人間による容易な読み取りを同時に実現することを目的としたフォーマットを採用している。人間はパスのテンプレート化された部分を、テンプレート内で|を使った場合よりも簡単に視覚的に検出することができる。より広い範囲のパスを1つのテンプレートでより簡潔かつ明確に定義できる。

使用目的とメリット

ウォレットソフトウェアの作者は、提案されているフォーマットを使って、そのソフトウェアが使用している導出パスを記述することができる。これにより、別のソフトウェアに切り替える際や、旧ウォレットの復元をする際のユーザーの利便性が向上する。

導出パスを無制限に使用するのは、特定の状況下では安全ではないかもしれない。特にあるトランザクションのお釣り用のアウトプットが、送信者にとって未知のパスで導出されたアドレスに送信された場合、送信者はお釣り全体へのアクセスができなくなる可能性がある。

既知のパスのチェックソフトウェアやファームウェアにハードコードするという単純なアプローチは、相互運用性の低下に繋がる。ベンダーは、特定の非汎用アプリケーション用にカスタムパスを選択することができず、既知のパスを使用するように改造するか、他のベンダーにカスタムパスをサポートするよう説得するしかなくなる。このアプローチでは、拡張性に欠ける。

本ドキュメントで提案する柔軟なアプローチは、「BIP32パステンプレート」の標準的な表記法を定義することである。このテンプレートには、導出パスに課すべき制約が簡潔に記述されている。

このパステンプレートを広くサポートすることで、ソリューションの相互運用性と柔軟性が向上し、ベンダーや個々の開発者が独自のカスタム制限を簡単に定義できるようになる。このようにして、制限のない導出パスの偶発的もしくは悪意ある使用のリスクに柔軟で正確な方法で対処できる。

既知のパステンプレートは、デバイスやアプリケーションでデフォルトで設定されるが、ユーザーは自分の用途に関係のないテンプレートを無効にするオプションを使用できる。

カスタムパステンプレートのフォーマットを標準化することで、デバイスやアプリケーション固有のパス制限を適用する際に、共通のアプローチで開発することができる。このようなアプローチの一連として、パステンプレートやその他のカスタムパラメータを含むアプリケーション固有のプロファイルを利用できるようになる。ただし、悪意あるプロファイルや不正なプロファイルが誤ってインストールされることがないよう、注意が必要だ。

仕様

テンプレートのフォーマットは、読みやすく便利で、視覚的に明確であるよう選択されている。

テンプレートはオプションのプレフィックスm/で始まり、スラッシュ文字(/)で区切られた1つ以上のセクションが続く。

実装は、セクションの最大数を制限する場合がある。

各セクションは、インデックステンプレートで構成され、オプションでアポストロフィ')もしくは文字hのいずれかの強化導出のマーカーが後ろに付く。

インデックステンプレートには、

  • 0〜2147483647の整数値(ユニットインデックステンプレート)
  • 0〜2147483647の任意の整数値を表す単一文字*ワイルドカードインデックステンプレート)
  • 文字{の後にカンマで区切られた複数のインデックスが続き、その後に文字}で終わる(範囲インデックステンプレート)

がある。

実装は、範囲インデックステンプレート内のインデックス範囲の最大数を制限することができる。

インデックステンプレートの直後に強化導出マーカーが続く場合、これは、そのインデックステンプレートで指定されたすべての値が、照合のために2147483648だけ増加することを意味する。

インデックス範囲は、

  • 0〜2147483647の整数値(ユニット範囲)
  • 0〜2147483647の整数値の後に文字-が続き、その後に0〜2147483647の別の整数値が続く(非ユニット範囲)

がある。

非ユニット範囲について、-の左側の値はrange_startで、右側の値はrange_end

ユニット範囲については、start/endはなくても、range_startrange_endは等しい。

ユニットインデックステンプレートにはユニット範囲である単一のインデックス範囲が含まれている。

ワイルドカードインデックステンプレートには、単一のインデックス範囲が含まれ、range_startは0、range_endは2147483647に設定される。

制約:

  1. 曖昧さをなくすため、パステンプレートに空白があってはいけない。
  2. 範囲インデックステンプレート内のカンマは、インデックス範囲の間にのみ出現する。
  3. 曖昧さをなくすため、単一の値にマッチするインデックス範囲はユニット範囲として指定する必要がある。
  4. 曖昧さをなくすため、0-2147483647といったインデックス範囲は許可されず、代わりにワイルドカードインデックステンプレートを使わなければならない。
  5. 非ユニット範囲について、range_endは必ずrange_startより大きくなければならない。
  6. 範囲インデックステンプレート内に複数のインデックス範囲がある場合、2つめ以降の範囲のrange_startは、前の範囲のrange_endよりも大きくなければならない。
  7. 曖昧さをなくすため、0より大きい整数値のすべての表現は0で始めてはならない(先頭0は許可されない)。
  8. パステンプレート内のいずれかのセクション内に強化導出マーカーが現れた場合、先行するすべてのセクションも強化導出を指定しなければならない。
  9. 曖昧さをなくすため、パステンプレート内で強化導出マーカーが現れた場合、先行するすべてのセクションでも同じ強化導出マーカー(hもしくは')を使用しなければならない。
  10. 曖昧さをなくすため、末尾のスラッシュ(例えば1/2/)や重複するスラッシュ(例えば0//1)が、テンプレート内にあらわれてはならない。

有効なパステンプレート文字列ごとに、まったく同じパスのセットにマッチする有効なテンプレート文字列が存在しないような、完全に曖昧さのないエンコーディングが望まれるかもしれない。この場合、誰ががテンプレートを比較する際、パースせずに単純に文字列の同一性をチェックすることができるようになる。

これを実現するには2つの追加ルールが必要だ。

  • 範囲インデックステンプレートでは、後続の範囲は前の範囲の終わりに1を加えた値で始まってはならない。したがって、{1,2,3-5}は許可されず、{1-5}と指定する必要がある。このルールにより、頻繁に編集する場合テンプレートの利便性が低下する可能性がある。
  • 強化導出のマーカーは1種類のみ(hもしくは')のみ許可される。

2つめのルールを必要とする代わり、実装はテンプレート文字列を比較する前に、あるタイプのマーカーを別のタイプに置き換えるだけで済む。

フルテンプレートと部分テンプレート

テンプレートがm/で始まる場合、これは完全なフルテンプレートで、パス全体にマッチすることを意味する。

テンプレートがm/で始まらない場合、これは部分テンプレートであることを意味し、適切な文脈において、パスの一部にマッチするのに使用できる(例えば、パスのサフィックスに対する制約が動的で、パスのプレフィックスに対する制約が固定されている場合など)。

フルテンプレートは、部分テンプレートを組み合わせることができる。部分テンプレートはフルテンプレートを拡張し、より長いフルテンプレートになる。

部分テンプレートは他の部分テンプレートと組み合わせることができ、その結果より長い新しい部分テンプレートになる。

フルテンプレートは別のフルテンプレートと組み合わせることはできない。

実装では、フルテンプレートのパースと、フルテンプレートに対するパスのマッチングをサポートしなければならない。

実装は、部分テンプレートのパースと、部分テンプレートに対するパスのマッチングおよびテンプレートの結合をサポートしてもよい。

パース結果

有効なパステンプレートのパースに成功すると、結果はセクションのリストで表すことができる。各セクションはインデックス範囲のリストで、インデックス範囲は(range_startrange_endの)タプル。セクションの長さはテンプレート長とも呼ばれる。

マッチング

マッチングは、BIP32パス(部分テンプレートの場合はBIP32パスの一部)を表す整数値のリストに対して実行されることになる。このリストの長さをパス長と呼ぶ。

このリストの非強化導出インデックスは、0〜2147483647の値で表す必要がある。

強化導出インデックスのリストは、2147483648〜4294967295の値で表す必要がある。

マッチングアルゴリズムは:

1. パス長がテンプレート長と異なる場合は失敗。
2. パス内の位置Nにある書く値Vについて、
   テンプレート内の位置Nにあるセクション内のすべてのインデックス範囲について、
   値Vがrange_startより小さいか、range_endより大きい場合は失敗
3. それ以外は成功

正式な仕様

記述されたテンプレートフォーマットのパーサーの有限状態マシン(FSM)とマッチング式は、TLA+仕様言語(https://github.com/dgpv/bip32_template_parse_tplaplus_spec)で規定されている。

この仕様は、TLCチェッカーと付属のスクリプトを使って実装のテストデータを生成することができる。

実装

正式な仕様ではFSMを指定しているが、これは豊富な文字列処理機能を利用できない実装には不向きである。しかし、そのような機能が利用できる場合は、テンプレートをまずセクションに分割し、次にセクションをインデックステンプレートに分割し、各インデックステンプレートを個別に解析するという文字列全体を分解するアプローチを採用することになるだろう。

しかしFSMベースのアプローチは、正式な仕様に近づけることができ、またTLCチェッカーで生成されたテストデータは、FSMベースの実装に対してより良いカバレッジを与えるだろう。テンプレート文字列にいくつかのエラーが含まれている場合、分解を行うアプローチを使用した実装は、FSMベースの実装よりも速くこれらのエラーを検出する可能性があり、その逆も同様。

現時点で3つの実装が存在する

互換性

ユニットインデックステンプレートのみを含むフルパステンプレートは、完全に有効なBIP32パスを表す。

現在作者が知っている他のパステンプレート標準は存在しない。

https://github.com/bitcoin/bitcoin/issues/17190には、Bitcoin Scriptのdescriptorのパステンプレートに関する議論があり、xpub...{0,1}/*というフォーマットが提案されているが、この内{0,1}/*の部分が、このBIPのフォーマットの部分パステンプレートに相当する。

サンプル

m/{44,49,84}'/0'/0'/{0-1}/{0-50000}は、アドレスインデックスが50000を超えることができないという制約付きのBIP44およびBIP49、BIP84パスの外部チェーンと内部チェーンの両方にマッチするフルテンプレートを定義する。

パース後の表現は次のようになる(Pythonシンタックスを使用し、フル/部分の区別は無視する):

[[(2147483692, 2147483692), (2147483697, 2147483697), (2147483732, 2147483732)),
 [(2147483648, 2147483648)],
 [(2147483648, 2147483648)],
 [(0, 1)],
 [(0, 50000)]
]

{0-2,33,123}/*は、1つめのインデックスとして非強化導出の値0、1、2、33、123にマッチし、2つめのインデックスは任意の非強化導出値にマッチする部分テンプレートを定義する。

パース後の表現は次のようになる:

[[(0, 2), (33, 33), (123, 123)], [(0, 2147483647)]]

*h/0は、強化導出されたインデックスの後に、非強化導出インデックスが続く部分を定義する。

パース後の表現は次のようになる:

[[(2147483648, 4294967295)], [(0, 0)]]

マルチシグウォレット用の新しいBIP-32階層仕様を提案するBIP-87

Bitcoinのウォレットの多くは、1つのマスターシードから各秘密鍵やアドレスの導出を行う階層的決定性ウォレットをサポートしている。この仕組みを使うことで、シードさえ覚えていれば、ウォレットを復元することができる。さらに、ウォレットが複数のアカウントを管理できるように、この各階層の使用方法を定義したのがBIP-44

そして、Segwitがアクティベートされると、P2SH-P2WPKHや、P2WPKH用のアドレスを導出するためにBIP-44を拡張したBIP-49BIP-88が定義された。

↑はいずれも単独の署名を必要とする(シングルシグの)ウォレット用の仕様で、マルチシグ用の導出仕様としてBIP-45が定義されている。

ただ、これらのアドレス仕様はスクリプトタイプ(P2PKH、P2SH、P2WPKH、P2SH-P2WPKHなど)を固定したもので、スクリプトタイプが増えると、その分新しいBIPが増えていくという状況になっている。

使用するスクリプトタイプについては、最近は↓のようなdescriptorやdescriptor templateを使って記述する方向になりつつあり、

# 2つの拡張公開鍵を使った1-of-2のP2WSHマルチシグアウトプットのdescriptor
wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/0/*))

マルチシグの鍵のソートをBIP-45の導出パスに含めるような方法と組み合わせると情報が冗長になる。そのため最近の方向性であるdescriptorやPSBTの仕様と合わせて利用できるように新しくマルチシグ用の導出パスを定義したのがBIP-87↓

https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki

といっても仕様を見ると、パス仕様は基本的にBIP-44と同じで、purposeがBIP番号によって変わるだけ。まぁマルチシグ用の鍵導出であることを明示的に宣言したもので、使用するスクリプトタイプなどはdescriptor側で処理してねという切り分け。そのため、ウォレットは基本的にdescriptorを保存する必要がある。それらを利用してマルチシグのセットアップする仕様が、こないだ記事に書いたBIP-129のBSMS↓

techmedia-think.hatenablog.com

※ 以下、BIP-87の訳

概要

このBIPは、BIP-0032(以降BIP32)に記載されているアルゴリズムおよび、BIP-0044(以降BIP44)に記載されているマルチアカウント階層に基づいて、決定性マルチシグウォレットの階層を定義する。

このBIPは、BIP43の特定のアプリケーションである。

Copyright

このBIPのライセンスは2-clause BSD license。

動機

よりユーザーフレンドリーな(オフラインの)マルチシグウォレットの増加や、descriptor言語やBIP-0174 (Partially Signed Bitcoin Transactions)などの新しい技術の採用に伴って、すべての新技術を利用した共通の導出スキームを作る必要がある。

背景として、BIP 44/49/84は、以下のように定義されている:

m / purpose' / coin_type' / account' / change / address_index

ここでBIP43のpurpose'パスは、各スクリプト(P2PKH、P2WPKH-in-P2SH、P2WPKH)毎に分かれている。シングルシグ用のウォレットでは、導出毎にスクリプトを用意することで、秘密鍵の情報だけで簡単にバックアップと復元ができる。

マルチシグウォレットでは、バックアップと復元に必要な情報が増える(すべての共同署名者の公開鍵など)。これらのスクリプト毎の導出は、その情報を提供するdescriptorによって冗長になる(同様にアウトプットスクリプトのコレクションも指定する)。マルチシグの導出パスには最新の標準化が必要だ。現存するものはいくつかあるが、どれも問題がある。例えばBIP45の定義は:

m / purpose' / cosigner_index / change / address_index

で、単一のスクリプトタイプ(ここではP2SH)であることを不必要に要求する。また、各共同署名者の公開鍵をソートするのにcosigner_indexを設定しているが、これも冗長である。descriptorではmultiを使って公開鍵の順序を設定したり、sortedmultiを使って(BIP67で定義されている)辞書式の順序でそれらをソートできるからだ。descriptorを作る前に鍵レコードをコーディネーターに送信して、完全な導出パスを作成するために共同署名者間で公開鍵をソートするようなことは、単に不必要な通信ラウンドを追加することになるだけだ。

2つめのマルチシグの標準は、m/48'で、これは次のように定義される:

m / purpose' / coin_type' / account' / script_type' / change / address_index

BIP 44/49/84のパスに従いスクリプト毎に個別のBIPを持つのではなく、ベンダーは導出パスにscript_type'を挿入することを決めた( P2SH-P2WSH=1、P2WSH=2、Future_Script=3など)。前述したように、descriptorがスクリプトを設定するため、これは不要だ。スクリプト毎に新しいBIPを用意する条件を取り除くことで保守作業を削減しようとしているが、この場合もscript_typeのリストを保守する必要がある。

このホワイトペーパーの後半で提案する構造は、これらの問題を解決し、非常に包括的だ。これによりスクリプトタイプに関係なく、マルチパーティ、マルチシグ、階層的決定性ウォレットにおいて、複数のアカウントやアカウント毎の外部/内部チェーン、チェーン毎に何百万のアドレスを扱うことができる*1

このペーパーは、BIP44からインスパイアされたもの。

仕様

鍵のソート

descriptorをサポートするウォレットは、BIP67にいよる決定論的な鍵のソートを(sortedmulti関数によって)本来サポートしているため、決定論的にソートされた公開鍵から可能なマルチシグのアドレス/スクリプトを導出することができる。

パス階層

鍵とスクリプトを同じレイヤーで混在させるべきではない。ウォレットは、スクリプトタイプとは無関係に拡張秘密鍵/拡張公開鍵を作成すべきで、一方でdescriptor言語はウォレットに指定された公開鍵でマルチシグアウトプットを監視できるように指示する。

BIP32のパスには5つの階層を定義する:

m / purpose' / coin_type' / account' / change / address_index

パス内のhおよび'はBIP32の強化導出が使用されることを示す。

それぞれの階層には特別な意味があり、以下の章で説明する。

Purpose

Purposeは、BIP43に従って定数87'を設定する。これは、このノードのサブツリーがこの仕様に従って使われることを示す。この階層では強化導出が使われる。

Coin type

1つのマスターノード(シード)を複数のBitcoinネットワークで使用できる。さまざまなネットワークで同じすケースを共有するといくつかの欠点がある。

この階層では、ネットワーク毎に個別のサブツリーが作成され、ネットワーク全体でアドレスの再利用が回避され、プライバシーの課題が改善する。

Coin type 0がmainnetで、1はテストネット(testnet、regtest、signet)。

この階層では強化導出が使われる。

Account

この階層は、BIP44のパターンに従って鍵スペースを個別のユーザーIDに分割するため、ウォレットが異なるアカウント間でコインを混合することはない。

ユーザーはこれらのアカウントを使って、銀行口座と同じ方法で資金を整理できる。(すべてのアドレスが公開される)寄付用や、貯蓄、共用など。

アカウントは、インデックス0から順番に昇順で番号が振られる。この番号はBIP32導出仕様の子インデックスとして使われる。

この階層では強化導出が使われる。

新しいウォレットが参加するたび、もしくは秘密鍵/公開鍵が作成されるたびに、プライバシー保護と暗号処理の両方の目的でこの階層をインクリメントするのが重要だ。例えば新しい鍵レコードをコーディネーターに送信する前に、ウォレットはアカウントの階層をインクリメントする必要がある。これにより、ECDSA署名およびSchnorr署名、異なるスクリプトタイプ、同じウォレットタイプ間での鍵の再利用を防止される。

Change

定数0は外部チェーンに使用され、定数1は内部チェーン(お釣り用のアドレス)に使用される。外部チェーンは、ウォレットの外部から見えることを意図したアドレスに使われる(支払いの受け取りなど)。内部チェーンはウォレットの外部に見えることお意図していないアドレスに使用され、トランザクションでお釣りを戻す際に使われる。

この階層では、公開導出が使用される。

Index

アドレスはインデックス0から順に昇順に番号が振られる。この番号はBIP32導出仕様の子インデックスとして使われる。

この階層では、公開導出が使用される。

アドレスの検出

アドレスの生成と検出には、共同署名者の結合された鍵レコードから生成されるmultisig descriptorもしくはdescriptor templateを使用する必要がある。

descripto templateについてはBIP-0129 (Bitcoin Secure Multisig Setup) 参照。descriptorもしくはdescriptor templateには、BIP-0174との互換性を最大限高めるため、key origin informationを含めるべきである。

例えば:

次のようなdescriptor templateと導出パスの制約は:

wsh(sortedmulti(2,[xfpForA/87'/0'/0']XpubA/**,[xfpForB/87'/0'/0']XpubB/**))
/0/*,/1/*

次の2つの具体的なdescriptorに分解される:

wsh(sortedmulti(2,[xfpForA/87'/0'/0']XpubA/0/*,[xfpForB/87'/0'/0']XpubB/0/*))
wsh(sortedmulti(2,[xfpForA/87'/0'/0']XpubA/1/*,[xfpForB/87'/0'/0']XpubB/1/*))

アドレスを検出するためには、受信とお釣りの両方のdescriptorをインポートし、以下に記載するgap limitを遵守する。

アドレスのgap limit

アドレスのgap limitは現在20に設定されている。ソフトウェアが連続して20の未使用アドレスに達すると、それ以降の未使用のアドレスはないと想定し、アドレスチェーンの検索を停止する。

ウォレットソフトウェアは、ユーザーが複数の未使用アドレスを生成して外部descriptorのgap limitを越えようとすると警告する必要がある。

後方互換

descriptor(および特定のウォレット実装)でサポートされているスクリプトはすべて、このBIPと互換性がある。

このBIPに準拠するウォレットは、descriptorウォレットであるため、後から適切に復元するためには、共同署名者が自分の秘密鍵の情報とdescriptorをバックアップする必要がある。これはユーザーの負担にはならないはずである。というのも、M of Nのマルチシグの復元操作では、M個のシードに加えてすべての協同署名者の公開鍵を提供する必要があり、descriptorは、key origin informationを含むこの情報を標準化された形式で提供し、エラー検出も行う。

サンプル

ネットワーク アカウント チェーン アドレス パス
mainnet first external first m / 87' / 0' / 0' / 0 / 0
mainnet first external second m / 87' / 0' / 0' / 0 / 1
mainnet first change first m / 87' / 0' / 0' / 1 / 0
mainnet first change second m / 87' / 0' / 0' / 1 / 1
mainnet second external first m / 87' / 0' / 1' / 0 / 0
mainnet second external second m / 87' / 0' / 1' / 0 / 1
testnet first external first m / 87' / 1' / 0' / 0 / 0
testnet first external second m / 87' / 1' / 0' / 0 / 1
testnet first change first m / 87' / 1' / 0' / 1 / 0
testnet first change second m / 87' / 1' / 0' / 1 / 1
testnet second external first m / 87' / 1' / 1' / 0 / 0
testnet second external second m / 87' / 1' / 1' / 0 / 1
testnet second change first m / 87' / 1' / 1' / 1 / 0
testnet second change second m / 87' / 1' / 1' / 1 / 1

*1:なぜマルチシグウォレットだけにこの構造を提案するのか? 現在、シングルシグのウォレットは、(通常、BIP39フォーマットの)マスター秘密鍵データのみを使って資金を復元することができる。ユーザーが使用した導出を覚えていなくても、ウォレットの実装は一般的なスキーム(BIP44/49/84)を反復できる。この提案された階層では、ユーザーは追加のデータ(descriptor)をバックアップするか、ウォレットが復元時にすべてのAccount階層のすべてのスクリプトタイプを試行する必要がある。このため、descriptor言語がスクリプトタイプと同様に署名タイプを処理するが、このスクリプトに依存しない階層は、マルチシグウォレットのみに制限するのが最適だ。