Develop with pleasure!

福岡でCloudとかBlockchainとか。

部分的に署名されたトランザクション(PSBT)のフォーマットを定義するBIP-174

通常のP2PKHやP2WPKHで決済をする分には特に関係ないが、P2SHベースのマルチシグやコントラクトなどで複数のユーザーによる署名が必要な場合、ユーザー間でトランザクションをやりとりしながら、各自署名を付与し、最終的にブロードキャスト可能なトランザクションを作成する必要がある。

この部分はブロックチェーン外で行われるけど、どういうフォーマットで途中のトランザクションをやり取りするかなどは、ウォレットやサービスによってそれぞれ異なる。標準仕様がないため、当然ウォレットが違えば互換性が無いのがあたり前だが、そういう部分的に署名されたトランザクションのフォーマットを標準定義しようというのが最近追加されたBIP-174↓

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

イントロダクション

概要

このBIPでは、署名者がトランザクションの署名を生成するのに必要な情報と、入力が完全な署名を持たない間の中間状態の署名データ保持するバイナリトランザクションフォーマットを提案している。トランザクションの署名に必要な全ての情報が提供されるため、署名者はオフラインでも可。

動機

未署名のトランザクションや部分的に署名されたトランザクションを他の複数の署名者に渡すために作成する場合、その方法は現在それぞれの実装に依存しており、異なるウォレット間でそういったトランザクションの交換をすることは難しい。このBIPの1つのゴールは、クライアント間で同じトランザクションをやりとりし、その過程でそれぞれ署名を行うための標準的で拡張可能なフォーマットを作成することである。このフォーマットは、将来のために簡単に拡張できるよう設計されている(既存のトランザクションフォーマットでそれを行うのは難しい)。

通常、トランザクションに署名する際はユーザーは使用するUTXOへアクセスできる必要がある。このトランザクションフォーマットでは、エアギャップウォレットやハードウェアウォレットなどが直接UTXOセットにアクセスすることなく、かつ不正行為のリスクを伴わない形でトランザクションへのオフライン署名を可能にする。

仕様

部分的に署名されたBitcoinトランザクション(Partially Signed Bitcoin Transaction = PSBT)フォーマットは、key-valueのマップで構成される。各keyとvalueのペアは、スコープ内で一意でなければならず、重複は許可されない。各マップはシーケンシャルなレコードで構成され、0x00バイトで終了する。レコードのフォーマットは以下の通り。

注意:<..>は、データの長さを表すCompact Size Unsigned Integerが先頭に付与されていることを示す。{..}はRAWデータを示す。

<key>|<value>
名称 タイプ 定義
Key Length Compact Size Unsigned Integer Keyの長さ
Key byte key自体。先頭のバイトがkey-valueペアのタイプになる。
Value Length Compact Size Unsigned Integer Valueの長さ
Value byte value自体

各key-valueマップのフォーマットは以下の通り。

{key-value pair}|{key-value pair}|...|{0x00}
サイズ 名称 タイプ 定義
1+ Key-valueペア key-valueペアの配列 任意 key-valueのペア
1 セパレータ char 0x00 必ず0x00

各keyの先頭バイトには、そのkey-valueペアのタイプを指定する。いくつかのタイプはグローバルフィールド用で、他のフィールドは各入力用。PSBTで唯一必須のタイプは、以下に定義されているTransactionタイプ。

現在定義されているグローバルタイプは以下の通り。

数値(タイプ) 名称 Keyデータ Valueデータ サンプル
0x00 Transaction 無し。keyは1バイトのタイプのみ。 ネットワークのシリアライゼーションフォーマットでシリアライズされたトランザクション。各入力のscriptSigとwitnessは完全なデータでなければ空でなければならない。トランザクションはBIP-144で定義されているwitnessなシリアライゼーションフォーマットであること。PSBTにはトランザクションが必要で、それ以外の場合は無効。 Key: {0x00}, Value: {transaction}
0x01 Redeem Script hash160されるredeem script Pay-To-Script-Hashの入力に署名するために必要なredeem script、もしくは出力で使われてるredeem script。 Key: {0x01}, Value: {redeem script}
0x02 Witness Script sha256されるwitness script Pay-To-Witness-Script-Hashの入力に署名するのに必要なwitness script、もしくは出力で使われてるwitness script。 Key: {0x02}, Value: {witness script}
0x03 BIP 32の導出パスと、公開鍵とマスター鍵のfingerprint 公開鍵 master key fingerprintは公開鍵の導出パスと連結されている。導出パスは互いに連結された32 bitの符号なし整数として表される。マスター鍵のインデックスは省略しなければならない。 Key: {0x03}, Value: {マスター鍵のfingerprint}|{32-bit int}|...|{32-bit int}
0x04 入力の数 無し。keyは1バイトのタイプのみ。 トランザクションが持つ入力の数を示すcompact size unsigned integer Key: {0x04}, Value: {入力の数}

