Bitcoin Message Signing
From Signatures to Private Proofs
Ben Knab
Outline
Proposal & motivation
Proposed April 24, 2011 on BitcoinTalk
"...send message to your customer and to prove it really comes from you..."
Two proposed RPC commands
signmessage <bitcoinaddress> <message>
verifymessage <pubkey> <sign> <message>Reception
Suggested use cases
"Anyway, it seems to be very useful. Especially for OTC."
"...purchase information via email, signed by the sending address."
Concerns
"…signing arbitrary data can be dangerous…"
"This signed message is not intended to form a contract"
Discussion summary on August 9, 2011
"Everybody likes this feature, ... very close to being ready for inclusion."
Rework & merge
- Verify against an address
- The public key is recovered from the signature and message hash.
verifymessage <address> <signature> <message>- One compact signature
- Separate public key and hex signature → 65-byte signature encoded as base64.
- Keep the hashing simple
- A fixed
"Bitcoin Signed Message:\n"prefix; no extra message nonce. - Timeline
- Reworked on September 20, 2011
- Merged on September 30, 2011
- October: Eligius uses signed messages to authorize Namecoin payouts.
- Released in Bitcoin 0.5.0 on November 21, 2011
Signing & verification
- Signing
signmessage <address> <message>- Hash the message with the Bitcoin message prefix.
- Use the address's private key for recoverable ECDSA.
- Header: recovery ID (0–3) and compressed/uncompressed public-key flag.
- Encode the header and signature as one base64 string.
- Verification
verifymessage <address> <signature> <message>- Recover the public key from the signature and message hash.
- Build its P2PKH address and compare it with the expected address.
Quiet years
- P2SH activates
- April 1, 2012.
signmessagecan't sign for P2SH addresses. - Examples
- Bitcoin-OTC signed logins, 2012
- Armory's own signed-text formats, 2013
- BitcoinTalk addresses staked for account recovery, 2015
- Keys for 145 addresses in Craig Wright's court filing sign one message:
"Craig Steven Wright is a liar and a fraud."
SegWit
The gap now reaches ordinary single-key wallets.
- New single-key addresses
- Nested SegWit and native SegWit, still one key each
- Litecoin activates May 10, 2017
- Bitcoin activates August 24, 2017
- Demand
- Trezor users want to sign with Litecoin SegWit addresses.
"more and more people asking"
- signmessage refuses them
- Trezor #169 opens June 6, 2017
- Bitcoin Core #10542 opens the same day
Conceptual pushback
Trezor needs it now. Core asks for a BIP.
- June 2017: Core developers reply within hours
"...[P2SH] doesn't have an associated public or private key..."
"It works with keys, not addresses…"
"...it is intentional... I would recommend writing a BIP..."
- Weeks of silence
- No new comments in #10542
"...I suggest we just disable the sign/verify for segwit accounts for now..."
- July 2017: Trezor fixes it alone
"...do something simple like encoding the address type in
v."- Bitcoin's SegWit activation is five weeks away
- Trezor posts its header-byte plan on July 18, 2017
Wallets go their own way
No BIP, no Core support. Each wallet picks its own format.
- Trezor: new header bytes
- 35–38 for nested SegWit, 39–42 for native SegWit
- Electrum: old header bytes
- Keeps the legacy header; the verifier tries each address type
- Incompatible
- Trezor to Electrum, December 2017:
"you should have used the scheme proposed in [#10542]"
- Neither verifies the other's SegWit signatures until 2022
Sparrow: choose a signing format
BIP137: documenting Trezor's format
- 2019: document Trezor's existing format
- Same message hash, recoverable ECDSA, and 65-byte signature
- 35–38 for nested SegWit; 39–42 for native SegWit
- Wallets adopt both conventions
- BIP137 and Electrum formats spread across wallets and services.
- Bitcoin Core retains P2PKH-only message signing.
- Keep the single-key scope
- No arbitrary scripts or multisig
Then Taproot arrives
- Traditional signing and the SegWit extensions recover an ECDSA public key.
- Taproot's Schnorr signatures do not support public-key recovery.
- Another header cannot fix this.
BIP322: generic message signing
A broader proposal was already underway in 2018.
- Goal: make message signing work for any script.
- How:
- Bind a proof to the message and the address's spending script.
- Supply the required signatures and data; verify the script.
- Proposed by Karl-Johan Alm in 2018; virtual transactions arrive in 2020.
BIP322 changes, Core support stalls
- October 2020: virtual transactions
- Replace the custom proof container with two virtual transactions.
- Reuse transaction signing and script validation.
- 2021: clearer verification rules
- Separate simple and full proofs.
- Two Bitcoin Core implementations, neither merged
- #16440, 2019–2020: closes to adapt to BIP simplifications.
- #24058, 2022–2023: a simpler attempt; closes August 3, 2023.
October 2025
"the effort required [...] vs the interest [...] did not at all match up."
Multisig gets left behind
A Bitcoin association's report, May 2024
- Regulatory context
- Swiss FINMA guidance requires checks on control of external wallets.
- Services use signed messages; the guidance does not mandate this method.
- The goal: shared control of the treasury
- An unnamed Bitcoin association collects dues through Swiss Bitcoin Pay.
- It wants to receive those payments into a multisig wallet.
- The obstacle: signing for the receiving address
- Swiss Bitcoin Pay requires a signed message to register the address.
- The association cannot find a wallet that can sign for its multisig.
How should message signing move forward?
- Design
- Each new address type needs another special case.
- Single-key formats cannot cover multisig or arbitrary scripts.
- Meaning
- Receiving payments, proving funds, and proving past payments are distinct.
- A valid signature does not establish spending authority or a past sender.
- Apps must define what users sign and what verification proves.
- KYC
- Services use message signing for withdrawal checks it was not designed for.
- Some developers oppose enabling KYC through this feature.
- 2023: #27515 proposes removal; closes as controversial, without a change.
2026: BIP322 reaches completion
After years in Draft, renewed work closes practical gaps in the specification.
- Defines PSBT-based message signing.
- Adds the message to PSBTs, with commitment checks and user review.
- Packages funds proofs with previous-output data in finalized PSBTs.
- Labels Simple (
smp), Full (ful), and proof of funds (pof). - Clarifies script support and
scriptSigrequirements. - Expands test vectors to cover more scripts.
- Allows
SIGHASH_DEFAULTfor Taproot. - Requires a challenge address for funds proofs; clarifies sequence rules.
May 2026: Complete, eight years after the proposal.
Simple signing
BIP322 2.1.0, simple format, Taproot key path (P2TR).
Message: "I love K-pop"
Address: bc1pjvtc2mkj9vmfneuj7w9dsqle70a040ms5tyfswhhz4vjyskznj5ql45vlj
(address, message) = ("bc1pjv", "I love K-pop")
privateKey = getSigningKey(address)
messageHash = taggedHash("BIP0322-signed-message", message)
messageChallenge = addressToScript(address)
toSpend = buildToSpend(messageHash, messageChallenge)
toSign = buildToSign(toSpend)
witness = signP2trKeypath(toSpend, toSign, privateKey)
signature = "smp" || base64Encode(serializeWitness(witness))
The shared demonstration key is the internal key. With no script tree, apply the BIP341 TapTweak to the even-Y internal public key and the matching private scalar before signing.
Internal x-only public key: e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13
tweak = taggedHash("TapTweak", internalPublicKey): 14dca26b1fe338d06dc934eaddaeffcbed89965982cad2c01ef44840cbb1869d
Output x-only public key: 9317856ed22b3699e792f38ad803f9f3fafabf70a2c8983af715592242c29ca8
The Bech32m address carries witness version 1 and this 32-byte output key. The script is OP_1 PUSH32[outputPublicKey].
privateKey = 4
tagHash = sha256(utf8("BIP0322-signed-message"))
messageHash = sha256(tagHash || tagHash || utf8(message))
No message length prefix, null terminator, or legacy message prefix.
Message hash: 2256ff3647837603415883b635e0da3b6c0b8fd16ba78e5166bdc1e4731ae628
Address script: 51209317856ed22b3699e792f38ad803f9f3fafabf70a2c8983af715592242c29ca8
buildToSpend creates version 0, locktime 0, one input with a zero previous-transaction hash, output index 0xffffffff, sequence 0, scriptSig = OP_0 PUSH32[messageHash], and an empty witness. Its sole output pays 0 sat to messageChallenge.
toSpend.txid = 76663a262f6d65a661b4b6f56829fddf87a28c0eff0e581c657d3aba97bf2e4b
buildToSign creates version 0, locktime 0, one input spending toSpend.txid:0, sequence 0, empty scriptSig and initially empty witness. Its sole output pays 0 sat to OP_RETURN. The helper returns only toSign. Signing and validation read the previous output's value and script from toSpend.vout[0].
toSign.txid = 758c9a96df38493089f6bc53929c3446878cd8e9f28e2860e254db0ffa5504e5
Neither transaction is broadcast. The synthetic input prevents this transaction pair from spending real coins. These message proofs need no blockchain lookup.
signP2trKeypath applies the key tweak, then signs input 0's BIP341 transaction digest using Schnorr and SIGHASH_DEFAULT. The digest commits to all input outpoints, amounts, scripts, sequences, all outputs, version, locktime, and the input index. It differs from the tagged message hash.
Transaction signature hash: c42bfbf93baa2871e37b1daed12a6ba3695519baddab375924492dfde955f2a6
witness = [schnorrSignature]
Schnorr signature: 3dd84d47c7aa005768f58e6879278f6e98221c9f6606021b5f35b0370fdfb13fe63907978524a1df7c14ea335515d3fa635b7fd3f7f38c741b03b9d9f7a44ea9
serializeWitness writes CompactSize item count 1, then length 64 and the signature: 66 bytes total. SIGHASH_DEFAULT is implicit; append no sighash byte. The public key comes from the address, not the witness. This key-path proof includes no script, control block, or annex. The fixture uses all-zero BIP340 auxiliary randomness.
Serialized witness: 01403dd84d47c7aa005768f58e6879278f6e98221c9f6606021b5f35b0370fdfb13fe63907978524a1df7c14ea335515d3fa635b7fd3f7f38c741b03b9d9f7a44ea9
Signature: smpAUA92E1Hx6oAV2j1jmh5J49umCIcn2YGAhtfNbA3D9+xP+Y5B5eFJKHffBTqM1UV0/pjW3/T9/OMdBsDudn3pE6p
Simple proofs require native SegWit, no added inputs, and zero version, locktime, and sequence. P2WPKH, P2WSH, and Taproot script paths can also use simple proofs without timelocks. Full proofs carry the signed transaction with a fulprefix; funds proofs carry a finalized PSBT with pof.
Simple verification
- Decode: read the prefix and decode the proof.
- Rebuild: construct toSpend and toSign from the message and address.
- Insert: put the witness into toSign.
- Structure: check the BIP322 transaction structure.
- Script: check spending conditions, here a valid Taproot signature.
| toSpend | |||
|---|---|---|---|
| txid | 76663a | ||
| input | 0 | prevout | 000000:ffffffff |
| scriptSig | OP_0 PUSH32[2256ff] | ||
| witness | |||
| output | 0 | value | 0 |
| script | OP_1 PUSH32[931785] | ||
| toSign | |||
|---|---|---|---|
| txid | 758c9a | ||
| input | 0 | prevout | 76663a:0 |
| scriptSig | |||
| witness | [3dd84d] | ||
| output | 0 | value | 0 |
| script | OP_RETURN | ||
Full signing & verification
900000 OP_CHECKLOCKTIMEVERIFY OP_DROP 144 OP_CHECKSEQUENCEVERIFY OP_DROP OP_2 <A> <B> OP_2 OP_CHECKMULTISIG- This P2SH example puts two signatures and a redeem script in scriptSig.
- Version, locktime, and sequence carry its time conditions.
- Encode the signed toSign transaction with the ful prefix:
fulAgAAAAHxw5YZ
- Valid: the script checks pass; report the absolute and relative timelocks.
- Invalid: a check fails, such as a bad signature or insufficient locktime.
- Inconclusive: the verifier lacks script support.
- Some general script interpreters exist, but wallets use templates to verify common scripts.
| toSpend | |||
|---|---|---|---|
| txid | 433a68 | ||
| version | 0 | ||
| locktime | 0 | ||
| input | 0 | prevout | 000000:ffffffff |
| sequence | 0 | ||
| scriptSig | OP_0 PUSH32[2256ff] | ||
| witness | |||
| output | 0 | value | 0 |
| script | a914a8 | ||
| toSign | |||
|---|---|---|---|
| txid | 90424d | ||
| version | 2 | ||
| locktime | 900000 | ||
| input | 0 | prevout | 433a68:0 |
| sequence | 144 | ||
| scriptSig | 004730 | ||
| witness | |||
| output | 0 | value | 0 |
| script | OP_RETURN | ||
Proof of funds
- Input 0 spends the same virtual message challenge.
- Extra inputs identify the coins; PSBT carries their amounts and scripts.
- Sign every input; encode the finalized PSBT with the pof prefix:
pofcHNidP8BAGYA
- Check the message challenge and each coin input's spending conditions.
- The verifier checks coin existence and unspentness against chain state.
| coin | |
|---|---|
| outpoint | 1d8731:0 |
| amount | 100000 |
| script | 512093 |
| psbt | |||||||||
|---|---|---|---|---|---|---|---|---|---|
| tx | data | ||||||||
| txid | a2158e | message | "I love K-pop" | ||||||
| input | 0 | prevout | 76663a:0 | inputs | 0 | witnessUtxo | amount | 0 | |
| scriptSig | script | 512093 | |||||||
| 1 | prevout | 1d8731:0 | witness | [49ef24] | |||||
| scriptSig | 1 | witnessUtxo | amount | 100000 | |||||
| output | 0 | value | 0 | script | 512093 | ||||
| script | OP_RETURN | witness | [3e13fe] | ||||||
Wallets
| Wallet | Sign | Scripts |
|---|---|---|
| Knots | Simple / full | Broader script support |
| Sparrow | Simple | P2WPKH / P2TR key-path |
| Xverse | Simple | P2WPKH / P2TR key-path |
| UniSat | Simple | P2WPKH / P2TR key-path |
| Coldcard Mk4/Mk5/Q | Simple / full / funds via PSBT | Common singlesig and multisig |
| Keystone 3 Pro | Simple via UniSat | P2WPKH / P2TR key-path |
| Ledger | Simple via Xverse | P2WPKH |
What can we keep private?
- What the verifier learns
- Your address, which it can use to look up coins and transaction history.
- Any public keys and spending scripts revealed by the proof.
What can we realistically keep hidden using existing software?
Greenhold
My experiment with existing building blocks
- BIP322: spending keys stay in the wallet.
- RISC Zero: prove that the checking program ran successfully.
- Utreexo: membership proofs checked against Utreexo state.
- Application: minimum amount and coin age; other checks are possible.
- The claim
- I can sign for a coin worth at least the chosen minimum.
- It stayed unspent from the starting block to the ending block.
Prepare and sign
- Choose: coin, minimum amount, starting block
- Fetch: coin data + Utreexo membership proof at the tip
- Save: public claim + private witness (coin data + Utreexo membership proof)
- Sign: message containing the claim hash
Prove
- Load: public claim, private witness, wallet signature
- Fetch: block headers + ending-block Utreexo state
- Check in RISC Zero:
- Utreexo membership proof against Utreexo state
- Wallet signature, minimum amount, coin age
- Export: receipt
- Public receipt
- ZK proof
- Minimum amount + challenge hash
- Starting/ending block heights + hashes
- Chain-state hash: block headers + Utreexo state
Verify
- Verify: receipt against expected image ID
- Fetch independently: block headers + ending-block Utreexo state
- Compare: chain-state ↔ receipt
- Expected image ID
- Identifies the exact checking program
- Practical limits
- Narrow claims: fewer possible coins
- Coin reuse: no way to tell whether proofs use the same coin.
- Program updates: new image ID
Similar projects
- OutputZero: the single-key example
- Proves control of an unspent Taproot coin while hiding which coin.
- Uses Utreexo and RISC Zero; publishes a private-key hash to link repeated use.
- og-zkp
- Proves a past payment to a hidden address and links it to a claimed identity.
- Uses RISC Zero; reveals the identity and payment month.
- aut-ct
- Private amount ranges and registration with repeat-use checks.
- The same key yields the same ID within an app, which can reject repeats.
Recap
- A simple idea stalled for years: demand unclear, even its existence debated.
- Complete is not done: generic script interpreters, wallet integration.
- Every gap means churn: people change how they hold coins to pass a check.
- Privacy is harder: a new spec or extension, wallet changes, back to debate.