bitbank techblog

BitcoinJS と PSBT の歴史

これは ビットバンク株式会社 Advent Calendar 2020 の 13 日目の記事です。

こんにちは、ビットバンク株式会社チーフビットコインオフィサーのジョナサン・アンダーウッドです。

本日は、私がメンテナーを勤めさせていただいている NodeJS ライブラリの BitcoinJS と PSBT という部分的に署名された取引のフォーマットについて書きたいと思っています。

背景

ビットコインを扱う上で欠かせないものは取引データである。 BitcoinJS ではそれを表すクラスとしてTransactionが存在し、一人で取引を作り、署名し、完成し、伝搬するのなら、Transactionのみで足りる。

ただし、実際に署名をするにあたっては各インプットが指している過去の取引のいくつかの情報が必要である。
よって、取引を作成する上で、その不足する部分をメモリ内に置き、適宜に活用したりする仕組みが必要になる。

BitcoinJS ではそのためにTransactionBuilderを開発した。
中にTransactionのオブジェクトを一個丸ごと内包すると同時に、主に各インプットについてはたくさんのメタデータを入れられるようにした。
例えば、過去の取引のアウトプットスクリプトや金額、公開鍵、署名なども格納している。

しかし、これにもいくつか制約や限界があった。
Transactionの中身をバイナリで表す形式がビットコインのプロトコルで定義されているが、TransactionBuilderは BitcoinJS が独自で考えた仕組みである。
例えば、C#のライブラリの NBitcoin にあるTransactionBuilderと名前が同じでも、微妙に内包しているデータの種類と形が異なる。
よって、ビットコイン関連技術の標準化を定める BIP を定義しない限り、TransactionBuilderを簡単に複数のアプリを通して渡し合うことが困難である。
なお、TransactionBuilderの外でインプットの金額の確認などする必要があるため、その API を活用した開発者の殆どが確認を飛ばして、確認を独自実装していなかったアプリにおいては大手採掘者にお金を盗まれる危険性があった。

例として挙げれば、マルチシグのやりとりが真っ先に思い浮かぶ。
複数人で署名しないと成立しない取引においては、部分的に署名された取引を送り合う必要があるが、ビットコインの取引のプロトコルは全ての署名が完了している前提で考案されているため、部分的な署名をその中に含める場合は標準化の外の実装となり、ライブラリによって大きな差が出た。
このせいで、coinbin で署名した取引を bitcoin core で署名しようとしてもエラーが出たりした。

PSBT の歴史

2016 年の後半に、Segwit の BIP9 による投票が始まり、コミュニティでの論争が活発になってきた頃、私はTransactionBuilderのシリアライゼーションの標準化について当時のメンテナーの Daniel Cousens 氏とたびたび議論を交わすようになった。
2017 年の頭あたりで話し合った中で出てきたものはTIF フォーマットである(PSBT によって廃止済み)。

このフォーマットの目的は「取引の署名を行う上で必要な要素を全て含めること」だった。
querystring などを活用したものだったが、core 開発者の何人かと話し合った結果、バイナリのシリアライゼーションが良いという結果になり、しばらく放置していた。

それから半年ほど経過したころ、Blockstream 社でインターンしていた Andrew Chow 氏が 8 月に bitcoin-dev ML にて Partially Signed Bitcoin Transactions (PSBT)(部分的署名ビットコイン取引)の仕組みを提案した。
そして、9 月 19 日に正式 BIP になった。 BIP174 である。

PSBT の仕組み

大きく分けると、PSBT には 3 つの領域が存在する。
Global、inputs、outputs である。

Global は特定のインプットやアウトプットにだけ関連しないデータを保有するための場所。
ここの0x00のキーの中で従来のビットコイン取引を格納する。
ただし、制約としては scriptSig も witness も空であること。
それらの含まる情報は inputs 領域に格納される。

inputs は複数あり、Global にある取引のインプットと同じ個数である必要がある。
中には P2SH のredeemScriptや P2WSH のwitnessScriptはもちろんのこと、重要なポイントとしてはインプットが指している取引の情報がを含めたwitnessUtxononWitnessUtxoも入っている。
witnessUtxoは UTXO のスクリプトと金額のみが入っており、nonWitnessUtxoはインプットが指している署名済み取引そのものが含まれている。
直近使う頻度が増えた HD ウォレットも考慮され BIP32Derivation というデータも入れることができる。
これによって、このインプットはどのパスの鍵で署名すべきかを探索することなく知ることができる。

outputs は inputs と似て複数あり、Global にある取引のアウトプットと同じ個数である必要がある。
主にアウトプットのスクリプトが自分のウォレットと関係あるかどうかを調べやすくするためにredeemScriptwitnessScriptBIP32Derivationなどを含めることができ、inputs と比べて限定的であることが特徴。
これで自らへのお釣りがどれかが分かる。

これを活用すれば、きちんとマルチシグのやり取りが別の開発者が作ったウォレット間で行うことができるし、バイナリデータのシリアライゼーションのため、ハードウェアウォレットに流し込んで署名してもらうことが非常に簡単である。

ただし、最近の話題として挙がったものとしてはwitnessUtxoのみで SegWit の署名さえしてれば大丈夫と思われたものが、巧妙な手口と一般的なユーザーの確認不足を悪用すれば、攻撃者が大手採掘者であれば、10BTC の UTXO を払っていると思い込ませ 100BTC を使わせ余った 90BTC を採掘手数料として受け取って盗むことができる脆弱性が見つかったため、Segwit であってもnonWitnessUtxoが推奨されるようになった。(とはいえ、攻撃のシナリオは限定的すぎて…危険性を感じる必要があるのは取引所レベル(数千 BTC を頻繁にコールドから出す程度)ぐらいだろうと思っている)

結論

Segwit の発明と同時期に考案され始めた取引のメタデータを格納するプロトコルは最終的に BIP174 の PSBT に落ち着いた。
これを BitcoinJS に実装することで、TransactionBuilder を廃止予定にし、PSBT を活用した安全な取引の署名を実現できた。

expand_less