BIP-137 vs BIP-322 é a escolha entre a forma antiga e a forma nova de provar, no Bitcoin, o controle de um endereço assinando uma mensagem. A BIP-137 documenta o formato de longa data “Bitcoin Signed Message”: uma assinatura ECDSA recuperável de 65 bytes cujo byte de cabeçalho indica qual tipo de endereço derivar da chave recuperada. A BIP-322 generaliza a assinatura para qualquer script: a mensagem fica vinculada a uma transação virtual, e a assinatura é qualquer witness que gastaria os fundos do endereço, verificado pelo interpretador de scripts normal.
Visão geral
| BIP-137 | BIP-322 | |
|---|---|---|
| Status | Deployed (em uso; número atribuído em 2019; o formato é bem mais antigo) | Complete (concluída) desde a versão 1.0.0, de 15 de abril de 2026; 2.0.0 em 4 de junho de 2026 |
| Hash da mensagem | SHA-256 duplo de "\x18Bitcoin Signed Message:\n", do comprimento e da mensagem | SHA-256 com tag da BIP-340, com a tag BIP0322-signed-message |
| O que é assinado | O hash da mensagem, diretamente | Uma transação virtual to_sign que gasta uma saída virtual to_spend |
| Tipos de endereço | P2PKH; P2SH-P2WPKH e P2WPKH por faixas de cabeçalho | Qualquer script: P2PKH, P2WPKH, P2WSH, P2TR, multisig, bloqueios de tempo |
| O verificador precisa de | Assinatura, mensagem, endereço; a chave é recuperada | Assinatura, mensagem, endereço; um interpretador de scripts |
| Conteúdo da assinatura | Byte de cabeçalho (27–42), r, s | Pilha de witness (smp), transação completa (ful) ou PSBT (pof) |
| Tamanho, base64 | 88 caracteres | 147 para P2WPKH, 88 para P2TR, 439 para um multisig P2WSH 3 de 3 (vetores de teste) |
| Prova de fundos | Não | Sim, com entradas adicionais em uma PSBT (pof) |
| Vincula-se à chave pública | Não (a recuperação pode ser ajustada para chaves relacionadas) | Sim, por meio do script que é satisfeito |
| Assinatura em carteira de hardware | Modo de assinatura de mensagens, onde houver suporte | Assinatura de transação no estilo PSBT |
Como a BIP-137 funciona
O signatário calcula o hash da mensagem com um prefixo fixo, assina com ECDSA e guarda o ID de recuperação em um byte de cabeçalho: 27–30 para P2PKH com chaves não comprimidas, 31–34 para chaves comprimidas, 35–38 para P2SH-P2WPKH e 39–42 para SegWit nativo P2WPKH. O verificador usa a recuperação da chave pública para calcular a chave a partir da assinatura e confere se o hash dela corresponde ao endereço.
Isso só funciona quando o endereço é o hash de uma única chave pública. Endereços Taproot se vinculam a uma chave ajustada (tweaked) e usam assinaturas Schnorr, que não permitem recuperar a chave; multisig e outros scripts nem sequer têm uma chave única. As faixas de cabeçalho para SegWit foram acrescentadas depois e nem toda carteira as adotou, então uma assinatura feita em uma carteira pode não ser aceita por outra, e os próprios signmessage e verifymessage do Bitcoin Core ainda aceitam apenas endereços P2PKH legados.
Como a BIP-322 funciona
A BIP-322 calcula um hash com tag da mensagem e o coloca no script de entrada de uma transação virtual to_spend, cuja única saída tem o scriptPubKey do endereço. Uma segunda transação, to_sign, gasta essa saída para um OP_RETURN. O signatário produz o mesmo witness que a carteira dele produziria para gastar os fundos do endereço; o verificador reconstrói as duas transações e executa o interpretador de scripts. Como to_spend gasta uma entrada inexistente, nenhuma das duas transações jamais pode ser válida na rede.
O formato simples (smp) traz apenas a pilha de witness. O formato completo (ful) traz a transação to_sign inteira, necessária para scripts com bloqueios de tempo ou entradas sem witness; nesse caso, o verificador informa o horário e a idade a partir dos quais a prova é válida. A prova de fundos (pof) é uma PSBT finalizada que também gasta UTXOs reais, então a assinatura mostra o controle de moedas específicas. O formato legado da BIP-137 continua permitido apenas para endereços P2PKH.
Os dois formatos, conferidos com os vetores de teste da BIP-322
O script abaixo usa a chave privada (em WIF) e o endereço dos vetores de teste básicos da BIP-322. A chave foi publicada ali para testes, então qualquer pessoa pode gastar o que for enviado a esse endereço, e ele nunca deve conter fundos. O script cria e verifica uma assinatura BIP-137 para “Hello World”, reconstrói o hash da mensagem da BIP-322 e os IDs das duas transações virtuais, e verifica a assinatura smp publicada nos vetores com um hash de assinatura da 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
O hash da mensagem e os IDs das duas transações são iguais aos valores de basic-test-vectors.json, e a assinatura publicada passa na verificação. A assinatura BIP-137 é determinística (RFC 6979), então a mesma chave e a mesma mensagem sempre geram os mesmos 88 caracteres; o cabeçalho 40 significa P2WPKH com ID de recuperação 1. O verificador de mensagens Bitcoin verifica os dois formatos para qualquer endereço.
O que muda para quem implementa
- A verificação é mais pesada: a BIP-322 exige serialização de transações, cálculo do hash de assinatura para cada tipo de script (legado, BIP-143 para SegWit v0, BIP-341 para Taproot) e, idealmente, um interpretador de scripts completo. Bibliotecas como o btcd (a origem dos vetores de teste) oferecem isso.
- A assinatura reaproveita o código de transações: carteiras e dispositivos de hardware assinam
to_signcomo qualquer outra transação, muitas vezes como uma PSBT, então coordenadores de multisig e carteiras Taproot funcionam sem código especial para mensagens. - Detecção do formato: a versão 1.0.0 tornou obrigatório o prefixo de três letras, uma mudança incompatível; os verificadores podem tratar assinaturas sem prefixo como
smp, por compatibilidade com implementações anteriores. - Sem recuperação da chave: o verificador precisa conhecer o endereço, que normalmente é informado de qualquer forma.
- Os mesmos limites de qualquer assinatura: nenhum dos formatos prova que o signatário ainda controla as moedas, nem que ele não assinaria em nome de outra pessoa.
Quando escolher cada uma
- Endereços Taproot, P2WSH, multisig ou de script: BIP-322; a BIP-137 não consegue representá-los.
- Corretoras e custodiantes que verificam endereços de saque (verificações da travel rule): aceite BIP-322 para todos os tipos e BIP-137 para P2PKH e SegWit de chave única.
- Prova de reservas ou de UTXOs específicos: prova de fundos da BIP-322 (
pof). - Endereços P2PKH legados e carteiras antigas: a BIP-137 continua válida e é o que o Bitcoin Core produz.
- Login com uma carteira Bitcoin: BIP-322 simples sobre uma mensagem que inclua domínio, nonce e expiração, pelos mesmos motivos do Sign-In with Ethereum.
Notas de migração
- Verifique os dois formatos: tente a BIP-322 quando a assinatura tiver prefixo ou o endereço não for P2PKH; recorra à BIP-137 para assinaturas de 65 bytes com cabeçalhos 27–42.
- Normalize o tratamento do cabeçalho: aceite assinaturas SegWit cujo cabeçalho esteja na faixa de P2PKH comprimido apenas se a chave recuperada corresponder ao endereço SegWit, e documente essa escolha.
- Emita assinaturas com prefixo quando assinar com a BIP-322, como a versão 1.0.0 exige.
- Teste com os vetores publicados (
basic-test-vectors.json,generated-test-vectors.json), incluindo os casos de erro de mensagem errada, endereço errado e prefixo inválido.
Referências
- BIP-322, formato genérico de mensagem assinada e vetores de teste básicos
- BIP-137, assinaturas de mensagens com chaves privadas
- BIP-340, assinaturas Schnorr e hashes com tag
- BIP-143, hash de assinatura do SegWit v0 e BIP-341, Taproot
- BIP-174, PSBT
- Pull request 2521 do btcd, suporte completo à BIP-322