Respuesta breve. Hay dos formatos. El heredado, especificado a posteriori como BIP-137, es una firma ECDSA recuperable de 65 bytes sobre "\x18Bitcoin Signed Message:\n" ‖ length ‖ message; solo demuestra el control de una clave, y su primer byte (de 27 a 42) indica a qué tipo de dirección se refería el firmante. Bitcoin Core solo firma en este formato para direcciones P2PKH heredadas, y las billeteras no se ponen de acuerdo sobre el encabezado para direcciones SegWit. BIP-322 (estado Complete) demuestra el control de una dirección: construye una transacción virtual que gasta desde la dirección, y la firma es el witness de ese gasto, así que funciona con P2WPKH, P2TR, multifirma y scripts. Desde la versión 2.0.0 del BIP, las firmas llevan un prefijo: smp (simple), ful (completa) o pof (prueba de fondos). Verifica con una biblioteca que entienda el prefijo y acepta las firmas simples sin prefijo de billeteras más antiguas.
El formato heredado (BIP-137)
El resumen es el doble SHA-256 de un prefijo fijo, la longitud del mensaje como varint de Bitcoin y los bytes del mensaje. La firma es header ‖ r ‖ s, de 65 bytes, normalmente codificada en base64. El encabezado lleva el ID de recuperación (de 0 a 3), que permite al verificador calcular la clave pública a partir de la firma, más una constante que indica el tipo de clave y de dirección (BIP-137, estado Deployed):
| Encabezado | Significado |
|---|---|
| 27-30 | P2PKH, clave pública sin comprimir |
| 31-34 | P2PKH, clave pública comprimida |
| 35-38 | P2SH-P2WPKH (SegWit anidado) |
| 39-42 | P2WPKH (SegWit nativo, bc1q…) |
La verificación recupera la clave pública, deriva la dirección del tipo que indica el encabezado y la compara con la dirección dada. No hay transacción ni script: el formato no puede expresar «esta dirección multifirma» ni «esta dirección Taproot», solo «el titular de esta clave». El propio BIP-137 remite a BIP-322 para todo lo que vaya más allá.
El módulo siguiente implementa el formato heredado y, más abajo, las firmas simples BIP-322 para P2WPKH, con @noble/curves y @scure/base:
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { ripemd160 } from "@noble/hashes/legacy.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { bech32, bech32m, createBase58check } from "@scure/base";
const b58 = createBase58check(sha256);
const sha256d = (b) => sha256(sha256(Uint8Array.from(b)));
const hash160 = (b) => ripemd160(sha256(b));
const utf8 = (s) => new TextEncoder().encode(s);
const cat = (...parts) => Uint8Array.from(parts.flatMap((p) => [...p]));
const varint = (n) => (n < 0xfd ? [n] : [0xfd, n & 0xff, n >> 8]); // messages under 64 KiB
export const NETWORKS = {
mainnet: { p2pkh: 0x00, p2sh: 0x05, hrp: "bc" },
regtest: { p2pkh: 0x6f, p2sh: 0xc4, hrp: "bcrt" },
};
/** Addresses of one compressed public key. */
export function addressesOf(pubkey, net = NETWORKS.mainnet) {
const h = hash160(pubkey);
const redeem = cat([0x00, 0x14], h); // P2SH-P2WPKH redeem script
return {
p2pkh: b58.encode(cat([net.p2pkh], h)),
"p2sh-p2wpkh": b58.encode(cat([net.p2sh], hash160(redeem))),
p2wpkh: bech32.encode(net.hrp, [0, ...bech32.toWords(h)]),
};
}
/** scriptPubKey of an address, for BIP-322's message_challenge. */
export function scriptPubKey(address, net = NETWORKS.mainnet) {
if (address.toLowerCase().startsWith(`${net.hrp}1`)) {
const isV0 = address.toLowerCase().startsWith(`${net.hrp}1q`);
const { words } = (isV0 ? bech32 : bech32m).decode(address);
const program = (isV0 ? bech32 : bech32m).fromWords(words.slice(1));
return cat([words[0] === 0 ? 0x00 : 0x50 + words[0], program.length], program);
}
const raw = b58.decode(address);
if (raw[0] === net.p2pkh) return cat([0x76, 0xa9, 0x14], raw.slice(1), [0x88, 0xac]);
if (raw[0] === net.p2sh) return cat([0xa9, 0x14], raw.slice(1), [0x87]);
throw new Error(`unsupported address ${address}`);
}
// ---------------------------------------------------------------------------- BIP-137 (legacy)
/** The digest every legacy message signature covers. */
export const legacyDigest = (message) => {
const m = utf8(message);
return sha256d(cat([0x18], utf8("Bitcoin Signed Message:\n"), varint(m.length), m));
};
const HEADER_BASE = { p2pkh: 31, "p2sh-p2wpkh": 35, p2wpkh: 39 }; // compressed keys
/** 65-byte compact signature, base64. headerFor "electrum" keeps 31-34 for every address type. */
export function signLegacy(message, privateKey, type = "p2pkh", headerFor = "bip137") {
const sig = secp256k1.Signature.fromBytes(secp256k1.sign(legacyDigest(message), privateKey, { prehash: false, format: "recovered" }), "recovered");
const base = headerFor === "electrum" ? 31 : HEADER_BASE[type];
return Buffer.from(cat([base + sig.recovery], sig.toBytes("compact"))).toString("base64");
}
/** Recovers the key and checks it against the address; strict mode also checks the header type. */
export function verifyLegacy(address, message, signatureBase64, { net = NETWORKS.mainnet, strict = false } = {}) {
const raw = Buffer.from(signatureBase64, "base64");
if (raw.length !== 65) throw new Error(`expected 65 bytes, got ${raw.length}`);
const header = raw[0];
if (header < 27 || header > 42) throw new Error(`header byte ${header} out of range`);
const recovery = (header - 27) % 4;
const typeFromHeader = header >= 39 ? "p2wpkh" : header >= 35 ? "p2sh-p2wpkh" : "p2pkh";
const compressed = header >= 31;
const sig = secp256k1.Signature.fromBytes(raw.subarray(1), "compact").addRecoveryBit(recovery);
const pubkey = sig.recoverPublicKey(legacyDigest(message)).toBytes(compressed);
const candidates = addressesOf(pubkey, net);
const match = Object.entries(candidates).find(([, a]) => a === address)?.[0];
if (!match) return { valid: false };
if (strict && match !== typeFromHeader) return { valid: false, reason: `header says ${typeFromHeader}, address is ${match}` };
return { valid: true, type: match, header };
}
// ---------------------------------------------------------------------------- BIP-322 (simple, P2WPKH)
const tag = sha256(utf8("BIP0322-signed-message"));
/** sha256(tag || tag || m), a BIP-340 tagged hash of the raw message. */
export const messageHash = (message) => sha256(cat(tag, tag, utf8(message)));
const le32 = (n) => [n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, n >>> 24];
const zero64 = new Array(8).fill(0);
/** The two virtual transactions of BIP-322, serialized without witness. Returns txids too. */
export function virtualTransactions(address, message, net = NETWORKS.mainnet) {
const challenge = scriptPubKey(address, net);
const toSpend = cat(
le32(0), [1], new Array(32).fill(0), le32(0xffffffff), // version 0, one input spending 000…000:FFFFFFFF
[34, 0x00, 0x20], messageHash(message), le32(0), // scriptSig OP_0 PUSH32[message_hash], nSequence 0
[1], zero64, [challenge.length], challenge, le32(0), // one output, value 0, scriptPubKey = challenge
);
const toSpendId = sha256d(toSpend); // internal byte order
const toSign = cat(le32(0), [1], toSpendId, le32(0), [0], le32(0), [1], zero64, [1, 0x6a], le32(0));
const display = (h) => Buffer.from(h).reverse().toString("hex");
return { challenge, toSpendId, toSpendTxid: display(toSpendId), toSignTxid: display(sha256d(toSign)) };
}
/** Verifies a BIP-322 simple signature for a P2WPKH address. Accepts the "smp" prefix or none. */
export function verifySimpleP2wpkh(address, message, signature, net = NETWORKS.mainnet) {
const b64 = signature.startsWith("smp") ? signature.slice(3) : signature;
const w = Buffer.from(b64, "base64");
// Witness stack: count, then <len><DER sig + sighash byte>, <len><33-byte pubkey>.
if (w[0] !== 2) throw new Error("expected a two-item witness stack");
const sigWithType = w.subarray(2, 2 + w[1]);
const pubkey = w.subarray(3 + w[1], 3 + w[1] + w[2 + w[1]]);
const { challenge, toSpendId } = virtualTransactions(address, message, net);
if (challenge[0] !== 0x00 || challenge[1] !== 20) throw new Error("not a P2WPKH address");
if (Buffer.compare(Buffer.from(hash160(pubkey)), Buffer.from(challenge.subarray(2))) !== 0) return false;
if (sigWithType.at(-1) !== 0x01) throw new Error("only SIGHASH_ALL is allowed");
// BIP-143 digest of to_sign input 0, spending to_spend:0 worth 0 sat.
const scriptCode = cat([0x19, 0x76, 0xa9, 0x14], challenge.subarray(2), [0x88, 0xac]);
const outputs = cat(zero64, [1, 0x6a]);
const preimage = cat(
le32(0), sha256d(cat(toSpendId, le32(0))), sha256d(le32(0)), toSpendId, le32(0),
scriptCode, zero64, le32(0), sha256d(outputs), le32(0), le32(1),
);
const der = sigWithType.subarray(0, -1);
return secp256k1.verify(der, sha256d(preimage), pubkey, { prehash: false, format: "der", lowS: true });
}
Qué admite Bitcoin Core
Los RPC signmessage y verifymessage de Bitcoin Core implementan el formato heredado, y solo para direcciones heredadas. Un nodo regtest, una dirección de cada tipo:
#!/bin/sh
# Bitcoin Core in regtest: which address types can sign and verify messages, and the exact errors.
set -e
DATA=/tmp/regtest
mkdir -p "$DATA"
bitcoind -regtest -datadir="$DATA" -daemonwait -fallbackfee=0.0001 >/dev/null
cli() { bitcoin-cli -regtest -datadir="$DATA" "$@"; }
cli -named createwallet wallet_name=lab >/dev/null
echo "$(bitcoind -version | head -1)"
MSG="ecdsa.com lab, 2026-10-20"
for type in legacy p2sh-segwit bech32 bech32m; do
addr=$(cli getnewaddress "" "$type")
printf '%-12s %s\n' "$type" "$addr"
if sig=$(cli signmessage "$addr" "$MSG" 2>/tmp/err); then
echo " signmessage: $sig"
echo " verifymessage: $(cli verifymessage "$addr" "$sig" "$MSG")"
echo "$addr $sig" > "/w/core-$type.txt"
else
echo " signmessage: $(tr '\n' ' ' < /tmp/err)"
fi
done
# A signature made elsewhere for a segwit address: Core cannot even attempt it.
seg=$(cli getnewaddress "" bech32)
leg=$(cut -d' ' -f2 /w/core-legacy.txt)
echo "verifymessage with a bech32 address: $(cli verifymessage "$seg" "$leg" "$MSG" 2>&1 | tr '\n' ' ')"
cli stop >/dev/null
Bitcoin Core daemon version v31.1.0 bitcoind
legacy mnaZRqP42LwTsVNQGfSb1eeTMvVzkaMoWp
signmessage: ICZ/Fs0I/i7XNJbC+ubYkwpDZXI0uFZU1oaqrSaJlS+hYavTSzdfPKmRkLJrUdi6/RXl9WsYNEW5OU9l9hfMHCI=
verifymessage: true
p2sh-segwit 2MyhmVFuFbTr4kAF1S4TtVmjEgkdouEntKK
signmessage: error code: -3 error message: Address does not refer to key
bech32 bcrt1qulxz0zapcld55a5twc9k88028y6dyxrggxug0n
signmessage: error code: -3 error message: Address does not refer to key
bech32m bcrt1pq8x39vj0qay20825g8m3kffzx7sqwu9lfzmtjejta94luunwkmfsdc0hnd
signmessage: error code: -3 error message: Address does not refer to key
verifymessage with a bech32 address: error code: -3 error message: Address does not refer to key
Todas las direcciones SegWit y Taproot fallan con Address does not refer to key, tanto al firmar como al verificar una firma que hizo otra persona. El primer byte de la firma heredada, 31 o 32 en estas ejecuciones (H o I en base64), indica una clave P2PKH comprimida.
Nuestra implementación produce los mismos bytes que Bitcoin Core. Con una clave generada para la prueba e importada en la billetera regtest como descriptor pkh(), ambos firmaron el mismo mensaje:
address mk1giEaBaAFPXNRMQcZNptPAp9kJmttKyp
core signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
our signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
identical: yes
core verifymessage(our signature): true
Las firmas son idénticas porque ambos usan nonces deterministas (RFC 6979) y normalización low-S; un firmante que usa nonces aleatorios produce cada vez una firma distinta e igual de válida.
El problema del encabezado en direcciones SegWit
Las billeteras que firman mensajes en formato heredado para direcciones SegWit no se ponen de acuerdo sobre el encabezado. Electrum firma con un encabezado de 27 a 34 para cualquier tipo de dirección y, al verificar, prueba todos los tipos de dirección de una sola clave que podría tener la clave recuperada (electrum/bitcoin.py). Trezor sigue BIP-137 y suma 4 para P2SH-P2WPKH y 8 para P2WPKH, con una opción (no_script_type) para producir en su lugar la forma de Electrum (trezor-firmware, sign_message.py). Por eso un verificador tiene que elegir entre una comprobación estricta (el encabezado debe indicar el tipo de la dirección) y una laxa (cualquier dirección de la clave recuperada). Las dos convenciones, con la misma clave y el mismo mensaje, frente a nuestro verificador y a bip322-js 4.0.0:
// Same key, same message, P2WPKH address: the BIP-137 header and the Electrum convention, and who accepts which.
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { Verifier } from "bip322-js";
import { addressesOf, signLegacy, verifyLegacy } from "./btcmsg.mjs";
const priv = secp256k1.utils.randomSecretKey();
const { p2pkh, p2wpkh } = addressesOf(secp256k1.getPublicKey(priv, true));
const message = "ecdsa.com lab, 2026-10-20";
const lib = (address, sig, strict) => {
try {
return Verifier.verifySignature(address, message, sig, strict);
} catch (e) {
return `throws: ${e.message}`;
}
};
for (const [label, sig] of [
["BIP-137 header (39-42)", signLegacy(message, priv, "p2wpkh", "bip137")],
["Electrum header (31-34)", signLegacy(message, priv, "p2wpkh", "electrum")],
]) {
const header = Buffer.from(sig, "base64")[0];
const loose = verifyLegacy(p2wpkh, message, sig);
const strict = verifyLegacy(p2wpkh, message, sig, { strict: true });
console.log(`${label}: header ${header}`);
console.log(` ours, loose: ${loose.valid} ours, strict: ${strict.valid}${strict.reason ? ` (${strict.reason})` : ""}`);
console.log(` bip322-js, loose: ${lib(p2wpkh, sig, false)} bip322-js, strict: ${lib(p2wpkh, sig, true)}`);
console.log(` same signature for the P2PKH address of the key: ours, loose ${verifyLegacy(p2pkh, message, sig).valid}`);
}
BIP-137 header (39-42): header 39
ours, loose: true ours, strict: true
bip322-js, loose: true bip322-js, strict: true
same signature for the P2PKH address of the key: ours, loose true
Electrum header (31-34): header 31
ours, loose: true ours, strict: false (header says p2pkh, address is p2wpkh)
bip322-js, loose: true bip322-js, strict: false
same signature for the P2PKH address of the key: ours, loose true
La verificación laxa acepta las dos, y además acepta la misma firma como prueba para la dirección P2PKH de la clave. Ese es el significado honesto de una firma heredada: quien tenga esta clave, para todas las direcciones derivadas de ella. La verificación estricta rechaza todas las firmas SegWit al estilo de Electrum. Si tienes que aceptar firmas heredadas para direcciones SegWit, verifica de forma laxa y deja constancia de que lo verificado es el control de una clave.
BIP-322: demostrar el control de una dirección
BIP-322 (bip-0322.mediawiki, estado Complete, versión 2.0.0) reutiliza la maquinaria de las transacciones en lugar de inventar un formato de mensaje. Se construyen dos transacciones virtuales:
to_spend: versión 0, una entrada que gasta la salida inexistente000…000:FFFFFFFFcon un scriptSigOP_0 PUSH32[message_hash], y una salida de 0 satoshis cuyo scriptPubKey es la dirección que se quiere demostrar.message_hashes un hash etiquetado de BIP-340,SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message)con la etiquetaBIP0322-signed-message.to_sign: versión 0, una entrada que gastato_spend:0y una salida de 0 satoshis aOP_RETURN.
Firmar consiste en gastar to_spend:0 en to_sign como una billetera gastaría cualquier salida de esa dirección. La verificación comprueba ese gasto con las reglas de script estándar (SIGHASH_ALL o el valor predeterminado de Taproot, low-S y DER estricto, CLEANSTACK y otras). Como ninguna de las dos transacciones es válida en la red, una firma no se puede difundir para mover fondos.
Codificaciones:
| Variante | Prefijo | Contenido | Uso |
|---|---|---|---|
| Simple | smp | la pila del witness, en base64 | SegWit nativo y Taproot, sin bloqueos temporales |
| Completa | ful | la transacción to_sign firmada entera, en base64 | cualquier script, incluidos SegWit anidado, direcciones heredadas y bloqueos temporales |
| Completa (prueba de fondos) | pof | una PSBT finalizada de to_sign con entradas adicionales | demostrar el control de UTXO concretos |
| Heredada | ninguno | la firma BIP-137 | solo se permite para direcciones P2PKH heredadas |
Los prefijos de variante son nuevos en la versión 2.0.0, y ahora los firmantes DEBEN incluirlos. Por compatibilidad, «un verificador podría suponer la variante simple en ausencia de prefijo», y así siguen funcionando las firmas de billeteras creadas según el texto anterior. La verificación tiene tres resultados, no dos: válida, no válida o no concluyente cuando el verificador no puede evaluar los scripts implicados.
Multifirma y billeteras de hardware: firmar mediante PSBT
Para una dirección multifirma, el witness necesita firmas de varias claves, en manos de distintas personas o dispositivos. BIP-322 lo resuelve con las herramientas que ya tienen los usuarios de multifirma: la transacción to_sign circula como una PSBT con un nuevo campo global, PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (tipo 0x09), que lleva el mensaje. Un firmante que ve ese campo, una primera entrada que gasta la salida 0 del to_spend correspondiente y una única salida OP_RETURN de valor cero debe mostrar al usuario el mensaje y la dirección en lugar de una transacción, y después firma como firmaría un gasto. Cada cofirmante añade una firma parcial; el resultado finalizado se convierte en una firma ful. Un dispositivo que no reconoce el campo ve lo que parece una transacción que gasta 0 satoshis, y por eso en este flujo importa más la compatibilidad de las billeteras que la criptografía.
El BIP enumera dos implementaciones de referencia: un pull request de Bitcoin Core con compatibilidad básica (#24058), que se cerró sin fusionarse, y un pull request de btcd con compatibilidad completa (#2521), origen de los vectores de prueba generados y todavía abierto el 20 de octubre de 2026. Ninguno de los dos forma parte de una versión publicada, y en parte por eso las billeteras y bibliotecas de la tabla de más abajo que admiten BIP-322 lo implementan cada una por su cuenta.
Contrastado con los vectores de prueba del BIP
BIP-322 incluye vectores de prueba básicos: hashes de mensaje e ID de transacción para tres mensajes, y firmas simples para una dirección P2WPKH. Para verificarlos solo hacen falta la dirección, el mensaje y la firma. Las mismas firmas, con y sin el nuevo prefijo, se pasaron también a bip322-js 4.0.0:
// BIP-322's published test vectors (addresses, messages and signatures only), against btcmsg.mjs and bip322-js 4.0.0.
import { readFileSync } from "node:fs";
import { Verifier } from "bip322-js";
import { messageHash, verifySimpleP2wpkh, virtualTransactions } from "./btcmsg.mjs";
const vectors = JSON.parse(readFileSync("bip322-basic-test-vectors.json", "utf8"));
for (const v of vectors.tx_hashes) {
const { toSpendTxid, toSignTxid } = virtualTransactions(v.address, v.message);
const ok = Buffer.from(messageHash(v.message)).toString("hex") === v.message_hash && toSpendTxid === v.to_spend_tx_hash && toSignTxid === v.to_sign_tx_hash;
console.log(`hashes for ${JSON.stringify(v.message).padEnd(16).slice(0, 16)} message_hash, to_spend, to_sign match: ${ok}`);
}
for (const v of vectors.simple.filter((x) => x.type === "p2wpkh")) {
for (const sig of v.bip322_signatures) {
const ours = verifySimpleP2wpkh(v.address, v.message, sig);
const call = (s) => { try { return Verifier.verifySignature(v.address, v.message, s); } catch (e) { return `throws: ${e.message}`; } };
console.log(`${JSON.stringify(v.message).padEnd(14)} ${sig.slice(0, 12)}... ours: ${ours} | bip322-js: ${call(sig)} | bip322-js, prefix removed: ${call(sig.slice(3))}`);
}
}
const wrong = vectors.error.find((e) => e.description.startsWith("wrong message for valid simple p2wpkh"));
console.log(`error vector "${wrong.description}": ours ${verifySimpleP2wpkh(wrong.address, wrong.message, wrong.signature)}`);
hashes for "" message_hash, to_spend, to_sign match: true
hashes for "Hello World" message_hash, to_spend, to_sign match: true
hashes for "UTF-8 support: message_hash, to_spend, to_sign match: true
"" smpAkcwRAIgM... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"" smpAkgwRQIhA... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"Hello World" smpAkcwRAIgZ... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
"Hello World" smpAkgwRQIhA... ours: true | bip322-js: throws: buffer too small | bip322-js, prefix removed: true
error vector "wrong message for valid simple p2wpkh signature (empty message was signed)": ours false
La implementación anterior reproduce todos los hashes, verifica todas las firmas simples P2WPKH, con prefijo o sin él, y rechaza el vector de error. bip322-js 4.0.0, la biblioteca de JavaScript de uso generalizado, no entiende el prefijo smp: decodifica el prefijo como base64 y falla con buffer too small, mientras que la misma firma sin el prefijo se verifica. Por tanto, las firmas que generen las billeteras más recientes fallarán en los servicios construidos sobre ella hasta que se actualice; quitar un prefijo conocido antes de llamarla es un puente viable para las firmas simples.
Quién produce y acepta qué
Según el código fuente de cada proyecto, o una prueba donde se indica, a octubre de 2026:
| Software | Heredado (BIP-137) | BIP-322 | Notas |
|---|---|---|---|
| Bitcoin Core 31.1 | solo P2PKH | no | probado arriba |
| Electrum (master) | P2PKH, P2WPKH, P2SH-P2WPKH, encabezado 27-34 | no | verifica las dos convenciones de encabezado; rechaza las direcciones que no tienen una sola clave |
| Firmware de Trezor (main) | P2PKH, P2SH-P2WPKH, P2WPKH, encabezados BIP-137 | no | Taproot: «Unsupported script type» |
| Sparrow (drongo master) | sí | simple, con prefijos smp/ful/pof | verificación: variante simple, P2WPKH y P2TR de una sola clave |
| bip322-js 4.0.0 | sí, laxa o estricta | simple, P2WPKH, P2SH-P2WPKH, P2TR de una sola clave | sin prefijos; consulta la nota anterior |
Las consecuencias prácticas: una dirección Taproot solo se puede demostrar con BIP-322; una dirección multifirma no se puede demostrar en absoluto con el formato heredado; y un servicio que quiera aceptar pruebas de los usuarios de todas estas billeteras tiene que aceptar firmas heredadas (de forma laxa, para SegWit), firmas simples BIP-322 sin prefijo y también las que llevan prefijo.
Cómo verificar en un servicio
- Fija el mensaje desde tu lado. Incluye tu dominio, un nonce o una marca de tiempo y el propósito (
"Proof of ownership for example.com, nonce 7f3a…"), y acepta cada nonce una sola vez, exactamente como en Sign-In with Ethereum. Una firma sobre un texto genérico se puede reutilizar ante cualquier otro servicio que pida el mismo texto. - Compara los bytes exactos. Los dos formatos firman el mensaje en bruto. La interfaz de Electrum elimina los espacios en blanco iniciales y finales antes de firmar; un salto de línea final que se cuela al copiar y pegar, o los finales de línea CRLF, producen un hash distinto y el resultado «firma no válida».
- Detecta el formato, en este orden: un prefijo
smp,fulopof; una cadena base64 que se decodifica en exactamente 65 bytes con un primer byte entre 27 y 42 (heredado); si no, prueba con una firma simple BIP-322 sin prefijo. - Decide qué demuestra una firma heredada para direcciones SegWit: la verificación laxa demuestra el control de la clave, que en direcciones de una sola clave equivale al control de la dirección.
- Trata lo no concluyente como no verificado. Un verificador que no puede evaluar un script (bloqueos temporales, scripts poco habituales) no debe informar de éxito.
- En una prueba de fondos, consulta la cadena de bloques. Una firma
pofdemuestra el control de las salidas enumeradas en el momento de firmar; si siguen sin gastar es una consulta aparte, y nunca demuestra que sean todas las salidas de una dirección.
Fuentes
- BIP-137: firmas de mensajes con claves privadas
- BIP-322: formato genérico de mensajes firmados, versión 2.0.0, y sus vectores de prueba básicos
- BIP-340 (hashes etiquetados), BIP-143 (hash de firma de SegWit), BIP-174 (PSBT)
- RFC 6979: DSA y ECDSA deterministas
- Bitcoin Core 31.1, RPC
signmessageyverifymessage bitcoin.pyde Electrum,sign_message.pyde Trezor,Bip322.javade Sparrow drongo, bip322-js