現在定義されている入力単位のタイプは以下の通り。

数値(タイプ) 名称 Keyデータ Valueデータ サンプル
0x00 非Witness UTXO 無し。keyは1バイトのタイプのみ。 入力が参照するネットワークのシリアライゼーションフォーマットでシリアライズされたトランザクション Key: {0x00}, Value: {transaction}
0x01 Witness UTXO 無し。keyは1バイトのタイプのみ。 入力が参照するネットワークのシリアライゼーションフォーマットでシリアライズされたトランザクション出力。 Key: {0x01}, Value: {serialized transaction output({output value}|<scriptPubKey>)}
`0x02 部分的な署名 公開鍵と対応する署名 scriptSigもしくはwitnessからスタックにプッシュされる署名 Key: {0x02}|{公開鍵}, Value: {署名}
0x03 Sighash Type 無し。keyは1バイトのタイプのみ。 この入力に使用することを推奨するsighash typeで32 bitの符号なし整数。sighash typeはあくまで推奨で、署名者は必ずしも指定されたsighash typeを使用する必要はない。 Key: {0x03}, Value: {sighash type}
`0x04 入力のインデックス 無し。keyは1バイトのタイプのみ。このフィールドは完了した入力をスキップできるようにするためのオプション。入力の1つでもこのタイプがある場合、全入力にこのタイプが必要になる。 この入力のインデックスを示すcompact size unsigned integer。 Key: {0x04}, Value: {入力のインデックス}

トランザクションのフォーマットは以下のように指定される。

{0x70736274}|{0xff}|{global key-value map}|{input key-value map}|...|{input key-value map}
サイズ 名称 タイプ 定義
4 マジックバイト int32_t 0x70736274 psbtのASCII文字であるマジックバイト。この整数は最上位バイト順にシリアライズする必要がある。
1 セパレータ char 0xff 必ず0xff
1+ グローバルデータ key-valueのマップ 任意 グローバルデータのkey-valueマップ
1+ 入力 key-valueマップの配列 任意 以下で説明する各入力のkey-valueペア

セパレータ間の各データブロックはスコープとみなすことができる。タイプは必要なければスキップできる。例えばwitnessな入力の場合、Non-Witness UTXOのkey-valueペアは必要無い。

署名者が理解できないkey-valueペアを検出した場合、署名者はトランザクションを再度シリアライズする場合、そのkey-valueのペアをセットしておく必要がある。

Keyが重複した場合の取り扱い

各スコープのKeyは重複しないようにすること。フォーマット内の全Keyは一意である必要がある。ただ実装者はKeyが重複していた場合にそれに対処する必要がある。Keyが重複している場合、ソフトウェアはどのvalueを使用するか選択することができる。

使い方

このトランザクションフォーマットを使用する際は、以下のようなロールが考えられる。ロールは1つのエンティティに結合されているが、ロールにはそれぞれ以下のような特定の機能がある。

Transaction Creator

トランザクションの作成者は、ネットワークのシリアライゼーションフォーマットでシリアライズしたトランザクションかPSBTのいずれかを受け入れ、PSBTを生成するか提供されたPSBTを更新する。scriptSigがまだファイナライズされていない場合は、トランザクションのそのscriptSigの部分は空にし、scriptSigのデータは入力フィールドにセットしなければならない。可能であれば、必要なredeem scriptとwitness scriptを探して、それらをPSBTのグローバルデータセクションに追加する必要がある。トランザクション作成者は、知っているUTXOのデータも記入する必要がある。

Transaction Signer

トランザクションの署名者はPSBTを受け入れ、PSBTで提供されたUTXOを使って、署名可能な入力の署名を生成する。署名に必要な情報は全てPSBT自体で提供されるため、署名者はUTXOセットへアクセスする必要はない。生成された署名は、対応する入力の部分的な署名のkey-valueペアのフィールドに追加する。

また、署名者は使用される金額、取引される金額、手数料、支払先のアドレスを計算することができる。これらのことについて全てインタラクティブにユーザーに問題ないか確認することができる。

Transaction Combiner

