メインコンテンツまでスキップ

Bitcoin(PSBT)

ur:crypto-psbtur:psbtBBQrPSBT v0 / v2

Bitcoin のリクエストは PSBT です。ウォレットが PSBT を作成し、BitTool Signer が署名を追加して PSBT を返し、ウォレットがファイナライズしてブロードキャストします。

トランスポート​

リクエストレスポンス備考
ur:crypto-psbtur:crypto-psbt推奨。 UR 本体は PSBT を格納した CBOR バイト文字列です。
ur:psbtur:psbtレジストリの新しい名前で、内容は同じです。
BBQr(B$…、エンコーディング H、2、Z)BBQr(2 または H)Coinkite のフォーマットです。Z(deflate)も受け付けます。レスポンスは 2(Base32)を使い、リクエストが H の場合は H を使います。
1 つの QR コードに収めた Base64(cHNidP…)または 16 進(70736274ff…)のテキスト1,800 文字以下なら同じ形式のテキスト、それを超える場合はアニメーション ur:crypto-psbt小さなトランザクション向けです。
.psbt ファイル(Base64 または 16 進、改行を含んでも可)同じ署名デバイス側の iPhone で、ファイル App や AirDrop 経由でやり取りします。

レスポンスは(注記した場合を除き)常にリクエストと同じフォーマットを使うため、既存のデコーダーをそのまま再利用できます。

PSBT に含めるべき内容​

要件詳細
バージョンPSBT v0 または v2(BIP 370)。最も安全なのは v0 です。
サイズインプット 1〜100 個、アウトプット 1〜100 個。
導出情報署名デバイスが署名すべきすべてのインプットに、その鍵の bip32Derivation(マスターフィンガープリント + フルパス)を含めます。Taproot のインプットは tapBip32Derivation と tapInternalKey を使います。
前のトランザクションレガシーと SegWit v0 のインプット(P2PKH、P2WPKH、P2SH-P2WPKH、P2WSH など)には、前のトランザクション全体を nonWitnessUtxo に含める必要があります。witnessUtxo だけで受け付けるのは、すべての インプットが Taproot の場合に限られます。
SighashSIGHASH_ALL、または Taproot の場合は SIGHASH_DEFAULT。それ以外は拒否されます。
おつりおつり出力にも bip32Derivation を付けてください。BitTool Signer は、自身の鍵からスクリプトを再導出して確認できた場合にのみ、そのアウトプットに「おつり」のラベルを付けます。
アウトプット標準スクリプトと、83 バイトまでの OP_RETURN。
ネットワークメインネットのみ。
nonWitnessUtxo が必須である理由

SegWit v0 の署名は、消費するインプットの金額にはコミットしますが、他のインプットの金額にはコミットしません。悪意あるコーディネーターは、2 回の署名ラウンドにわたってインプットの金額を偽り、署名者をだまして巨額の手数料を支払わせることができます(2020 年に公表された SegWit 手数料攻撃)。前のトランザクション全体があれば、署名デバイスはすべての金額を自ら確認できます。Taproot の署名はすべてのインプットの金額にコミットするため、すべてが Taproot のトランザクションは例外となります。

対応スクリプト​

スクリプト署名
P2PKH、P2WPKH、P2SH-P2WPKH対応(単一鍵)
P2TR キーパス(BIP86)対応
マルチシグ P2SH、P2WSH、P2SH-P2WSH(multi/sortedmulti)対応。ローカルの鍵ごとに 1 つの部分署名
P2TR スクリプトパス、CHECKSIG/CHECKSIGVERIFY および CHECKSIGADD のしきい値リーフ(リーフバージョン 0xc0)対応
MuSig2 およびその他のスクリプト非対応

レスポンス​

