Pular para o conteúdo
ecdsa.com
← Todos os artigos

Assinatura de mensagens Bitcoin: BIP-137 e BIP-322

“Assine esta mensagem com seu endereço” parece uma operação só. Na verdade, são dois formatos incompatíveis, três convenções de cabeçalho e um padrão que mudou de codificação este ano. Veja o que cada formato assina, quais carteiras produzem o quê, e verificadores conferidos com o Bitcoin Core e com os próprios vetores de teste da BIP.

Resposta curta. Existem dois formatos. O legado, especificado a posteriori como BIP-137, é uma assinatura ECDSA recuperável de 65 bytes sobre "\x18Bitcoin Signed Message:\n" ‖ length ‖ message; ele só prova o controle de uma chave, e o primeiro byte (de 27 a 42) diz qual tipo de endereço o signatário quis indicar. O Bitcoin Core só assina nesse formato para endereços P2PKH legados, e as carteiras divergem quanto ao cabeçalho para endereços SegWit. A BIP-322 (status Complete) prova o controle de um endereço: ela monta uma transação virtual que gasta a partir do endereço, e a assinatura é o witness desse gasto; por isso, funciona para P2WPKH, P2TR, multisig e scripts. Desde a versão 2.0.0 da BIP, as assinaturas levam um prefixo: smp (simples), ful (completa) ou pof (prova de fundos). Verifique com uma biblioteca que entenda o prefixo. Aceite também assinaturas simples sem prefixo vindas de carteiras mais antigas.

O formato legado (BIP-137)

O digest é o SHA-256 duplo de um prefixo fixo, do tamanho da mensagem como varint do Bitcoin e dos bytes da mensagem. A assinatura é header ‖ r ‖ s, com 65 bytes, normalmente codificada em base64. O cabeçalho traz o ID de recuperação (de 0 a 3), que permite ao verificador calcular a chave pública a partir da assinatura, mais uma constante para o tipo de chave e de endereço (BIP-137, status Deployed):

CabeçalhoSignificado
27-30P2PKH, chave pública não comprimida
31-34P2PKH, chave pública comprimida
35-38P2SH-P2WPKH (SegWit aninhado)
39-42P2WPKH (SegWit nativo, bc1q…)

A verificação recupera a chave pública, deriva o endereço do tipo que o cabeçalho indica e compara os dois. Não há transação nem script: o formato não consegue expressar “este endereço multisig” ou “este endereço Taproot”, só “o detentor desta chave”. A própria BIP-137 remete à BIP-322 para tudo o que vai além disso.

O módulo abaixo implementa o formato legado e, mais adiante, as assinaturas simples da BIP-322 para P2WPKH, com @noble/curves e @scure/base:

btcmsg.mjs (@noble/curves 2.4.0, @noble/hashes 2.4.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 { 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 });
}

O suporte do Bitcoin Core

Os RPCs signmessage e verifymessage do Bitcoin Core implementam o formato legado, e só para endereços legados. Um nó regtest, com um endereço de cada tipo:

run.sh (bitcoin/bitcoin:31.1)
#!/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
Saída
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 

Com todos os endereços SegWit e Taproot, o Core retorna Address does not refer to key, tanto ao assinar quanto ao verificar uma assinatura feita por outra pessoa. O primeiro byte da assinatura legada, 31 ou 32 nestas execuções (H ou I em base64), indica uma chave P2PKH comprimida.

Nossa implementação produz os mesmos bytes que o Bitcoin Core. Com uma chave gerada para a execução e importada na carteira regtest como descritor pkh(), as duas assinaram a mesma mensagem:

Saída: legacy-prepare.mjs e depois crosscheck.sh em bitcoin/bitcoin:31.1
address mk1giEaBaAFPXNRMQcZNptPAp9kJmttKyp
core signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
our signature:  H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
identical: yes
core verifymessage(our signature): true

As assinaturas são idênticas porque as duas implementações usam nonces determinísticos (RFC 6979) e normalização low-S; um signatário que usa nonces aleatórios produz a cada vez uma assinatura diferente e igualmente válida.

O problema do cabeçalho em endereços SegWit

