Elegir entre BIP-137 y BIP-322 es elegir entre la forma antigua y la nueva de demostrar en Bitcoin el control de una dirección firmando un mensaje. BIP-137 documenta el formato «Bitcoin Signed Message», en uso desde hace muchos años: una firma ECDSA recuperable de 65 bytes cuyo byte de encabezado indica qué tipo de dirección derivar de la clave recuperada. BIP-322 generaliza la firma a cualquier script: el mensaje se incorpora a una transacción virtual, y la firma es el witness, sea cual sea, que gastaría desde la dirección, comprobado por el intérprete de scripts habitual.
De un vistazo
| BIP-137 | BIP-322 | |
|---|---|---|
| Estado | Desplegado (asignado en 2019; el formato es mucho más antiguo) | Completo desde la versión 1.0.0, del 15 de abril de 2026; 2.0.0 el 4 de junio de 2026 |
| Hash del mensaje | Doble SHA-256 de "\x18Bitcoin Signed Message:\n", la longitud y el mensaje | SHA-256 etiquetado de BIP-340 con la etiqueta BIP0322-signed-message |
| Qué se firma | El hash del mensaje, directamente | Una transacción virtual to_sign que gasta una salida virtual to_spend |
| Tipos de dirección | P2PKH; P2SH-P2WPKH y P2WPKH mediante rangos de encabezado | Cualquier script: P2PKH, P2WPKH, P2WSH, P2TR, multifirma, bloqueos temporales |
| Qué necesita el verificador | Firma, mensaje y dirección; la clave se recupera | Firma, mensaje y dirección; un intérprete de scripts |
| Contenido de la firma | Byte de encabezado (27–42), r, s | Pila de witness (smp), transacción completa (ful) o PSBT (pof) |
| Tamaño, base64 | 88 caracteres | 147 para P2WPKH, 88 para P2TR, 439 para una multifirma P2WSH 3 de 3 (vectores de prueba) |
| Prueba de fondos | No | Sí, con entradas adicionales en una PSBT (pof) |
| Vinculación con la clave pública | No (la recuperación se puede alterar para obtener claves relacionadas) | Sí, mediante el script que se satisface |
| Firma en billeteras de hardware | Modo de firma de mensajes, donde se admite | Firma de transacciones al estilo PSBT |
Cómo funciona BIP-137
El firmante calcula el hash del mensaje con un prefijo fijo, lo firma con ECDSA y guarda el ID de recuperación en un byte de encabezado: del 27 al 30 para P2PKH con claves sin comprimir, del 31 al 34 para claves comprimidas, del 35 al 38 para P2SH-P2WPKH y del 39 al 42 para P2WPKH de SegWit nativo. El verificador usa la recuperación de la clave pública para calcular la clave a partir de la firma y comprueba que su hash corresponde a la dirección.
Esto solo funciona cuando una dirección es el hash de una única clave pública. Las direcciones Taproot codifican una clave ajustada con un tweak y usan firmas Schnorr, que no permiten recuperar la clave; las multifirmas y otros scripts ni siquiera tienen una clave única. Los rangos de encabezado para SegWit se añadieron más tarde y no todas las billeteras los adoptaron, así que una firma hecha con una billetera puede fallar en otra, y los propios signmessage y verifymessage de Bitcoin Core siguen aceptando solo direcciones P2PKH heredadas.
Cómo funciona BIP-322
BIP-322 calcula el hash del mensaje con un hash etiquetado y lo coloca en el script de entrada de una transacción virtual to_spend cuya única salida tiene el scriptPubKey de la dirección. Una segunda transacción, to_sign, gasta esa salida hacia un OP_RETURN. El firmante produce el witness que su billetera produciría para gastar desde la dirección; el verificador reconstruye ambas transacciones y ejecuta el intérprete de scripts. Como to_spend gasta una entrada inexistente, ninguna de las dos transacciones puede ser válida nunca en la red.
El formato simple (smp) contiene solo la pila de witness. El formato completo (ful) contiene la transacción to_sign entera, necesaria para scripts con bloqueos temporales o con entradas sin witness; en ese caso, el verificador indica el momento y la antigüedad a partir de los cuales la prueba es válida. La prueba de fondos (pof) es una PSBT finalizada que también gasta UTXO reales, de modo que la firma demuestra el control de monedas concretas. El formato heredado de BIP-137 sigue permitido solo para direcciones P2PKH.
Ambos formatos, comprobados con los vectores de prueba de BIP-322
El script siguiente usa la clave privada (en WIF) y la dirección de los vectores de prueba básicos de BIP-322. La clave está publicada ahí para hacer pruebas, así que cualquiera puede gastar desde su dirección, que nunca debe contener fondos. El script crea y verifica una firma BIP-137 para «Hello World», reconstruye el hash del mensaje de BIP-322 y los ID de las dos transacciones virtuales, y verifica la firma smp publicada en los vectores con un hash de firma de BIP-143 (Node.js 26, @noble/curves 2.3.0, @noble/hashes 2.3.0, @scure/base 2.4.0).
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { ripemd160 } from "@noble/hashes/legacy.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex, concatBytes, utf8ToBytes } from "@noble/hashes/utils.js";
import { base64, bech32, createBase58check } from "@scure/base";
const sha256d = (b) => sha256(sha256(b));
const u32 = (n) => new Uint8Array([n, n >>> 8, n >>> 16, n >>> 24]);
const zero = (n) => new Uint8Array(n);
const push = (b) => concatBytes(new Uint8Array([b.length]), b); // lengths here are all below 0xfd
const txid = (tx) => bytesToHex(sha256d(tx).reverse());
// Test key published in the BIP-322 basic test vectors (bip-0322/basic-test-vectors.json).
// It is public, so never send funds to its address.
const wif = createBase58check(sha256).decode("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k");
const secretKey = wif.subarray(1, 33);
const pub = secp256k1.getPublicKey(secretKey, true);
const h160 = ripemd160(sha256(pub));
const address = bech32.encode("bc", [0, ...bech32.toWords(h160)]);
console.log("address:", address);
// BIP-137: double SHA-256 of a prefixed message, compact signature with a header byte
const message = "Hello World";
const m = utf8ToBytes(message);
const legacyHash = sha256d(concatBytes(push(utf8ToBytes("Bitcoin Signed Message:\n")), push(m)));
const rec = secp256k1.sign(legacyHash, secretKey, { prehash: false, format: "recovered" });
const encoded = base64.encode(concatBytes(new Uint8Array([39 + rec[0]]), rec.subarray(1))); // 39-42: P2WPKH
console.log("\nBIP-137 signature:", encoded);
// Verifier side: read the header, recover the public key, compare its hash with the address
const sig137 = base64.decode(encoded);
const recovered = secp256k1.recoverPublicKey(concatBytes(new Uint8Array([(sig137[0] - 27) & 3]), sig137.subarray(1)), legacyHash, { prehash: false });
console.log("header", sig137[0], "-> recovered key matches address:", bytesToHex(ripemd160(sha256(recovered))) === bytesToHex(h160));
// BIP-322: tagged message hash, virtual to_spend and to_sign transactions
const tag = sha256(utf8ToBytes("BIP0322-signed-message"));
const messageHash = sha256(concatBytes(tag, tag, m));
const challenge = concatBytes(new Uint8Array([0x00, 0x14]), h160); // P2WPKH scriptPubKey
const toSpend = concatBytes(u32(0), new Uint8Array([1]), zero(32), u32(0xffffffff),
push(concatBytes(new Uint8Array([0x00, 0x20]), messageHash)), u32(0), new Uint8Array([1]), zero(8), push(challenge), u32(0));
const prevout = concatBytes(sha256d(toSpend), u32(0));
const opReturn = concatBytes(zero(8), push(new Uint8Array([0x6a])));
const toSign = concatBytes(u32(0), new Uint8Array([1]), prevout, push(zero(0)), u32(0), new Uint8Array([1]), opReturn, u32(0));
console.log("\nBIP-322 message hash:", bytesToHex(messageHash));
console.log("to_spend txid:", txid(toSpend));
console.log("to_sign txid: ", txid(toSign));
// Verify the "simple" signature from the test vectors: witness stack [signature, public key]
const vector = "smpAkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
const stack = base64.decode(vector.slice(3));
const der = stack.subarray(2, 2 + stack[1]);
const witnessKey = stack.subarray(3 + stack[1]);
// BIP-143 signature hash for input 0 of to_sign, SIGHASH_ALL
const scriptCode = push(concatBytes(new Uint8Array([0x76, 0xa9, 0x14]), h160, new Uint8Array([0x88, 0xac])));
const preimage = concatBytes(u32(0), sha256d(prevout), sha256d(u32(0)), prevout, scriptCode, zero(8), u32(0),
sha256d(opReturn), u32(0), u32(1));
const ok = secp256k1.verify(der.subarray(0, -1), sha256d(preimage), witnessKey, { prehash: false, format: "der" });
console.log("vector signature:", vector.slice(0, 3), "prefix,", stack[0], "witness items,", ok ? "valid" : "invalid");
console.log("witness key is the address key:", bytesToHex(witnessKey) === bytesToHex(pub));
address: bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l
BIP-137 signature: KOW2xi+ebJLeBtr674l4QH76dqDoVjLV80R9EFKFQX5rBrlCXPIZaYs8Yuayg0ZqjyiCbLy9pzZIS7JWT65/nsU=
header 40 -> recovered key matches address: true
BIP-322 message hash: f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a
to_spend txid: b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b
to_sign txid: 88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf
vector signature: smp prefix, 2 witness items, valid
witness key is the address key: true
El hash del mensaje y los ID de ambas transacciones coinciden con los valores de basic-test-vectors.json, y la firma publicada se verifica. La firma BIP-137 es determinista (RFC 6979), así que la misma clave y el mismo mensaje dan siempre los mismos 88 caracteres; el encabezado 40 significa P2WPKH con ID de recuperación 1. El verificador de mensajes de Bitcoin comprueba ambos formatos para cualquier dirección.
Qué cambia para quienes lo implementan
- La verificación es más pesada: BIP-322 necesita serializar transacciones, calcular el hash de firma de cada tipo de script (el heredado, BIP-143 para SegWit v0, BIP-341 para Taproot) e, idealmente, un intérprete de scripts completo. Bibliotecas como btcd (el origen de los vectores de prueba) lo proporcionan.
- La firma reutiliza el código de transacciones: las billeteras y los dispositivos de hardware firman
to_signcomo cualquier otra transacción, a menudo como PSBT, así que los coordinadores de multifirma y las billeteras Taproot funcionan sin código especial para mensajes. - Detección del formato: la versión 1.0.0 hizo obligatorio el prefijo de tres letras, un cambio incompatible; los verificadores pueden tratar las firmas sin prefijo como
smppor compatibilidad con implementaciones anteriores. - Sin recuperación de la clave: el verificador debe conocer la dirección, que normalmente se proporciona de todos modos.
- Los mismos límites que cualquier firma: ninguno de los dos formatos demuestra que el firmante siga controlando las monedas, ni que no vaya a firmar en nombre de otra persona.
Cuándo elegir cada uno
- Direcciones Taproot, P2WSH, multifirma o de script: BIP-322; BIP-137 no puede expresarlas.
- Plataformas de intercambio y custodios que verifican direcciones de retiro (comprobaciones de la regla de viaje o travel rule): acepta BIP-322 para todos los tipos y BIP-137 para P2PKH y SegWit de clave única.
- Prueba de reservas o de UTXO concretos: prueba de fondos de BIP-322 (
pof). - Direcciones P2PKH heredadas y billeteras antiguas: BIP-137 sigue siendo válido y es lo que produce Bitcoin Core.
- Inicio de sesión con una billetera de Bitcoin: BIP-322 en formato simple sobre un mensaje que incluya dominio, nonce y caducidad, por las mismas razones que Sign-In with Ethereum.
Notas de migración
- Verifica ambos: prueba BIP-322 cuando la firma lleve prefijo o la dirección no sea P2PKH; recurre a BIP-137 para las firmas de 65 bytes con encabezados del 27 al 42.
- Normaliza el tratamiento del encabezado: acepta firmas SegWit cuyo encabezado caiga en el rango P2PKH comprimido solo si la clave recuperada corresponde a la dirección SegWit, y documenta esa decisión.
- Emite firmas con prefijo cuando firmes con BIP-322, como exige la versión 1.0.0.
- Prueba con los vectores publicados (
basic-test-vectors.json,generated-test-vectors.json), incluidos los casos de error de mensajes incorrectos, direcciones incorrectas y prefijos no válidos.
Referencias
- BIP-322, formato genérico de mensajes firmados y vectores de prueba básicos
- BIP-137, firmas de mensajes con claves privadas
- BIP-340, firmas Schnorr y hashes etiquetados
- BIP-143, hash de firma de SegWit v0 y BIP-341, Taproot
- BIP-174, PSBT
- Pull request 2521 de btcd, compatibilidad completa con BIP-322