跳到主要内容

Bitcoin(PSBT)

ur:crypto-psbtur:psbtBBQrPSBT v0 / v2

Bitcoin 请求就是 PSBT。由你的钱包构建 PSBT;BitTool Signer 添加签名后返回 PSBT;再由你的钱包执行 finalize 并广播。

传输方式​

请求响应说明
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。
单个二维码中的 Base64(cHNidP…)或十六进制(70736274ff…)文本不超过 1,800 个字符时返回相同形式的文本,否则返回动态 ur:crypto-psbt适用于体积较小的交易。
.psbt 文件(Base64 或十六进制,可以换行)相同通过签名手机上的“文件”App 或隔空投送(AirDrop)传递。

响应始终使用请求自身的格式(上表注明的情况除外),因此你可以复用已有的解码器。

PSBT 必须包含的内容​

要求详情
版本PSBT v0 或 v2(BIP 370)。v0 是最稳妥的选择。
大小1–100 个输入,1–100 个输出。
派生信息签名器需要签名的每个输入,都必须为其密钥携带 bip32Derivation(主指纹 + 完整路径)。Taproot 输入使用 tapBip32Derivation 和 tapInternalKey。
前序交易Legacy 和 SegWit v0 输入(P2PKH、P2WPKH、P2SH-P2WPKH、P2WSH …)需要在 nonWitnessUtxo 中提供完整的前序交易。只有当所有输入都是 Taproot 时,才接受仅提供 witnessUtxo。
SighashSIGHASH_ALL,Taproot 也可使用 SIGHASH_DEFAULT。其他类型一律拒绝。
找零找零输出也要带上 bip32Derivation。只有在 BitTool Signer 用自己的密钥重新推导出某个输出的脚本之后,才会将其标记为“找零”。
输出标准脚本,以及不超过 83 字节的 OP_RETURN。
网络仅限主网。
为什么必须提供 nonWitnessUtxo

SegWit v0 签名承诺了所花费输入的金额,但没有承诺其他输入的金额。恶意的协调方可以在两轮签名中谎报输入金额,诱使签名器支付巨额手续费(即 2020 年披露的 SegWit 手续费攻击)。有了完整的前序交易,签名器就能自行核对每一个金额。Taproot 签名会承诺所有输入的金额,因此全部由 Taproot 输入构成的交易可以豁免。

支持的脚本​

脚本是否签名
P2PKH、P2WPKH、P2SH-P2WPKH是(单密钥)
P2TR 密钥路径(BIP86)是
多签 P2SH、P2WSH、P2SH-P2WSH(multi/sortedmulti)是,每个本地密钥一个部分签名
P2TR 脚本路径,CHECKSIG/CHECKSIGVERIFY 和 CHECKSIGADD 门限叶子(叶子版本 0xc0)是
MuSig2 及其他脚本否

响应​

响应就是你的 PSBT 加上签名:ECDSA 输入使用 partialSig,Taproot 输入使用 tapKeySig 或 tapScriptSig。签名器不会执行 finalize,其他内容也都保持不变。在 finalize 之前:

  1. 检查未签名交易与你发送的逐字节一致(PSBT 传输方式中没有请求 ID)。
  2. 针对对应的输入验证每个新增签名。
  3. 如果是多签,与其他共同签名方的 PSBT 合并。
  4. 执行 finalize、提取交易并广播。
1.0.0 中的已知问题
  • 带找零的销毁(R3-01,低,仅 iPhone)。对于包含金额非零的 OP_RETURN 输出、同时又有找零的销毁交易,BitTool Signer 结果页面和活动记录中的一行摘要显示为“Every output returns to your wallet”(所有输出都回到你的钱包)。签名界面本身会正确显示销毁、其金额以及高风险,PSBT 也不受影响。BitTool Signer Android 1.0.0 会在摘要中同样显示销毁。
  • 任意索引的找零(BTS-08,中,两款 App)。只要输出的脚本与钱包自身任一单签配置在任意索引上相符,就会被视为找零,包括观察钱包永远不会扫描到的索引。请把找零放在常规索引上;如果某个请求把全部资金转到用户钱包中一个不寻常的路径,请保持警惕。

参见安全评审。

代码​

使用 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,检查后执行 finalize:

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
}

测试向量​

输入 100,000 sats,位于 m/84'/0'/0'/0/0;向 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 发送 60,000 sats,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==
显示二维码文本和原始 CBOR
二维码文本 · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
UR:CRYPTO-PSBT/2-3/LPAOAXCFADJLCYRFDSIHPRHDKGAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAMGWBSRYCW
UR:CRYPTO-PSBT/3-3/LPAXAXCFADJLCYRFDSIHPRHDKGAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEGWCEHDPR
UR CBOR(十六进制)
59016c70736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

响应(BitTool Signer → 你的钱包)

返回的已签名 PSBT(Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iICAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88SDBFAiEAnxTBvsytWOKIqCL+jVnOZoU8wKKB3sg9FqBU6YKagoUCIC4lC/U2vuJKimL4vEacD8f6CnzzwQjDbBZOBoNWSJMgASIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
显示二维码文本和原始 CBOR
二维码文本 · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADUYCYVWCKDTWYHDNEHKADTPJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADCLBBLDOY
UR:CRYPTO-PSBT/2-3/LPAOAXCFADUYCYVWCKDTWYHDNEADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAOAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNFDDYFEAOCLAENEBBSERNSFPMHDVOLOPDCPZELGHKTOIYLPFNRTOELYUESPFSCMNBGHWLLFNYLFLPAONLJTAAPK
UR:CRYPTO-PSBT/3-3/LPAXAXCFADUYCYVWCKDTWYHDNECXDMDABDYKENRNVOGELEIDYARFFGNSBSSTZSBKKEWFSEAYSRJZCMGLAMLSHFFDMUCXADCPAMAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEHHDLWYGS
UR CBOR(十六进制)
5901d870736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222020330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c4830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e0683564893200122060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

由你的钱包组装

finalTxHex
02000000000101e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c024830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e06835648932001210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000
txid
6f492df497b5e455fb3305d0272fe9c6dc3d8cc9d0054be3aaefee77daca82b7

多签​

  1. 导入每个共同签名方的密钥:BitTool Signer 如何导出其 BIP48 共同签名方密钥,参见币种级页面(P2WSH 为 m/48'/0'/0'/2')。
  2. 构建 PSBT 时带上 witnessScript(P2SH-P2WSH 还需要 redeemScript),并在每个输入上为每一个共同签名方密钥添加一条 bip32Derivation。
  3. 依次发送给每个签名器,或者并行发送后再合并。BitTool Signer 只为它持有的密钥添加签名。如果一部手机在不同的钱包中持有两个共同签名方密钥,它会分别要求输入每个钱包的密码。
  4. 已有的签名会被验证并保留,因此 PSBT 可以按任意顺序流转。

BitTool Signer 会审核它被要求签名的脚本,但无法知道该策略是否符合用户的本意。请在创建多签钱包时,在你的钱包中展示完整的策略(门限、所有指纹)。