Saltar al contenido principal

Bitcoin (PSBT)

ur:crypto-psbtur:psbtBBQrPSBT v0 / v2

Las solicitudes de Bitcoin son PSBT. Tu cartera construye el PSBT; BitTool Signer añade sus firmas y devuelve el PSBT; tu cartera lo finaliza y lo difunde.

Transporte​

SolicitudRespuestaNotas
ur:crypto-psbtur:crypto-psbtRecomendado. Cuerpo del UR = cadena de bytes CBOR que contiene el PSBT.
ur:psbtur:psbtEl nombre más reciente en el registro, con el mismo contenido.
BBQr (B$…, codificaciones H, 2, Z)BBQr (2 o H)El formato de Coinkite. Se acepta Z (deflate); las respuestas usan 2 (Base32), o H si la solicitud lo usaba.
Texto Base64 (cHNidP…) o hex (70736274ff…) en un solo código QREl mismo texto si ocupa ≤ 1800 caracteres; si no, ur:crypto-psbt animadoPara transacciones pequeñas.
Archivo .psbt (Base64 o hex; puede tener saltos de línea)IgualMediante Archivos/AirDrop en el móvil firmante.

La respuesta usa siempre el mismo formato que la solicitud (salvo lo indicado), así que puedes reutilizar el decodificador que ya tienes.

Qué debe contener el PSBT​

RequisitoDetalle
VersiónPSBT v0 o v2 (BIP 370). v0 es la opción más segura.
TamañoDe 1 a 100 entradas y de 1 a 100 salidas.
DerivaciónCada entrada que deba firmar el firmante lleva bip32Derivation (huella maestra + ruta completa) para su clave. Las entradas Taproot usan tapBip32Derivation y tapInternalKey.
Transacciones anterioresLas entradas legacy y SegWit v0 (P2PKH, P2WPKH, P2SH-P2WPKH, P2WSH …) necesitan la transacción anterior completa en nonWitnessUtxo. Solo se acepta witnessUtxo por sí solo cuando todas las entradas son Taproot.
SighashSIGHASH_ALL, o SIGHASH_DEFAULT para Taproot. Cualquier otro se rechaza.
CambioIncluye también bip32Derivation en las salidas de cambio. BitTool Signer solo etiqueta una salida como «cambio» después de volver a derivar su script a partir de sus propias claves.
SalidasScripts estándar y OP_RETURN de hasta 83 bytes.
RedSolo mainnet.
Por qué se exige nonWitnessUtxo

Las firmas SegWit v0 se comprometen con el importe de la entrada que se gasta, pero no con los importes de las demás entradas. Un coordinador malicioso puede mentir sobre los importes de las entradas a lo largo de dos rondas de firma y engañar a un firmante para que pague una comisión enorme (el ataque de comisiones de SegWit que se hizo público en 2020). Con la transacción anterior completa, el firmante puede comprobar cada importe por sí mismo. Las firmas Taproot se comprometen con los importes de todas las entradas, y por eso las transacciones exclusivamente Taproot quedan exentas.

Scripts compatibles​

Script¿Se firma?
P2PKH, P2WPKH, P2SH-P2WPKHSí (una sola clave)
P2TR por key path (BIP86)Sí
Multifirma P2SH, P2WSH, P2SH-P2WSH (multi/sortedmulti)Sí, una firma parcial por cada clave local
P2TR por script path, hojas CHECKSIG/CHECKSIGVERIFY y de umbral CHECKSIGADD (versión de hoja 0xc0)Sí
MuSig2 y otros scriptsNo

La respuesta​

La respuesta es tu PSBT con las firmas añadidas: partialSig para las entradas ECDSA, y tapKeySig o tapScriptSig para Taproot. No se finaliza nada y no cambia nada más. Antes de finalizar:

  1. Comprueba que la transacción sin firmar es, byte a byte, la que enviaste (en los transportes PSBT no hay ID de solicitud).
  2. Verifica cada firma nueva con su entrada.
  3. En multifirma, combínalo con los PSBT de los demás cofirmantes.
  4. Finaliza, extrae y difunde.
