Saltar al contenido principal

Bitcoin (PSBT)

ur:crypto-psbtur:psbtBBQrPSBT v0 / v2

Las solicitudes de Bitcoin son PSBT. Tu billetera construye el PSBT; BitTool Signer agrega sus firmas y devuelve el PSBT; tu billetera lo finaliza y lo transmite.

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, 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 usó.
Texto Base64 (cHNidP…) o hex (70736274ff…) en un solo código QREl mismo texto si tiene ≤ 1,800 caracteres; si no, ur:crypto-psbt animadoPara transacciones pequeñas.
Archivo .psbt (Base64 o hex; se admiten saltos de línea)IgualA través de Archivos/AirDrop en el teléfono firmador.

La respuesta siempre usa el mismo formato de 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ño1–100 entradas, 1–100 salidas.
DerivaciónCada entrada que el firmador deba firmar lleva bip32Derivation (huella digital maestra + ruta completa) para su clave. Las entradas Taproot usan tapBip32Derivation y tapInternalKey.
Transacciones previasLas entradas legacy y SegWit v0 (P2PKH, P2WPKH, P2SH-P2WPKH, P2WSH …) necesitan la transacción previa completa en nonWitnessUtxo. witnessUtxo por sí solo se acepta únicamente 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 etiqueta una salida como "cambio" solo 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 requiere nonWitnessUtxo

Las firmas SegWit v0 se comprometen con el monto de la entrada que se gasta, pero no con los montos de las demás entradas. Un coordinador malicioso puede mentir sobre los montos de las entradas a lo largo de dos rondas de firma y engañar a un firmador para que pague una comisión enorme (el ataque de comisiones de SegWit divulgado en 2020). Con la transacción previa completa, el firmador puede verificar cada monto por sí mismo. Las firmas Taproot se comprometen con los montos de todas las entradas, y por eso las transacciones compuestas solo por entradas Taproot están exentas.

Scripts compatibles​

ScriptFirma
P2PKH, P2WPKH, P2SH-P2WPKHSí (clave única)
P2TR por ruta de clave (key path, BIP86)Sí
Multisig P2SH, P2WSH, P2SH-P2WSH (multi/sortedmulti)Sí, una firma parcial por cada clave local
P2TR por ruta de script (script path), hojas de umbral CHECKSIG/CHECKSIGVERIFY y CHECKSIGADD (versión de hoja 0xc0)Sí
MuSig2 y otros scriptsNo

La respuesta​

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

  1. Comprueba que la transacción sin firmar sea, byte por byte, la que enviaste (en los transportes PSBT no hay ID de solicitud).
  2. Verifica cada firma nueva con su entrada.
  3. En multisig, combínalo con los PSBT de los demás cofirmantes.
  4. Finaliza, extrae y transmite.
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 de vuelta el PSBT firmado, verifícalo 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ía 60,000 sats a bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4, 39,000 sats de cambio a m/84'/0'/0'/1/0 y 1,000 sats de comisión. La transacción previa es sintética.

Solicitud (tu billetera → 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 billetera)

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 billetera

finalTxHex
02000000000101e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c024830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e06835648932001210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000
txid
6f492df497b5e455fb3305d0272fe9c6dc3d8cc9d0054be3aaefee77daca82b7

Multisig​

  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 elemento bip32Derivation para cada clave de cofirmante en cada entrada.
  3. Envíalo a cada firmador por turnos, o en paralelo y luego combina los resultados. BitTool Signer agrega firmas solo para las claves que tiene. Si un teléfono tiene dos claves de cofirmante en billeteras distintas, pide la contraseña de cada billetera 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 digitales) en tu billetera cuando se cree la billetera multisig.