Develop with pleasure!

福岡でCloudとかBlockchainとか。

サイレントペイメントの仕様を定義したBIP-352

BIP-352はBitcoinの静的な支払いアドレスプロトコルを定義するBIP↓

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

Bitcoinにおけるプライバシーに関する推奨事項として、アドレスの再利用をしないというのがよく挙げられる。複数のトランザクションで同じアドレスを使用するとトランザクションが同じ人物によるものだということがリンクされてしまうから。

再利用しないと言っても、支払い先の情報としてアドレスは必要になるため、毎回生成して相手に伝えるか、拡張公開鍵(xpub)から都度導出してもらうかする必要がある。支払いのたびにこういった対話を必要とするのは面倒なので、代表的なアドレスを公開し、実際の支払いではそのアドレスからワンタイムアドレスを導出して使用するようにしようというのがBIP-352サイレントペイメントの目的。

似たような目的の提案としては、過去にBIP-47のペイメントコードという提案があるけど、これは支払いとは別に通知用のトランザクションが必要になる。今回のサイレントペイメントと仕組み的に近いのはMoneroのステルスアドレスかな。

サイレントペイメント

サイレントペイメントを使って資金を受け取りたいユーザーは、

  • 入金を検知するためのスキャン用の公開鍵と
  • 受け取った資金を使用する際に必要となる支払い用の公開鍵

の2つの公開鍵を用意する。この2つの公開鍵はサイレントペイメントアドレスとして公開する。スキャン用の鍵ペアを {(B_{scan}, b_{scan})}、支払い用の鍵ペアを {(B_{spend}, b_{spend})}と表記する(大文字が公開鍵、小文字が秘密鍵)。

