跳到主要内容
ecdsa.com
← 全部文章

secp256k1 详解:比特币和以太坊背后的椭圆曲线

y² = x³ + 7,定义在 256 位素数域上。我们用几行 Python 检查 SEC 2 中的每个常量,展示 libsecp256k1 所依托的捷径,并测试哪些库接受 high-S 签名;所有代码均于2026年10月20日运行。

简短回答:secp256k1 是素域 p = 2²⁵⁶ − 2³² − 977 上的椭圆曲线 y² = x³ + 7,由 SEC 2 于2000年定义。它的群包含素数个点(余因子为 1),安全强度约为 128 位。它的系数是为了速度而选定的,并非由种子生成:a = 0 简化了公式,而且这条曲线具有可高效计算的自同态,能加快标量乘法。比特币从2009年的首次提交起就使用它,以太坊随后跟进,如今 Nostr 和大多数 EVM 链也在使用它。WebCrypto、TLS 1.3 和大多数安全硬件都不支持它;在区块链协议之外,应优先选择 P-256 或 Ed25519。在普通 ECDSA 之上,它的生态系统还加入了各自的规则:比特币的中继策略和以太坊交易只接受 low-S 签名;用公钥恢复代替传输公钥;以 Keccak-256 或双重 SHA-256 作为哈希。

参数

SEC 2 v2.0 §2.4.1 用六个数定义这条曲线(SEC 2):

参数值含义
p2²⁵⁶ − 2³² − 977(0xffff…fffefffffc2f)域的素数;它的形式让模 p 约简的代价很低
a, b0, 7曲线 y² = x³ + 7
Gx = 0x79be667e…16f81798,y = 0x483ada77…fb10d4b8基点
n0xffff…fffebaaedce6af48a03bbfd25e8cd0364141G 的阶,是素数
h1余因子:除无穷远点外,每个点都能生成整个群

这里的一切都无须盲目相信,也没有任何数值来自来历不明的种子。下面的脚本用普通的 Python 整数检查这些常量:

curve.py(节选):检查 SEC 2 参数,Python 3.14.8
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7:  ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime:  ", probable_prime(n))
print("n * G is infinity:      ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n:    ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
输出(python:3.14.8-slim-trixie)
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7:   True
n is a probable prime:   True
n * G is infinity:       True
trace t = p + 1 - n:     432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1

add 和 mul 是教科书式的仿射坐标点加和倍点-点加(double-and-add)算法(用来检查常量没有问题,但不能用来处理秘密值:它们不是常量时间的)。迹与 SafeCurves 公布的值一致。由于 n 是素数且等于点的个数,这里不存在小子群:让 Curve25519 协议变得复杂的余因子问题,在这条曲线上并不存在。

为什么这种形式让它更快

这些数有三个特性是刻意设计的:

  • 素数是 2²⁵⁶ 减去一个较小的数,因此把 512 位的乘积模 p 约简时,只需乘几次 2³² + 977,而不必做一般的除法。
  • a = 0 让每次倍点运算都省去一次乘法。
  • 自同态:由于 p ≡ 1 (mod 3),存在模 p 的三次单位根 β,映射 (x, y) → (βx, y) 会把每个点映射到曲线上的另一个点。这个映射等同于乘以模 n 的三次单位根 λ。于是,标量 k 可以拆成各约 128 位的两半,即 k = k₁ + k₂λ,kP 则按 k₁P + k₂(λP) 计算,其中 λP 只需一次域乘法。这就是 GLV 方法(Gallant、Lambert、Vanstone,2001年)。

同一个脚本会求出 β 和 λ,并检查这个恒等式:

curve.py(节选):自同态
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
    lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy):        ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta   =", hex(beta))
print("lambda =", hex(lam))
输出
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy):         True
beta   = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72

libsecp256k1 用的正是这些常量。Hal Finney 在 2011年2月的一个 bitcointalk 帖子中针对比特币介绍了这个技巧,并预计验证速度能提高 20% 到 25%;Pieter Wuille 正是以他的代码为起点开发了 libsecp256k1。Bitcoin Core 在 PR #20147(Bitcoin Core 0.21,2021年)中随 libsecp256k1 的更新默认启用了 GLV 优化,该 PR 中测得,在启用签名检查的情况下,初始同步快了约 16%。这些优化有多大意义取决于所用的库:OpenSSL 没有为 secp256k1 编写专门的代码,在 P-256 上要快得多(具体数字见 P-256 vs secp256k1)。

