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çalho | Significado |
|---|---|
| 27-30 | P2PKH, chave pública não comprimida |
| 31-34 | P2PKH, chave pública comprimida |
| 35-38 | P2SH-P2WPKH (SegWit aninhado) |
| 39-42 | P2WPKH (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:
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:
#!/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
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:
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:
// 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
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 inexistente000…000:FFFFFFFFcom o scriptSigOP_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 tagBIP0322-signed-message.to_sign: versão 0, uma entrada que gastato_spend:0, uma saída de 0 satoshis paraOP_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:
| Variante | Prefixo | Conteúdo | Uso |
|---|---|---|---|
| Simples | smp | a pilha de witness, em base64 | SegWit nativo e Taproot, sem bloqueios de tempo |
| Completa | ful | a transação to_sign assinada inteira, em base64 | qualquer script, inclusive SegWit aninhado, legado e bloqueios de tempo |
| Completa (prova de fundos) | pof | uma PSBT finalizada de to_sign com entradas adicionais | provar o controle de UTXOs específicos |
| Legada | nenhum | a assinatura BIP-137 | permitida 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:
// 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
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:
| Software | Legado (BIP-137) | BIP-322 | Observações |
|---|---|---|---|
| Bitcoin Core 31.1 | só P2PKH | não | testado acima |
| Electrum (master) | P2PKH, P2WPKH, P2SH-P2WPKH, cabeçalho 27-34 | não | verifica 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-137 | não | Taproot: “Unsupported script type” |
| Sparrow (drongo master) | sim | simples, com os prefixos smp/ful/pof | verificação: variante simples, P2WPKH e P2TR de chave única |
| bip322-js 4.0.0 | sim, permissiva ou estrita | simples, P2WPKH, P2SH-P2WPKH, P2TR de chave única | sem 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
- 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. - 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”.
- Detecte o formato nesta ordem: um prefixo
smp,fuloupof; 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. - 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.
- 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.
- Para prova de fundos, consulte a blockchain. Uma assinatura
pofprova 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
- BIP-137: assinaturas de mensagens com chaves privadas
- BIP-322: formato genérico de mensagem assinada, versão 2.0.0, e seus vetores de teste básicos
- BIP-340 (hashes com tag), BIP-143 (hash de assinatura do SegWit), BIP-174 (PSBT)
- RFC 6979: DSA e ECDSA determinísticos
- Bitcoin Core 31.1, RPCs
signmessageeverifymessage - Electrum
bitcoin.py, Trezorsign_message.py, Sparrow drongoBip322.java, bip322-js