Перейти к содержимому
ecdsa.com

BIP-137 или BIP-322: подпись сообщений для адресов Bitcoin

BIP-137 подписывает ключом, который стоит за адресом с одним ключом, и позволяет проверяющей стороне восстановить этот ключ. BIP-322 доказывает, что вы могли бы потратить средства с любого адреса, подписывая транзакцию, которую невозможно отправить в сеть.

BIP-137 или BIP-322 — это выбор между старым и новым способами доказать в Bitcoin контроль над адресом, подписав сообщение. BIP-137 описывает давно существующий формат «Bitcoin Signed Message»: 65-байтную подпись ECDSA, из которой можно восстановить ключ; байт заголовка этой подписи указывает, адрес какого типа выводить из восстановленного ключа. BIP-322 обобщает подпись на любой скрипт: сообщение фиксируется в виртуальной транзакции, а подписью служит тот witness, который потратил бы средства с этого адреса; проверяет его обычный интерпретатор скриптов.

Коротко

BIP-137BIP-322
СтатусDeployed, внедрён (номер присвоен в 2019 году; сам формат намного старше)Complete (завершён) начиная с версии 1.0.0 от 15 апреля 2026 года; версия 2.0.0 — 4 июня 2026 года
Хеш сообщенияДвойной SHA-256 от "\x18Bitcoin Signed Message:\n", длины и самого сообщенияТегированный SHA-256 по BIP-340 с тегом BIP0322-signed-message
Что подписываетсяНепосредственно хеш сообщенияВиртуальная транзакция to_sign, которая тратит виртуальный выход to_spend
Типы адресовP2PKH; P2SH-P2WPKH и P2WPKH через диапазоны значений заголовкаЛюбой скрипт: P2PKH, P2WPKH, P2WSH, P2TR, мультиподпись, блокировки по времени
Что нужно проверяющей сторонеПодпись, сообщение, адрес; ключ восстанавливаетсяПодпись, сообщение, адрес; интерпретатор скриптов
Содержимое подписиБайт заголовка (27–42), r, sСтек witness (smp), полная транзакция (ful) или PSBT (pof)
Размер в base6488 символов147 для P2WPKH, 88 для P2TR, 439 для мультиподписи P2WSH 3 из 3 (тестовые векторы)
Доказательство наличия средствНетДа, с дополнительными входами в PSBT (pof)
Привязка к открытому ключуНет (восстановление можно подстроить так, чтобы получить связанный ключ)Да, через скрипт, условия которого должны быть выполнены
Подпись на аппаратном кошелькеРежим подписи сообщений, если он поддерживаетсяПодпись транзакции в стиле PSBT

Как работает BIP-137

Подписант хеширует сообщение с фиксированным префиксом, подписывает его с помощью ECDSA и сохраняет идентификатор восстановления в байте заголовка: 27–30 для P2PKH с несжатыми ключами, 31–34 для сжатых ключей, 35–38 для P2SH-P2WPKH и 39–42 для нативного SegWit P2WPKH. Проверяющая сторона с помощью восстановления открытого ключа вычисляет ключ по подписи и проверяет, что его хеш соответствует адресу.

Это работает, только если адрес — хеш одного открытого ключа. Адреса Taproot привязаны к модифицированному (tweaked) ключу и используют подписи Schnorr, из которых ключ восстановить нельзя; у мультиподписи и других скриптов единственного ключа нет вообще. Диапазоны заголовка для SegWit добавили позже, и не все кошельки их поддержали, поэтому подпись из одного кошелька может не пройти проверку в другом, а собственные команды Bitcoin Core signmessage и verifymessage до сих пор принимают только устаревшие адреса P2PKH.

Как работает BIP-322

BIP-322 хеширует сообщение тегированным хешем и помещает результат в скрипт входа виртуальной транзакции to_spend, единственный выход которой содержит scriptPubKey адреса. Вторая транзакция, to_sign, тратит этот выход в выход с OP_RETURN. Подписант создаёт тот же witness, который его кошелёк создал бы для траты средств с этого адреса; проверяющая сторона заново собирает обе транзакции и запускает интерпретатор скриптов. Поскольку to_spend тратит несуществующий вход, ни одна из этих транзакций никогда не может стать действительной в сети.