比特币和以太坊为什么使用它

比特币的首次提交(2009年8月)用 OpenSSL 的 EC_KEY_new_by_curve_name(NID_secp256k1) 创建密钥。中本聪从未解释过这个选择。通常给出的理由是曲线本身的特性,而不是其使用者的说法:比特币 wiki 指出,它“以一种特殊的非随机方式构造,因而可以进行特别高效的计算”,并且“与流行的 NIST 曲线不同,secp256k1 的常量是以可预测的方式选取的”(Bitcoin wiki)。以太坊沿用了比特币的曲线,但换掉了哈希:交易和消息用 Keccak-256 计算哈希,地址也用它从公钥派生。

标准化组织后来才跟上。RFC 8812(2020年)为 JOSE 和 COSE 注册了 ES256K;NIST SP 800-186(2023年)§H.2 则规定,这条曲线“允许用于区块链相关应用”(它不属于推荐用于一般用途的曲线)。

安全性如何

针对这个群中的离散对数问题,已知最好的攻击是 Pollard rho 算法,据 SafeCurves 计算,约需 0.886·√n ≈ 2¹²⁷·⁸ 次加法,与 P-256 的数字相同,也就是通常所说的“128 位安全强度”。(在这条曲线上,还可以利用 3 阶自同态把 rho 算法再加快一点,约为 √3 倍,但这不会改变整体结论。)SafeCurves 没有把 secp256k1 列为“安全”曲线,但理由涉及的是实现,而不是离散对数:

SafeCurves 标准secp256k1对你意味着什么
rho、迁移(transfer)、刚性(rigidity)通过离散对数难以求解,常量的来历也有说明
CM 判别式未通过(D = −3)判别式小正是自同态的来源;它并不会导致任何攻击
扭曲安全性(twist security)通过,2¹⁰⁹·⁵低于曲线本身;只对跳过点校验的代码有影响
梯形算法(ladder)、完备公式未通过与 Curve25519 相比,编写常量时间、无例外情况的代码需要更加小心
不可区分性未通过点看起来不像随机字符串;这影响的是抗审查协议,而不是签名

实践中的教训与 P-256 相同:使用专为这条曲线编写的库(libsecp256k1、@noble/curves、Rust 的 k256、go-ethereum 的绑定),在输入时校验公钥,切勿自己实现处理秘密值的标量乘法。以点的形式传入的密钥,使用前必须检查它是否位于 y² = x³ + 7 上;跳过这一步,正是经典的无效曲线攻击得以成立的原因,扭曲安全性的数字也正是在这里起作用。此外,与所有 ECDSA 曲线一样,重复或有偏差的 nonce 会泄露密钥(ECDSA nonce 重用)。

密钥与地址

私钥是 [1, n − 1] 中的一个数 d;公钥是点 Q = dG。同一个 Q 有多种写法,把它们混为一谈是“地址不对”这类 bug 的常见原因。下面以 d = 1 为例,它的公钥就是 G 本身(这是人人都知道的密钥,切勿使用):

