graph TD
Curve[有限域上的椭圆曲线<br/>y² = x³ + ax + b mod p] --> Add[点加法 P + Q]
Add --> Dbl[倍点 P + P = 2P]
Dbl --> Scalar[标量乘法 k·G<br/>Double-and-Add 算法]
Scalar --> Pub[公钥 = 私钥 · G]
Scalar --> ECDLP[安全假设 ECDLP<br/>已知公钥难推私钥]
Pub --> Sig[ECDSA 签名/验签]
style Curve fill:#bbdefb
style ECDLP fill:#ffcdd2
style Sig fill:#c8e6c9
2.3 节澄清了区块链中的非对称密码学本质是数字签名,并指出 ECC 在密钥尺寸和签名效率上远超 RSA。本节深入 ECC 的数学心脏——椭圆曲线群上的点运算,并用 TypeScript 从零实现 secp256k1 的点加法、倍点和标量乘法。
2.4.1 椭圆曲线的定义与 secp256k1 参数
标准方程(Weierstrass 形式)
素数域(即模一个素数 )上的椭圆曲线定义为满足以下方程的所有点 的集合:
其中 ,且判别式 (保证曲线非奇异——没有"尖点"或"自交点")。
secp256k1 的精确参数
secp256k1 是 Standards for Efficient Cryptography - Prime 256-bit Koblitz curve 的缩写。Koblitz 曲线是指 的特殊形式,这种选择允许额外的优化。
| 参数 | 十六进制值 | 意义 |
|---|---|---|
0 | 曲线参数,简化为 | |
7 | 曲线参数 | |
| (域阶) | 0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F | 一个接近 的素数,定义有限域 |
| (生成点阶) | 0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 | 生成点 的乘法阶,是一个大素数,约为 |
| (生成点) | 04 79BE667E F9DCBBAC 55A06295... | 曲线上一个特定点,所有公钥都是 的标量倍 |
1 | 余因子(cofactor),为 1 表示曲线没有小的子群 |
/**
* secp256k1 参数定义
*/
const SECP256K1 = {
a: 0n,
b: 7n,
p: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2Fn,
n: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n,
// 生成点 G 的坐标
G: {
x: 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798n,
y: 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8n,
},
h: 1n,
} as const;2.4.2 有限域上的算术
椭圆曲线上的所有运算都在有限域 中进行,即所有计算结果必须对 取模。核心运算包括:
模加/模减
模乘
模逆元(Fermat 小定理)
因为 是素数,且由 Fermat 小定理 ,所以 。
/**
* 模幂运算:快速幂算法(二进制法)
* 计算 base^exponent mod mod
* 时间复杂度:O(log exponent)
*/
function modPow(base: bigint, exponent: bigint, mod: bigint): bigint {
if (mod === 1n) return 0n;
let result = 1n;
let b = ((base % mod) + mod) % mod;
let e = exponent;
while (e > 0n) {
if (e & 1n) {
result = (result * b) % mod;
}
b = (b * b) % mod;
e >>= 1n;
}
return result;
}
/**
* 模逆元:使用 Fermat 小定理
* a^{-1} ≡ a^{p-2} (mod p),要求 p 为素数
*/
function modInverse(a: bigint, p: bigint): bigint {
return modPow(((a % p) + p) % p, p - 2n, p);
}
// --- 模运算验证 ---
const p = 17n; // 用小素数验证
console.log(`模逆 3^{-1} mod 17 = ${modInverse(3n, p)}`);
// 应输出 6,因为 3 * 6 = 18 ≡ 1 (mod 17)2.4.3 点加法:椭圆曲线上的群运算法则
椭圆曲线上的点集(包含一个特殊的"无穷远点" )在点加法下构成一个阿贝尔群。
几何直觉
给定两个不同的点 和 :
- 过 画一条直线,与曲线相交于第三点 。
- 将 关于 轴对称,得到 。
代数公式(需在有限域 中计算):
倍点运算()
当 时,直线变为切线:
/**
* 椭圆曲线上的点
* 使用 bigint 支持 256 位大整数运算
*/
class ECPoint {
constructor(
public x: bigint | null, // null 表示无穷远点 O
public y: bigint | null,
) {}
isInfinity(): boolean {
return this.x === null || this.y === null;
}
isEqual(other: ECPoint): boolean {
return this.x === other.x && this.y === other.y;
}
toString(): string {
if (this.isInfinity()) return "O (Infinity)";
return `({this.y!.toString(16).slice(0, 16)}...)`;
}
}
/**
* 在 secp256k1 曲线上执行点加法 P + Q
*/
function ecAdd(P: ECPoint, Q: ECPoint, a: bigint, p: bigint): ECPoint {
// O + P = P, P + O = P
if (P.isInfinity()) return new ECPoint(Q.x, Q.y);
if (Q.isInfinity()) return new ECPoint(P.x, P.y);
// 如果 P 和 Q 的 x 相同但 y 相反,P + Q = O
if (P.x === Q.x && P.y !== Q.y) {
return new ECPoint(null, null);
}
let lambda: bigint;
const x1 = P.x!, y1 = P.y!, x2 = Q.x!, y2 = Q.y!;
if (P.isEqual(Q)) {
// 倍点:P = Q,lambda = (3x₁² + a) / (2y₁)
const numerator = (3n * x1 * x1 + a) % p;
const denominator = modInverse(2n * y1, p);
lambda = (numerator * denominator) % p;
} else {
// 普通点加:lambda = (y₂ - y₁) / (x₂ - x₁)
const numerator = ((y2 - y1) % p + p) % p;
const denominator = modInverse(((x2 - x1) % p + p) % p, p);
lambda = (numerator * denominator) % p;
}
const x3 = ((lambda * lambda) % p - x1 - x2) % p;
const y3 = (lambda * ((x1 - x3) % p) - y1) % p;
return new ECPoint(
((x3 % p) + p) % p,
((y3 % p) + p) % p,
);
}
// --- 用小参数验证 ---
// 使用一个小素数 p=97,曲线 y^2 = x^3 + 7 进行可视化和验证
const testP = 97n;
const testA = 0n;
const testB = 7n;
// 手动验证点 (3, 91) 是否在曲线上:91^2 mod 97 = 8281 mod 97 = 42; 3^3 + 7 = 34 mod 97;不相等,重新计算
// 找到曲线上的点:x=10: 10^3+7=1007, 1007 mod 97 = 1007 - 97*10 = 37
// y^2 ≡ 37 (mod 97) → 需要找 y 使得 y^2 mod 97 = 37
// y=17: 289 mod 97 = 289 - 2*97 = 95; y=23: 529 - 5*97 = 529 - 485 = 44;
// 实际上在 secp256k1 的大参数下验证更直接
console.log("\n点加法实现完成,准备用 secp256k1 参数测试...");2.4.4 标量乘法:私钥到公钥的核心运算
标量乘法是椭圆曲线密码学中最重要的运算:( 次)。
核心安全假设:椭圆曲线离散对数问题(ECDLP)
给定 和 ,求 在计算上是不可行的。当前已知最好的通用算法(如 Pollard's Rho)的时间复杂度约为 次运算——这在当前及可预见的计算能力下是不可能的。
快速标量乘法:双倍-加算法(Double-and-Add)
简单的 次重复加法需要 次点加法。但使用快速幂思想(将 展开为二进制),可以将复杂度降到 :
输入:标量 k(二进制:k_{n-1} ... k_1 k_0)
结果 = O
当前点 = P
对 i 从 0 到 n-1:
如果 k_i == 1:结果 = 结果 + 当前点
当前点 = 2 * 当前点
返回 结果/**
* 快速标量乘法:k * P
* 使用 Double-and-Add 算法
*/
function scalarMultiply(k: bigint, P: ECPoint, a: bigint, p: bigint): ECPoint {
let result = new ECPoint(null, null); // O (无穷远点,群的单位元)
let addend = new ECPoint(P.x, P.y);
let n = k;
while (n > 0n) {
if (n & 1n) {
result = ecAdd(result, addend, a, p);
}
addend = ecAdd(addend, addend, a, p); // 2 * addend
n >>= 1n;
}
return result;
}
// --- 完整验证链 ---
const p = SECP256K1.p;
const a = SECP256K1.a;
const G = new ECPoint(SECP256K1.G.x, SECP256K1.G.y);
// 1. 验证 G 在曲线上:y^2 = x^3 + 7 (mod p)
const lhs = (G.y! * G.y!) % p;
const rhs = ((modPow(G.x!, 3n, p) + SECP256K1.b) % p);
console.log(`G 点验证: y^2 ({lhs === rhs}`);
// 2. 测试标量乘法: 2*G, 3*G
const twoG = scalarMultiply(2n, G, a, p);
const threeG = scalarMultiply(3n, G, a, p);
console.log(`2*G 计算完成: ${twoG.toString()}`);
console.log(`3*G 计算完成: ${threeG.toString()}`);
// 3. 验证 2*G + G = 3*G(标量乘法的分配律)
const addCheck = ecAdd(twoG, G, a, p);
console.log(`(2G + G = 3G)? ${addCheck.isEqual(threeG)}`);
// 4. 一个随机私钥对应的公钥
const d = 0x4c656f6e206973206120676f6f6420626f79n; // "Leon is a good boy" → 用熵编码的私钥
const P = scalarMultiply(d, G, a, p);
console.log(`\n私钥 d=0x${d.toString(16).slice(0, 16)}...`);
console.log(`公钥 P=${P.toString()}`);
console.log(`验证: d*G 在曲线上? ${((P.y! * P.y!) % p === (modPow(P.x!, 3n, p) + 7n) % p)}`);2.4.5 公钥的压缩表示
未压缩公钥占用 65 字节(0x04 + x 坐标 32 字节 + y 坐标 32 字节)。为了节省区块空间,比特币和以太坊使用压缩公钥:
如何从 恢复 ?
已知 ,计算 的平方根。在有限域中,(当 时,secp256k1 的 满足此条件)。
/**
* 从 x 坐标和奇偶性恢复 y 坐标
* 使用 Tonelli-Shanks 的简化版:y = sqrt(x^3 + 7) mod p
* 对 secp256k1,因 p ≡ 3 (mod 4),可用简化式:sqrt = a^((p+1)/4) mod p
*/
function recoverY(x: bigint, isEven: boolean, p: bigint): bigint {
const y2 = (modPow(x, 3n, p) + 7n) % p;
const y = modPow(y2, (p + 1n) / 4n, p);
// 选择正确的奇偶性
const isYEven = (y % 2n === 0n);
if (isEven === isYEven) return (y + p) % p;
return (p - y) % p;
}
// 从生成点 G 测试恢复
const gy = recoverY(G.x!, true, p); // G.y 是偶数
console.log(`\n从 x 恢复 y: 计算值={G.y!.toString(16).slice(0, 16)}...`);
console.log(`恢复一致性: ${gy === G.y!}`);2.4.6 为什么 secp256k1 而不是其他曲线?
| 曲线 | 方程 | 特点 | 主要使用方 |
|---|---|---|---|
| secp256k1 | Koblitz 曲线,高效实现,参数选择无"魔术数"嫌疑 | 比特币、以太坊 | |
| secp256r1 / P-256 | NIST 标准化曲线,b 参数被认为有"后门"嫌疑 | TLS、政府系统 | |
| Curve25519 | Montgomery 曲线,更简洁的常数时间实现 | Signal 协议、WireGuard |
中本聪选择 secp256k1 而非 NIST 曲线 P-256,有说法是避免美国政府可能植入的后门。secp256k1 的 选择简单透明,没有未解释的"随机"常数。
核心认知
- 椭圆曲线上的点构成一个阿贝尔群——支持加法、减法,有单位元(无穷远点 )。群结构是离散对数难题存在的前提。
- 标量乘法是"易算难逆"的密码学单向函数。 可在毫秒内计算,但从 和 反推 需要 次运算。
- 有限域中的模运算需要特别小心——除法变为乘模逆元,利用 Fermat 小定理高效实现。所有中间结果必须保持模 归一化,否则链式错误会迅速放大。
- 压缩公钥节省 50% 空间。 从 恢复 依赖 的特殊性质,这并非所有素域都满足——这也是 secp256k1 参数设计的一部分。
下一预告:2.5 将基于椭圆曲线的标量乘法实现完整的 ECDSA 签名与验签流程,并深入剖析著名的 Sony PS3 私钥泄露事件——为什么一个"随机数重复"的错误可以导致巨额经济损失。
评论
0评论加载中…