As carteiras que assinam mensagens no formato legado para endereços SegWit não chegam a um acordo sobre o cabeçalho. O Electrum assina com cabeçalho de 27 a 34 para todos os tipos de endereço e, ao verificar, testa todos os tipos de endereço de chave única que a chave recuperada poderia ter (electrum/bitcoin.py). O Trezor segue a BIP-137 e soma 4 para P2SH-P2WPKH e 8 para P2WPKH, com uma opção (no_script_type) para produzir, em vez disso, a forma do Electrum (trezor-firmware, sign_message.py). Por isso, um verificador precisa escolher entre uma verificação estrita (o cabeçalho precisa indicar o tipo do endereço) e uma permissiva (qualquer endereço da chave recuperada). As duas convenções, com a mesma chave e a mesma mensagem, no nosso verificador e na bip322-js 4.0.0:

headers.mjs
// 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}`);
}
Saída (node:24.22.0-bookworm-slim)
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

A verificação permissiva aceita as duas e também aceita a mesma assinatura como prova para o endereço P2PKH da chave. Esse é o significado honesto de uma assinatura legada: quem detém esta chave, para todos os endereços derivados dela. A verificação estrita rejeita todas as assinaturas SegWit no estilo do Electrum. Se você precisa aceitar assinaturas legadas para endereços SegWit, verifique de forma permissiva e registre que o que você verificou foi o controle de uma chave.

BIP-322: como provar o controle de um endereço

A BIP-322 (bip-0322.mediawiki, status Complete, versão 2.0.0) reaproveita o mecanismo de transações em vez de inventar um formato de mensagem. São montadas duas transações virtuais:

  • to_spend: versão 0, uma entrada que gasta o outpoint inexistente 000…000:FFFFFFFF com o scriptSig OP_0 PUSH32[message_hash], uma saída de 0 satoshis cujo scriptPubKey é o endereço que se quer provar. message_hash é um hash com tag da BIP-340, SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message), com a tag BIP0322-signed-message.
  • to_sign: versão 0, uma entrada que gasta to_spend:0, uma saída de 0 satoshis para OP_RETURN.

Assinar é gastar to_spend:0 em to_sign, como uma carteira gastaria qualquer saída daquele endereço. A verificação confere esse gasto com as regras padrão de script (SIGHASH_ALL ou o padrão do Taproot, low-S e DER estrito, CLEANSTACK e outras). Como nenhuma das duas transações é válida na rede, uma assinatura não pode ser transmitida para movimentar fundos.

Codificações:

VariantePrefixoConteúdoUso
Simplessmpa pilha de witness, em base64SegWit nativo e Taproot, sem bloqueios de tempo
Completafula transação to_sign assinada inteira, em base64qualquer script, inclusive SegWit aninhado, legado e bloqueios de tempo
Completa (prova de fundos)pofuma PSBT finalizada de to_sign com entradas adicionaisprovar o controle de UTXOs específicos
Legadanenhuma assinatura BIP-137permitida só para endereços P2PKH legados

Os prefixos de variante são novos na versão 2.0.0, e agora os signatários DEVEM incluí-los. Por compatibilidade, “um verificador pode presumir a variante simples na ausência de prefixo”, e é assim que as assinaturas de carteiras feitas com base no texto anterior continuam funcionando. A verificação tem três resultados, não dois: válida, inválida ou inconclusiva, quando o verificador não consegue avaliar os scripts envolvidos.

Multisig e carteiras de hardware: assinatura via PSBT

Em um endereço multisig, o witness precisa de assinaturas de várias chaves, guardadas por pessoas ou dispositivos diferentes. A BIP-322 resolve isso com as ferramentas que os usuários de multisig já têm: a transação to_sign circula como uma PSBT com um novo campo global, PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (tipo 0x09), que carrega a mensagem. Um signatário que encontra esse campo, uma primeira entrada que gasta a saída 0 da to_spend correspondente e uma única saída OP_RETURN de valor zero deve mostrar ao usuário a mensagem e o endereço, e não uma transação, e depois assina como assinaria um gasto. Cada cossignatário adiciona uma assinatura parcial; o resultado finalizado vira uma assinatura ful. Um dispositivo que não reconhece o campo vê o que parece ser uma transação que gasta 0 satoshis; por isso, o suporte das carteiras a esse fluxo importa mais do que a criptografia.

