如今,ECDSA 与 Schnorr 之争主要是比特币领域的问题,因为 Taproot(BIP 340~342,2021年11月激活)在 ECDSA 之外,于同一条曲线 secp256k1 上新增了 Schnorr 签名。两种方案使用相同的密钥和相同的困难问题,但 Schnorr 的线性结构让多重签名、门限签名和批量验证变得水到渠成,而 ECDSA 的结构让它们实现起来很别扭。
概览
| ECDSA(secp256k1) | Schnorr(BIP-340,secp256k1) | |
|---|---|---|
| 签名方程 | s = k⁻¹·(z + r·d) mod n | s = k + e·d mod n,e = H(R ‖ P ‖ m) |
| 公钥 | 压缩形式 33 字节(未压缩形式 65 字节) | 32 字节,仅含 x 坐标(x-only) |
| 签名 | 紧凑格式 64 字节;比特币脚本中的 DER 格式为 70~72 字节 | 64 字节 |
| 延展性 | (r, n − s) 同样有效;比特币强制要求 low-S | 按 BIP-340 验证时不存在 |
| 公钥恢复 | 可以(以太坊的 ecrecover) | 不可以 |
| 密钥聚合 | 需要多方计算协议 | 原生支持(MuSig2,BIP-327) |
| 门限签名 | 复杂的协议(GG18/20、CGGMP) | FROST(RFC 9591) |
| 批量验证 | 不支持 | 支持 |
| 安全性证明 | 仅限通用群模型 | 随机预言机模型,离散对数假设 |
| 标准 | FIPS 186-5、SEC 1 | BIP-340;EdDSA(RFC 8032)是 Schnorr 的一种变体 |
| 使用场景 | 比特币传统输出和 SegWit v0 输出、以太坊、大多数 EVM 链 | 比特币 Taproot、Nostr、FROST 部署 |
两者的区别
两者签名时都使用一个秘密 nonce k 及其对应的点 R = k·G。ECDSA 通过 k 的模逆,把哈希 z、私钥 d 和 r(R 的 x 坐标)混合在一起。Schnorr 则把承诺值、公钥和消息一起哈希成挑战值 e,并以 s = k + e·d 作为响应,验证者通过 s·G = R + e·P 进行检查。
这一个设计选择决定了其余的一切差异:
- 线性。Schnorr 签名可以相加。多个签名者可以把各自的 nonce 和部分响应合并成一个签名,该签名可以用他们公钥之和来验证。MuSig2(BIP-327)通过两轮交互完成 n-of-n 签名;FROST(RFC 9591)实现 t-of-n 门限签名。在 ECDSA 中,
k的逆破坏了线性,因此门限 ECDSA 需要繁重的多方协议;钱包和托管机构都在使用这些协议,但它们出现过严重的漏洞。 - 批量验证。许多 Schnorr 签名可以一起检查,比逐个检查更快,这对区块验证很重要。ECDSA 没有这样的捷径。
- 对公钥的承诺。由于
e的哈希输入包含公钥,Schnorr 签名无法换到另一个公钥下复用,也无法从签名中恢复出公钥。ECDSA 支持公钥恢复,以太坊依赖它来节省字节;使用 Schnorr 时,你必须传输公钥或事先知道公钥。 - 延展性。ECDSA 的
(r, s)和(r, n − s)都能通过验证,因此比特币强制要求 low-S 和严格 DER 编码。BIP-340 规定了唯一的编码,因此第三方无法修改一个有效的签名。 - 历史。Claus Schnorr 于1989年提出的方案申请了专利,DSA(以及后来的 ECDSA)就是为了绕开这项专利而设计的;该专利于2008年到期,那时 ECDSA 早已根深蒂固。EdDSA(2011年)是 Edwards 曲线上的一种确定性 Schnorr 变体。
Taproot 在实践中带来的变化
Taproot 输出承诺的是单个 32 字节的 x-only 公钥。通过密钥路径花费只需要一个 64 字节的 Schnorr 签名,无论这个公钥属于一个人,还是多个公钥的 MuSig2 聚合,因此 2-of-2 或 3-of-3 的多签花费在链上看起来和普通的单签名花费没有区别。其他花费条件放在一棵脚本 Merkle 树中,只有在使用时才会公开。在 SegWit v0 中,等价的单密钥花费要在见证数据中携带一个最长 72 字节的 DER ECDSA 签名、一个 sighash 字节和一个 33 字节的公钥。
下面的脚本在 Node.js 26.10 上用 @noble/curves 2.3.0 运行,展示了同一私钥下两种签名的大小差异。
import { secp256k1, schnorr } from "@noble/curves/secp256k1.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { utf8ToBytes } from "@noble/hashes/utils.js";
const secretKey = secp256k1.utils.randomSecretKey();
const msg = sha256(utf8ToBytes("pay 0.01 BTC to bc1p..."));
// ECDSA: 33-byte compressed public key, 64-byte compact (r, s) signature.
const ecdsaPub = secp256k1.getPublicKey(secretKey, true);
const ecdsaSig = secp256k1.sign(msg, secretKey, { prehash: false });
// BIP-340 Schnorr: 32-byte x-only public key, 64-byte (R.x, s) signature.
const schnorrPub = schnorr.getPublicKey(secretKey);
const schnorrSig = schnorr.sign(msg, secretKey);
console.log("ECDSA public key:", ecdsaPub.length, "bytes, signature:", ecdsaSig.length, "bytes, valid:", secp256k1.verify(ecdsaSig, msg, ecdsaPub, { prehash: false }));
console.log("Schnorr public key:", schnorrPub.length, "bytes, signature:", schnorrSig.length, "bytes, valid:", schnorr.verify(schnorrSig, msg, schnorrPub));
ECDSA public key: 33 bytes, signature: 64 bytes, valid: true
Schnorr public key: 32 bytes, signature: 64 bytes, valid: true
ECDSA 的紧凑格式同样是 64 字节,但比特币脚本中的 ECDSA 签名采用 DER 编码,以太坊则会多加一个恢复字节;而 Schnorr 签名就以这种形式原样上链。
共同的陷阱
两种方案都容不下糟糕的 nonce:对两条消息重复使用同一个 k,或者反复泄露它的几位,在两种方案中都会暴露私钥。BIP-340 建议由私钥和消息派生 nonce,并混入 32 字节新鲜的辅助随机数。多方 Schnorr 还多了一条规则:MuSig2 的 nonce 绝不能仅凭消息确定性地派生,因为控制了另一个参与方的攻击者可以让同一个 nonce 为两个不同的聚合挑战签名。应使用经过审计的实现,例如 libsecp256k1 的 MuSig2 模块,而不要自己组合密码原语。
如何选择
- 新的比特币钱包和输出:使用 Schnorr 签名的 Taproot 密钥路径花费;参与方能够相互协调时,多方密钥用 MuSig2,备用方案用脚本路径。
- 比特币消息签名:Taproot 地址使用 BIP-322,因为较早的 BIP-137 格式只支持 ECDSA。
- 以太坊和 EVM 链:ECDSA,因为交易、
ecrecover和钱包都以 ECDSA 为前提;智能账户可以在合约代码中验证其他方案。 - Nostr:BIP-340 Schnorr,这是 NIP-01 的要求。
- 门限托管:验证者接受 Schnorr 或 EdDSA 签名时用 FROST;只有目标链要求 ECDSA 时才用门限 ECDSA。
- 非区块链系统:Ed25519 提供了 Schnorr 式设计,同时具备标准(RFC 8032)、库支持和确定性 nonce。
迁移说明
在比特币上,同一个私钥可以生成两种签名,但需要哪一种由输出决定:把资金从 P2WPKH 或 P2PKH 输出转移到 P2TR 输出,这就是迁移。
- 升级钱包软件和硬件,换成支持 Taproot(BIP-86 派生路径
m/86'/0'/0')和 PSBT Taproot 字段的版本。 - 正确使用 x-only 公钥:BIP-340 公钥去掉了表示奇偶性的那个字节,而且公钥会用脚本树承诺进行调整(tweak);不要复用 ECDSA 的公钥编码。
- 把消息签名切换到 BIP-322,让 Taproot 地址也能证明所有权。
- 对于多签,在 MuSig2 密钥路径聚合(隐私性最好、成本最低,但需要交互)和 Tapscript
OP_CHECKSIGADD脚本路径(无需交互,使用时会暴露策略)之间做出选择。 - 继续验证 ECDSA,以支持较早的输出;共识规则会无限期地同时支持两者。