Zum Inhalt springen
ecdsa.com
← Alle Artikel

Bitcoin-Nachrichten signieren: BIP-137 und BIP-322 erklärt

„Signiere diese Nachricht mit deiner Adresse“ klingt nach einem einzigen Vorgang. Tatsächlich sind es zwei inkompatible Formate, drei Header-Konventionen und ein Standard, der in diesem Jahr seine Kodierung geändert hat. Hier erfährst du, was jedes Format signiert und welche Wallet was erzeugt; dazu gibt es Verifizierer, abgeglichen mit Bitcoin Core und den Testvektoren aus dem BIP selbst.

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“):

HeaderBedeutung
27–30P2PKH, unkomprimierter öffentlicher Schlüssel
31–34P2PKH, komprimierter öffentlicher Schlüssel
35–38P2SH-P2WPKH (verschachteltes SegWit)
39–42P2WPKH (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:

btcmsg.mjs (@noble/curves 2.4.0, @noble/hashes 2.4.0, @scure/base 2.4.0)
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:

run.sh (bitcoin/bitcoin:31.1)
#!/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
Ausgabe
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:

Ausgabe: legacy-prepare.mjs, dann crosscheck.sh in bitcoin/bitcoin:31.1
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:

headers.mjs
// 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}`);
}
Ausgabe (node:24.22.0-bookworm-slim)
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 Outpoint 000…000:FFFFFFFF mit dem scriptSig OP_0 PUSH32[message_hash] ausgibt, und eine Ausgabe über 0 Satoshi, deren scriptPubKey die nachzuweisende Adresse ist. message_hash ist ein Tagged Hash nach BIP-340, SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message) mit dem Tag BIP0322-signed-message.
  • to_sign: Version 0, eine Eingabe, die to_spend:0 ausgibt, und eine Ausgabe über 0 Satoshi an OP_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:

VariantePräfixInhaltVerwendung
Einfachsmpder Witness-Stack, base64natives SegWit und Taproot, keine Timelocks
Vollständigfuldas ganze signierte to_sign, base64jedes Skript, auch verschachteltes SegWit, Legacy und Timelocks
Vollständig (Guthabennachweis)pofeine finalisierte PSBT von to_sign mit zusätzlichen EingabenKontrolle über bestimmte UTXOs nachweisen
Legacykeinesdie BIP-137-Signaturnur 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:

vectors.mjs
// 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)}`);
Ausgabe (node:24.22.0-bookworm-slim)
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:

SoftwareLegacy (BIP-137)BIP-322Anmerkungen
Bitcoin Core 31.1nur P2PKHneinoben getestet
Electrum (master)P2PKH, P2WPKH, P2SH-P2WPKH, Header 27–34neinprüft beide Header-Konventionen; lehnt Adressen ohne einzelnen Schlüssel ab
Trezor-Firmware (main)P2PKH, P2SH-P2WPKH, P2WPKH, BIP-137-HeaderneinTaproot: „Unsupported script type“
Sparrow (drongo master)jaeinfach, mit den Präfixen smp/ful/pofPrüfung: einfache Variante, P2WPKH und P2TR mit einzelnem Schlüssel
bip322-js 4.0.0ja, locker oder strikteinfach, P2WPKH, P2SH-P2WPKH, P2TR mit einzelnem Schlüsselkeine 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

  1. 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.
  2. 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“.
  3. Erkenne das Format, in dieser Reihenfolge: ein Präfix smp, ful oder pof; 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.
  4. 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.
  5. Werte „unbestimmt“ als nicht geprüft. Ein Verifizierer, der ein Skript nicht auswerten kann (Timelocks, ungewöhnliche Skripte), darf keinen Erfolg melden.
  6. Prüfe beim Guthabennachweis die Blockchain. Eine Signatur vom Typ pof beweist 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