トランザクションのコンバイナは複数のPSBTを受け入れ、可能であれば各PSBTをパースし1つのトランザクションにマージする。複数のPSBTが同じトランザクションに対応する場合、コンバイナはそれらの全てのkey-valueペアを含む1つのPSBTを作成する必要がある。また重複したキーがあれば仕様に従って重複を除去する。

Transaction Finalizer

トランザクションのファイナライザーは、PSBTを取得し、各入力について最終的なscriptSigを構築する。入力に十分な署名が含まれている場合、ファイナライザーはそれらの署名と必要なredeem script及びwitness scriptを取得し、完全なscriptSigを構築する。作成したscriptSigはトランザクションに含める必要があり、scriptSigを作成した入力についてはその全てのkey-valueペアを削除する。また、他の入力には不要なグローバルデータを削除することがある。

Transaction Broadcaster

トランザクションブロードキャスターは、ファイナライズされたPSBTを受け取り、トランザクションを取り出し、その有効性を検証し、Bitcoinネットワークにブロードキャストする。

拡張性

PSBTフォーマットは、将来、新しいkey-valueタイプを追加することで拡張することができる。新しいタイプについて知らない署名者にとっては新しいタイプはパススルーされるだけなので、後方互換性は引き続き維持される。

さらに、異なるタイプkey-valueペアを持つkey-valueマップをフォーマットの最後に追加することができる。続く各マップの数については、パーサーがデータタイプの異なる定義をいつ使用するか知るためグローバルセクションで指定する必要がある。

互換性

このトランザクションフォーマットは、通常のトランザクションアンシリアライザーにはたらしくシリアライズされないよう設計されている。通常のトランザクションがPSBTフォーマットのトランザクションアンシリアライザーでアンシリアライズできないのと同じように。

サンプル

CoinJoinワークフローの手順

https://github.com/bitcoin/bips/raw/master/bip-0174/coinjoin-workflow.png

2-of-3マルチシグのワークフロー

https://github.com/bitcoin/bips/raw/master/bip-0174/multisig-workflow.png

論拠

  1. セパレータが0xffでなく0x00なのはなぜ?
    このセパレータはデータの各チャンクを区別するために使われる。0x00セパレータであれば、アンシリアライズが長さ0のkeyとして読み取ることを意味する。セパレータとして使用でき、シリアライザの実装を簡単にすることができる。
  2. なぜredees scriptとwitness scriptはグローバルなのか?
    重複を避けるためグローバルデータにしている。これらのスクリプトを必要とする複数の入力で、redeem scriptとwitness scriptをそれぞれ指定するのではなく、グローバルデータで1つだけ指定する。
  3. なぜ出力のredeem scriptとwitness scriptは含まれているのか?
    トランザクション出力のredeem scriptとwitness scriptが含まれていると、署名しているトランザクションが正しいredeem scriptやwitness scriptを持つ正しい出力を持つトランザクションであることを確認することができる。PSBTの作者が署名者に信頼されていない場合に適してる。
  4. なぜpsbtに4バイト使うのか?
    トランザクションフォーマットは一意に識別するため5バイトのヘッダから始まる必要があった。先頭のバイトはPartially Signed Bitcoin Transactionの略語であるpsbtのASCIIとして選択した。
  5. マジックバイトの後ろにセパレータを使う理由は?
    セパレータはPSBTの5バイトヘッダの一部だ。非PSBTのアンシリアライザーが通常のトランザクションとしてPSBTをアンシリアライズできないようにするための、0xffをセパレータにしている。5バイトのヘッダは固定されているので、非PSBTフォーマットのトランザクションがPSBTのシリアライザーによってアンシリアライズできないのと同様。
  6. なぜ全てのセパレータが必要なのか?
    セパレータはアンシリアライザーがどの入力がシリアライズされていないか知るために必要。

参照実装

github.com

所感

  • マルチシグやペイメントチャネルを構築する際のトランザクションなどは、実際には複数のユーザーでの署名が必要で、署名前のトランザクションをどう管理するかについては標準的な決まりは無いので、BIPでその標準仕様が決めるのは大事。
  • 入力のキーにUTXOが参照するトランザクションが定義されているのは、オフラインデバイスでも署名できるようにするためっぽい。
  • アプリケーションレイヤーのBIPでコンセンサスに関与するものではないので、基本的にこの仕様を採用するかどうかはアプリケーション次第。
  • トランザクションフォーマットのセパレータが0xffなのは、BIP-144のフォーマットであればちょうどマーカー(値は0)に該当して、パースしようとするとその違いからエラーになるってことかな。