レスポンスは、送信した PSBT に署名を追加したものです。ECDSA のインプットには partialSig、Taproot には tapKeySig または tapScriptSig が追加されます。ファイナライズは行われず、それ以外は何も変更されません。ファイナライズする前に、次のことを行ってください。

  1. 未署名トランザクションが、送信したものとバイト単位で同一であることを確認します(PSBT のトランスポートにはリクエスト ID がありません)。
  2. 新しく追加された各署名を、対応するインプットに対して検証します。
  3. マルチシグの場合は、他の共同署名者の PSBT と結合します。
  4. ファイナライズし、トランザクションを取り出してブロードキャストします。
1.0.0 の既知の問題
  • おつりのあるバーン(R3-01、低、iPhone のみ)。 金額がゼロでない OP_RETURN 出力でコインをバーンし、おつりも戻るトランザクションでは、BitTool Signer の結果画面とアクティビティの 1 行要約が「Every output returns to your wallet」(すべての出力がウォレットに戻ります)になります。署名画面そのものはバーン、その金額、高リスクを正しく表示し、PSBT にも影響はありません。Android 版 BitTool Signer 1.0.0 では、要約にもバーンが表示されます。
  • どのインデックスでもおつりと見なす(BTS-08、中、両アプリ)。 出力のスクリプトがウォレット自身のいずれかのシングルシグ構成と一致すれば、どのインデックスでもおつりと見なされます。閲覧専用ウォレットが決してスキャンしないインデックスも含まれます。おつりは通常のインデックスに置き、全額をユーザーのウォレットの不自然なパスに移すリクエストには注意してください。

セキュリティレビューを参照してください。

コード​

bitcoinjs-lib で PSBT を作成し、Keystone の @keystonehq/bc-ur-registry で ur:crypto-psbt に包みます。

examples/bitcoin.mjs
import * as bitcoin from 'bitcoinjs-lib';
import { CryptoPSBT } from '@keystonehq/bc-ur-registry';
import { URDecoder } from '@ngraveio/bc-ur';
import { secp256k1 } from '@noble/curves/secp256k1.js';

// utxos: [{ txid, vout, value, prevTxHex, pubkey, path }] path like "m/84'/0'/0'/0/3"
export function buildPsbt({ utxos, to, amount, change, masterFingerprint }) {
const fingerprint = Buffer.from(masterFingerprint, 'hex');
const psbt = new bitcoin.Psbt(); // mainnet
for (const u of utxos) {
const prevTx = bitcoin.Transaction.fromHex(u.prevTxHex);
psbt.addInput({
hash: u.txid,
index: u.vout,
nonWitnessUtxo: Buffer.from(u.prevTxHex, 'hex'), // required for SegWit v0 and legacy inputs
witnessUtxo: { script: prevTx.outs[u.vout].script, value: u.value },
bip32Derivation: [{ masterFingerprint: fingerprint, path: u.path, pubkey: u.pubkey }],
});
}
psbt.addOutput({ address: to, value: amount });
psbt.addOutput({ // change: give the path so the signer can recognize it
address: change.address, value: change.value,
bip32Derivation: [{ masterFingerprint: fingerprint, path: change.path, pubkey: change.pubkey }],
});
const encoder = new CryptoPSBT(psbt.toBuffer()).toUREncoder(400);
const frames = [];
for (let i = 0; i < encoder.fragmentsLength; i++) frames.push(encoder.nextPart().toUpperCase());
return { psbt, frames };
}

署名済み PSBT を読み込み、確認してファイナライズします。

examples/bitcoin.mjs
const validator = (pubkey, msghash, signature) => secp256k1.verify(signature, msghash, pubkey, { prehash: false });