BIP-32を使って導出する場合のパスは↓(purpose=352

スキャン用の鍵:m / 352' / coin_type' / account' / 1' / 0
支払い用の鍵:m / 352' / coin_type' / account' / 0' / 0

サイレントペイメントアドレス

サイレントペイメントのアドレスは、上記の2つの公開鍵をbech32mエンコードすることで生成される。

ここで、サイレントペイメントアドレスを公開する受信者が、入金を区別できるようにラベルを付与する機能がある。ラベルと行っても1からインクリメントされる数値で、このラベルを支払い用の公開鍵 {B_{spend}}に以下のように付与してラベル付きの公開鍵 {B_m}を導出する。

 {B_m = B_{spend} + hash_{BIP0352/Label}(b_{scan} || m)}\cdot G

受信者はスキャン用の公開鍵1つとラベル付きの支払い用公開鍵のペアを以下の形式でbech32mエンコードする↓

  • hrpパートがsp(testnetの場合はtsp
  • セパレーター1
  • データパート:
    • バージョン:現在のバージョン=0を表すbech32文字q
    • 2つの公開鍵を連結した合計66バイトの値( {B_{scan} || B_{m}}

ラベル機能はオプションなので、使用しない場合は、 {B_m = B_{spend}}。スキャン用の公開鍵が共通なのは、それも個別にするとラベルの数分スキャンのコストが増加するため。

支払い先アドレスの導出

公開されているサイレントペイメントアドレスから、実際の支払いに使用するアドレスを導出する際は、ECDHを用いる。これはBIP-47など他のアプローチとも同様で、

  • 送信者は、自分の秘密鍵とペイメントアドレスのスキャン用の公開鍵を使って、
  • 受信者は、送信者の公開鍵と自分のスキャン用の秘密鍵を使って

ECDHにより相手の公開鍵と自分の秘密鍵を使って共有シークレットを導出する。アドレス導出の対話を不要とするためには、送信者側の鍵を何と定めるか決める必要があり、サイレントペイメントの場合、支払いに使用するUTXOに関連付けられた鍵を送信者の公開鍵とする。

具体的には、送信者が支払い用のトランザックションを構築する際、以下のタイプのUTXOのscriptSigまたはwitnessから公開鍵を抽出する。サイレントペイメントを使用する支払いトランザックションのインプットには、以下のタイプのインプットが必ず1つは含まれている必要がある。

タイプ 公開鍵の入手先
P2PKH scriptSig内にセットされている公開鍵
P2WPKH witnessの最後の要素としてセットされている公開鍵
P2SH-P2WPKH witnessの最後の要素としてセットされている公開鍵
P2TR TaprootのscriptPubkeyとして設定されている公開鍵(witness version 1のwitness program)。ただし、script-pathのみのTaprootで、内部鍵をNUMSポイントに設定している場合は、鍵の計算から除外される。

これらのインプットは当然複数あることも考えられるため、全インプットから抽出した公開鍵を合算した結果の公開鍵が鍵共有に用いる送信者の鍵となる。インプットの各秘密鍵 {a_1, a_2, ..., a_n}とした場合、 {a = a_1 + a_2 + ... + a_n}が合算した公開鍵に対応する秘密鍵になる*1*2

インプットの鍵が合算できたら、それを使って送信先のアドレスを計算する。サイレントペイメントでは送信先はTaprootアウトプットとなる。

  1. まず、トランザックションの全インプットのOutPoint(TXID + vout)を辞書順にソートし、最も小さい値を {outpoint_L}とする。
  2. 続いて、タグ付きハッシュ関数を使用してinput_hash =  {hash_{BIP0352/inputs}(outpoint_L || A)}を計算(A = aG)。
  3. サイレントペイメントアドレスのスキャン用公開鍵 {B_{scan}}秘密鍵aを使用して共有シークレットecdh_shared_secret = input_hash { \cdot a \cdot B_{scan}}を計算する。
  4. サイレントペイメントアドレス支払い用の公開鍵 {B_m}について、支払いのアウトプット数(通常の支払いであれば1個)分、宛先のTaprootアウトプットを構築する。最初をk = 0として、
    •  {t_k = hash_{BIP0352/SharedSecret}(}ecdh_shared_secret {|| k)}を計算し、
    •  {P_{mn} = B_{m} + t_{k} \cdot G}を計算し、これをTaprootアウトプットとしてエンコードする。
    • kをインクリメントしてアウトプットの数分これを続ける。
  5. (オプション)もし、送信者のウォレットもサイレントペイメントに対応している場合、ラベルm = 0として自身のサイレントペイメントアドレスに送信するお釣り用のアウトプットを作成することもできる*3

上記の手順で、送信者は受信者のサイレントペイメントアドレスから決定論的に支払先のTaprootアドレスを導出する。

上記の仕様の特性として、

  • 対象のインプットの公開鍵を合算しているのは、スキャン時に各インプット毎に個別にチェックする必要がないようにするためで、これによりスキャン要件が軽減される。
  • トランザックションのOutPointの値(input_hash)を鍵導出に組み込むことで、アドレスの再利用がプロトコルレベルで防止される。
  • ECDHにより共有シークレットを導出するのがスキャン用の公開鍵のみで、支払い用の公開鍵はその後最終的なTaproot用の公開鍵を導出する際の加算に使用するのみであるため、受信者は入金の検知のためにスキャン用の秘密鍵のみオンラインにし、支払い用の秘密鍵はオフラインで保管することが可能になる。

スキャン

受信者が入金を検知するためには、オンチェーントランザクションに対して以下のチェックを行う必要がある。

まず、それがサイレントペイメントのTx候補か以下をチェックする。

  • トランザクション内に少なくとも1つTaprootのアウトプットが存在する(既に使用済みならスキップしてもいい)
  • トランザクションのインプットに共有シークレットを導出するためのインプットが少なくとも1つある(上記の表のUTXOタイプが使用されている)
  • segwit versionが1より大きいUTXOをインプットで使用していない

上記をパスしたトランザクションについて、以下の手順で自身宛の入金かチェックする。

  1. トランザクションのインプットの内、上記リストに該当するインプットから公開鍵を抽出し、合算する {A = A_1 + A_2 + ... + A_n}。Aが無限遠点となる場合は、トランザクションをスキップ。
  2. input_hash =  {hash_{BIP0352/inputs}(outpoint_L || A)}を計算。
  3. スキャン用の秘密鍵を使用して、ecdh_shared_secret = input_hash {\cdot b_{scan} \cdot A}を計算
  4. 各アウトプットをチェック。k = 0から開始して
    •  {t_k = hash_{BIP0352/SharedSecret}(}ecdh_shared_secret  {) || k)}を計算し、
    •  {P_k = t_k \cdot G + B_{spend}}を計算する。
    • トランザクション内の各Taprootアウトプットに対して、 {P_k}がアウトプットの公開鍵と一致するかチェック
      • 一致する場合は、これをウォレットに追加し、kをインクリメントして残りのアウトプットをスキャンする。
      • 一致しない場合、ラベルが付与されている可能性があるため(お釣り用のラベル(m=0)は常にチェック)
        •  {label = output - P_k}を計算し、ウォレットが使用するラベルリストに、そのラベルが存在するかチェックする。ラベルを付与している場合、 {B_m = B_{spend} + label \cdot G}になっているので、チェックは簡単。

共有シークレットの導出にはスキャン用の秘密鍵を用いるため、支払い用の鍵の法は公開鍵のみあれば上記チェックは行える。

支払い

上記のように、サイレントペイメントのTaprootアウトプットは、 {B_{spend} + t_k \cdot G}となっているため(ラベル付きの場合は、さらに{hash_{BIP0352/Lavel}(b_{scan} || m)\cdot G}が加算されれる)、{(b_{spend} + t_k}) \mod n秘密鍵として(ラベル付きの場合はさらに{hash_{BIP0352/Lavel}(b_{scan} || m)}を加算して)使用すれば、Taprootのアウトプットをアンロックできる。

軽量クライアント

サイレントペイメントは基本的に入金を確認するためにすべてのトランザクションデータをスキャンする必要が出てくるので、それをトラストレスに行おうとするとフルノードが必要になる。BIP-352自体では、軽量クライアント用のプロトコルは定義しておらず今後の研究対象になっているけど、いくつかのアイディアは掲載されている。

スキャンに必要な調整用のデータは、サイレントペイメントの対象トランザクションのインプットの公開鍵の合算値に、input_hashを乗算したデータで、これは楕円曲線上の点なので33バイトになる。

この調整用のデータがあれば、受信者は

  1. 上記のスキャンで行っている内容と同じく、それにスキャン用の秘密鍵を乗算して共有シークレットを導出し、
  2. TaprootアウトプットのscriptPubkeyを導出し、
  3. BIP-158コンパクトブロックフィルターを使って候補となるアウトプットを持つTxがブロック内に含まれるかを検証する。
  4. 3で対象のブロックがあれば、ブロック全体を要求し、対象のUTXOが見つかればそれをウォレットに追加する

といった形でスキャンを実行できる。

1ブロックに約3,500 Txが格納されており、そのすべてがTaprootへ送信していると仮定すると、1ブロックあたり3,500×33B = 約112KBの調整用データがスキャンに必要となる。

その他、

  • 該当ブロック全体をダウンロードする代わりに、UTXOの情報のみを要求できるようにしたり、
  • スキャンするブロックの間隔が空いているのであれば、トランザックションカットスルーにより未使用のもののみに絞る
  • 帯域外の別の通知の仕組みを導入する

などの案が上がっている。

*1:秘密鍵について、Y座標が偶数でない場合はnegateする

*2:a = 0となる場合は失敗

*3:お釣り用のラベルを0とするため、他のラベルは1から始まる