secp256k1 椭圆曲线点运算
把两个曲线点相加、对一个点做点倍,或把生成元乘以一个标量 —— 每一个比特币公钥都建立在这套最原始的群运算之上。用 k·G 看私钥如何变成一个点;把一个点与它的相反数相加,看它如何塌缩成无穷远点。
转换结果
第 1 步 —— 按解析结果展示的操作数
在开始任何运算之前,点操作数都会先通过曲线方程校验。
y² ≡ x³ + 7 (mod p) 的校验。若 B 留空,则按生成元 G 处理。第 2 步 —— 实际用到的曲线常量
坐标对 p 取模;标量对 n 取模。余因子 h = 1。
02 开头。第 3 步 —— 结果点
群运算的结果要么是一个带两个坐标的普通点,要么是单位元 ∞。
P + (−P) = ∞ 就是椭圆曲线版本的 x + (−x) = 0。由于 ∞ 没有 x 坐标,所以没有压缩或非压缩序列化可以展示。第 4 步 —— 序列化
这条曲线上的点要么序列化为带奇偶前缀的 33 字节,要么把 Y 也写出来成为 65 字节。
本页计算了什么
这是一个直接操作比特币所用椭圆曲线 secp256k1 群运算的「裸机」试验台。它只提供三种运算:
- mul —— 标量 × 生成元:
k · G,把私钥变成公钥的那一步。 - add —— 点 + 点:
P + Q,群运算里的「弦」。 - double —— 点倍:
2 · P,群运算里的「切线」。
全部计算都在本服务器上用纯 PHP + GMP 大整数实时完成。输出包括:按解析结果展示的操作数、实际用到的曲线常量、 结果点、它的奇偶性、两种标准序列化形式、结果是否为无穷远点,以及对结果独立执行的曲线方程校验。
1. 曲线,以及为什么它的点能构成群
secp256k1 是所有满足下面方程、坐标取模素数 p 的 (x, y) 对
再加上一个额外的元素——无穷远点 ∞——所组成的集合。由于方程是模 p 的,
它在纸上并不是一条光滑的弧线,而是散布在 256 位宽有限网格上约 n 个离散点。真正的奇迹在于:可以在这些
点上定义一种加法,它满足结合律、交换律,有单位元(∞),并且每个点都有逆元 −P。
凡是满足这四条性质的集合就叫做阿贝尔群,而群正是「可以把一个运算反复迭代」所需要的结构——
「乘以一个整数」的全部含义就在于此。
逆元是免费的:如果 (x, y) 在曲线上,那么 (x, −y) 也在曲线上,因为曲线方程里只有
y²。所以 −P = (x, p − y)。注意 p − y 与 y 的奇偶性必然相反,
这一点正是压缩编码所利用的。
| 常量 | 取值 | 含义 |
|---|---|---|
域素数 p | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F | 坐标取值范围 [0, p−1]。等于 2²⁵⁶ − 2³² − 977,十进制为 115792089237316195423570985008687907853269984665640564039457584007908834671663。 |
群的阶 n | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | 点的总数(含 ∞)。私钥取值范围 [1, n−1]。十进制为 115792089237316195423570985008687907852837564279074904382605163141518161494337。 |
生成元 Gx | 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 | 约定俗成的基点。每一个比特币公钥都是它的倍数。 |
生成元 Gy | 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 | 为偶数,所以 G 的压缩形式以 02 开头。 |
余因子 h | 1 | 群的阶恰好是 n,且 n 是素数。见第 5 节。 |
p mod 4 | 3 | 正因如此模平方根才有廉价公式:√a = a^((p+1)/4)。 |
p 和 n 是两个不同的数
这条曲线上几乎所有的初学者 bug 都来自把它们搞混:p 约束的是坐标,所有域运算都对
p 取模;n 约束的是标量,所有标量运算都对 n 取模。两者在数值上
都接近 2²⁵⁶,所以极易混淆;一旦用错,得到的结果仍然是「看起来合法的 32 字节数字」。
2. 群运算的几何解释:弦与切线
经典构造是在实数域上作图。取曲线上的两点 P、Q,作过它们的直线。三次曲线与一条直线
恰好交于三点(按重数计),于是有第三个交点 R。把 R 沿 x 轴翻转,就定义为和:
为什么要翻转?因为曲线上的点关于 x 轴对称后仍在曲线上,而且如果不翻转,这个构造就不满足结合律——
只有翻转后的版本才能让 (P+Q)+R = P+(Q+R) 成立。有三种特殊情况值得单独命名,因为它们正好是
实现里必须分支的地方:
- P = Q。 过一个点的直线不唯一,极限情形就是该点处的切线。这就是「弦与切线」里的 「切线」,也是点倍运算所做的事。
- Q = −P(x 相同、y 相反)。过这两点的直线是竖直的,它与曲线只交于这两点,第三个交点就是
无穷远点。所以
P + (−P) = ∞,完全符合「加法逆元」的定义。 - P = ∞ 或 Q = ∞。 既然
∞是单位元,和就等于另一个操作数。
在有限域上,「过 P、Q 的直线」依然有意义——它是满足某个一次方程(模 p)的点集——同样的
「三交点」论证依然成立。图不好画了,但代数毫发无损。
3. 群运算的代数解释
模 p 下除以一个非零元素,等于乘以它的模逆元,所以下面每一个「斜率」都是真实可计算的域元素。
设 λ 为过两点的弦(或切线)的斜率,结果点为 (x₃, y₃):
x₃ = λ² − x₁ − x₂ y₃ = λ(x₁ − x₃) − y₁ (全部 mod p)
整套群运算就这些公式。x₃ 只依赖 λ 和两个 x 坐标,而 y₃ 随后由
「关于直线翻转」唯一确定。还要注意:公式关于 x₁ ↔ x₂ 对称,但 y 项不对称——这正是
P + Q = Q + P 成立、而 P + (−P) 必须单独处理的原因。
一次点加的成本是 1 次模逆元(最贵的部分:本实现用费马小定理做幂运算 a^(p−2))加上 2–3 次模乘/模平方。
真正的库会改用射影坐标或雅可比坐标来「推迟」求逆,用更多乘法换掉逆元,因为一次逆元的代价大约是乘法的上百倍。
| 情形 | 条件 | 使用的公式 | 结果 |
|---|---|---|---|
| 一般点加 | x₁ ≠ x₂ | 弦斜率 (y₂−y₁)/(x₂−x₁) | 一个普通曲线点 |
| 点倍 | P = Q(即 x₁ = x₂、y₁ = y₂) | 切线斜率 3x₁²/(2y₁) | 一个普通曲线点 |
| 互逆点对 | x₁ = x₂ 且 y₁ + y₂ ≡ 0 | 回避除零 | ∞,单位元 |
| 单位元 | P = ∞ 或 Q = ∞ | 直接短路 | 另一个操作数 |
y = 0 | 点倍时 y₁ = 0 | 2y₁ ≡ 0 | ∞;secp256k1 上不存在这种点 |
4. 为什么 ∞ 是单位元,以及它何时出现
无穷远点不是一对坐标,也无法被序列化成公钥——标准编码里根本没有它的位置。它存在的意义是回答 「当直线竖直时第三个交点是什么」,而它的行为与普通算术中的零完全一致:
有三种情形会产生它,本页会明确显示「结果是否为无穷远点」:
- 把一个点与它的相反数相加。
P + (−P) = ∞。你可以自己看: 把演示公钥与它的相反数相加 —— 两个输入 x 坐标相同,只有02/03前缀不同。 - 对一个
y = 0的点做点倍。 切线竖直,结果为∞。但 secp256k1 上 不存在这样的点:y = 0的点满足2P = ∞,即它是一个 2 阶元素;而阶为奇素数n的群中根本没有 2 阶元素。代码里保留这个分支是为了安全,而不是因为你真能走到它。 - 把生成元乘以
n的倍数。 按阶的定义n · G = ∞,于是(n+1) · G = G。这正是私钥被限制在[1, n−1]的原因:一个等于n+1的「私钥」是合法的 32 字节数字,行为上却悄悄等同于1。
5. 余因子为 1、循环群,以及为什么每个公钥都是 G 的倍数
曲线上的点数等于 h · n,其中 h 是余因子。secp256k1 的 h = 1,
所以群的阶就是 n,而 n 是素数。这带来几个非常舒服的推论:
- 没有小子群。 素数阶的群只有两个子群:
{∞}和整个群。余因子大于 1 的曲线 (例如 Curve25519 的h = 8)含有小阶点,一旦攻击者能让受害者在这些小阶点上做运算, 就能通过「无效曲线攻击」套出秘密的若干比特。那些实现必须乘以余因子或拉黑这些点;这里没有任何东西需要清除。 - 群是
n阶循环群。 每个非单位元点的阶都恰好是n(它的阶整除n且不为 1)。所以除∞之外的任何点都能生成整个群—— 在数学上G并不特殊,它只是大家公开约定好的起点。 - 每个公钥都是某个
k · G,其中k ∈ [1, n−1],而且这个k唯一。存在性来自循环性;唯一性来自:若k₁·G = k₂·G,则(k₁−k₂)·G = ∞,这只可能在k₁ ≡ k₂ (mod n)时发生。 - 标量可以分配。
(a + b)·G = a·G + b·G,且(a·b)·G = a·(b·G)。 这正是 BIP32 子密钥可以用标量加法计算的原因,也是「把点倍一下」等于「用两倍的标量」的原因——两者都写在 下面的实例演算里。
6. 倍点-加点:k · G 到底怎么算出来
标量乘法没有闭式捷径,所以实现只能逐位迭代标量。本项目里的版本从最高位到最低位遍历 256 个比特:
R = ∞ # 单位元
for bit in bits_of(k): # 256 位标量 → 256 次循环
R = double(R) # R = R + R
if bit == 1:
R = add(R, G) # R = R + G
return R # R = k · G
成本是每位一次点倍,外加每个为 1 的比特一次点加。演示私钥 0824c314…171ca 的比特普查结果是:
256 位中有 116 位为 1,因此循环执行 256 次点倍 + 116 次点加——共 372 次点运算,而每一次内部又是
一次求逆加若干次乘法。生产级实现使用窗口法(预先算出 G, 2G, …, 15G,一次处理 4 位)把次数减少约三分之一;
当标量必须保密时则改用 Montgomery 阶梯。
| 运算 | 点运算次数(256 位标量 / 单次调用) | 主要开销 |
|---|---|---|
double —— 2 · P | 1 次点倍 | 1 次求逆 + 约 2 次平方 + 约 2 次乘法 |
add —— P + Q(P ≠ ±Q) | 1 次点加 | 1 次求逆 + 约 3 次乘法/平方 |
add —— P + (−P) | 0(命中分支) | 只比较坐标;直接返回 ∞ |
mul —— k · G | 256 次点倍 + (k 的 popcount)次点加 | 每一步都要求逆,合计约数百次 |
mul —— k · G,窗口法(真实库) | 约 64 次点倍 + 约 64 次点加 + 预计算 | 同样开销,但次数少 2–3 倍 |
从 k 到 k·G 只要 256 次迭代。但从点反推标量就是椭圆曲线离散对数问题;
目前最通用的算法(Pollard rho)需要大约 √n ≈ 2¹²⁸ 步量级。正是这种「正向很便宜、反向贵得离谱」的
不对称性,让公钥可以公开。
7. 时间侧信道,以及这里的「常数时间」是什么意思
如果标量是真的私钥,上面的伪代码就是一个安全隐患,因为它的控制流依赖秘密:比特为 0 就跳过点加, 为 1 就执行点加。任何能测量签名或派生耗时、或观察芯片功耗的人,都可以一个比特一个比特地把标量还原出来。 这不是纸上谈兵:对朴素 ECDSA 实现的计时攻击已经成功跨网络恢复过私钥。
常数时间的意思是:执行的操作序列与内存访问模式不依赖秘密值——只有数据依赖。具体做法包括:
无论如何都同时执行点倍与点加,然后用算术方式挑选正确结果;使用没有特殊分支的统一加法公式;标量盲化
(对随机 r 计算 (k + r·n)·G,点不变但比特模式变了);以及点盲化。libsecp256k1 综合使用了
其中若干手段,并且专门为了避免依赖秘密的分支而编写。
本页背后的纯 PHP + GMP 实现不是常数时间的:GMP 大数的运算时间依赖数据,倍点-加点循环会按标量比特 分支,也没有做盲化。用它试验本页这样的公开输入完全安全;但绝不能用它处理保护真实资金的私钥。那件事请交给 硬件钱包或经过充分验证的常数时间库。
8. 实例演算:对 G 做点倍
任何 secp256k1 实现最经典的健全性检查就是 2G。用上面的真实常量代入切线公式,本页算出:
| 步骤 | 值 |
|---|---|
| 起点 | G = (79be667e…f81798,483ada77…10d4b8) |
斜率 λ = 3Gx² / (2Gy) mod p | 约化为一个 256 位域元素 |
x₃ = λ² − 2Gx | c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 |
y₃ = λ(Gx − x₃) − Gy | 1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a |
y₃ 的奇偶性 | 偶数 → 压缩前缀 02 |
2G 压缩公钥 | 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 |
2G 非压缩公钥 | 04c6047f…b95c709ee51ae168…50cfe52a(65 字节) |
| 独立验证 | 把运算切换为 double 且 A = G:本页会对生成元做点倍,再额外执行 pointMul(2, G) 做交叉验证,并报告两者一致 —— 它们就是私钥 2 的公钥 |
| hash160(2G) / P2PKH | 06afd46bcdfd22ef94ac122aa11f241244a37ecc → 1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP |
| 私钥 2 的 WIF | KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4(压缩) |
另外两个可以用上方表单复现的检查,它们正好验证第 5 节的恒等式:
- 标量乘法与点倍相容:在
mul模式下输入演示私钥,得到k·G=02ff812e…8c67ee80,正是演示公钥。在double模式下对这个点做点倍,得到的压缩结果 以028c65af…开头;而结果面板里的(2k mod n) · G交叉验证行给出的正是同一个值 —— 这就是恒等式2·(k·G) = (2k)·G的实际体现(两个值都会完整打印在面板里,可以逐位比对)。 - 互逆点相加归零:在
add模式下输入02ff812e…8c67ee80与03ff812e…8c67ee80,结果为无穷远点,并且没有任何序列化输出,因为∞没有坐标。
9. 常见错误
- 把标量丢给点的解析器。 一个 64 位十六进制字符串既是合法的标量,也是合法的 x-only 公钥。 本页根据模式判断你的意图,每个字段下方的提示也写明了这一点。判断错了,得到的是一个看起来合法却毫不相干的结果。
- 跳过曲线方程校验。 把任意的
(x, y)当作点接受,正是无效曲线攻击的起点。 这里每次解析都会验证y² ≡ x³ + 7 (mod p),否则抛出可读错误——故意输入一个曲线外的非压缩公钥试试, 你会看到红色错误框。 - 以为小标量是安全的。
1·G = G是公开的。像 1、2、3、0x1234 这样的标量可以被 轻易穷举;这正是真实密钥必须来自 256 位熵、而不能是「我喜欢的某个数字」的原因。 - 指望把
∞序列化。 它没有前缀。没有做检查的代码要么崩溃,要么吐出一个伪造的04公钥。 - 把本页和 ECDSA 混为一谈。 签名需要随机数与模
n的算术;本页只讲群运算, 也就是它下面那一层算术。
10. 速查表
| 项目 | 值 |
|---|---|
| 曲线 | y² = x³ + 7,定义在 GF(p) 上,p = 2²⁵⁶ − 2³² − 977 |
| 群的阶 | n(素数,256 位),余因子 h = 1 |
| 单位元 | ∞ —— 没有坐标,无法序列化 |
| 逆元 | −P = (Px, p − Py);会翻转 02/03 前缀 |
| 点加斜率 | λ = (y₂ − y₁)(x₂ − x₁)⁻¹ mod p |
| 点倍斜率 | λ = 3x₁²(2y₁)⁻¹ mod p |
| 结果 | x₃ = λ² − x₁ − x₂,y₃ = λ(x₁ − x₃) − y₁(均 mod p) |
| 标量乘成本 | 256 次点倍 + k 的每个为 1 的比特一次点加 |
| 公钥 | k · G,序列化为 02/03 ‖ x(33 字节)或 04 ‖ x ‖ y(65 字节) |
| 合法标量 | 1 ≤ k ≤ n−1;用 Btc::normalizePrivateKey 校验 |