export function finalizeSignedPsbt(frames, original) {
const decoder = new URDecoder();
for (const frame of frames) decoder.receivePart(frame);
if (!decoder.isSuccess()) throw new Error('Incomplete or invalid QR');
const ur = decoder.resultUR();
if (ur.type !== 'crypto-psbt') throw new Error(`Expected crypto-psbt, got ${ur.type}`);

const signed = bitcoin.Psbt.fromBuffer(CryptoPSBT.fromCBOR(ur.cbor).getPSBT());
// The signer only adds signatures: the transaction itself must be unchanged.
if (!signed.data.globalMap.unsignedTx.toBuffer().equals(original.data.globalMap.unsignedTx.toBuffer()))
throw new Error('The signed PSBT is for a different transaction');
if (!signed.validateSignaturesOfAllInputs(validator)) throw new Error('Invalid signature');

signed.finalizeAllInputs();
return signed.extractTransaction().toHex(); // ready to broadcast
}

テストベクター​

m/84'/0'/0'/0/0 にある 100,000 sats のインプットから、60,000 sats を bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 に送り、39,000 sats をおつりとして m/84'/0'/0'/1/0 に、手数料は 1,000 sats です。前のトランザクションは合成したものです。

リクエスト(ウォレット → BitTool Signer)

crypto-psbt3 フレーム
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
1 / 3
未署名 PSBT(Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
QR テキストと生の CBOR を表示
QR テキスト · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
UR:CRYPTO-PSBT/2-3/LPAOAXCFADJLCYRFDSIHPRHDKGAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAMGWBSRYCW
UR:CRYPTO-PSBT/3-3/LPAXAXCFADJLCYRFDSIHPRHDKGAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEGWCEHDPR
UR CBOR(16 進)
59016c70736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

レスポンス(BitTool Signer → ウォレット)

返される署名済み PSBT(Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iICAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88SDBFAiEAnxTBvsytWOKIqCL+jVnOZoU8wKKB3sg9FqBU6YKagoUCIC4lC/U2vuJKimL4vEacD8f6CnzzwQjDbBZOBoNWSJMgASIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
QR テキストと生の CBOR を表示
QR テキスト · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADUYCYVWCKDTWYHDNEHKADTPJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADCLBBLDOY
UR:CRYPTO-PSBT/2-3/LPAOAXCFADUYCYVWCKDTWYHDNEADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAOAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNFDDYFEAOCLAENEBBSERNSFPMHDVOLOPDCPZELGHKTOIYLPFNRTOELYUESPFSCMNBGHWLLFNYLFLPAONLJTAAPK
UR:CRYPTO-PSBT/3-3/LPAXAXCFADUYCYVWCKDTWYHDNECXDMDABDYKENRNVOGELEIDYARFFGNSBSSTZSBKKEWFSEAYSRJZCMGLAMLSHFFDMUCXADCPAMAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEHHDLWYGS
UR CBOR(16 進)
5901d870736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222020330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c4830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e0683564893200122060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

ウォレット側で組み立てた結果

finalTxHex
02000000000101e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c024830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e06835648932001210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000
txid
6f492df497b5e455fb3305d0272fe9c6dc3d8cc9d0054be3aaefee77daca82b7

マルチシグ​

  1. 各共同署名者の鍵をインポートします。BitTool Signer は BIP48 の共同署名者鍵を コインレベルのページ のとおりにエクスポートします(P2WSH の場合は m/48'/0'/0'/2')。
  2. witnessScript(P2SH-P2WSH の場合は redeemScript も)と、各インプットで すべての 共同署名者の鍵に対する bip32Derivation エントリを含む PSBT を作成します。
  3. 各署名デバイスに順番に送るか、並行して送って結合します。BitTool Signer は自身が保持する鍵の署名だけを追加します。1 台の iPhone が別々のウォレットで 2 つの共同署名者鍵を保持している場合は、ウォレットごとにパスワードを求めます。
  4. 既存の署名は検証されたうえで保持されるため、PSBT はどの順番で回しても構いません。

BitTool Signer は署名を求められたスクリプトを確認しますが、そのポリシーがユーザーの意図したものかどうかまでは判断できません。マルチシグウォレットを作成する際に、ウォレット側でポリシー全体(しきい値、すべてのフィンガープリント)を表示してください。