Bitcoin Message Signing

From Signatures to Private Proofs

Ben Knab

Hoseki

Outline

  1. Traditional

  2. SegWit

  3. Generic

  4. Using message proofs

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. signmessage can'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

Sparrow's format selector: Standard (Electrum), BIP137 (Trezor), and BIP322 (Simple).

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 scriptSig requirements.
  • Expands test vectors to cover more scripts.
  • Allows SIGHASH_DEFAULT for 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
txid76663a
input0prevout000000:ffffffff
scriptSigOP_0 PUSH32[2256ff]
witness
output0value0
scriptOP_1 PUSH32[931785]
toSign
txid758c9a
input0prevout76663a:0
scriptSig
witness[3dd84d]
output0value0
scriptOP_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
txid433a68
version0
locktime0
input0prevout000000:ffffffff
sequence0
scriptSigOP_0 PUSH32[2256ff]
witness
output0value0
scripta914a8
toSign
txid90424d
version2
locktime900000
input0prevout433a68:0
sequence144
scriptSig004730
witness
output0value0
scriptOP_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
outpoint1d8731:0
amount100000
script512093
psbt
txdata
txida2158emessage"I love K-pop"
input0prevout76663a:0inputs0witnessUtxoamount0
scriptSigscript512093
1prevout1d8731:0witness[49ef24]
scriptSig1witnessUtxoamount100000
output0value0script512093
scriptOP_RETURNwitness[3e13fe]

Wallets

WalletSignScripts
KnotsSimple / fullBroader script support
SparrowSimpleP2WPKH / P2TR key-path
XverseSimpleP2WPKH / P2TR key-path
UniSatSimpleP2WPKH / P2TR key-path
Coldcard Mk4/Mk5/QSimple / full / funds via PSBTCommon singlesig and multisig
Keystone 3 ProSimple via UniSatP2WPKH / P2TR key-path
LedgerSimple via XverseP2WPKH

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

  1. Choose: coin, minimum amount, starting block
  2. Fetch: coin data + Utreexo membership proof at the tip
  3. Save: public claim + private witness (coin data + Utreexo membership proof)
  4. Sign: message containing the claim hash

Prove

  1. Load: public claim, private witness, wallet signature
  2. Fetch: block headers + ending-block Utreexo state
  3. Check in RISC Zero:
    • Utreexo membership proof against Utreexo state
    • Wallet signature, minimum amount, coin age
  4. Export: receipt
Public receipt
  • ZK proof
  • Minimum amount + challenge hash
  • Starting/ending block heights + hashes
  • Chain-state hash: block headers + Utreexo state

Verify

  1. Verify: receipt against expected image ID
  2. Fetch independently: block headers + ending-block Utreexo state
  3. 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.

Thank you!

Contact:

QR code linking to https://benknab.com/decks/bitcoin-message-signing-from-signatures-to-private-proofs.html

bitcoin++ Seoul 2026

Slide 1 of 27: From signatures to private proofs

SlidesG