Pular para o conteúdo
ecdsa.com

BIP-137 vs BIP-322: assinatura de mensagens para endereços Bitcoin

A BIP-137 assina com a chave por trás de um endereço de chave única e deixa o verificador recuperá-la. A BIP-322 prova que você poderia gastar os fundos de qualquer endereço assinando uma transação que nunca pode ser transmitida à rede.

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-137BIP-322
StatusDeployed (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 mensagemSHA-256 duplo de "\x18Bitcoin Signed Message:\n", do comprimento e da mensagemSHA-256 com tag da BIP-340, com a tag BIP0322-signed-message
O que é assinadoO hash da mensagem, diretamenteUma transação virtual to_sign que gasta uma saída virtual to_spend
Tipos de endereçoP2PKH; P2SH-P2WPKH e P2WPKH por faixas de cabeçalhoQualquer script: P2PKH, P2WPKH, P2WSH, P2TR, multisig, bloqueios de tempo
O verificador precisa deAssinatura, mensagem, endereço; a chave é recuperadaAssinatura, mensagem, endereço; um interpretador de scripts
Conteúdo da assinaturaByte de cabeçalho (27–42), r, sPilha de witness (smp), transação completa (ful) ou PSBT (pof)
Tamanho, base6488 caracteres147 para P2WPKH, 88 para P2TR, 439 para um multisig P2WSH 3 de 3 (vetores de teste)
Prova de fundosNãoSim, com entradas adicionais em uma PSBT (pof)
Vincula-se à chave públicaNão (a recuperação pode ser ajustada para chaves relacionadas)Sim, por meio do script que é satisfeito
Assinatura em carteira de hardwareModo de assinatura de mensagens, onde houver suporteAssinatura 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).

BIP-137 e BIP-322 para um endereço P2WPKH
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));
Saída
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_sign como 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

  1. 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.
  2. 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.
  3. Emita assinaturas com prefixo quando assinar com a BIP-322, como a versão 1.0.0 exige.
  4. 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

Todas as comparações