PSBT: 秘密鍵を持たない計算機が取引を組み立てる方法
エアギャップウォレットとマルチシグを実際に成立させている形式。BIP-174の六つの役割、署名デバイスが必ず検証すべきこと、オフライン機器に手数料を偽る攻撃と、タップルートがそれを終わらせた仕組みまで。
ウォレットアプリで送信を押すと、一台の機械がすべてを行う。使うUTXOを選び、出力を作り、手数料を計算し、秘密鍵で署名してネットワークに送り出す。すべてが一箇所で起きるため、途中で何かを受け渡す必要がない。
自己保管を本気で行おうとした瞬間、この前提は崩れる。秘密鍵はインターネットに一度も繋がったことのない機器の中にあり、自分がどのUTXOを持っているかを知っているのはオンラインの計算機である。2-of-3のマルチシグなら、署名すべき鍵はそもそも三箇所に散っている。
取引を知っている側と署名できる側が分離すると、未完成の取引を機械の間で運ぶ器が必要になる。その器がPSBTである。
標準がなかった頃の問題
PSBTはPartially Signed Bitcoin Transaction、部分署名ビットコイン取引の略である。ビットコインコアのメンテナであるエイヴァ・チャウ(Ava Chow)が2017年に提出したBIP-174によって標準となった。
BIP-174が自ら挙げる動機は二つある。第一に、未完成の取引を複数の署名者に回す方法が当時は実装ごとにばらばらで、異なるウォレットを使う人同士が協調できなかった。第二の方がより重い。取引に署名するには支払おうとしているUTXOの中身を知る必要があるが、ハードウェアウォレットやエアギャップ署名機はブロックチェーンを見ることができない。BIP-174はこの課題をこう定めている。オフライン署名者がUTXO集合に直接アクセスすることなく、しかも欺かれる危険なしに署名できるようにすること。
後半が核心である。オフライン機器に「これに署名しろ」と渡すことは易しい。難しいのは、その機器が自分は何に署名しているのかを自力で検証できるようにすることだ。
ファイルの中身
PSBTは人が読む形式ではなくバイト構造である。5バイトのマジックで始まる。
0x70 0x73 0x62 0x74 0xFF
p s b t 区切り
先頭4バイトはpsbtのASCIIである。末尾の0xFFは意図的な安全装置だ。通常の取引パーサがこのファイルを読めば必ず失敗し、PSBTパーサもまた通常の取引をPSBTと誤読しない。二つの形式が静かに混ざらないよう、ヘッダの段階で分けてある。
その後ろにはキー・値マップが三層続く。
<マジック> <グローバルマップ> <入力マップ>* <出力マップ>*
グローバルマップには取引全体に関わる情報が、入力マップと出力マップには各入力・各出力に属する情報が入る。各マップは0x00バイトで終わる。長さ0の鍵は存在しえないので、このバイトに出会えばマップが終わったという意味に一意に定まり、パーサの実装が単純になる。
値はタイプ番号で区別される。実際に目にするものをいくつか見れば構造がつかめる。
| フィールド | 何を持つか |
|---|---|
PSBT_GLOBAL_UNSIGNED_TX | 署名がすべて空の取引本体 (v0) |
PSBT_IN_NON_WITNESS_UTXO | この入力が参照する以前の取引の全体 |
PSBT_IN_WITNESS_UTXO | この入力が参照する出力ひとつだけ |
PSBT_IN_PARTIAL_SIG | 署名者一人が付けた署名 |
PSBT_IN_BIP32_DERIVATION | この鍵がどのウォレットの何番目の鍵か |
PSBT_IN_WITNESS_SCRIPT | マルチシグなどのスクリプト本体 |
PSBT_OUT_BIP32_DERIVATION | その出力が自分のお釣りだと判定する根拠 |
PSBT_IN_FINAL_SCRIPTWITNESS | 完成した証人データ |
PSBT_IN_NON_WITNESS_UTXOとPSBT_IN_WITNESS_UTXOの違いは、一見すると単なる容量の問題に見える。後で見るように、この選択ひとつに実在の盗難経路がかかっている。
六つの役割
BIP-174の本当の設計は形式ではなく役割の分離にある。仕様はPSBTを扱う主体を六つに分ける。一つのプログラムが複数の役割を兼ねてよいが、各役割にできることは厳しく限定される。
作成者(Creator)は空のPSBTを作る。署名の空の取引を入れ、入力と出力の場所を空けておく。
更新者(Updater)は自分の知る情報を埋める。各入力が参照するUTXO、リディームスクリプト、ウィットネススクリプト、BIP32の導出パス。オンラインにある監視専用ウォレットが通常この役を担う。
署名者(Signer)は署名だけを付ける。仕様が署名者に課す制約こそがこの設計の中心である。署名者はPSBTの中に含まれるUTXO情報のみを用いなければならず、他のデータ源を必要としてはならず、PSBTにはデータを追加することしかできない。署名できなければ何も付けずにそのまま返す。
結合者(Combiner)は複数のPSBTを一つにまとめる。驚くべきことに、結合者はスクリプトを理解する必要がまったくない。キー・値の対を統合し重複を除くだけである。
入力確定者(Input Finalizer)は集まった部分署名が検証を通すのに十分かを判断し、十分なら最終的なscriptSigとscriptWitnessを組み立てる。その際、材料として使われた中間データは消す。
抽出者(Transaction Extractor)は確定したPSBTから、そのままネットワークに送出できる取引を取り出す。
役割をここまで細かく割った理由ははっきりしている。最も危険なもの、すなわち秘密鍵を握る主体が行うことを最小化するためだ。署名者は取引を作らず、直さず、完成させもしない。ただ自分が検証したものに署名を一つ加えるだけである。
エアギャップウォレットで実際に起きていること
コールドストレージの運用は、これらの役割が順に実行される過程そのものである。
- オンライン計算機の監視専用ウォレットが取引を作る。このウォレットは公開鍵しか知らず秘密鍵を持たないので署名できない。作成者かつ更新者の役である。
- 未署名のPSBTをmicroSDカードにファイルとして書き出すか、QRコードとして画面に表示する。バイナリファイルは
.psbt拡張子を使い、QRで運ぶ際はBase64文字列に変換する。 - エアギャップ機器がそれを読み、送り先のアドレスと金額を画面に出す。利用者が確認すれば署名を付ける。USBも無線も使わない。
- 署名済みのPSBTが同じ経路でオンライン計算機に戻る。
- オンライン計算機が確定・抽出してブロードキャストする。
ハードウェアウォレットのうち、ColdcardがSDカードを、SeedSignerやJadeがQRを使うのは、この3番の段階をどの物理媒体で渡るかの違いにすぎない。行き来する中身は同じPSBTである。
マルチシグではここに結合の段階が一つ加わる。同じPSBTを三人にそれぞれ送り、各自が自分の署名だけを付けて返せば、結合者がそれらを一つにまとめる。仕様はこの性質を明示的に保証している。AとBが各自独立に処理した結果を結合したものは、Aを経てからBを経たものと機能的に等価である。署名の順序が結果を変えないため、三人が同時に、別々の都市で、別々のウォレットソフトで署名してよい。
署名機器が必ず確かめるべきこと
機器が画面に何かを出し、利用者が承認を押したからといって安全になるわけではない。機器が渡されたデータを検証しなければ、画面に表示された内容そのものが偽りでありうる。そこでBIP-174は署名者が確認すべき項目を明記している。
- 以前の取引の全体が提供されているなら、そのハッシュが入力の指す
prevoutのハッシュと一致しなければならない。 - ウィットネスUTXOのみが提供されているなら、非ウィットネス署名を作ってはならない。
- リディームスクリプトが提供されているなら、
scriptPubKeyがそれに対応しなければならない。 - ウィットネススクリプトが提供されているなら、
scriptPubKeyまたはリディームスクリプトがそれに対応しなければならない。 - サイハッシュ型が指定されているなら、受け入れ可能かを確認し、そうでなければ失敗しなければならない。
お釣りの判定も同じ問題に属する。2-of-3の金庫から1 BTCを出して0.1を送れば、0.9は自分のお釣りとして戻ってくる。ところが署名機器の立場からは、その0.9の出力が本当に自分のものなのか、それとも攻撃者のアドレスにすり替えられたものなのか、アドレスを見ただけでは判別できない。判別の根拠がPSBT_OUT_BIP32_DERIVATIONフィールドである。機器はその導出パスをたどって自ら鍵を導き、自分が作りうる鍵に行き着けば、それが自分のお釣りだと確認する。このフィールドの欠けたPSBTに何気なく署名すれば、機器はお釣りを他人のものと取り違えたまま、利用者に問い返すことすらしない。
オフライン機器に手数料を偽る攻撃
PSBTを理解するうえで最も実務的な箇所である。
SegWit入力の署名に使うBIP-143の方式は、署名対象の中に自分の入力の金額を含める。おかげで署名機器は以前の取引の全体を受け取らず、支払おうとしている出力ひとつだけを受け取れば署名できる。PSBT_IN_WITNESS_UTXOが存在する理由であり、データがはるかに小さいのでQRで渡すにも都合がよい。
問題は、署名が自分の入力の金額しかコミットしていない点にある。他の入力の金額は署名に縛られていない。入力が複数ある取引では、攻撃者が他の入力の金額を実際より小さく記して渡すことができ、機器にはそれを検証する手立てがない。機器が計算して画面に出す手数料は、したがって誤った値になる。
同じ取引に利用者を複数回署名させられれば、攻撃は完成する。毎回異なる入力について改竄した金額を入れて署名を取れば、出来上がった取引における入力総額と出力総額の差、すなわち採掘者へ渡る手数料が、利用者の見たことのない額に膨れ上がる。コインは攻撃者には渡らないが、利用者からは消える。
そのため多くのウォレットは、SegWit入力であってもPSBT_IN_NON_WITNESS_UTXO、つまり以前の取引の全体を要求する。全体を受け取れば機器がそのハッシュを自ら計算して入力の指す値と照合でき、金額を改竄できなくなる。BIP-174も、このデータのないSegWit入力への署名を署名者が拒んでよいと明記している。ハードウェアウォレットに大きな取引を署名させるとき妙に時間がかかるのは、たいていこれである。参照された以前の取引を丸ごと受け取ってハッシュしているのだ。
タップルートはこの問題を迂回ではなく根本で終わらせた。BIP-341の署名メッセージは、自分の入力だけでなくすべての入力の金額とscriptPubKeyにコミットする。BIP-341の原文は、そう設計した理由を脚注に一行で残している。オフライン署名機器に取引の手数料を偽る可能性を取り除くためである。タップルート入力に署名するとき、機器は以前の取引を丸ごと受け取らずとも手数料を正確に知ることができる。
v2が解いた問題: 後から入力を足す
BIP-174のv0には構造的な限界が一つある。グローバルマップに置かれた未署名の取引が、入力と出力の一覧を丸ごと固定してしまう点だ。一度作られたPSBTに新しい入力を加えることはできない。
一人で使う分には支障がないが、協調取引では即座に行き詰まる。複数人が各自の入力を持ち寄って一つの取引を作るコインジョインがそうであり、受取人が自分の入力をそっと紛れ込ませて取引グラフ分析を無効化するPayJoinがそうである。
BIP-370が定めたPSBT v2は、固定された取引本体をなくし、入力と出力を個別のフィールドに分解した。PSBT_IN_PREVIOUS_TXID、PSBT_IN_OUTPUT_INDEX、PSBT_OUT_AMOUNT、PSBT_OUT_SCRIPTといったものである。副次的な効果として、ある入力に関する情報が二箇所に散らずその入力のマップの中にすべて集まるようになった。
v2は役割も一つ増やした。構成者(Constructor)である。構成者は作成者が作った空のPSBTに入力と出力を追加する。ただし好きなときに追加できるわけではなく、まずPSBT_GLOBAL_TX_MODIFIABLEフラグを確認しなければならない。このフラグは署名が付く時点で署名者自身が更新する。誰かがSIGHASH_ANYONECANPAYでない署名を付けたなら入力追加可のフラグが降り、SIGHASH_NONEでない署名を付けたなら出力追加可のフラグが降りる。既にある署名を無効にする変更が、そもそも試みられないよう形式自体が塞ぐ構造である。
一方BIP-371は、タップルート用のフィールドをv0とv2の双方に追加する。PSBT_IN_TAP_KEY_SIG、PSBT_IN_TAP_SCRIPT_SIG、PSBT_IN_TAP_LEAF_SCRIPT、PSBT_IN_TAP_INTERNAL_KEY、PSBT_OUT_TAP_TREEなどである。シュノア署名とスクリプトツリーは既存のフィールドでは表現できず、別に定義する必要があった。
自分で試す
ビットコインコアには各役割に対応するRPCがそのまま備わっている。signetやtestnetなら実際のコインを危険に晒さず確認できる。
# 作成者 + 更新者: 資金の充填されたPSBTを作る
bitcoin-cli walletcreatefundedpsbt '[]' '[{"アドレス":0.001}]'
# 中身を人が読める形で見る
bitcoin-cli decodepsbt "cHNidP8B..."
# いま何が欠けていて次にどの役割が要るかを教える
bitcoin-cli analyzepsbt "cHNidP8B..."
# 署名者: 署名を付ける
bitcoin-cli walletprocesspsbt "cHNidP8B..."
# 結合者: 複数の署名者から戻ったものをまとめる
bitcoin-cli combinepsbt '["cHNidP8B...","cHNidP8B..."]'
# 確定者 + 抽出者: ネットワーク用の取引を取り出す
bitcoin-cli finalizepsbt "cHNidP8B..."
Base64でエンコードされたPSBTがいつもcHNidP8で始まるのは偶然ではない。先に見た5バイトのマジック70 73 62 74 ffをBase64に移した結果である。文字列を見ればそれがPSBTかどうかを目で判別できる。
analyzepsbtはとりわけ役に立つ。いまこのPSBTに何が欠けているか、次に必要な役割は何かを教えてくれるので、複数の機器を行き来して流れを見失ったときの基準点になる。
まとめ
PSBTは華やかな機能ではなく配管に近い。しかしこの配管がなければ、秘密鍵をインターネットから完全に切り離す運用も、複数の人が別々のウォレットで一つの金庫を守る構成も成り立たない。ハードウェアウォレットとマルチシグが実際に回っているのは、この形式がその下に敷かれているからである。
そしてこの形式は、データを運ぶ器にとどまらない。署名機器が何を検証すべきかを仕様が規定し、検証に要る材料をどのフィールドに載せるかまで定めている。手数料を偽る攻撃の存在と、それを防ぐ方法が仕様の脚注に残っているのもそのためだ。署名とは承認ボタンを押す行為ではなく、機器が自ら確かめた事実に印を押す行為でなければならない。PSBTはその確認を可能にする形式である。