Problemas conocidos en 1.0.0
  • Quema con cambio (R3-01, Baja, solo iPhone). En una transacción que quema monedas con una salida OP_RETURN de importe distinto de cero y que además devuelve cambio, el resumen de una línea de la pantalla de resultado y de la Actividad de BitTool Signer dice "Every output returns to your wallet" (todas las salidas vuelven a tu cartera). La pantalla de firma sí muestra correctamente la quema, su importe y el riesgo alto, y el PSBT no se ve afectado. BitTool Signer para Android 1.0.0 muestra la quema también en el resumen.
  • Cambio en cualquier índice (BTS-08, Media, ambas apps). Una salida se trata como cambio cuando su script coincide con uno de los perfiles de firma única de la propia cartera en cualquier índice, incluidos índices que una cartera de solo lectura nunca revisará. Mantén el cambio en índices habituales y desconfía de una solicitud que mueva todo a una ruta inusual de la cartera del usuario.

Consulta Revisiones de seguridad.

Código​

Construye el PSBT con bitcoinjs-lib y envuélvelo en ur:crypto-psbt con @keystonehq/bc-ur-registry, de Keystone:

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 };
}

Lee el PSBT firmado, compruébalo y finalízalo:

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
}

Vectores de prueba​

Entrada de 100 000 sats en m/84'/0'/0'/0/0; envío de 60 000 sats a bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4, 39 000 sats de cambio a m/84'/0'/0'/1/0 y 1000 sats de comisión. La transacción anterior es sintética.

Solicitud (tu cartera → BitTool Signer)

crypto-psbt3 fotogramas
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
1 / 3
PSBT sin firmar (Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
Mostrar el texto del QR y el CBOR sin procesar
Texto del QR · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
UR:CRYPTO-PSBT/2-3/LPAOAXCFADJLCYRFDSIHPRHDKGAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAMGWBSRYCW
UR:CRYPTO-PSBT/3-3/LPAXAXCFADJLCYRFDSIHPRHDKGAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEGWCEHDPR
CBOR del UR (hex)
59016c70736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

Respuesta (BitTool Signer → tu cartera)

PSBT firmado devuelto (Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iICAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88SDBFAiEAnxTBvsytWOKIqCL+jVnOZoU8wKKB3sg9FqBU6YKagoUCIC4lC/U2vuJKimL4vEacD8f6CnzzwQjDbBZOBoNWSJMgASIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
Mostrar el texto del QR y el CBOR sin procesar
Texto del QR · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADUYCYVWCKDTWYHDNEHKADTPJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADCLBBLDOY
UR:CRYPTO-PSBT/2-3/LPAOAXCFADUYCYVWCKDTWYHDNEADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAOAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNFDDYFEAOCLAENEBBSERNSFPMHDVOLOPDCPZELGHKTOIYLPFNRTOELYUESPFSCMNBGHWLLFNYLFLPAONLJTAAPK
UR:CRYPTO-PSBT/3-3/LPAXAXCFADUYCYVWCKDTWYHDNECXDMDABDYKENRNVOGELEIDYARFFGNSBSSTZSBKKEWFSEAYSRJZCMGLAMLSHFFDMUCXADCPAMAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEHHDLWYGS
CBOR del UR (hex)
5901d870736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222020330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c4830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e0683564893200122060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

Ensamblado por tu cartera

finalTxHex
02000000000101e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c024830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e06835648932001210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000
txid
6f492df497b5e455fb3305d0272fe9c6dc3d8cc9d0054be3aaefee77daca82b7

Multifirma​

  1. Importa la clave de cada cofirmante: BitTool Signer exporta su clave de cofirmante BIP48 en la página de nivel de moneda (m/48'/0'/0'/2' para P2WSH).
  2. Construye el PSBT con witnessScript (y redeemScript para P2SH-P2WSH) y un registro bip32Derivation para cada clave de cofirmante en cada entrada.
  3. Envíalo a cada firmante por turnos, o en paralelo y combínalos. BitTool Signer solo añade firmas para las claves que tiene. Si un mismo móvil tiene dos claves de cofirmante en carteras distintas, pide la contraseña de cada cartera por separado.
  4. Las firmas existentes se verifican y se conservan, así que el PSBT puede circular en cualquier orden.

BitTool Signer revisa el script que se le pide firmar, pero no puede saber si la política es la que el usuario pretendía. Muestra la política completa (umbral, todas las huellas) en tu cartera cuando se cree la cartera multifirma.