简短回答:格式有两种。传统格式事后才以 BIP-137 的形式成文,它是对 "\x18Bitcoin Signed Message:\n" ‖ length ‖ message 计算的 65 字节可恢复 ECDSA 签名;它只能证明对某个密钥的控制权,第一个字节(27~42)表明签名者所指的地址类型。Bitcoin Core 只为传统 P2PKH 地址生成这种签名,而对于 SegWit 地址,各钱包使用的头部字节并不一致。BIP-322(状态为 Complete)证明的是对某个地址的控制权:它构造一笔从该地址花费的虚拟交易,签名就是这笔花费的见证数据,因此适用于 P2WPKH、P2TR、多签和各种脚本。自该 BIP 的 2.0.0 版起,签名带有前缀:smp(简单)、ful(完整)或 pof(资金证明)。验证时应使用能识别前缀的库,并接受旧版钱包生成的无前缀简单签名。
传统格式(BIP-137)
摘要是对固定前缀、按比特币 varint 编码的消息长度和消息字节计算的双重 SHA-256。签名为 header ‖ r ‖ s,共 65 字节,通常以 base64 编码。头部字节包含恢复 ID(0~3),验证者据此可以从签名中算出公钥;此外还加上一个表示密钥和地址类型的常量(BIP-137,状态为 Deployed):
| 头部字节 | 含义 |
|---|---|
| 27~30 | P2PKH,未压缩公钥 |
| 31~34 | P2PKH,压缩公钥 |
| 35~38 | P2SH-P2WPKH(嵌套 SegWit) |
| 39~42 | P2WPKH(原生 SegWit,bc1q…) |
验证时先恢复公钥,再推导出头部字节所指类型的地址,然后进行比较。这里没有交易,也没有脚本:该格式无法表达“这个多签地址”或“这个 Taproot 地址”,只能表达“这个密钥的持有者”。对于超出这一范围的需求,BIP-137 本身也指向了 BIP-322。
下面的模块基于 @noble/curves 和 @scure/base 实现了传统格式,后半部分还实现了 P2WPKH 的 BIP-322 简单签名:
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 });
}
Bitcoin Core 的支持情况
Bitcoin Core 的 signmessage 和 verifymessage RPC 实现的是传统格式,而且只支持传统地址。下面在一个 regtest 节点上为每种类型各生成一个地址:
#!/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
无论是签名,还是验证别人生成的签名,所有 SegWit 和 Taproot 地址都会报错:Address does not refer to key。传统签名的第一个字节在这几次运行中为 31 或 32(base64 中的 H 或 I),表示压缩的 P2PKH 密钥。
我们的实现生成的字节与 Bitcoin Core 完全相同。用一个专为本次运行生成、并以 pkh() 描述符导入 regtest 钱包的密钥,两者对同一条消息签名:
address mk1giEaBaAFPXNRMQcZNptPAp9kJmttKyp
core signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
our signature: H0bepOQGQpAS+FqLbBoVqjiXqArVq4AQlD59fhwpAX8bdTwCAlE407C2Ik9hTp5pxcsCv6C3arMw+bGjzYtH4mE=
identical: yes
core verifymessage(our signature): true
两个签名完全相同,是因为两者都使用确定性 nonce(RFC 6979)和 low-S 规范化;使用随机 nonce 的签名者每次都会生成不同但同样有效的签名。
SegWit 地址的头部字节问题
为 SegWit 地址签署传统格式消息的钱包,所用的头部字节并不一致。Electrum 对所有地址类型都使用 27~34 的头部字节签名;验证时,它会逐一尝试恢复出的密钥可能对应的每一种单密钥地址类型(electrum/bitcoin.py)。Trezor 遵循 BIP-137,对 P2SH-P2WPKH 加 4,对 P2WPKH 加 8,并提供一个选项(no_script_type)改为生成 Electrum 形式(trezor-firmware 的 sign_message.py)。因此,验证者必须在严格检查(头部字节必须指明该地址的类型)和宽松检查(恢复出的密钥的任一地址均可)之间做出选择。下面用同一个密钥按两种约定分别对同一条消息签名,并交给我们的验证器和 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
宽松验证接受这两种签名,还会把同一个签名当作该密钥 P2PKH 地址的证明。这正是传统签名如实表达的含义:签名者是这个密钥的持有者,对由该密钥派生的每一个地址都成立。严格验证会拒绝所有 Electrum 风格的 SegWit 签名。如果必须接受 SegWit 地址的传统签名,就采用宽松验证,并记录下你验证的是对密钥的控制权。
BIP-322:证明对地址的控制权
BIP-322(bip-0322.mediawiki,状态为 Complete,版本 2.0.0)没有另造一种消息格式,而是复用了交易机制。它会构造两笔虚拟交易:
to_spend:版本 0;一个输入,花费不存在的输出点000…000:FFFFFFFF,其 scriptSig 为OP_0 PUSH32[message_hash];一个金额为 0 聪的输出,其 scriptPubKey 就是待证明的地址。message_hash是 BIP-340 标签哈希,即SHA256(SHA256(tag) ‖ SHA256(tag) ‖ message),标签为BIP0322-signed-message。to_sign:版本 0;一个输入,花费to_spend:0;一个金额为 0 聪、指向OP_RETURN的输出。
签名就是在 to_sign 中花费 to_spend:0,方式与钱包花费该地址的任何输出相同。验证则按标准脚本规则检查这笔花费(SIGHASH_ALL 或 Taproot 的默认类型、low-S 和严格 DER、CLEANSTACK 等)。由于这两笔交易在网络上都无效,签名无法被广播出去转移资金。
编码方式:
| 变体 | 前缀 | 内容 | 用途 |
|---|---|---|---|
| 简单 | smp | 见证栈,base64 | 原生 SegWit 和 Taproot,无时间锁 |
| 完整 | ful | 已签名的完整 to_sign,base64 | 任意脚本,包括嵌套 SegWit、传统地址和时间锁 |
| 完整(资金证明) | pof | 带有额外输入、已最终确定的 to_sign PSBT | 证明对特定 UTXO 的控制权 |
| 传统 | 无 | BIP-137 签名 | 仅允许用于传统 P2PKH 地址 |
变体前缀是 2.0.0 版新增的,签名者现在必须(MUST)带上前缀。出于兼容性考虑,“在没有前缀时,验证者可以假定为简单变体”,按早期文本实现的钱包所生成的签名正是靠这一点继续可用。验证结果有三种,而不是两种:有效、无效,以及验证者无法评估所涉及的脚本时的无法判定(inconclusive)。
多签与硬件钱包:通过 PSBT 签名
对于多签地址,见证需要多个密钥的签名,而这些密钥由不同的人或设备持有。BIP-322 用多签用户现有的工具来处理这个问题:to_sign 交易以 PSBT 的形式在各方之间传递,其中新增一个全局字段 PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE(类型 0x09),用于携带消息。签名者如果看到该字段、一个花费对应 to_spend 第 0 个输出的首个输入,以及唯一一个金额为零的 OP_RETURN 输出,就必须向用户显示消息和地址,而不是一笔交易,然后像签署一笔花费那样签名。每个共同签名者添加一个部分签名;最终确定的结果就是一个 ful 签名。不识别该字段的设备看到的则像是一笔花费 0 聪的交易,因此,钱包对这一流程的支持比密码学本身更重要。
该 BIP 列出了两个参考实现:一个是提供基础支持的 Bitcoin Core pull request(#24058),已关闭且未合并;另一个是提供完整支持的 btcd pull request(#2521),它是生成的测试向量的来源,截至2026年10月20日仍未关闭。两者都没有进入任何发布版本,这也是下表中支持 BIP-322 的钱包和库各自实现该标准的原因之一。
用该 BIP 的测试向量检验
BIP-322 附带了基础测试向量:三条消息的消息哈希和交易 ID,以及一个 P2WPKH 地址的简单签名。验证这些签名只需要地址、消息和签名。同样的签名,分别带上和去掉新前缀,也交给了 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
上面的实现复现了每一个哈希,验证通过了每一个 P2WPKH 简单签名(无论是否带前缀),并拒绝了错误向量。广泛使用的 JavaScript 库 bip322-js 4.0.0 不识别 smp 前缀:它把前缀当作 base64 解码,然后报错 buffer too small,而去掉前缀的同一个签名则能验证通过。因此,在该库更新之前,基于它构建的服务验证新版钱包生成的签名时会失败;对于简单签名,在调用前去掉已知前缀是一种可行的过渡办法。
各软件生成和接受哪些格式
以下依据各项目的源代码(标注处为实测结果),截至2026年10月:
| 软件 | 传统(BIP-137) | BIP-322 | 备注 |
|---|---|---|---|
| Bitcoin Core 31.1 | 仅 P2PKH | 不支持 | 见上文实测 |
| Electrum(master) | P2PKH、P2WPKH、P2SH-P2WPKH,头部字节 27~34 | 不支持 | 两种头部约定都能验证;拒绝非单密钥地址 |
| Trezor 固件(main) | P2PKH、P2SH-P2WPKH、P2WPKH,BIP-137 头部字节 | 不支持 | Taproot:“Unsupported script type” |
| Sparrow(drongo master) | 支持 | 简单签名,带 smp/ful/pof 前缀 | 验证:简单变体,单密钥 P2WPKH 和 P2TR |
| bip322-js 4.0.0 | 支持,宽松或严格 | 简单签名,P2WPKH、P2SH-P2WPKH、单密钥 P2TR | 不支持前缀;见上方提示 |
实际影响如下:Taproot 地址只能用 BIP-322 证明;多签地址完全无法用传统格式证明;而一个想要接受所有这些钱包用户所提交证明的服务,需要同时接受传统签名(对 SegWit 地址采用宽松验证)、无前缀的 BIP-322 简单签名,以及带前缀的签名。
如何在服务中验证
- 由你这一方确定消息内容。消息中应包含你的域名、nonce 或时间戳以及用途(
"Proof of ownership for example.com, nonce 7f3a…"),并且每个 nonce 只接受一次,做法与使用以太坊登录(Sign-In with Ethereum)完全相同。对通用文本的签名可以被重放到任何要求相同文本的其他服务上。 - 逐字节比对。两种格式签名的都是原始消息。Electrum 的界面会在签名前去掉首尾空白;复制粘贴带来的末尾换行符,或 CRLF 换行,都会产生不同的哈希,并导致“签名无效”。
- 识别格式,按以下顺序:
smp、ful或pof前缀;解码后恰好为 65 字节、且第一个字节在 27~42 之间的 base64 字符串(传统格式);否则尝试按无前缀的 BIP-322 简单签名处理。 - 确定传统签名对 SegWit 地址证明了什么:宽松验证证明的是对密钥的控制权,对单密钥地址而言,这就等于对地址的控制权。
- 把“无法判定”视为未验证。无法评估脚本(时间锁、不常见的脚本)的验证器不得报告成功。
- 对于资金证明,要查询链上数据。
pof签名证明的是签名时对所列输出的控制权;这些输出是否仍未花费需要另行查询,而且它永远无法证明这些就是某个地址的全部输出。
参考资料
- BIP-137:使用私钥的消息签名
- BIP-322:通用签名消息格式,版本 2.0.0,及其基础测试向量
- BIP-340(标签哈希)、BIP-143(SegWit 签名哈希)、BIP-174(PSBT)
- RFC 6979:确定性 DSA 与 ECDSA
- Bitcoin Core 31.1 的
signmessage和verifymessageRPC - Electrum
bitcoin.py、Trezorsign_message.py、Sparrow drongoBip322.java、bip322-js