Kurz gesagt. Es gibt zwei Formate. Das Legacy-Format, nachträglich als BIP-137 spezifiziert, ist eine 65-Byte-ECDSA-Signatur über "\x18Bitcoin Signed Message:\n" ‖ length ‖ message, aus der sich der öffentliche Schlüssel wiederherstellen lässt; es beweist nur die Kontrolle über einen Schlüssel, und sein erstes Byte (27 bis 42) gibt an, welchen Adresstyp der Signierer gemeint hat. Bitcoin Core signiert in diesem Format nur für Legacy-P2PKH-Adressen, und beim Header für SegWit-Adressen sind sich die Wallets uneinig. BIP-322 (Status „Complete“) beweist die Kontrolle über eine Adresse: Dabei wird eine virtuelle Transaktion gebaut, die von der Adresse ausgibt, und die Signatur ist deren Witness; daher funktioniert das für P2WPKH, P2TR, Multisig und Skripte. Seit Version 2.0.0 des BIP tragen Signaturen ein Präfix: smp (einfach), ful (vollständig) oder pof (Guthabennachweis, Proof of Funds). Prüfe mit einer Bibliothek, die das Präfix versteht, und akzeptiere einfache Signaturen ohne Präfix aus älteren Wallets.
Das Legacy-Format (BIP-137)
Der Digest ist ein doppelter SHA-256-Hash über ein festes Präfix, die Länge der Nachricht als Bitcoin-Varint und die Bytes der Nachricht. Die Signatur ist header ‖ r ‖ s, 65 Byte lang und meist base64-kodiert. Der Header enthält die Recovery-ID (0 bis 3), mit der der Verifizierer den öffentlichen Schlüssel aus der Signatur berechnen kann, dazu eine Konstante für den Schlüssel- und Adresstyp (BIP-137, Status „Deployed“):
| Header | Bedeutung |
|---|---|
| 27–30 | P2PKH, unkomprimierter öffentlicher Schlüssel |
| 31–34 | P2PKH, komprimierter öffentlicher Schlüssel |
| 35–38 | P2SH-P2WPKH (verschachteltes SegWit) |
| 39–42 | P2WPKH (natives SegWit, bc1q…) |
Die Prüfung stellt den öffentlichen Schlüssel wieder her, leitet daraus die Adresse des Typs ab, den der Header nennt, und vergleicht sie mit der angegebenen Adresse. Es gibt keine Transaktion und kein Skript: Das Format kann weder „diese Multisig-Adresse“ noch „diese Taproot-Adresse“ ausdrücken, nur „der Inhaber dieses Schlüssels“. BIP-137 selbst verweist für alles, was darüber hinausgeht, auf BIP-322.
Das folgende Modul implementiert das Legacy-Format und weiter unten einfache BIP-322-Signaturen für P2WPKH, mit @noble/curves und @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 });
}
Was Bitcoin Core unterstützt
Die RPCs signmessage und verifymessage von Bitcoin Core implementieren das Legacy-Format, und zwar nur für Legacy-Adressen. Ein Regtest-Knoten, je eine Adresse pro Typ:
#!/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
Jede SegWit- und Taproot-Adresse scheitert mit Address does not refer to key, sowohl beim Signieren als auch beim Prüfen einer Signatur, die jemand anderes erstellt hat. Das erste Byte der Legacy-Signatur, in diesen Durchläufen 31 oder 32 (in base64 H oder I), kennzeichnet einen komprimierten P2PKH-Schlüssel.
Unsere Implementierung erzeugt dieselben Bytes wie Bitcoin Core. Mit einem Schlüssel, der für den Durchlauf erzeugt und als Deskriptor pkh() in die Regtest-Wallet importiert wurde, haben beide dieselbe Nachricht signiert:
address mk1giEaBaAFPXNRMQcZNptPAp9kJmttKyp
core signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
our signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
identical: yes
core verifymessage(our signature): true
Die Signaturen sind identisch, weil beide deterministische Nonces (RFC 6979) und Low-S-Normalisierung verwenden; ein Signierer, der zufällige Nonces verwendet, erzeugt jedes Mal eine andere, ebenso gültige Signatur.
Das Header-Problem bei SegWit-Adressen
Wallets, die Nachrichten im Legacy-Format für SegWit-Adressen signieren, sind sich beim Header nicht einig. Electrum signiert für jeden Adresstyp mit Header 27 bis 34 und probiert beim Prüfen jeden Adresstyp mit einem einzelnen Schlüssel durch, den der wiederhergestellte Schlüssel haben könnte (electrum/bitcoin.py). Trezor folgt BIP-137 und addiert 4 für P2SH-P2WPKH und 8 für P2WPKH; eine Option (no_script_type) erzeugt stattdessen die Electrum-Form (trezor-firmware, sign_message.py). Ein Verifizierer muss sich deshalb zwischen einer strikten Prüfung (der Header muss den Typ der Adresse nennen) und einer lockeren (jede Adresse des wiederhergestellten Schlüssels) entscheiden. Die beiden Konventionen mit demselben Schlüssel und derselben Nachricht, geprüft mit unserem Verifizierer und mit 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
Die lockere Prüfung akzeptiert beide und akzeptiert dieselbe Signatur auch als Nachweis für die P2PKH-Adresse des Schlüssels. Das ist die ehrliche Bedeutung einer Legacy-Signatur: wer auch immer diesen Schlüssel besitzt, und zwar für jede daraus abgeleitete Adresse. Die strikte Prüfung lehnt jede SegWit-Signatur im Electrum-Stil ab. Wenn du Legacy-Signaturen für SegWit-Adressen akzeptieren musst, prüfe locker und halte fest, dass du damit die Kontrolle über einen Schlüssel geprüft hast.
BIP-322: Kontrolle über eine Adresse nachweisen
BIP-322 (bip-0322.mediawiki, Status „Complete“, Version 2.0.0) nutzt die vorhandene Transaktionsmechanik, statt ein eigenes Nachrichtenformat zu erfinden. Dazu werden zwei virtuelle Transaktionen gebaut:
to_spend: Version 0, eine Eingabe, die den nicht existierenden Outpoint000…000:FFFFFFFFmit dem scriptSigOP_0 PUSH32[message_hash]ausgibt, und eine Ausgabe über 0 Satoshi, deren scriptPubKey die nachzuweisende Adresse ist.message_hashist ein Tagged Hash nach BIP-340,SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message)mit dem TagBIP0322-signed-message.to_sign: Version 0, eine Eingabe, dieto_spend:0ausgibt, und eine Ausgabe über 0 Satoshi anOP_RETURN.
Signieren heißt, to_spend:0 in to_sign so auszugeben, wie eine Wallet jede Ausgabe dieser Adresse ausgeben würde. Die Prüfung bewertet dieses Ausgeben nach den Standard-Skriptregeln (SIGHASH_ALL oder der Standardwert von Taproot, Low-S und striktes DER, CLEANSTACK und weitere). Weil keine der beiden Transaktionen im Netzwerk gültig ist, lässt sich eine Signatur nicht ins Netzwerk senden, um Coins zu bewegen.
Kodierungen:
| Variante | Präfix | Inhalt | Verwendung |
|---|---|---|---|
| Einfach | smp | der Witness-Stack, base64 | natives SegWit und Taproot, keine Timelocks |
| Vollständig | ful | das ganze signierte to_sign, base64 | jedes Skript, auch verschachteltes SegWit, Legacy und Timelocks |
| Vollständig (Guthabennachweis) | pof | eine finalisierte PSBT von to_sign mit zusätzlichen Eingaben | Kontrolle über bestimmte UTXOs nachweisen |
| Legacy | keines | die BIP-137-Signatur | nur für Legacy-P2PKH-Adressen erlaubt |
Die Präfixe der Varianten sind neu in Version 2.0.0, und Signierer MÜSSEN sie jetzt angeben. Aus Kompatibilitätsgründen gilt: „Ein Verifizierer könnte bei fehlendem Präfix die einfache Variante annehmen“ – so funktionieren Signaturen von Wallets, die nach der früheren Fassung gebaut wurden, weiterhin. Die Prüfung hat drei Ergebnisse, nicht zwei: gültig, ungültig oder unbestimmt (inconclusive), wenn der Verifizierer die beteiligten Skripte nicht auswerten kann.
Multisig und Hardware-Wallets: Signieren über PSBT
Bei einer Multisig-Adresse braucht der Witness Signaturen mehrerer Schlüssel, die bei verschiedenen Personen oder auf verschiedenen Geräten liegen. BIP-322 löst das mit den Tools, die bei Multisig ohnehin im Einsatz sind: Die Transaktion to_sign wird als PSBT weitergereicht, mit einem neuen globalen Feld PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (Typ 0x09), das die Nachricht enthält. Sieht ein Signierer dieses Feld, eine erste Eingabe, die Ausgabe 0 des passenden to_spend ausgibt, und eine einzige OP_RETURN-Ausgabe mit dem Wert null, muss er dem Benutzer statt einer Transaktion die Nachricht und die Adresse anzeigen; danach signiert er so, als würde er Coins ausgeben. Jeder Mitsignierer fügt eine Teilsignatur hinzu; das finalisierte Ergebnis wird zu einer Signatur vom Typ ful. Ein Gerät, das das Feld nicht kennt, sieht etwas, das wie eine Transaktion über 0 Satoshi aussieht. Deshalb zählt bei diesem Ablauf die Unterstützung durch die Wallets mehr als die Kryptografie.
Im BIP sind zwei Referenzimplementierungen aufgeführt: ein Pull Request für Bitcoin Core mit Grundunterstützung (#24058), der ohne Merge geschlossen wurde, und ein Pull Request für btcd mit vollständiger Unterstützung (#2521), aus dem die generierten Testvektoren stammen und der am 20. Oktober 2026 noch offen war. Keiner der beiden ist in einem Release enthalten – ein Grund dafür, dass diejenigen Wallets und Bibliotheken aus der Tabelle unten, die BIP-322 unterstützen, jeweils eine eigene Implementierung mitbringen.
Abgleich mit den Testvektoren des BIP
BIP-322 liefert Basis-Testvektoren mit: Nachrichten-Hashes und Transaktions-IDs für drei Nachrichten sowie einfache Signaturen für eine P2WPKH-Adresse. Für ihre Prüfung genügen Adresse, Nachricht und Signatur. Dieselben Signaturen, mit und ohne das neue Präfix, gingen auch an 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
Die Implementierung oben reproduziert jeden Hash, erkennt jede einfache P2WPKH-Signatur als gültig, ob mit oder ohne Präfix, und lehnt den Fehlervektor ab. bip322-js 4.0.0, die verbreitete JavaScript-Bibliothek, versteht das Präfix smp nicht: Die Bibliothek dekodiert das Präfix als base64 und scheitert mit buffer too small, während dieselbe Signatur ohne Präfix die Prüfung besteht. Signaturen neuerer Wallets scheitern deshalb in Diensten, die auf bip322-js aufbauen, bis die Bibliothek aktualisiert ist; ein bekanntes Präfix vor dem Aufruf zu entfernen, ist für einfache Signaturen eine brauchbare Übergangslösung.
Wer was erzeugt und akzeptiert
Aus dem Quellcode des jeweiligen Projekts oder, wo vermerkt, aus einem Testlauf; Stand Oktober 2026:
| Software | Legacy (BIP-137) | BIP-322 | Anmerkungen |
|---|---|---|---|
| Bitcoin Core 31.1 | nur P2PKH | nein | oben getestet |
| Electrum (master) | P2PKH, P2WPKH, P2SH-P2WPKH, Header 27–34 | nein | prüft beide Header-Konventionen; lehnt Adressen ohne einzelnen Schlüssel ab |
| Trezor-Firmware (main) | P2PKH, P2SH-P2WPKH, P2WPKH, BIP-137-Header | nein | Taproot: „Unsupported script type“ |
| Sparrow (drongo master) | ja | einfach, mit den Präfixen smp/ful/pof | Prüfung: einfache Variante, P2WPKH und P2TR mit einzelnem Schlüssel |
| bip322-js 4.0.0 | ja, locker oder strikt | einfach, P2WPKH, P2SH-P2WPKH, P2TR mit einzelnem Schlüssel | keine Präfixe; siehe Hinweis oben |
Die praktischen Folgen: Eine Taproot-Adresse lässt sich nur mit BIP-322 nachweisen; eine Multisig-Adresse lässt sich mit dem Legacy-Format überhaupt nicht nachweisen; und ein Dienst, der Nachweise aus all diesen Wallets akzeptieren will, muss Legacy-Signaturen (bei SegWit locker geprüft), einfache BIP-322-Signaturen ohne Präfix und solche mit Präfix akzeptieren.
So prüfst du Signaturen in einem Dienst
- Lege die Nachricht auf deiner Seite fest. Nimm deine Domain, eine Nonce oder einen Zeitstempel und den Zweck auf (
"Proof of ownership for example.com, nonce 7f3a…") und akzeptiere jede Nonce nur einmal, genau wie bei Sign-In with Ethereum. Eine Signatur über einen allgemeinen Text lässt sich bei jedem anderen Dienst erneut vorlegen, der denselben Text verlangt. - Vergleiche die exakten Bytes. Beide Formate signieren die rohe Nachricht. Die Oberfläche von Electrum entfernt vor dem Signieren Whitespace am Anfang und am Ende; ein Zeilenumbruch am Ende durch Kopieren und Einfügen oder CRLF-Zeilenenden ergeben einen anderen Hash und eine „ungültige Signatur“.
- Erkenne das Format, in dieser Reihenfolge: ein Präfix
smp,fuloderpof; ein String in base64, der sich zu genau 65 Byte dekodieren lässt und dessen erstes Byte zwischen 27 und 42 liegt (Legacy); andernfalls versuche es mit einer einfachen BIP-322-Signatur ohne Präfix. - Entscheide, was eine Legacy-Signatur für SegWit-Adressen beweist: Die lockere Prüfung beweist die Kontrolle über den Schlüssel, und das ist bei Adressen mit einem einzelnen Schlüssel die Kontrolle über die Adresse.
- Werte „unbestimmt“ als nicht geprüft. Ein Verifizierer, der ein Skript nicht auswerten kann (Timelocks, ungewöhnliche Skripte), darf keinen Erfolg melden.
- Prüfe beim Guthabennachweis die Blockchain. Eine Signatur vom Typ
pofbeweist die Kontrolle über die aufgeführten Ausgaben zum Zeitpunkt des Signierens; ob sie noch nicht ausgegeben sind, ist eine separate Abfrage, und die Signatur beweist nie, dass es alle Ausgaben einer Adresse sind.
Quellen
- BIP-137: Signatures of Messages using Private Keys
- BIP-322: Generic Signed Message Format, Version 2.0.0, und die zugehörigen Basis-Testvektoren
- BIP-340 (Tagged Hashes), BIP-143 (Signatur-Hash für SegWit), BIP-174 (PSBT)
- RFC 6979: Deterministic DSA and ECDSA
- Bitcoin Core 31.1, RPCs
signmessageundverifymessage - Electrum
bitcoin.py, Trezorsign_message.py, Sparrow drongoBip322.java, bip322-js