跳至主要内容

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較新的 registry 名稱,內容相同。
BBQr(B$…,編碼 H、2、Z)BBQr(2 或 H)Coinkite 的格式。可接受 Z(deflate);回應使用 2(Base32),若請求使用 H 則回應也使用 H。
單一 QR Code 中的 Base64(cHNidP…)或 hex(70736274ff…)文字若 ≤ 1,800 個字元則為相同格式的文字,否則為動態 ur:crypto-psbt適用於小型交易。
.psbt 檔案(Base64 或 hex,有換行也沒關係)相同透過簽名器手機上的「檔案」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 簽名會承諾(commit)所花費輸入的金額,但不涵蓋其他輸入的金額。惡意的協調者可以在兩輪簽名中謊報輸入金額,誘使簽名器支付極高的手續費(即 2020 年揭露的 SegWit 手續費攻擊)。有了完整的前一筆交易,簽名器就能自行檢查每一筆金額。Taproot 簽名會承諾所有輸入的金額,因此全部為 Taproot 輸入的交易可以豁免。

支援的腳本​

腳本是否簽名
P2PKH、P2WPKH、P2SH-P2WPKH是(單一金鑰)
P2TR key path(BIP86)是
多重簽名 P2SH、P2WSH、P2SH-P2WSH(multi/sortedmulti)是,每把本機金鑰產生一個部分簽名
P2TR script path、CHECKSIG/CHECKSIGVERIFY 與 CHECKSIGADD 門檻葉節點(leaf version 0xc0)是
MuSig2 與其他腳本否

回應​

回應就是加入簽名後的 PSBT:ECDSA 輸入使用 partialSig,Taproot 則使用 tapKeySig 或 tapScriptSig。不會進行 finalize,其他內容也不會變更。在 finalize 之前:

  1. 檢查未簽名交易與你送出的版本逐位元組完全相同(PSBT 傳輸方式中沒有請求 ID)。
  2. 針對各自的輸入驗證每個新簽名。
  3. 多重簽名時,與其他共同簽名者的 PSBT 合併。
  4. Finalize、擷取(extract)並廣播。
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;傳送 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(hex)
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(hex)
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 會審核它被要求簽署的腳本,但無法得知該策略是否就是使用者所預期的。建立多重簽名錢包時,請在你的錢包中顯示完整的策略(門檻值、所有指紋)。