Choisir entre BIP-137 et BIP-322, c’est choisir entre l’ancienne et la nouvelle manière, dans Bitcoin, de prouver le contrôle d’une adresse en signant un message. BIP-137 documente le format « Bitcoin Signed Message », en usage depuis longtemps : une signature ECDSA récupérable de 65 octets dont l’octet d’en-tête indique quel type d’adresse dériver de la clé récupérée. BIP-322 généralise la signature à n’importe quel script : une transaction virtuelle s’engage sur le message, et la signature est le témoin, quel qu’il soit, qui permettrait de dépenser depuis l’adresse, vérifié par l’interpréteur de script habituel.
Vue d’ensemble
| BIP-137 | BIP-322 | |
|---|---|---|
| Statut | Déployé (numéro attribué en 2019 ; format bien plus ancien) | Complet depuis la version 1.0.0 du 15 avril 2026 ; 2.0.0 le 4 juin 2026 |
| Condensat du message | Double SHA-256 de "\x18Bitcoin Signed Message:\n", de la longueur et du message | SHA-256 étiqueté BIP-340 avec l’étiquette BIP0322-signed-message |
| Ce qui est signé | Directement le condensat du message | Une transaction virtuelle to_sign qui dépense une sortie virtuelle to_spend |
| Types d’adresse | P2PKH ; P2SH-P2WPKH et P2WPKH via des plages d’en-tête | N’importe quel script : P2PKH, P2WPKH, P2WSH, P2TR, multisig, verrouillages temporels |
| Ce dont le vérificateur a besoin | Signature, message, adresse ; la clé est récupérée | Signature, message, adresse ; un interpréteur de script |
| Contenu de la signature | Octet d’en-tête (27–42), r, s | Pile de témoin (smp), transaction complète (ful) ou PSBT (pof) |
| Taille, base64 | 88 caractères | 147 pour P2WPKH, 88 pour P2TR, 439 pour un multisig P2WSH 3 sur 3 (vecteurs de test) |
| Preuve de fonds | Non | Oui, avec des entrées supplémentaires dans un PSBT (pof) |
| Engagement sur la clé publique | Non (la récupération peut être détournée vers des clés apparentées) | Oui, par le script qui doit être satisfait |
| Signature avec un portefeuille matériel | Mode de signature de messages, là où il est pris en charge | Signature de transaction de type PSBT |
Fonctionnement de BIP-137
Le signataire hache le message précédé d’un préfixe fixe, le signe avec ECDSA et stocke l’identifiant de récupération dans un octet d’en-tête : de 27 à 30 pour P2PKH avec des clés non compressées, de 31 à 34 pour des clés compressées, de 35 à 38 pour P2SH-P2WPKH et de 39 à 42 pour P2WPKH en SegWit natif. Le vérificateur utilise la récupération de la clé publique pour calculer la clé à partir de la signature et vérifie que son hachage donne l’adresse.
Cela ne fonctionne que lorsqu’une adresse est le condensat d’une seule clé publique. Les adresses Taproot s’engagent sur une clé ajustée (tweaked key) et utilisent des signatures Schnorr, qui ne permettent pas de récupérer la clé ; les multisig et autres scripts n’ont même pas de clé unique. Les plages d’en-tête pour SegWit ont été ajoutées plus tard et tous les portefeuilles ne les ont pas adoptées : une signature produite par un portefeuille peut donc être rejetée par un autre, et les commandes signmessage et verifymessage de Bitcoin Core lui-même n’acceptent toujours que les adresses P2PKH historiques.
Fonctionnement de BIP-322
BIP-322 hache le message avec un hachage étiqueté et place le résultat dans le script d’entrée d’une transaction virtuelle to_spend dont l’unique sortie porte le scriptPubKey de l’adresse. Une seconde transaction, to_sign, dépense cette sortie vers un OP_RETURN. Le signataire produit le témoin, quel qu’il soit, que son portefeuille produirait pour dépenser depuis l’adresse ; le vérificateur reconstruit les deux transactions et exécute l’interpréteur de script. Comme to_spend dépense une entrée inexistante, aucune des deux transactions ne peut jamais être valide sur le réseau.
Le format simple (smp) ne transporte que la pile de témoin. Le format complet (ful) transporte toute la transaction to_sign, ce qui est nécessaire pour les scripts à verrouillage temporel ou les entrées sans témoin ; le vérificateur indique alors la date et l’âge à partir desquels la preuve est valide. La preuve de fonds (pof) est un PSBT finalisé qui dépense aussi de vrais UTXO, de sorte que la signature démontre le contrôle de pièces précises. L’ancien format BIP-137 reste autorisé uniquement pour les adresses P2PKH.
Les deux formats, vérifiés avec les vecteurs de test BIP-322
Le script ci-dessous utilise la clé privée (au format WIF) et l’adresse des vecteurs de test de base de BIP-322. La clé y est publiée à des fins de test : n’importe qui peut donc dépenser depuis son adresse, qui ne doit jamais détenir de fonds. Le script crée et vérifie une signature BIP-137 pour « Hello World », reconstruit le condensat du message BIP-322 et les identifiants des deux transactions virtuelles, puis vérifie la signature smp publiée dans les vecteurs à l’aide d’un condensat de signature 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
Le condensat du message et les deux identifiants de transaction sont identiques aux valeurs de basic-test-vectors.json, et la vérification de la signature publiée réussit. La signature BIP-137 est déterministe (RFC 6979) : la même clé et le même message donnent donc toujours les mêmes 88 caractères ; l’en-tête 40 correspond à P2WPKH avec l’identifiant de récupération 1. Le vérificateur de messages Bitcoin vérifie les deux formats pour n’importe quelle adresse.
Ce qui change côté implémentation
- La vérification est plus lourde : BIP-322 nécessite la sérialisation des transactions, le hachage de signature pour chaque type de script (historique, BIP-143 pour SegWit v0, BIP-341 pour Taproot) et, idéalement, un interpréteur de script complet. Des bibliothèques comme btcd (d’où proviennent les vecteurs de test) le fournissent.
- La signature réutilise le code des transactions : les portefeuilles et les appareils matériels signent
to_signcomme n’importe quelle autre transaction, souvent sous forme de PSBT, si bien que les coordinateurs multisig et les portefeuilles Taproot fonctionnent sans code spécifique aux messages. - Détection du format : la version 1.0.0 a rendu obligatoire le préfixe de trois lettres, un changement incompatible ; les vérificateurs peuvent traiter les signatures sans préfixe comme
smppour rester compatibles avec les implémentations antérieures. - Pas de récupération de clé : le vérificateur doit connaître l’adresse, qui est normalement fournie de toute façon.
- Les mêmes limites que toute signature : aucun des deux formats ne prouve que le signataire contrôle toujours les pièces, ni qu’il ne signerait pas pour le compte de quelqu’un d’autre.
Quand choisir l’un ou l’autre
- Adresses Taproot, P2WSH, multisig ou de script : BIP-322 ; BIP-137 ne peut pas les exprimer.
- Plateformes d’échange et dépositaires qui vérifient des adresses de retrait (contrôles liés à la règle de voyage, ou travel rule) : acceptez BIP-322 pour tous les types, et BIP-137 pour P2PKH et SegWit à clé unique.
- Preuve de réserves ou d’UTXO précis : preuve de fonds BIP-322 (
pof). - Adresses P2PKH historiques et anciens portefeuilles : BIP-137 reste valable, et c’est ce que produit Bitcoin Core.
- Connexion avec un portefeuille Bitcoin : BIP-322 simple, sur un message qui inclut le domaine, un nonce et une date d’expiration, pour les mêmes raisons que Sign-In with Ethereum.
Notes de migration
- Vérifiez les deux : essayez BIP-322 lorsque la signature porte un préfixe ou que l’adresse n’est pas P2PKH ; rabattez-vous sur BIP-137 pour les signatures de 65 octets dont l’en-tête est compris entre 27 et 42.
- Normalisez le traitement des en-têtes : n’acceptez les signatures SegWit dont l’en-tête tombe dans la plage P2PKH compressée que si la clé récupérée correspond à l’adresse SegWit, et documentez ce choix.
- Émettez des signatures préfixées lorsque vous signez avec BIP-322, comme l’exige la version 1.0.0.
- Testez avec les vecteurs publiés (
basic-test-vectors.json,generated-test-vectors.json), y compris les cas d’erreur pour les mauvais messages, les mauvaises adresses et les préfixes invalides.
Références
- BIP-322, format générique de message signé et vecteurs de test de base
- BIP-137, signatures de messages à l’aide de clés privées
- BIP-340, signatures Schnorr et hachages étiquetés
- BIP-143, hachage de signature SegWit v0 et BIP-341, Taproot
- BIP-174, PSBT
- Pull request 2521 de btcd, prise en charge complète de BIP-322