SeedSignerSeedSigner は何を信頼して動いているのかSeedSignerについて紹介したが、使っていながらも正直PSBTについてあんまり理解していない。
我々は何をエアギャップで送信しているのだろうとふと思った。
最近自前でDIYSignerも作ってみようと思っているので、なおさら理解しないといけない。
なので、PSBTを理解するために手を動かしたりまとめてみた。
PSBT とは
PSBT (Partially Signed Bitcoin Transaction) は BIP174 で定められた、署名が揃っていない取引をやりとりするためのバイナリ形式。
2017 年に提案され、Bitcoin Core には 0.17 で RPC として入った。
BIP174: Partially Signed Bitcoin Transaction Format署名者が署名を作るのに必要な情報を、取引と一緒に運ぶ形式。Status: Deployedgithub.comBIP174 の Abstract では、PSBT を「署名に必要な情報を含み、すべての署名が揃うまで部分署名を保持する形式」と説明している。
必要な情報が取引と一緒に入っているため、署名者はオフラインのまま署名できる。
BIP174 の Motivation では、主に 2 つの問題を挙げている。
- 未署名の取引を複数の署名者に渡す方法がウォレットごとに異なり、別のソフト間で受け渡せなかったこと
- 署名に使う UTXO の情報が必要なのに、エアギャップ端末やハードウェアウォレットは UTXO セットを直接参照できないこと
エアギャップなSignerで送金処理を整理すると以下のような流れになり、
PSBT形式のデータをアニメーションQRにすればやりとりができるということだろう。
確かに、ウォレットや署名端末の互換性は、このPSBT形式のQRでやりとりできるかというところだけ考えればよいことになる。
ウォレットが画面に出した未署名の PSBT を署名端末のカメラで読み、署名を足した PSBT を今度は署名端末の画面からウォレットのカメラで読み返す。QR はどちらも実データで、署名だけダミー
役割ごとの PSBT の利用方法
PSBTは上の図のように、送る内容が異なる。
BIP174 は、PSBT を扱う処理を役割 (Roles) に分けて定義している。
1 つのソフトが複数の役割を兼ねてもよく、実際そのような実装が多い。
なんとなく、整理してみた表が以下。
| 役割 |
図での担当 |
やること |
| Creator |
ウォレット |
未署名の取引を作って PSBT に入れる。input / output の map は空のまま |
| Updater |
ウォレット |
使う UTXO、script、BIP32 の導出パスなど、知っている情報を足す |
| Signer |
署名端末 |
中身を確かめ、自分の鍵で署名できる input に部分署名を足す |
| Combiner |
ウォレット (複数署名時) |
同じ取引の PSBT を複数受け取り、1 つにまとめる |
| Input Finalizer |
ウォレット |
署名が揃った input の scriptSig / scriptWitness を組み立てる |
| Transaction Extractor |
ウォレット |
全 input が揃った PSBT から、ネットワークに流せる取引を取り出す |
Bitcoin Core の doc/psbt.md では、RPC ごとの役割が整理されている。たとえば walletcreatefundedpsbt は Creator と Updater、walletprocesspsbt は Updater・Signer・Finalizer、finalizepsbt は Finalizer と Extractor を担う。
こういうユースケース的なものもRPCで整理されてるんだぁと関心した。
Bitcoin Core doc/psbt.mdPSBT Howto for Bitcoin Core。役割ごとの流れと対応する RPCgithub.com図の流れでは、署名端末が署名を加え、ウォレット側で取引を完成させてブロードキャストする。
Signer は情報を追加できるが、受け取った PSBT から何かを削除してはいけない。
ウォレットと署名端末の間では、PSBT を UR 形式のテキストに変換し、QR で受け渡す。
送金情報が UR になるまで
PSBT はバイナリ形式だ。でもバイナリというよりQRを扱っているというほうが我々の直感的にはあっていると思う。
バイナリデータを QR で運ぶために定めた形式がUR (Uniform Resources) というそう。
Blockchain Commons が定めた。
BIP ではないが、多くのエアギャップ端末やウォレットが採用している。
BCR-2020-005: Uniform Resourcesバイナリを型付きの URI 風テキストにし、分割して複数の QR で運ぶ形式github.com送金内容が PSBT になり、UR を経て QR で受け渡されるまでを、送信側から順に見ていく。
送金画面の入力とウォレットが足す UTXO・導出パスが PSBT になり、CBOR で包まれ、4 つに分けた part が Bytewords のテキストになって QR に入る。4 つの fragment を順に強調し、それぞれから作られる part・Bytewords・UR・QR を切り替えて見せる。値はすべてデモと同じもの
送信側では、次の順にデータを組み立てる。
- ウォレットに送金内容を入力する。 送金先・金額・手数料を指定する。使うコインと署名に使う鍵の情報は、ウォレットが選ぶ。
- ウォレットが PSBT を作る。 Creator が送金内容を反映した未署名の取引を作って global map に入れ、Updater が使うコインの情報 (witness UTXO) や鍵の導出パスを input map に加える。
- PSBT をバイト列にする。 この例の PSBT は 187 byte。UR にする前のデータはバイナリだ。
- CBOR で包む。 PSBT のバイト列を CBOR の byte string に入れ、全体の CRC32 を計算する。先頭の
58 bb は、長さ 187 byte の byte string を表す。
- QR に収まる大きさに分ける。 この例では 48 byte ずつに分ける。各断片にフレーム番号・総数・元データの長さ・全体の checksum を加えて CBOR 配列にしたものが
part だ。
- Bytewords でテキストにする。
part の各 byte を 2 文字の英字に置き換え、part 自身の CRC32 を末尾に加える。英大文字と数字、/:- で表せるため、QR の英数字モードに収まる。
- UR 文字列を QR にする。
UR:CRYPTO-PSBT/1-4/ は、型が crypto-psbt で、4 枚中の 1 枚目であることを示す。
PSBT をバイト列で見る
送られるイメージはわたったけど、実際にバイナリにそんな内容入ってんの?と思ったので
バイナリの対応もみてみる。
ここからは、復元した PSBT のバイト列そのものを読む。
この記事で扱う BIP174 version 0 は、unsigned transaction を global map に持つ。
※BIP370 version 2 は unsigned transaction を持たず、後から input や output を追加できるよう、取引の要素をそれぞれの map に分けて持つ。
BIP370: PSBT Version 2PSBT_GLOBAL_UNSIGNED_TX を除き、取引の要素を各 map に分けて持つ版github.com16 byte ごとに左から並べている。
PSBT は magic の後に key 長 → key → value 長 → value の組を並べた map が続き、key 長が 00 のところで map が終わる。
長さは CompactSize で表すが、この例では全部 253 未満なので 1 byte で済んでいる。
global map の中の unsigned transaction
PSBT v0 では、global map の key 00 の value が未署名の transaction になる。
このバイト列の内側は、transaction のルールでさらに分解される。
| フィールド |
バイト列 |
意味 |
| version |
02 00 00 00 |
取引形式の version (little-endian) |
| input count |
01 |
入力は 1 つ |
| prevout |
00 × 32 · 00 00 00 00 |
参照する過去取引の ID (ダミー) と、その中の出力番号 0 |
| scriptSig length |
00 |
未署名なので空 |
| sequence |
ff ff ff ff |
入力の sequence 値 |
| output count |
01 |
出力は 1 つ |
| amount |
00 e1 f5 05 00 00 00 00 |
100,000,000 sat (1 BTC)、little-endian |
| scriptPubKey |
16 · 00 14 · 11 × 20 |
長さ 22 byte。P2WPKH の形をしたダミーの script |
| locktime |
00 00 00 00 |
locktime なし |
送金先と金額は unsigned transaction に記録されている。
署名端末が画面に出す「誰にいくら送るか」は、この transaction から読み取ったものになる。
input map と output map の情報
input map には、transaction 自体には含まれない署名用の情報が入る。
この例では 2 つの key を持たせている。
key type 01 witness UTXO の value は、金額 8 byte + script 長 + script。
この例では 1 BTC と P2WPKH 形式の script で、unsigned transaction の prevout が指す出力の中身を署名計算のために渡している。
key type 06 BIP32 derivation は、key に公開鍵、value に 4 byte の fingerprint と導出パスが入る。
例では m/84'/0'/0'/0/7。
秘密鍵ではなく、端末が署名に使う鍵を探すための導出経路だ。
パーサは、fingerprint が一致する導出情報を候補にし、P2WPKH か特定の Taproot 鍵パスとして扱えるかを確かめる。
PSBT v0 では、global map の unsigned transaction に記録された入力数・出力数に応じて、input map と output map が続く。
この例の output map は空で、終端の 00 だけだ。出力の金額と scriptPubKey は unsigned transaction に入っているため、output map に追加情報がなくても送金内容を読める。
署名端末は逆にたどって署名する
署名端末は QR を受け取り、次の順に PSBT を復元する。
- QR から UR 文字列を読み取り、Bytewords を byte に戻して、各
part の CRC32 を確かめる。
- フレーム番号を使って断片をそろえ、全体の checksum を確かめてから CBOR を外す。
- 元の PSBT のバイト列を取り出し、送金内容、使うコインの情報、鍵の導出パスを読む。QR を読み取る順番は問わない。
署名端末は QR から PSBT を戻して読み、画面で確かめたうえで、sighash とシードフレーズから導出した鍵で ECDSA の署名を作り、input map に足して QR で返す。署名だけダミー
署名端末は、次の順に確認して署名する。
- 送金内容を確認する。 送金先・金額・手数料を画面に表示し、利用者が署名前に確認する。手数料は PSBT に含まれないため、使うコインの金額の合計から出力の合計を引いて端末が計算する。
- 署名対象の sighash を計算する。 SegWit v0 では BIP143 の手順で unsigned transaction と使うコインの金額・script をまとめてハッシュする。署名はこの 32 byte のハッシュに対して作る。PSBT の金額を偽っても、その金額を前提にした署名はネットワークで受理されない。
- 秘密鍵を導出する。 鍵は PSBT からは得られない。端末に入力したシードフレーズから BIP39 で seed を作り、PSBT に書かれた導出パス (
m/84'/0'/0'/0/7) を BIP32 でたどって秘密鍵を導出する。master fingerprint が自分のものか、導出した公開鍵が使うコインの script と一致するかも確認する。
- 署名を作り、PSBT に加える。 sighash と秘密鍵を使い、secp256k1 の ECDSA で署名する。署名を検証してから、input map に部分署名 (
PSBT_IN_PARTIAL_SIG、key は 02 + 公開鍵) として加える。この例では PSBT が 187 byte から 294 byte になり、7 枚の QR に分けてウォレットへ返る。
自前の署名モジュールで確認する
最近DIY Signerとして自分で署名モジュール jitsu-inを開発している。
habakan/jitsu-ingithub.comjitsu-inはRaspberry Pi Pico 2などのマイコンで使えるようにしたことがメインだが、WebAssemblyベースなので、wasmランタイムがあればどこでも動く。
下のデモは parser.wasm で、QR から復元した PSBT を読み取る。アニメーション QR の受信状況、復元した PSBT の hex、署名端末が使う plan を確認できる。
本当に動いているので実際のカメラで 実際のunsigned psbtのURを読むこともできる。
この parser.wasm が読めるのは PSBT v0 で、サイズは 32,768 byte、input と output は各 16 まで。UR の分割は最大 1,024 枚まで扱える。PSBT v2 はまだ未対応だ。
4 枚目の QR を受け取ると 187 byte の PSBT が復元され、parser.wasm が plan を作る。plan には、送金内容や入力金額、鍵の導出パスなど、これまで見てきた情報が並ぶ。