Простой формат (smp) содержит только стек witness. Полный формат (ful) содержит всю транзакцию to_sign; он нужен для скриптов с блокировками по времени или со входами без witness, и тогда проверяющая сторона сообщает время и возраст, начиная с которых доказательство действительно. Доказательство наличия средств (pof) — это финализированная PSBT, которая также тратит реальные UTXO, поэтому подпись показывает контроль над конкретными монетами. Устаревший формат BIP-137 остаётся допустимым только для адресов P2PKH.

Оба формата: проверка по тестовым векторам BIP-322

Скрипт ниже использует закрытый ключ (в формате WIF) и адрес из базовых тестовых векторов BIP-322. Ключ опубликован там для тестов, поэтому потратить средства с его адреса может кто угодно, и на этом адресе никогда не должно быть средств. Скрипт создаёт и проверяет подпись BIP-137 для «Hello World», заново вычисляет хеш сообщения BIP-322 и идентификаторы обеих виртуальных транзакций, а затем проверяет опубликованную в векторах подпись smp с хешем подписи по BIP-143 (Node.js 26, @noble/curves 2.3.0, @noble/hashes 2.3.0, @scure/base 2.4.0).

BIP-137 и BIP-322 для одного адреса P2WPKH
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

Хеш сообщения и оба идентификатора транзакций совпадают со значениями в basic-test-vectors.json, а опубликованная подпись проходит проверку. Подпись BIP-137 детерминированная (RFC 6979), поэтому один и тот же ключ и одно и то же сообщение всегда дают одни и те же 88 символов; байт заголовка 40 означает P2WPKH с идентификатором восстановления 1. Проверка сообщений Bitcoin поддерживает оба формата и адреса любого типа.

Что меняется для разработчиков

  • Проверка становится тяжелее: для BIP-322 нужны сериализация транзакций, вычисление хеша подписи для каждого типа скрипта (устаревшего, BIP-143 для SegWit v0, BIP-341 для Taproot) и в идеале полноценный интерпретатор скриптов. Всё это есть в таких библиотеках, как btcd (источник тестовых векторов).
  • Подпись использует готовый код для транзакций: кошельки и аппаратные устройства подписывают to_sign как любую другую транзакцию, часто в виде PSBT, поэтому координаторы мультиподписи и кошельки Taproot работают без специального кода для сообщений.
  • Определение формата: версия 1.0.0 сделала трёхбуквенный префикс обязательным, и это изменение несовместимо с прежними версиями; для совместимости с более ранними реализациями проверяющие стороны могут считать подписи без префикса подписями smp.
  • Ключ не восстанавливается: проверяющая сторона должна знать адрес, но обычно он и так передаётся.
  • Те же ограничения, что у любой подписи: ни один из форматов не доказывает, что подписант всё ещё контролирует монеты или что он не стал бы подписывать от чужого имени.

Что когда выбирать

  • Taproot, P2WSH, мультиподпись или адреса скриптов: BIP-322; BIP-137 не может их выразить.
  • Биржи и кастодиальные сервисы, которые проверяют адреса для вывода средств (проверки в рамках travel rule): принимайте BIP-322 для всех типов адресов и BIP-137 для P2PKH и SegWit с одним ключом.
  • Доказательство резервов или владения конкретными UTXO: доказательство наличия средств BIP-322 (pof).
  • Устаревшие адреса P2PKH и старые кошельки: BIP-137 по-прежнему допустим, и именно такие подписи создаёт Bitcoin Core.
  • Вход с помощью кошелька Bitcoin: простой формат BIP-322 для сообщения, которое содержит домен, nonce и срок действия, по тем же причинам, что и в Sign-In with Ethereum.

Заметки о миграции

  1. Проверяйте оба формата: пробуйте BIP-322, если у подписи есть префикс или адрес не P2PKH; для 65-байтных подписей с заголовками 27–42 возвращайтесь к BIP-137.
  2. Унифицируйте обработку заголовка: принимайте подписи SegWit, у которых заголовок попадает в диапазон P2PKH для сжатых ключей, только если восстановленный ключ соответствует адресу SegWit, и задокументируйте это решение.
  3. Выдавайте подписи с префиксом, когда подписываете по BIP-322, как требует версия 1.0.0.
  4. Тестируйте на опубликованных векторах (basic-test-vectors.json, generated-test-vectors.json), включая случаи ошибок: неверные сообщения, неверные адреса и некорректные префиксы.

Источники

Все сравнения