Bitcoin Message Signing
Two specifications:
- BIP137 (Hourglass) — legacy. Works only for P2PKH (uncompressed
or compressed). Output: 65-byte recoverable ECDSA + Base64.
- BIP322 (modern) — works for any output type (P2PKH, P2WPKH,
P2WSH, P2TR, even script-only). Output: a virtual PSBT-like
signature.
BIP137 (legacy)
prefix = "Bitcoin Signed Message:\n"
preamble = varint(len(prefix)) || prefix || varint(len(msg)) || msg
hash = SHA256d(preamble)
signature = recoverable_ecdsa_sign(privkey, hash) # 65 bytes
encoded = base64(signature)
Verify: recover pubkey from signature, derive P2PKH address, compare.
Limitations:
- Only P2PKH. Cannot prove ownership of a SegWit / Taproot address.
- Wallets don't always agree on the prefix (some include trailing
newline, others don't).
- Recoverable ECDSA implies pubkey isn't directly transmitted.
BIP322 (modern)
Generalizes signing by constructing a virtual transaction:
to_spend = transaction with:
version=0, locktime=0,
vin = [{ prevout: 0xffffffff..., scriptSig: OP_0 PUSH msg_hash, sequence: 0 }],
vout = [{ value: 0, scriptPubKey: <message-target scriptPubKey> }]
to_sign = transaction with:
version=0, locktime=0,
vin = [{ prevout: txid_of_to_spend, vout: 0, sequence: 0 }],
vout = [{ value: 0, scriptPubKey: OP_RETURN }]
Then sign to_sign as a normal transaction spending to_spend's
output. The signature can be:
- A full BIP322 signature with witness data, OR
- A "simple" form where only the witness/scriptSig is encoded.
Address types supported
- P2PKH, P2SH-anything, P2WPKH, P2WSH, P2TR.
- Multi-key: produce multiple sigs across signers and combine into one
BIP322 witness.
- Even script-only addresses (no key) can produce a "proof of
knowledge" of preimage.
Encoded forms
- BIP322 simple — Base64 of a witness stack only. Suitable for
P2WPKH/P2WSH/P2TR.
- BIP322 full — Base64 of
to_sign PSBT-like blob. Required for
P2SH or anything with non-witness data.
Verification flow
def verify_bip322(address, message, signature_b64):
spk = address_to_scriptPubKey(address)
msg_hash = bip322_hash(message)
to_spend = build_to_spend(spk, msg_hash)
to_sign = build_to_sign(to_spend)
apply_signature(to_sign, signature_b64)
return script_executes_successfully(to_sign, vin[0])
Common use cases
- Exchange withdraw verification — "prove you own the destination
before we send".
- Federated/KYC-free login — sign a server-issued nonce with the
Bitcoin address you want to associate with the account.
- Off-chain attestation — sign a public commitment.
- DLC oracle attestation — adapter signatures + BIP322.
Library support
bitcoinjs-lib (since 6.x) — BIP322 sign/verify.
python-bitcoinlib — BIP137 native, BIP322 via plugin.
rust-bitcoin — BIP322 in signed-message crate.
bitcoinj — BIP137; BIP322 incomplete.
Common bugs
- Mixing BIP137 prefix conventions across wallets — verify with the
same library/wallet that signed where possible.
- Trying to BIP137-sign a SegWit address by deriving an arbitrary
P2PKH from the same key → semantically wrong; use BIP322 instead.
- For Taproot: the witness stack must use
SIGHASH_DEFAULT to be
byte-identical to a real Taproot spend; some libraries default to
SIGHASH_ALL (0x01) which encodes a different sighash.
See also
1---2name: bitcoin-message-signing3description: Generic Bitcoin message signing: BIP137 (legacy P2PKH-only) and BIP322 (modern, supports any output type). Used for proof-of-ownership, withdraw verification, federated authentication, exchange KYC-free attestation. USE WHEN: implementing "sign this message with your Bitcoin address", verifying ownership of an address, building auth via signing.4---56# Bitcoin Message Signing78Two specifications:910- **BIP137** (Hourglass) — legacy. Works only for P2PKH (uncompressed11 or compressed). Output: 65-byte recoverable ECDSA + Base64.12- **BIP322** (modern) — works for any output type (P2PKH, P2WPKH,13 P2WSH, P2TR, even script-only). Output: a virtual PSBT-like14 signature.1516## BIP137 (legacy)1718```19prefix = "Bitcoin Signed Message:\n"20preamble = varint(len(prefix)) || prefix || varint(len(msg)) || msg21hash = SHA256d(preamble)22signature = recoverable_ecdsa_sign(privkey, hash) # 65 bytes23encoded = base64(signature)24```2526Verify: recover pubkey from signature, derive P2PKH address, compare.2728**Limitations**:29- Only P2PKH. Cannot prove ownership of a SegWit / Taproot address.30- Wallets don't always agree on the prefix (some include trailing31 newline, others don't).32- Recoverable ECDSA implies pubkey isn't directly transmitted.3334## BIP322 (modern)3536Generalizes signing by constructing a **virtual transaction**:3738```39to_spend = transaction with:40 version=0, locktime=0,41 vin = [{ prevout: 0xffffffff..., scriptSig: OP_0 PUSH msg_hash, sequence: 0 }],42 vout = [{ value: 0, scriptPubKey: <message-target scriptPubKey> }]4344to_sign = transaction with:45 version=0, locktime=0,46 vin = [{ prevout: txid_of_to_spend, vout: 0, sequence: 0 }],47 vout = [{ value: 0, scriptPubKey: OP_RETURN }]48```4950Then sign `to_sign` as a normal transaction spending `to_spend`'s51output. The signature can be:52- A full BIP322 signature with witness data, OR53- A "simple" form where only the witness/scriptSig is encoded.5455### Address types supported5657- P2PKH, P2SH-anything, P2WPKH, P2WSH, P2TR.58- Multi-key: produce multiple sigs across signers and combine into one59 BIP322 witness.60- Even script-only addresses (no key) can produce a "proof of61 knowledge" of preimage.6263## Encoded forms6465- **BIP322 simple** — Base64 of a witness stack only. Suitable for66 P2WPKH/P2WSH/P2TR.67- **BIP322 full** — Base64 of `to_sign` PSBT-like blob. Required for68 P2SH or anything with non-witness data.6970## Verification flow7172```python73def verify_bip322(address, message, signature_b64):74 spk = address_to_scriptPubKey(address)75 msg_hash = bip322_hash(message)76 to_spend = build_to_spend(spk, msg_hash)77 to_sign = build_to_sign(to_spend)78 apply_signature(to_sign, signature_b64)79 return script_executes_successfully(to_sign, vin[0])80```8182## Common use cases8384- **Exchange withdraw verification** — "prove you own the destination85 before we send".86- **Federated/KYC-free login** — sign a server-issued nonce with the87 Bitcoin address you want to associate with the account.88- **Off-chain attestation** — sign a public commitment.89- **DLC oracle attestation** — adapter signatures + BIP322.9091## Library support9293- `bitcoinjs-lib` (since 6.x) — BIP322 sign/verify.94- `python-bitcoinlib` — BIP137 native, BIP322 via plugin.95- `rust-bitcoin` — BIP322 in `signed-message` crate.96- `bitcoinj` — BIP137; BIP322 incomplete.9798## Common bugs99100- Mixing BIP137 prefix conventions across wallets — verify with the101 same library/wallet that signed where possible.102- Trying to BIP137-sign a SegWit address by deriving an arbitrary103 P2PKH from the same key → semantically wrong; use BIP322 instead.104- For Taproot: the witness stack must use `SIGHASH_DEFAULT` to be105 byte-identical to a real Taproot spend; some libraries default to106 `SIGHASH_ALL` (0x01) which encodes a different sighash.107108## See also109110- [psbt/SKILL.md](../psbt/SKILL.md)111- [../../wallets/hd/SKILL.md](../../wallets/hd/SKILL.md)112- [../../cryptography/schnorr/SKILL.md](../../cryptography/schnorr/SKILL.md)