A BIP lista duas implementações de referência: um pull request do Bitcoin Core com suporte básico (#24058), que foi fechado sem merge, e um pull request do btcd com suporte completo (#2521), a origem dos vetores de teste gerados e ainda aberto em 20 de outubro de 2026. Nenhum dos dois faz parte de uma versão lançada, e isso explica em parte por que as carteiras e bibliotecas da tabela abaixo que oferecem suporte à BIP-322 a implementam, cada uma, por conta própria.

Conferência com os vetores de teste da BIP

A BIP-322 traz vetores de teste básicos: hashes de mensagem e IDs de transação para três mensagens, e assinaturas simples para um endereço P2WPKH. Para verificá-las, bastam o endereço, a mensagem e a assinatura. As mesmas assinaturas, com e sem o novo prefixo, também foram passadas à bip322-js 4.0.0:

vectors.mjs
// 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)}`);
Saída (node:24.22.0-bookworm-slim)
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

A implementação acima reproduz todos os hashes, verifica todas as assinaturas P2WPKH simples, com ou sem prefixo, e rejeita o vetor de erro. A bip322-js 4.0.0, a biblioteca JavaScript amplamente usada, não entende o prefixo smp: ela decodifica o prefixo como base64 e lança buffer too small, enquanto a mesma assinatura sem o prefixo passa na verificação. Por isso, a verificação de assinaturas produzidas por carteiras mais novas vai falhar nos serviços construídos sobre ela até que a biblioteca seja atualizada; remover um prefixo conhecido antes de chamá-la é uma solução provisória viável para assinaturas simples.

Quem produz e quem aceita o quê

Segundo o código-fonte de cada projeto, ou uma execução de teste onde indicado, em outubro de 2026:

SoftwareLegado (BIP-137)BIP-322Observações
Bitcoin Core 31.1só P2PKHnãotestado acima
Electrum (master)P2PKH, P2WPKH, P2SH-P2WPKH, cabeçalho 27-34nãoverifica as duas convenções de cabeçalho; recusa endereços que não sejam de chave única
Firmware do Trezor (main)P2PKH, P2SH-P2WPKH, P2WPKH, cabeçalhos BIP-137nãoTaproot: “Unsupported script type”
Sparrow (drongo master)simsimples, com os prefixos smp/ful/pofverificação: variante simples, P2WPKH e P2TR de chave única
bip322-js 4.0.0sim, permissiva ou estritasimples, P2WPKH, P2SH-P2WPKH, P2TR de chave únicasem prefixos; veja a nota acima

As consequências práticas: um endereço Taproot só pode ser provado com a BIP-322; um endereço multisig não pode ser provado de jeito nenhum com o formato legado; e um serviço que queira aceitar provas de usuários de todas essas carteiras precisa aceitar assinaturas legadas (de forma permissiva, para SegWit), assinaturas BIP-322 simples sem prefixo e as com prefixo.

Como verificar em um serviço

  1. Defina a mensagem do seu lado. Inclua seu domínio, um nonce ou timestamp e a finalidade ("Proof of ownership for example.com, nonce 7f3a…"), e aceite cada nonce uma única vez, exatamente como no Sign-In with Ethereum. Uma assinatura sobre um texto genérico pode ser reapresentada a qualquer outro serviço que peça o mesmo texto.
  2. Compare os bytes exatos. Os dois formatos assinam a mensagem bruta. A interface do Electrum remove os espaços em branco no início e no fim antes de assinar; uma quebra de linha no fim, vinda de um copiar e colar, ou finais de linha CRLF produzem outro hash e uma “assinatura inválida”.
  3. Detecte o formato nesta ordem: um prefixo smp, ful ou pof; uma string base64 que se decodifica em exatamente 65 bytes, com o primeiro byte entre 27 e 42 (legado); caso contrário, tente uma assinatura BIP-322 simples sem prefixo.
  4. Decida o que uma assinatura legada prova para endereços SegWit: a verificação permissiva prova o controle da chave, o que, para endereços de chave única, equivale ao controle do endereço.
  5. Trate inconclusivo como não verificado. Um verificador que não consegue avaliar um script (bloqueios de tempo, scripts incomuns) não deve informar sucesso.
  6. Para prova de fundos, consulte a blockchain. Uma assinatura pof prova o controle das saídas listadas no momento da assinatura; se elas continuam não gastas é uma consulta à parte, e a assinatura nunca prova que essas são todas as saídas de um endereço.

Fontes