Skip to content
ecdsa.com

BIP-137 vs BIP-322: message signing for Bitcoin addresses

BIP-137 signs with the key behind a single-key address and lets the verifier recover it. BIP-322 proves you could spend from any address by signing a transaction that can never be broadcast.

BIP-137 vs BIP-322 is the choice between Bitcoin's old and new ways to prove control of an address by signing a message. BIP-137 documents the long-standing "Bitcoin Signed Message" format: a 65-byte recoverable ECDSA signature whose header byte says which address type to derive from the recovered key. BIP-322 generalizes signing to any script: the message is committed to in a virtual transaction, and the signature is whatever witness would spend from the address, checked by the normal script interpreter.

At a glance

BIP-137BIP-322
StatusDeployed (assigned 2019; format much older)Complete since version 1.0.0, 15 April 2026; 2.0.0 on 4 June 2026
Message hashDouble SHA-256 of "\x18Bitcoin Signed Message:\n", length and messageBIP-340 tagged SHA-256 with tag BIP0322-signed-message
What is signedThe message hash directlyA virtual to_sign transaction spending a virtual to_spend output
Address typesP2PKH; P2SH-P2WPKH and P2WPKH through header rangesAny script: P2PKH, P2WPKH, P2WSH, P2TR, multisig, time locks
Verifier needsSignature, message, address; key is recoveredSignature, message, address; a script interpreter
Signature contentHeader byte (27–42), r, sWitness stack (smp), full transaction (ful) or PSBT (pof)
Size, base6488 characters147 for P2WPKH, 88 for P2TR, 439 for a 3-of-3 P2WSH multisig (test vectors)
Proof of fundsNoYes, with additional inputs in a PSBT (pof)
Commits to the public keyNo (recovery can be tweaked to related keys)Yes, through the script being satisfied
Hardware wallet signingMessage signing mode, where supportedPSBT-style transaction signing

How BIP-137 works

The signer hashes the message with a fixed prefix, signs it with ECDSA and stores the recovery ID in a header byte: 27–30 for P2PKH with uncompressed keys, 31–34 for compressed keys, 35–38 for P2SH-P2WPKH and 39–42 for native SegWit P2WPKH. The verifier uses public key recovery to compute the key from the signature and checks that it hashes to the address.

This only works where an address is the hash of a single public key. Taproot addresses commit to a tweaked key and use Schnorr signatures, which have no key recovery; multisig and other scripts have no single key at all. Header ranges for SegWit were added later and not every wallet adopted them, so a signature from one wallet may fail in another, and Bitcoin Core's own signmessage and verifymessage still accept only legacy P2PKH addresses.

How BIP-322 works

BIP-322 hashes the message with a tagged hash and places it in the input script of a virtual to_spend transaction whose single output has the address's scriptPubKey. A second transaction, to_sign, spends that output to an OP_RETURN. The signer produces whatever witness its wallet would produce to spend from the address; the verifier rebuilds both transactions and runs the script interpreter. Because to_spend spends a non-existent input, neither transaction can ever be valid on the network.

The simple format (smp) carries only the witness stack. The full format (ful) carries the whole to_sign transaction, which is needed for scripts with time locks or non-witness inputs; the verifier then reports the time and age at which the proof is valid. Proof of funds (pof) is a finalized PSBT that also spends real UTXOs, so the signature shows control of specific coins. The legacy BIP-137 format remains allowed only for P2PKH addresses.

Both formats, checked against the BIP-322 test vectors

The script below uses the private key (in WIF) and address from the BIP-322 basic test vectors. The key is published there for testing, so anyone can spend from its address and it must never hold funds. The script creates and verifies a BIP-137 signature for "Hello World", rebuilds the BIP-322 message hash and both virtual transaction IDs, and verifies the smp signature published in the vectors with a BIP-143 signature hash (Node.js 26, @noble/curves 2.3.0, @noble/hashes 2.3.0, @scure/base 2.4.0).

BIP-137 and BIP-322 for one P2WPKH address
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));
Output
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

The message hash and both transaction IDs equal the values in basic-test-vectors.json, and the published signature verifies. The BIP-137 signature is deterministic (RFC 6979), so the same key and message always give the same 88 characters; header 40 means P2WPKH with recovery ID 1. The Bitcoin message verifier checks both formats for any address.

What changes for implementers

  • Verification is heavier: BIP-322 needs transaction serialization, signature hashing for each script type (legacy, BIP-143 for SegWit v0, BIP-341 for Taproot) and ideally a full script interpreter. Libraries such as btcd (the source of the test vectors) provide this.
  • Signing reuses transaction code: wallets and hardware devices sign to_sign like any other transaction, often as a PSBT, so multisig coordinators and Taproot wallets work without special message code.
  • Format detection: version 1.0.0 made the three-letter prefix mandatory, a breaking change; verifiers may treat unprefixed signatures as smp for compatibility with earlier implementations.
  • No key recovery: the verifier must know the address, which is normally given anyway.
  • Same limits as any signature: neither format proves that the signer still controls the coins, or that the signer would not sign on someone else's behalf.

When to choose which

  • Taproot, P2WSH, multisig or script addresses: BIP-322; BIP-137 cannot express them.
  • Exchanges and custodians verifying withdrawal addresses (travel rule checks): accept BIP-322 for all types and BIP-137 for P2PKH and single-key SegWit.
  • Proof of reserves or of specific UTXOs: BIP-322 proof of funds (pof).
  • Legacy P2PKH addresses and old wallets: BIP-137 remains valid and is what Bitcoin Core produces.
  • Login with a Bitcoin wallet: BIP-322 simple over a message that includes domain, nonce and expiry, for the same reasons as Sign-In with Ethereum.

Migration notes

  1. Verify both: try BIP-322 when the signature carries a prefix or the address is not P2PKH; fall back to BIP-137 for 65-byte signatures with headers 27–42.
  2. Normalize header handling: accept SegWit signatures whose header falls in the P2PKH compressed range only if the recovered key matches the SegWit address, and document that choice.
  3. Emit prefixed signatures when you sign with BIP-322, as version 1.0.0 requires.
  4. Test with the published vectors (basic-test-vectors.json, generated-test-vectors.json), including the error cases for wrong messages, wrong addresses and bad prefixes.

References

All comparisons