Bei BIP-137 vs. BIP-322 geht es um den alten und den neuen Weg, in Bitcoin mit einer signierten Nachricht nachzuweisen, dass jemand über eine Adresse verfügt. BIP-137 dokumentiert das altbekannte Format „Bitcoin Signed Message“: eine 65-Byte-ECDSA-Signatur, aus der sich der Schlüssel wiederherstellen lässt und deren Header-Byte angibt, welcher Adresstyp aus dem wiederhergestellten Schlüssel abzuleiten ist. BIP-322 verallgemeinert das Signieren auf beliebige Skripte: Die Nachricht wird in einer virtuellen Transaktion festgeschrieben, und die Signatur ist genau der Witness, der Coins dieser Adresse ausgeben würde; geprüft wird sie vom normalen Skript-Interpreter.
Auf einen Blick
| BIP-137 | BIP-322 | |
|---|---|---|
| Status | Im Einsatz (Nummer 2019 vergeben; das Format ist viel älter) | „Complete“ seit Version 1.0.0 vom 15. April 2026; 2.0.0 am 4. Juni 2026 |
| Nachrichten-Hash | Doppelter SHA-256 über "\x18Bitcoin Signed Message:\n", Länge und Nachricht | Tagged Hash mit SHA-256 nach BIP-340, Tag BIP0322-signed-message |
| Was signiert wird | Direkt der Nachrichten-Hash | Eine virtuelle Transaktion to_sign, die eine virtuelle Ausgabe von to_spend ausgibt |
| Adresstypen | P2PKH; P2SH-P2WPKH und P2WPKH über Header-Bereiche | Jedes Skript: P2PKH, P2WPKH, P2WSH, P2TR, Multisig, Timelocks |
| Was der Verifizierer braucht | Signatur, Nachricht, Adresse; der Schlüssel wird wiederhergestellt | Signatur, Nachricht, Adresse; einen Skript-Interpreter |
| Inhalt der Signatur | Header-Byte (27–42), r, s | Witness-Stack (smp), vollständige Transaktion (ful) oder PSBT (pof) |
| Größe, base64 | 88 Zeichen | 147 für P2WPKH, 88 für P2TR, 439 für ein 3-von-3-Multisig mit P2WSH (Testvektoren) |
| Guthabennachweis (Proof of Funds) | Nein | Ja, mit zusätzlichen Eingaben in einer PSBT (pof) |
| Bindung an den öffentlichen Schlüssel | Nein (die Wiederherstellung lässt sich so verändern, dass sie verwandte Schlüssel liefert) | Ja, über das Skript, das erfüllt wird |
| Signieren mit Hardware-Wallets | Modus für Nachrichtensignaturen, wo unterstützt | Transaktionssignatur im PSBT-Stil |
So funktioniert BIP-137
Der Signierer bildet den Hash der Nachricht mit einem festen Präfix, signiert ihn mit ECDSA und speichert die Recovery-ID in einem Header-Byte: 27–30 für P2PKH mit unkomprimierten Schlüsseln, 31–34 für komprimierte Schlüssel, 35–38 für P2SH-P2WPKH und 39–42 für natives SegWit (P2WPKH). Der Verifizierer berechnet den Schlüssel per Wiederherstellung des öffentlichen Schlüssels aus der Signatur und prüft, ob sein Hash die Adresse ergibt.
Das funktioniert nur, wenn eine Adresse der Hash eines einzelnen öffentlichen Schlüssels ist. Taproot-Adressen legen sich auf einen per Tweak veränderten Schlüssel fest und verwenden Schnorr-Signaturen, die keine Schlüsselwiederherstellung kennen; Multisig und andere Skripte haben überhaupt keinen einzelnen Schlüssel. Die Header-Bereiche für SegWit kamen erst später hinzu, und nicht jede Wallet hat sie übernommen. Eine Signatur aus einer Wallet kann daher in einer anderen an der Prüfung scheitern, und signmessage und verifymessage von Bitcoin Core akzeptieren nach wie vor nur Legacy-P2PKH-Adressen.
So funktioniert BIP-322
BIP-322 bildet aus der Nachricht einen Tagged Hash und legt ihn im Eingabeskript einer virtuellen Transaktion to_spend ab, deren einzige Ausgabe den scriptPubKey der Adresse trägt. Eine zweite Transaktion, to_sign, gibt diese Ausgabe an ein OP_RETURN aus. Der Signierer erzeugt genau den Witness, den seine Wallet zum Ausgeben von dieser Adresse erzeugen würde; der Verifizierer baut beide Transaktionen nach und lässt den Skript-Interpreter laufen. Weil to_spend eine nicht existierende Eingabe ausgibt, kann keine der beiden Transaktionen jemals im Netzwerk gültig sein.
Das einfache Format (smp) enthält nur den Witness-Stack. Das vollständige Format (ful) enthält die ganze Transaktion to_sign; das ist bei Skripten mit Timelocks oder Eingaben ohne Witness nötig, und der Verifizierer meldet dann Zeitpunkt und Alter, ab denen der Nachweis gültig ist. Der Guthabennachweis (Proof of Funds, pof) ist eine finalisierte PSBT, die auch echte UTXOs ausgibt, sodass die Signatur die Verfügung über bestimmte Coins belegt. Das Legacy-Format von BIP-137 bleibt nur für P2PKH-Adressen zulässig.
Beide Formate, geprüft mit den Testvektoren von BIP-322
Das folgende Skript verwendet den privaten Schlüssel (im WIF-Format) und die Adresse aus den grundlegenden Testvektoren von BIP-322. Der Schlüssel ist dort zu Testzwecken veröffentlicht: Jeder kann Coins von seiner Adresse ausgeben, deshalb darf auf ihr nie Guthaben liegen. Das Skript erzeugt und prüft eine BIP-137-Signatur für „Hello World“, rekonstruiert den BIP-322-Nachrichten-Hash und die IDs beider virtueller Transaktionen und prüft die in den Vektoren veröffentlichte Signatur im Format smp mit einem Signatur-Hash nach BIP-143 (Node.js 26, @noble/curves 2.3.0, @noble/hashes 2.3.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 { bytesToHex, concatBytes, utf8ToBytes } from "@noble/hashes/utils.js";
import { base64, bech32, createBase58check } from "@scure/base";
const sha256d = (b) => sha256(sha256(b));
const u32 = (n) => new Uint8Array([n, n >>> 8, n >>> 16, n >>> 24]);
const zero = (n) => new Uint8Array(n);
const push = (b) => concatBytes(new Uint8Array([b.length]), b); // lengths here are all below 0xfd
const txid = (tx) => bytesToHex(sha256d(tx).reverse());
// Test key published in the BIP-322 basic test vectors (bip-0322/basic-test-vectors.json).
// It is public, so never send funds to its address.
const wif = createBase58check(sha256).decode("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k");
const secretKey = wif.subarray(1, 33);
const pub = secp256k1.getPublicKey(secretKey, true);
const h160 = ripemd160(sha256(pub));
const address = bech32.encode("bc", [0, ...bech32.toWords(h160)]);
console.log("address:", address);
// BIP-137: double SHA-256 of a prefixed message, compact signature with a header byte
const message = "Hello World";
const m = utf8ToBytes(message);
const legacyHash = sha256d(concatBytes(push(utf8ToBytes("Bitcoin Signed Message:\n")), push(m)));
const rec = secp256k1.sign(legacyHash, secretKey, { prehash: false, format: "recovered" });
const encoded = base64.encode(concatBytes(new Uint8Array([39 + rec[0]]), rec.subarray(1))); // 39-42: P2WPKH
console.log("\nBIP-137 signature:", encoded);
// Verifier side: read the header, recover the public key, compare its hash with the address
const sig137 = base64.decode(encoded);
const recovered = secp256k1.recoverPublicKey(concatBytes(new Uint8Array([(sig137[0] - 27) & 3]), sig137.subarray(1)), legacyHash, { prehash: false });
console.log("header", sig137[0], "-> recovered key matches address:", bytesToHex(ripemd160(sha256(recovered))) === bytesToHex(h160));
// BIP-322: tagged message hash, virtual to_spend and to_sign transactions
const tag = sha256(utf8ToBytes("BIP0322-signed-message"));
const messageHash = sha256(concatBytes(tag, tag, m));
const challenge = concatBytes(new Uint8Array([0x00, 0x14]), h160); // P2WPKH scriptPubKey
const toSpend = concatBytes(u32(0), new Uint8Array([1]), zero(32), u32(0xffffffff),
push(concatBytes(new Uint8Array([0x00, 0x20]), messageHash)), u32(0), new Uint8Array([1]), zero(8), push(challenge), u32(0));
const prevout = concatBytes(sha256d(toSpend), u32(0));
const opReturn = concatBytes(zero(8), push(new Uint8Array([0x6a])));
const toSign = concatBytes(u32(0), new Uint8Array([1]), prevout, push(zero(0)), u32(0), new Uint8Array([1]), opReturn, u32(0));
console.log("\nBIP-322 message hash:", bytesToHex(messageHash));
console.log("to_spend txid:", txid(toSpend));
console.log("to_sign txid: ", txid(toSign));
// Verify the "simple" signature from the test vectors: witness stack [signature, public key]
const vector = "smpAkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
const stack = base64.decode(vector.slice(3));
const der = stack.subarray(2, 2 + stack[1]);
const witnessKey = stack.subarray(3 + stack[1]);
// BIP-143 signature hash for input 0 of to_sign, SIGHASH_ALL
const scriptCode = push(concatBytes(new Uint8Array([0x76, 0xa9, 0x14]), h160, new Uint8Array([0x88, 0xac])));
const preimage = concatBytes(u32(0), sha256d(prevout), sha256d(u32(0)), prevout, scriptCode, zero(8), u32(0),
sha256d(opReturn), u32(0), u32(1));
const ok = secp256k1.verify(der.subarray(0, -1), sha256d(preimage), witnessKey, { prehash: false, format: "der" });
console.log("vector signature:", vector.slice(0, 3), "prefix,", stack[0], "witness items,", ok ? "valid" : "invalid");
console.log("witness key is the address key:", bytesToHex(witnessKey) === bytesToHex(pub));
address: bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l
BIP-137 signature: KOW2xi+ebJLeBtr674l4QH76dqDoVjLV80R9EFKFQX5rBrlCXPIZaYs8Yuayg0ZqjyiCbLy9pzZIS7JWT65/nsU=
header 40 -> recovered key matches address: true
BIP-322 message hash: f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a
to_spend txid: b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b
to_sign txid: 88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf
vector signature: smp prefix, 2 witness items, valid
witness key is the address key: true
Der Nachrichten-Hash und beide Transaktions-IDs stimmen mit den Werten in basic-test-vectors.json überein, und die veröffentlichte Signatur besteht die Prüfung. Die BIP-137-Signatur ist deterministisch (RFC 6979), derselbe Schlüssel und dieselbe Nachricht ergeben also immer dieselben 88 Zeichen; Header 40 bedeutet P2WPKH mit Recovery-ID 1. Der Bitcoin-Nachrichten-Verifizierer prüft beide Formate für jede Adresse.
Was sich bei der Implementierung ändert
- Die Prüfung ist aufwendiger: BIP-322 braucht Transaktionsserialisierung, Signatur-Hashing für jeden Skripttyp (Legacy, BIP-143 für SegWit v0, BIP-341 für Taproot) und idealerweise einen vollständigen Skript-Interpreter. Bibliotheken wie btcd (die Quelle der Testvektoren) bringen das mit.
- Das Signieren nutzt vorhandenen Transaktionscode: Wallets und Hardwaregeräte signieren
to_signwie jede andere Transaktion, oft als PSBT, sodass Multisig-Koordinatoren und Taproot-Wallets ohne speziellen Code für Nachrichten funktionieren. - Formaterkennung: Version 1.0.0 hat das Präfix aus drei Buchstaben verpflichtend gemacht, eine inkompatible Änderung; Verifizierer dürfen Signaturen ohne Präfix aus Gründen der Kompatibilität mit früheren Implementierungen als
smpbehandeln. - Keine Schlüsselwiederherstellung: Der Verifizierer muss die Adresse kennen, die aber normalerweise ohnehin angegeben ist.
- Dieselben Grenzen wie bei jeder Signatur: Keines der beiden Formate beweist, dass der Signierer noch über die Coins verfügt oder dass er nicht im Auftrag eines anderen signieren würde.
Wann welches Format passt
- Taproot-, P2WSH-, Multisig- oder Skriptadressen: BIP-322; BIP-137 kann sie nicht abbilden.
- Börsen und Verwahrer, die Auszahlungsadressen prüfen (Prüfungen nach der Travel Rule): Akzeptiere BIP-322 für alle Typen und BIP-137 für P2PKH und SegWit mit einem einzelnen Schlüssel.
- Nachweis von Reserven oder bestimmten UTXOs: BIP-322-Guthabennachweis (
pof). - Legacy-P2PKH-Adressen und alte Wallets: BIP-137 bleibt gültig und ist das, was Bitcoin Core erzeugt.
- Anmeldung mit einer Bitcoin-Wallet: BIP-322 im einfachen Format über eine Nachricht mit Domain, Nonce und Ablaufzeit – aus denselben Gründen wie bei Sign-In with Ethereum.
Hinweise zur Migration
- Prüfe beide: Versuche es mit BIP-322, wenn die Signatur ein Präfix trägt oder die Adresse nicht P2PKH ist; greife bei 65-Byte-Signaturen mit Headern von 27–42 auf BIP-137 zurück.
- Vereinheitliche den Umgang mit Headern: Akzeptiere SegWit-Signaturen, deren Header im Bereich für P2PKH mit komprimiertem Schlüssel liegt, nur dann, wenn der wiederhergestellte Schlüssel zur SegWit-Adresse passt, und dokumentiere diese Entscheidung.
- Gib Signaturen mit Präfix aus, wenn du mit BIP-322 signierst, wie es Version 1.0.0 verlangt.
- Teste mit den veröffentlichten Vektoren (
basic-test-vectors.json,generated-test-vectors.json), einschließlich der Fehlerfälle für falsche Nachrichten, falsche Adressen und ungültige Präfixe.
Quellen
- BIP-322, generisches Format für signierte Nachrichten und grundlegende Testvektoren
- BIP-137, Signaturen von Nachrichten mit privaten Schlüsseln
- BIP-340, Schnorr-Signaturen und Tagged Hashes
- BIP-143, Signatur-Hashing für SegWit v0 und BIP-341, Taproot
- BIP-174, PSBT
- Pull Request 2521 bei btcd, vollständige Unterstützung für BIP-322