keys.py:同一个密钥的多种编码(Keccak 和 RIPEMD-160 使用 pycryptodome 3.23.0)
compressed   (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
  • 压缩格式(SEC 1 §2.3.3):y 为偶数时用 02,为奇数时用 03,后接 x。十多年来,比特币默认使用压缩公钥。解压缩需要计算模 p 的平方根,对于这个 p(p ≡ 3 mod 4),只需一次模幂运算:y = (x³ + 7)^((p+1)/4)。
  • 未压缩格式:04 ‖ x ‖ y。同一个密钥在这种形式下会得到不同的比特币地址,因此使用未压缩公钥的旧钱包会为同一个私钥显示其他地址。
  • 仅 x 坐标(32 字节):Taproot 和 Nostr 使用 BIP-340 Schnorr 签名,其公钥省略 y 坐标,并默认 y 为偶数。
  • 以太坊地址:对 64 字节的 x ‖ y(不含 04 前缀)计算 Keccak-256,取最后 20 字节。Keccak-256 不是 SHA3-256:两者的填充不同,使用 hashlib.sha3_256 会得到错误的地址。

low-S:一个签名,两种有效形式

ECDSA 签名 (r, s) 有一个同样能通过验证的孪生签名 (r, n − s),因为对 nonce 点取负不会改变 r。在 TLS 中使用 P-256 时,没有人在意这一点。但对比特币来说,这很重要:孪生签名会让同一笔交易产生不同的交易 ID,按 ID 跟踪付款的服务就可能被欺骗。Bitcoin Core 0.11.1(2015年)让节点“在中继或挖矿时要求 ECDSA 签名使用规范的‘low-s’编码”(版本说明),这是一条中继策略,而不是共识规则。以太坊通过 EIP-2(Homestead,2016年)走得更远:“所有 s 值大于 secp256k1n/2 的交易签名现在都被视为无效”,而“ECDSA 恢复预编译合约保持不变,将继续接受高 s 值”。

通用库不遵守这些规则,区块链库则遵守。我们的实验用 Node.js 内置的 OpenSSL 对 1,000 条随机消息签名,然后用三个库验证其中一个 high-S 签名及其 low-S 孪生签名:

lows.mjs(节选):node 24.22.0、@noble/curves 2.4.0、ethers 6.17.0
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify  high-S: ${opensslOk(sig)}  low-S twin: ${opensslOk(low)}`);
console.log(`noble default       high-S: ${secp256k1.verify(sig, msg, pub)}  low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false   high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
输出(node:24.22.0-bookworm-slim)
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify  high-S: true  low-S twin: true
noble default       high-S: false  low-S twin: true
noble lowS: false   high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s

OpenSSL 生成的签名约有一半是 high-S,OpenSSL 两种形式都接受;除非另行设置,@noble/curves 在 secp256k1 上会拒绝 high-S 签名;ethers 甚至拒绝用 high-S 签名构造签名对象。用通用库或 HSM 签名、再提交到区块链的系统必须做规范化:如果 s > n/2,就把它替换为 n − s,并翻转恢复位。Google Cloud KMS 在文档中说明,它的 secp256k1 密钥只返回 low-S 签名;AWS KMS 的 ECC_SECG_P256K1 文档没有这样的承诺,因此需要你自己对它的输出做规范化。这些签名传输时使用的 DER 编码和原始编码,在 ECDSA 签名格式中有详细介绍。

公钥恢复

有了签名、消息哈希和额外的一两位信息,就能算出签名者的公钥,因此以太坊交易根本不携带公钥,比特币签名消息则直接对照地址验证。这些额外的位就是恢复 ID(v、yParity,或比特币消息签名的头部字节)。恢复的原理、可能出现的问题以及 ecrecover 的行为,详见 ECDSA 公钥恢复。

哪些平台支持它,哪些不支持

平台secp256k1
WebCrypto(浏览器、Deno、Node 的 crypto.subtle)不支持;使用 @noble/curves 或 libsecp256k1 的 WASM 构建
OpenSSL 和 Node.js crypto支持,使用通用(较慢的)代码
BoringSSL不支持:它只内置了 P-224、P-256、P-384 和 P-521
Java(SunEC)已在 JDK 16 中移除:Curve not supported: secp256k1;改用 Bouncy Castle
Apple CryptoKit、Secure Enclave、Android Keystore、TPM 2.0、PIV 卡不支持
云 KMSAWS KMS ECC_SECG_P256K1;Google Cloud KMS EC_SIGN_SECP256K1_SHA256(仅限 HSM 保护级别);Azure Key Vault P-256K 配合 ES256K
TLS 1.3、Web PKI 中的 X.509、WebAuthn不支持

手机和笔记本电脑的安全硬件不支持这条曲线,因此移动钱包要么在软件中保存 secp256k1 密钥(由一个确实存放在硬件中的密钥保护),要么依赖单独的硬件钱包。

常见错误

  • 用 SHA3-256 代替 Keccak-256 计算哈希(以太坊),或者用单次 SHA-256 代替比特币在消息和交易中使用的双重 SHA-256。这样签名验证的是另一条消息,公钥恢复得到的则是另一个看似有效的地址。
  • 提交 high-S 签名:把 OpenSSL、HSM 或云 KMS 生成的签名提交到以太坊或比特币网络。应先做规范化。
  • 混用压缩公钥和未压缩公钥来推导比特币地址。
  • 原始字节长度:64 字节的 r ‖ s 和 65 字节的 r ‖ s ‖ v 是不同的格式;库会明确报错,例如 ethers 的 invalid raw signature length。
  • 想当然地认为平台支持它:在 Java 11 上能运行的代码,在 Java 17 上会报错;WebCrypto 从来就没有这条曲线。
  • 在没有要求的场合使用 secp256k1:用于令牌、TLS、设备身份或通行密钥(passkey)时,它没有任何好处,还会失去硬件支持。

来源