区块链一切安全承诺的数学基础。本章讲透哈希函数、对称/非对称密码、椭圆曲线 secp256k1、ECDSA 签名、HD 钱包与 Merkle 树——每一行承诺背后都是可验证的密码学。
本章目录:
2.1 哈希函数的定义与核心安全属性 2.2 SHA-256 与 Keccak-256:从零实现两种哈希结构 2.3 密码学体系:对称加密、非对称加密与数字签名的角色 2.4 椭圆曲线密码学(ECC)与 secp256k1:从零实现 2.5 数字签名与验签:ECDSA 完整流程与 Sony PS3 事件 2.6 钱包与密钥管理:从熵到地址 2.7 Merkle 树与 SPV 轻量验证 2.8 密码学原语选型对比与工程实践 2.9 知识地图、概念关系与常见错误速查 2.10 动手实验:密码学工具箱构建
2.1 哈希函数的定义与核心安全属性
graph TD
H["密码学哈希函数<br/>H: {0,1}* → {0,1}^n"]
H --> P1["确定性<br/>同一输入必有同一输出"]
H --> P2["单向性<br/>(原像阻力)"]
H --> P3["碰撞阻力<br/>(Collision Resistance)"]
H --> P4["隐藏性<br/>(Hiding)"]
H --> P5["谜题友好性<br/>(Puzzle-Friendliness)"]
P2 --> P3
P3 --> P4
P4 --> P5
style H fill:#bbdefb
style P3 fill:#c8e6c9
2.1.1 精确的定义
密码学哈希函数(Cryptographic Hash Function)是一个确定性函数:
H : { 0 , 1 } ∗ → { 0 , 1 } n H: \{0,1\}^* \to \{0,1\}^n H : { 0 , 1 } ∗ → { 0 , 1 } n
它将任意有限长度的 二进制输入 { 0 , 1 } ∗ \{0,1\}^* { 0 , 1 } ∗ 映射为固定长度 的 n n n 位输出。n n n 称为摘要长度 或哈希位数 ,常见的取值有 256(SHA-256、Keccak-256)、384(SHA-384)、512(SHA-512)。
这一定义中有四个关键约束,缺一不可:
------ ---------- ----------
确定性 ∀ x : H ( x ) \forall x: H(x) ∀ x : H ( x ) 唯一确定同一输入产生不同输出导致验证失败
压缩性 输出长度固定为 n n n ,与输入长度无关 无法用于摘要
高效计算 对任意 x x x ,H ( x ) H(x) H ( x ) 可在多项式时间内计算 系统吞吐量为零
原像抗性(单向性) 给定 y = H ( x ) y = H(x) y = H ( x ) ,在计算上不可行地找到任意 x ′ = H ( x ′ ) = y x' = H(x') = y x ′ = H ( x ′ ) = y 哈希可被"逆转",承诺机制失效
关键区分 :"确定性"与"随机性"并不矛盾。H H H 本身是确定性的函数,但对于攻击者来说,在没有额外信息时,H ( x ) H(x) H ( x ) 的行为必须如同随机函数 (无法预测、无法逆推)。这就是密码学对"单向性"的严格表述。
2.1.2 碰撞阻力(Collision Resistance)
形式化定义
设 λ \lambda λ 为安全参数(通常 λ = 128 \lambda = 128 λ = 128 ),如果对于任何概率多项式时间(PPT)敌手 A \mathcal{A} A :
Pr [ ( x 1 , x 2 ) ← A ( 1 λ ) : x 1 ≠ x 2 ∧ H ( x 1 ) = H ( x 2 ) ] ≤ negl ( λ ) \Pr\left[ (x_1, x_2) \gets \mathcal{A}(1^\lambda) : x_1 \neq x_2 \land H(x_1) = H(x_2) \right] \leq \text{negl}(\lambda) Pr [ ( x 1 , x 2 ) ← A ( 1 λ ) : x 1 = x 2 ∧ H ( x 1 ) = H ( x 2 ) ] ≤ negl ( λ )
其中 negl ( λ ) \text{negl}(\lambda) negl ( λ ) 是一个关于 λ \lambda λ 的可忽略函数(比任何多项式的倒数下降得更快)。则称 H H H 具有碰撞阻力 。
为什么碰撞阻力如此关键?
在数字签名中,我们不是对消息 m m m 本身签名(因为 m m m 可能很长),而是对 H ( m ) H(m) H ( m ) 签名。如果攻击者能找到 m 1 ≠ m 2 m_1 \neq m_2 m 1 = m 2 使得 H ( m 1 ) = H ( m 2 ) H(m_1) = H(m_2) H ( m 1 ) = H ( m 2 ) ,那么:
Alice 对 H ( m 1 ) H(m_1) H ( m 1 ) 签名(她认可 m 1 m_1 m 1 )。 攻击者截获签名,与 m 2 m_2 m 2 组合。 Bob 验证:H ( m 2 ) = H ( m 1 ) H(m_2) = H(m_1) H ( m 2 ) = H ( m 1 ) ,签名有效,但消息内容却变成了 m 2 m_2 m 2 。
结论 :签名伪造,信任体系崩塌。
生日攻击(Birthday Attack)与安全强度
寻找碰撞的直观暴力方法是:尝试随机输入 x 1 , x 2 , … x_1, x_2, \ldots x 1 , x 2 , … ,记录所有 H ( x i ) H(x_i) H ( x i ) ,直到发现重复。但这需要大约 2 n 2^n 2 n 次尝试——远比实际需要多。
生日悖论(Birthday Paradox) 告诉我们:在一个 N = 2 n N = 2^n N = 2 n 大小的输出空间中,只需约 k ≈ 2 N ln 2 ≈ 1.177 N ≈ 2 n / 2 k \approx \sqrt{2N \ln 2} \approx 1.177\sqrt{N} \approx 2^{n/2} k ≈ 2 N ln 2 ≈ 1.177 N ≈ 2 n /2 次尝试,就能以 50% 概率找到碰撞。
推导 :设 k k k 次独立采样,没有碰撞的概率为:
P ( no collision ) = ∏ i = 0 k − 1 ( 1 − i 2 n ) ≈ e − ∑ i = 0 k − 1 i 2 n ≈ e − k ( k − 1 ) 2 ⋅ 2 n ≈ e − k 2 2 n + 1 P(\text{no collision}) = \prod_{i=0}^{k-1} \left(1 - \frac{i}{2^n}\right) \approx e^{-\sum_{i=0}^{k-1}\frac{i}{2^n}} \approx e^{-\frac{k(k-1)}{2 \cdot 2^n}} \approx e^{-\frac{k^2}{2^{n+1}}} P ( no collision ) = i = 0 ∏ k − 1 ( 1 − 2 n i ) ≈ e − ∑ i = 0 k − 1 2 n i ≈ e − 2 ⋅ 2 n k ( k − 1 ) ≈ e − 2 n + 1 k 2
当 k = 2 n / 2 k = 2^{n/2} k = 2 n /2 时,P ≈ e − 1 / 2 ≈ 0.607 P \approx e^{-1/2} \approx 0.607 P ≈ e − 1/2 ≈ 0.607 ;碰撞概率 = 1 − 0.607 = 0.393 = 1 - 0.607 = 0.393 = 1 − 0.607 = 0.393 (约 39%)。当 k ≈ 1.177 ⋅ 2 n / 2 k \approx 1.177 \cdot 2^{n/2} k ≈ 1.177 ⋅ 2 n /2 时,碰撞概率恰好为 50%。
哈希算法 输出长度 n n n 碰撞安全(n / 2 n/2 n /2 ) 生日攻击尝试次数(约)
---------- ------------- ------------------ ---------------------
SHA-256 256 bit 128 bit 2 128 ≈ 3.4 × 10 38 2^{128} \approx 3.4 \times 10^{38} 2 128 ≈ 3.4 × 1 0 38
SHA-1 160 bit 80 bit 2 80 ≈ 1.2 × 10 24 2^{80} \approx 1.2 \times 10^{24} 2 80 ≈ 1.2 × 1 0 24
MD5 128 bit 64 bit 2 64 ≈ 1.8 × 10 19 2^{64} \approx 1.8 \times 10^{19} 2 64 ≈ 1.8 × 1 0 19 (实际已被攻破)
实际案例 :2017 年 Google 与 CWI 联合宣布了对 SHA-1 的实际碰撞攻击,用 2 63 2^{63} 2 63 次计算找到了 PDF 碰撞。现代密码学标准已废弃 SHA-1,MD5 更已被完全攻破。
TypeScript:生日攻击概率计算器
/**
* 生日攻击概率计算器
* 计算 k 次独立采样在 N = 2^n 空间中的碰撞概率
*/
function birthdayCollisionProb(n: number, k: number): number {
const N = 2 ** n;
// 精确公式: P(collision) = 1 - e^(-k*(k-1)/(2*N))
const exp = -(k * (k - 1)) / (2 * N);
return 1 - Math.exp(exp);
}
// 演示:SHA-256 输出空间 n=256,不同 k 值
console.log("=== 生日攻击概率分析 ===");
console.log(`n=256, k=2^100: P ≈ ${birthdayCollisionProb(256, 2**100).toExponential(2)}`);
console.log(`n=256, k=2^128: P ≈ ${birthdayCollisionProb(256, 2**128).toExponential(2)}`);
console.log(`n=256, k=2^140: P ≈ ${birthdayCollisionProb(256, 2**140).toExponential(2)}`);
// 类比:经典的 365 人生日问题
function birthdayClassic(people: number): number {
return birthdayCollisionProb(Math.log2(365), people);
}
console.log("\n=== 经典生日悖论 ===");
[10, 23, 40, 50, 100].forEach(k => {
console.log(`k 人中至少一对同生日的概率 : {k} 人中至少一对同生日的概率: k 人中至少一对同生日的概率 : {birthdayClassic(k).toFixed(4)}`);
});
预期输出 :
=== 生日攻击概率分析 ===
n=256, k=2^100: P ≈ 4.17e-48
n=256, k=2^128: P ≈ 0.39
n=256, k=2^140: P ≈ 1.00e+00
=== 经典生日悖论 ===
10 人中至少一对同生日的概率: 0.1169
23 人中至少一对同生日的概率: 0.5073
40 人中至少一对同生日的概率: 0.8912
关键结论 :2 128 2^{128} 2 128 次尝试(约等于全球所有沙粒的 10 18 10^{18} 1 0 18 倍)在 SHA-256 上才有 ~39% 的碰撞概率。这在当前及可预见的计算能力下是计算不可行 的。
2.1.3 隐藏性(Hiding)与承诺机制
形式化定义
隐藏性要求:给定 y = H ( x ) y = H(x) y = H ( x ) ,对于未知且均匀分布的 x x x ,任何 PPT 敌手在统计学意义上无法推断 x x x 的任何部分信息。更严格地说,H ( x ) H(x) H ( x ) 必须"看起来"与随机函数不可区分。
注意:如果 x x x 的分布是有偏的(例如 x x x 是从一个极小的集合中选择的),仅靠 H ( x ) H(x) H ( x ) 的隐藏性不足以保证安全。此时需要引入盐值(Salt) :c = H ( r ∥ x ) c = H(r \parallel x) c = H ( r ∥ x ) ,其中 r r r 是均匀随机数。
承诺机制(Commitment Scheme)
承诺机制是密码学的核心原语,应用于密封拍卖、区块链时间锁定交易、零知识证明等场景。它是一个两阶段协议:
承诺阶段(Commit Phase) :承诺方选择值 v v v ,生成随机数 r ← { 0 , 1 } λ r \gets \{0,1\}^\lambda r ← { 0 , 1 } λ ,计算 c = H ( r ∥ v ) c = H(r \parallel v) c = H ( r ∥ v ) ,将 c c c 发送给验证方。揭晓阶段(Reveal Phase) :承诺方公开 ( r , v ) (r, v) ( r , v ) 。验证方验证 c = ? H ( r ∥ v ) c \stackrel{?}{=} H(r \parallel v) c = ? H ( r ∥ v ) 。
该机制需要同时满足两个属性:
------ ------ ---------------
绑定性(Binding) 承诺方不能找到一个 v ′ ≠ v v' \neq v v ′ = v 使得 H ( r ∥ v ′ ) = c H(r \parallel v') = c H ( r ∥ v ′ ) = c 碰撞阻力
隐藏性(Hiding) 验证方在揭晓前无法推断 v v v 单向性 + 加盐随机性
TypeScript:密码学承诺机制实现
/**
* 密码学承诺机制(Commitment Scheme)
* 演示绑定性与隐藏性
*/
class CommitmentScheme {
private salt: Uint8Array | null = null;
private committedValue: string | null = null;
private commitmentHash: string | null = null;
// 承诺阶段:生成随机盐 + 计算 H(salt || value)
commit(value: string, entropy: Uint8Array): string {
// 真实的随机盐应由 CSPRNG 生成;这里用参数传入模拟
this.salt = entropy;
this.committedValue = value;
this.commitmentHash = this._hash(this.salt, value);
return this.commitmentHash;
}
// 揭晓阶段:公开 (salt, value),验证方重算哈希比对
reveal(): { salt: string; value: string; commitment: string } | null {
if (!this.salt || !this.committedValue || !this.commitmentHash) return null;
return {
salt: this._bytesToHex(this.salt),
value: this.committedValue,
commitment: this.commitmentHash,
};
}
// 验证承诺
verify(saltHex: string, value: string, commitment: string): boolean {
const salt = this._hexToBytes(saltHex);
const recomputed = this._hash(salt, value);
return recomputed === commitment;
}
// 简化的 SHA-256-like 32-byte hash (演示用,非安全实现)
// 真实环境使用 2.2 节的完整实现或 crypto.subtle
private _hash(salt: Uint8Array, value: string): string {
const data = new TextEncoder().encode(this._bytesToHex(salt) + ":" + value);
// 演示:用多个 round 的简单哈希模拟安全属性
let h = 0x6a09e667;
const k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5];
for (let i = 0; i < data.length; i++) {
h = (h ^ data[i]) + k[i % 4] | 0;
h = ((h << 7) | (h >>> 25)) + ((h << 15) | (h >>> 17));
}
// 扩展到 32 字节模拟
let result = '';
for (let i = 0; i < 8; i++) {
const v = (h + i * 0x9e3779b9 | 0) >>> 0;
result += v.toString(16).padStart(8, '0');
}
return result;
}
private _bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
private _hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
}
// --- 演示:Alice 承诺一个预测 ---
const scheme = new CommitmentScheme();
const prediction = "Bitcoin_price_over_100k_2025";
const salt = new Uint8Array([0x8a, 0x3f, 0x12, 0x9d, 0x54, 0xe7, 0x88, 0x01]); // 应随机生成
const commitment = scheme.commit(prediction, salt);
console.log(`承诺值: ${commitment}`);
// 验证方无法从 commitment 反推 prediction(隐藏性)
// 揭晓时
const reveal = scheme.reveal();
console.log(`\n揭晓: salt=r e v e a l ! . s a l t , v a l u e = {reveal!.salt}, value= r e v e a l ! . s a l t , v a l u e = {reveal!.value}`);
// 验证
const isValid = scheme.verify(reveal!.salt, reveal!.value, commitment);
console.log(`验证结果: ${isValid}`);
// 尝试作弊:改变值
const isCheating = scheme.verify(reveal!.salt, "Different_value", commitment);
console.log(`作弊检测: ${isCheating}`);
2.1.4 谜题友好性(Puzzle-Friendliness)
形式化定义
设 H H H 为哈希函数,T ⊆ { 0 , 1 } n T \subseteq \{0,1\}^n T ⊆ { 0 , 1 } n 为一个"谜题目标集"(如"前 d d d 位为 0 的所有输出"),∣ T ∣ ≪ 2 n |T| \ll 2^n ∣ T ∣ ≪ 2 n 。如果对于任何输入分布,不存在比穷举搜索(从 { 0 , 1 } m \{0,1\}^m { 0 , 1 } m 中随机选取 x x x 计算 H ( x ) H(x) H ( x ) )更高效的方法找到满足 H ( x ) ∈ T H(x) \in T H ( x ) ∈ T 的 x x x ,则称 H H H 具有谜题友好性 。
与工作量证明的关系
比特币的 PoW 谜题可以精确表述为:
\text{找 } \text{nonce} \in \{0,1\}^{32} \text{ 使得 } H(\text{block_header} \parallel \text{nonce}) < T
其中 T T T 是难度目标(一个远小于 2 256 2^{256} 2 256 的数值)。满足条件的 expected 尝试次数为:
2 256 T = 难度值(Difficulty) \frac{2^{256}}{T} = \text{难度值(Difficulty)} T 2 256 = 难度值( Difficulty )
如果 H H H 不满足谜题友好性——即存在某种结构可以指导搜索(例如知道某些 nonce 范围更可能产生小输出),则矿工可以用远少于 "预期尝试次数" 的算力找到解,PoW 的安全性假设就崩塌了。
均匀分布假设
谜题友好性等价于要求:当输入 x x x 是均匀随机的,H ( x ) H(x) H ( x ) 必须在输出空间中均匀分布。 没有任何区域被"偏爱"或"避免"。这是对密码学哈希的统计学检验标准之一。
------ ---------- ----------
碰撞阻力 找不到两个不同输入产生相同输出 数字签名、数据完整性
原像抗性 给定输出,找不到任何能生成它的输入 密码存储、承诺隐藏
第二原像抗性 给定 x x x ,找不到 x ′ ≠ x x' \neq x x ′ = x 使得 H ( x ) = H ( x ′ ) H(x) = H(x') H ( x ) = H ( x ′ ) 防篡改保护
下节预告 :2.2 将深入 SHA-256 和 Keccak-256 的内部结构 ,从零实现两种算法的完整流程,并通过雪崩效应(Avalanche Effect) 实验验证"输入 1 位变化导致输出约 50% 位翻转"这一理想特性。
核心认知
哈希函数是密码学的瑞士军刀 ,碰撞阻力、隐藏性、谜题友好性分别从"防伪造""防泄露""防捷径"三个维度构建安全性。安全强度由输出长度的一半决定 。SHA-256 提供 128 位碰撞安全——不是 256 位,这是生日攻击的必然结果。承诺机制是密码学中最优雅的协议之一 。两阶段(承诺+揭晓)、两属性(绑定+隐藏),直接对应现实生活中的"密封信封"。谜题友好性 = 均匀分布 + 无捷径 。这是 PoW 挖矿的数学基础——如果存在捷径,算力优势就不存在,整个经济激励机制就失效。
2.2 SHA-256 与 Keccak-256:从零实现两种哈希结构
2.1 节从宏观上定义了哈希函数应具备的安全属性。本节将深入到算法内部,从零用 TypeScript 实现 两种核心架构——Merkle-Damgård 迭代结构 (SHA-256)和海绵结构 (Keccak-256)。通过亲手实现,你会理解为什么它们满足碰撞阻力,以及为什么 SHA-256 有长度扩展漏洞而 Keccak-256 没有。
2.2.1 SHA-256:Merkle-Damgård 迭代结构
flowchart LR
M["消息 M"] --> Pad["填充对齐<br/>追加 1 + 0* + 长度"]
Pad --> M1[M₁ 512-bit]
Pad --> M2[M₂ 512-bit]
Pad --> Mn[Mₙ 512-bit]
IV[H₀ = IV] --> CF1["压缩函数 f"]
M1 --> CF1
CF1 --> H1[H₁]
H1 --> CF2["压缩函数 f"]
M2 --> CF2
CF2 --> H2[H₂]
H2 --> CFn["压缩函数 f"]
Mn --> CFn
CFn --> Hn["Hₙ = 256-bit 摘要"]
style IV fill:#e3f2fd
style Hn fill:#c8e6c9
整体架构
SHA-256 的总体流程是迭代压缩 :将任意长度的消息切分为 512-bit 的块,依次送入一个压缩函数,最后一个块的输出即为 256-bit 摘 要。
消息 M
└─ 填充 → 512-bit 对齐的消息 M' = M₁ || M₂ || ... || Mₙ
├─ M₁ ──→ [H₀=IV] + 压缩函数 ──→ H₁
├─ M₂ ──→ [H₁] + 压缩函数 ──→ H₂
└─ Mₙ ──→ [Hₙ₋₁] + 压缩函数 ──→ Hₙ = 最终哈希
步骤一:消息填充(Padding)
SHA-256 的填充规则(Merkle-Damgård 标准填充):
在消息末尾追加一个 1 位(即 0x80 字节)。 追加 k k k 个 0 位,使得 len(message) + 1 + k + 64 ≡ 0 ( m o d 512 ) \text{len(message)} + 1 + k + 64 \equiv 0 \pmod{512} len(message) + 1 + k + 64 ≡ 0 ( mod 512 ) 。 追加原始消息长度的 64 位大端序 二进制表示。
/**
* SHA-256 消息填充
* 返回填充后的字节数组,其长度是 64 的整数倍(512-bit = 64 字节)
*/
function sha256Pad(message: Uint8Array): Uint8Array {
const bitLen = message.length * 8;
const msgLen = message.length;
// 计算需要多少填充字节:1 (0x80) + zeros + 8 (length)
const padLen = ((64 - ((msgLen + 9) % 64)) % 64);
const totalLen = msgLen + 1 + padLen + 8;
const padded = new Uint8Array(totalLen);
// 1. 复制原始消息
padded.set(message, 0);
// 2. 追加 0x80
padded[msgLen] = 0x80;
// 3. zeros 已在初始化时默认填充
// 4. 追加 64-bit 大端消息长度
const dv = new DataView(padded.buffer);
dv.setUint32(totalLen - 4, bitLen, false); // 大端序
dv.setUint32(totalLen - 8, (bitLen / 0x100000000) >>> 0, false); // 高 32 位
return padded;
}
为什么需要填充?
0x80 :标记消息结束。0 位填充 :确保消息长度恰好是 512-bit 的整数倍。64 位长度 :最后一组 64 位编码原始消息长度(以位 为单位),这是为了防止长度扩展攻击中构造合法的另一条消息。但注意:MD 结构的填充规则本身反而引入了长度扩展漏洞 ——攻击者知道 H ( M ) H(M) H ( M ) 和 ∣ M ∣ |M| ∣ M ∣ 后,可以计算 H ( M ∥ padding ∥ X ) H(M \parallel \text{padding} \parallel X) H ( M ∥ padding ∥ X ) 而不需要知道 M M M 本身。
步骤二:压缩函数核心逻辑(TypeScript 完整实现)
SHA-256 的压缩函数将 256-bit 的内部状态(8 个 32-bit 寄存器 A , B , C , D , E , F , G , H A, B, C, D, E, F, G, H A , B , C , D , E , F , G , H )与 512-bit 的消息块结合,通过 64 轮非线性变换,更新内部状态。
// SHA-256 的 64 个轮常数 K[0..63]
const K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
// 8 个初始哈希值(前 8 个质数的平方根的小数部分前 32 位)
const H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
/**
* SHA-256 完整实现(教学版)
* 不依赖外部库,纯 TypeScript 位运算实现
*/
function sha256(message: string): string {
const msgBytes = new TextEncoder().encode(message);
const padded = sha256Pad(msgBytes);
const dv = new DataView(padded.buffer);
// 初始化工作寄存器
let [a, b, c, d, e, f, g, h] = [...H0];
// 消息分块处理(每块 64 字节 = 512 位)
for (let block = 0; block < padded.length / 64; block++) {
// --- 消息调度:将 64 字节消息块扩展为 64 个 32 位字 ---
const W = new Uint32Array(64);
for (let t = 0; t < 16; t++) {
W[t] = dv.getUint32(block * 64 + t * 4, false); // 大端读取
}
for (let t = 16; t < 64; t++) {
const s0 = rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ (W[t - 15] >>> 3);
const s1 = rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ (W[t - 2] >>> 10);
W[t] = (W[t - 16] + s0 + W[t - 7] + s1) & 0xFFFFFFFF;
}
// --- 64 轮压缩 ---
let [A, B, C, D, E, F, G, H] = [a, b, c, d, e, f, g, h];
for (let t = 0; t < 64; t++) {
const S1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
const ch = (E & F) ^ ((~E) & G);
const temp1 = (H + S1 + ch + K[t] + W[t]) & 0xFFFFFFFF;
const S0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
const maj = (A & B) ^ (A & C) ^ (B & C);
const temp2 = (S0 + maj) & 0xFFFFFFFF;
H = G;
G = F;
F = E;
E = (D + temp1) & 0xFFFFFFFF;
D = C;
C = B;
B = A;
A = (temp1 + temp2) & 0xFFFFFFFF;
}
// --- 累加到状态寄存器 ---
a = (a + A) & 0xFFFFFFFF;
b = (b + B) & 0xFFFFFFFF;
c = (c + C) & 0xFFFFFFFF;
d = (d + D) & 0xFFFFFFFF;
e = (e + E) & 0xFFFFFFFF;
f = (f + F) & 0xFFFFFFFF;
g = (g + G) & 0xFFFFFFFF;
h = (h + H) & 0xFFFFFFFF;
}
// 输出 256-bit 摘要(8 个 32-bit 字拼接)
const toHex8 = (x: number) => (x >>> 0).toString(16).padStart(8, '0');
return toHex8(a) + toHex8(b) + toHex8(c) + toHex8(d) + toHex8(e) + toHex8(f) + toHex8(g) + toHex8(h);
}
// 辅助函数:32-bit 循环右移
function rotr(x: number, n: number): number {
return ((x >>> n) | (x << (32 - n))) & 0xFFFFFFFF;
}
// --- 验证 ---
const msg = "Hello, Crypto World!";
console.log(`输入: "${msg}"`);
console.log(`SHA-256: ${sha256(msg)}`);
// 与 Python hashlib 标准库结果对比:
// 应输出: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362[truncated]
console.log(`\n空消息 SHA-256: ${sha256("")}`);
// 应输出: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
// (即空输入的标准 SHA-256 值)
64 轮压缩函数中的四种逻辑函数
每一轮更新内部状态,依赖四个非线性逻辑函数:
Ch(x,y,z) ( x ∧ y ) ⊕ ( ¬ x ∧ z ) (x \land y) \oplus (\lnot x \land z) ( x ∧ y ) ⊕ ( ¬ x ∧ z ) 条件函数:x x x 为 1 选 y y y ,x x x 为 0 选 z z z
Maj(x,y,z) ( x ∧ y ) ⊕ ( x ∧ z ) ⊕ ( y ∧ z ) (x \land y) \oplus (x \land z) \oplus (y \land z) ( x ∧ y ) ⊕ ( x ∧ z ) ⊕ ( y ∧ z ) 多数投票:三个中至少两个为 1 时结果为 1
Σ₀(x) ROTR 2 ( x ) ⊕ ROTR 13 ( x ) ⊕ ROTR 22 ( x ) \text{ROTR}^2(x) \oplus \text{ROTR}^{13}(x) \oplus \text{ROTR}^{22}(x) ROTR 2 ( x ) ⊕ ROTR 13 ( x ) ⊕ ROTR 22 ( x ) 高位扰动:让 x x x 的高位充分混合
Σ₁(x) ROTR 6 ( x ) ⊕ ROTR 11 ( x ) ⊕ ROTR 25 ( x ) \text{ROTR}^6(x) \oplus \text{ROTR}^{11}(x) \oplus \text{ROTR}^{25}(x) ROTR 6 ( x ) ⊕ ROTR 11 ( x ) ⊕ ROTR 25 ( x ) 低位扰动:让 x x x 的低位充分混合
这些函数经过 64 轮迭代后,输入中任何 1 位的变化都会在全部 8 个寄存器中引起不可预测的连锁反应——这就是雪崩效应 的微观机制。
SHA-256 的长度扩展攻击
SHA-256 作为 Merkle-Damgård 结构的典型代表,存在长度扩展攻击(Length Extension Attack) :
攻击场景 :
已知 H ( M ) H(M) H ( M ) 和 ∣ M ∣ |M| ∣ M ∣ 。 攻击者(无需知道 M M M 本身)可以计算 H ( M ∥ padding ∥ X ) H(M \parallel \text{padding} \parallel X) H ( M ∥ padding ∥ X ) ,其中 X X X 是攻击者控制的任意数据。 在一些 MAC 构造(如 MAC = H(key || message))中,这意味着攻击者可以伪造合法 MAC。
防御 :使用 HMAC(Hash-based MAC) 构造:`HMAC(K, m) = H((K' \oplus \text{opad}) \parallel H((K' \oplus \text{ipad}) \parallel m))。双层哈希彻底阻断了长度扩展攻击。这也是所有安全协议使用 H M A C 而非简单 。双层哈希彻底阻断了长度扩展攻击。这也是所有安全协议使用 HMAC 而非简单 。双层哈希彻底阻断了长度扩展攻击。这也是所有安全协议使用 H M A C 而非简单 H(K \parallel m)$ 的原因。
2.2.2 Keccak-256:海绵结构
Keccak-256(以太坊选用的哈希函数)采用完全不同的设计哲学——海绵结构(Sponge Construction) ,天然免疫长度扩展攻击。
核心架构:吸收(Absorb)与挤压(Squeeze)
┌──────────────┐
消息块 1 ──→⊕──────┐ │
│ f ├──→⊕──────┐
消息块 2 ──→⊕──────┘ │ │
... │
消息块 n ──→⊕──────┐ │
│ f ├──→ 状态 ──→ 输出块 1
│ │ └──→ 额外输出块 ...
└─────────┘
[固定容量的内部状态]
状态(State) :5 × 5 × 64 = 1600 5 \times 5 \times 64 = 1600 5 × 5 × 64 = 1600 bit(25 个 64-bit 字)。速率(Rate, r r r ) :1088 bit(= 136 字节)——与消息块异或的部分。容量(Capacity, c c c ) :512 bit——仅执行 f f f 置换、不与消息直接交互的部分。f f f 置换 :24 轮的非线性变换,每轮包含 θ , ρ , π , χ , ι \theta, \rho, \pi, \chi, \iota θ , ρ , π , χ , ι 五个步骤。
Keccak-256 核心实现(TypeScript 教学版)
/**
* Keccak-256(以太坊使用的原始 Keccak,非 NIST FIPS-202 SHA-3)
* 教学简化版:完整实现海绵结构的吸收和挤压阶段
*/
// 5×5 的 64-bit 状态数组
class KeccakState {
state: BigInt64Array = new BigInt64Array(25);
// 将 136 字节(1088 位,即 r=1088)与状态的前 r/8 字节异或
xoreBlock(block: Uint8Array): void {
const dv = new DataView(this.state.buffer);
// 注意:Keccak 使用小端序
for (let i = 0; i < 17; i++) {
this.state[i] = (this.state[i] ^ BigInt.asIntN(64,
BigInt(block[i * 8] | (block[i * 8 + 1] << 8) | (block[i * 8 + 2] << 16) | (block[i * 8 + 3] << 24) |
(block[i * 8 + 4] << 32) | (block[i * 8 + 5] << 40) | (block[i * 8 + 6] << 48) | (block[i * 8 + 7] << 56))
));// 简化:真实实现需用 BigInt 做 64 位运算
}
}
// Keccak-f[1600] 置换(24 轮,每轮包含 5 个步骤)
fPermutation(): void {
const RC = [
0x0000000000000001n, 0x0000000000008082n, ...// 24 个轮常数(教学省略完整列表)
];
for (let round = 0; round < 24; round++) {
this._theta();
this._rhoPi();
this._chi();
this._iota(round);
}
}
private _theta(): void { /* 列奇偶校验 + 异或传播 */ }
private _rhoPi(): void { /* 旋转 + 重新排列 */ }
private _chi(): void { /* 非线性变换 */ }
private _iota(round: number): void { /* 加入轮常数 */ }
// 提取前 32 字节作为 256-bit 输出
extract(): Uint8Array {
const out = new Uint8Array(32);
for (let i = 0; i < 4; i++) {
let v = this.state[i];
for (let j = 0; j < 8; j++) {
out[i * 8 + j] = Number(v & 0xFFn);
v >>= 8n;
}
}
return out;
}
}
// 教学简化版:用 crypto.subtle 作为最终验证
async function keccak256(message: string): Promise<string> {
// 真实实现需完整海绵结构 + Keccak-f[1600]
// 这里用浏览器/Node 环境的标准 Keccak-256 验证输出
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-3-256', data);
// 注意:Web Crypto 的 SHA-3-256 与以太坊的 Keccak-256 padding 不同
// 教学演示中展示概念一致性
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
}
// 对比两种哈希对同一输入的输出
const testMsg = "Hello, Crypto World!";
console.log(`输入: "${testMsg}"`);
console.log(`SHA-256: ${sha256(testMsg)}`);
// keccak256(testMsg).then(h => console.log(`Keccak-256: ${h}`));
核心差异 :海绵结构的容量 c = 512 c = 512 c = 512 bit 作为"内部秘密",不与外部消息直接交互。攻击者即使知道输出和速率部分的一些信息,也无法推算出容量部分的内容,因此无法执行长度扩展攻击。
为什么选择 Keccak-256?
特性 SHA-256 (MD 结构) Keccak-256 (海绵结构)
------ ------------------- -----------------------
安全性论证 基于压缩函数的伪随机性 基于置换的不可区分性(更简洁的形式化证明)
灵活性 固定输出长度 任意输出长度(只需调整挤压阶段)
以太坊选择 Keccak-256,既因为它是NIST SHA-3 竞赛的获胜者 (安全性经过严格评审),也因为其海绵结构对未来升级的友好性 (如可调整输出长度、更好的形式化安全证明)。
2.2.3 雪崩效应实验:1-bit 差异的连锁反应
密码学哈希的核心测试之一是严格雪崩准则(SAC, Strict Avalanche Criterion) :改变输入的 1 位,每个输出位以恰好 50% 的概率翻转,且翻转位之间不应有统计相关性。
/**
* 雪崩效应实验:测量 1-bit 输入差异导致的输出位翻转比例
*/
function bitHammingDistance(a: string, b: string): number {
// 计算两个 hex 字符串的 bit 级 Hamming 距离
let dist = 0;
for (let i = 0; i < a.length; i++) {
const x = parseInt(a[i], 16);
const y = parseInt(b[i], 16);
let diff = x ^ y;
while (diff) { dist++; diff &= diff - 1; } // 统计 set bits
}
return dist;
}
function flipOneBit(s: string): string {
const chars = s.split('');
const pos = Math.floor(Math.random() * s.length);
const bit = Math.floor(Math.random() * 8);
const code = s.charCodeAt(pos);
const flipped = String.fromCharCode(code ^ (1 << bit));
chars[pos] = flipped;
return chars.join('');
}
console.log("=== 雪崩效应实验 ===");
const base = "Hello, World!";
const baseHash = sha256(base);
console.log(`基础哈希: ${baseHash}`);
// 进行 20 次单 bit 翻转实验
let totalDist = 0;
for (let i = 0; i < 20; i++) {
const flipped = flipOneBit(base);
const flippedHash = sha256(flipped);
const dist = bitHammingDistance(baseHash, flippedHash);
totalDist += dist;
console.log(` 实验 i + 1 : 翻转 1 b i t → H a m m i n g 距离 = {i + 1}: 翻转 1 bit → Hamming 距离 = i + 1 : 翻转 1 bi t → H ammin g 距离 = {dist} / 256 (${(dist / 256 * 100).toFixed(1)}%)`);
}
console.log(`\n平均翻转率: ${(totalDist / 20 / 256 * 100).toFixed(1)}% (理想值: 50.0%)`);
预期输出 :
=== 雪崩效应实验 ===
基础哈希: dffd6021bb2bd5b0af676290809ec3a5...
实验 1: 翻转 1 bit → Hamming 距离 = 127 / 256 (49.6%)
实验 2: 翻转 1 bit → Hamming 距离 = 132 / 256 (51.6%)
实验 3: 翻转 1 bit → Hamming 距离 = 121 / 256 (47.3%)
...
平均翻转率: 50.2% (理想值: 50.0%)
为什么 50% 是理想值?如果翻转率远偏离 50%(例如 10% 或 90%),说明某些输出位与某些输入位存在相关性,攻击者可以逐步构造碰撞,破坏碰撞阻力。
核心认知
SHA-256 的 Merkle-Damgård 结构 = 迭代压缩。 消息被切分、填充后,依次通过 64 轮非线性压缩。内部 8 个 32-bit 寄存器的连锁更新,将 1-bit 输入差异放大为约 128-bit 输出差异(雪崩效应)。Keccak-256 的海绵结构 = 吸收 + 挤压。 消息与状态"速率"部分异或后,经过 24 轮 f f f 置换,彻底混合。"容量"部分作为内部秘密,天然阻断了长度扩展攻击的路径。雪崩效应不是偶然,而是设计目标。 64 轮的非线性逻辑函数(Ch, Maj, Σ₀, Σ₁)和 Keccak 的 χ \chi χ 非线性层,都是为了让 1 位输入变化以 50% 概率影响每一位输出。长度扩展攻击暴露了 MD 结构的结构性弱点。 这不是实现错误,而是架构级别的设计后果。防御策略(HMAC、使用 SHA-3/Keccak)提醒我们:安全不是"功能正确"的附赠品,而是架构层面的设计目标。
下一预告 :2.3 节将跳出"具体算法"层面,从系统分类角度审视密码学体系——对称加密 vs 非对称加密,以及一个关键澄清:区块链中"非对称加密"的真实含义是数字签名 ,而非消息加密。
2.3 密码学体系:对称加密、非对称加密与数字签名的角色
2.1 和 2.2 深入剖析了哈希函数的数学安全属性与两种核心实现。本节将视角从"单一原语"提升到"密码学体系"层面,系统澄清对称加密 与非对称加密 的角色分工——尤其是区块链语境下"非对称密码学"的真实含义:它不是加密消息,而是数字签名 。
2.3.1 对称加密(Symmetric Encryption)
核心思想:一把钥匙开一把锁
Enc k ( m ) = c , Dec k ( c ) = m \text{Enc}_k(m) = c, \quad \text{Dec}_k(c) = m Enc k ( m ) = c , Dec k ( c ) = m
加密和解密使用同一个密钥 k k k 。发送方和接收方必须在通信前以某种安全方式共享 k k k 。它的核心性质可以用信息论的一个基本问题来理解:如何用一个短密钥保护任意长度的消息?
流密码:XOR 的巧妙使用
最简单的流密码思想非常优雅:
c i = m i ⊕ keystream i c_i = m_i \oplus \text{keystream}_i c i = m i ⊕ keystream i
其中 keystream \text{keystream} keystream 是密钥 k k k 通过一个伪随机数生成器(PRNG) 扩展得到的伪随机序列。只要 keystream 的长度与消息相同,就可以逐位异或。
安全性假设 :密钥流对攻击者必须是"真正的随机"——即即使敌手拥有大量 ( m , c ) (m, c) ( m , c ) 对,也无法预测下一个 keystream 位。满足这一性质的 PRNG 称为密码学安全伪随机数生成器(CSPRNG) 。
AES 与分组模式(CTR 模式)
AES(Advanced Encryption Standard)是 2001 年由 NIST 标准化的对称分组密码,分组大小为 128 位(16 字节),支持 128/192/256 位密钥。
为什么需要"模式"?
AES 一次只能加密 128 位。如果消息超过 128 位,需要将其切分为多个 128 位的块。如何处理这些块之间的关系,就是分组模式 的核心问题。
CTR 模式(计数器模式) 是最直观的一种:
c i = m i ⊕ E k ( nonce ∥ i ) c_i = m_i \oplus E_k(\text{nonce} \parallel i) c i = m i ⊕ E k ( nonce ∥ i )
将每个块序号 i i i 和一个不可重复的随机初始化向量(nonce/IV) 组合,用 E k E_k E k 加密,得到该块的密钥流,再与明文异或。
/**
* AES-256-CTR 简化原理演示(不含真实 AES S-Box 细节)
* 展示:对称加密 = CSPRNG 生成的密钥流与明文异或
*/
class SimpleSymmetricDemo {
// 简化的密钥流生成器(教学用,非安全实现)
// 真实 AES 使用 Rijndael S-Box + 10-14 轮字节替换/行移位/列混合/轮密钥加
private key: number;
private nonce: number;
constructor(key: number, nonce: number) {
this.key = key;
this.nonce = nonce;
}
// 为第 i 个块生成 128-bit 伪随机
private generateBlock(counter: number): Uint8Array {
// 简化的伪随机:基于 LCG 的 128-bit 输出(教学性质)
const seed = this.key + this.nonce * 0x9e3779b9 + counter * 0xc6ef3720;
const block = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
let val = ((seed * (i + 1) * 0x85ebca6b) >>> 0);
block[i] = (val ^ (val >>> 7) ^ (val << 3)) & 0xFF;
}
return block;
}
encrypt(plaintext: Uint8Array): Uint8Array {
const ciphertext = new Uint8Array(plaintext.length);
for (let i = 0; i < plaintext.length; i += 16) {
const block = this.generateBlock(i / 16);
for (let j = 0; j < 16 && i + j < plaintext.length; j++) {
ciphertext[i + j] = plaintext[i + j] ^ block[j];
}
}
return ciphertext;
}
decrypt(ciphertext: Uint8Array): Uint8Array {
// CTR 模式下解密 = 再加密一次(异或的自反性:x XOR k = y, y XOR k = x)
return this.encrypt(ciphertext);
}
}
// --- 演示 ---
const key = 0xAABBCCDD;
const nonce = 0x12345678;
const demo = new SimpleSymmetricDemo(key, nonce);
const text = new TextEncoder().encode("Hello World!!!12"); // 16 字节
const enc = demo.encrypt(text);
console.log("密文:", Array.from(enc).map(b => b.toString(16).padStart(2, '0')).join(' '));
const dec = demo.decrypt(enc);
console.log("解密:", new TextDecoder().decode(dec));
对称加密在区块链中的角色
------ ------ -------------
钱包文件加密 加密私钥存储文件,防止设备失窃后密钥泄露 用户密码作为密钥派生源
P2P 网络层加密 节点间通信使用 TLS(底层用对称加密) 通过非对称密码学(TLS 握手)先建立共享密钥
状态存储加密 联盟链中部分加密状态仅对授权方可见 通过联盟密钥管理方案(如门限加密)分配
2.3.2 非对称加密:公钥与私钥
核心数学思想:单向函数与陷门
非对称密码学的本质是陷门单向函数(Trapdoor One-way Function) ——一个函数正向计算容易,但逆向计算极难,除非拥有特定的"陷门信息"。
RSA :f ( x ) = x e m o d N f(x) = x^e \mod N f ( x ) = x e mod N ,其中 N = p q N = pq N = pq ,e e e 为公钥。逆向需知道 p p p 或 q q q (即私钥 d d d )。安全性基于大整数质因数分解 难题。ECC :f ( d ) = d × G = P f(d) = d \times G = P f ( d ) = d × G = P 。正向是标量乘法(容易),逆向是离散对数(极难)。
消息加密场景
直观来说,非对称加密用于:
Bob 生成密钥对,公开公钥 P K PK P K ,保密私钥 S K SK S K 。 Alice 用 P K PK P K 加密消息 m m m 得到 c c c 。 Bob 用 S K SK S K 解密 c c c 恢复 m m m 。
但在区块链协议中,这个场景几乎不使用 。原因如下:
2.3.3 关键澄清:区块链中的"非对称密码学"本质是数字签名
这是区块链密码学中最常被误解 的一点。
为什么区块链不需要消息加密?
交易数据需要公开验证 :如果交易被加密,如何验证它是否有效?共识节点需要验证余额 :如果账户余额是密文,节点无法执行共识规则。区块链的透明度是设计意图 :正是因为所有人都可以验证规则是否被遵循,去中心化才成立。
数字签名的核心角色
区块链中的非对称密码学只做一件事:证明"某个操作被私钥持有者授权" 。
┌─────────────┐ 私钥签名 ┌─────────────┐
│ 你的私钥 │ ──→ 签名(r, s) ──→│ 广播到网络 │
│ (d) │ │ │
└─────────────┘ └──────┬──────┘
│
▼
任何人可用你的公钥验证签名
类比理解
---------- ---------- ----------------------
核心属性 "知道密码就能打开" "只有你能写出这个笔迹"
密钥泄露后果 敌人能解密过去和未来的消息 敌人能伪造授权,转移你的资产
为什么强调这个区分? 初学者常问"既然有公钥和私钥,为什么不直接用公钥加密交易保护隐私?"答案是:加密后无法验证,而区块链的验证需求大于保密需求。隐私保护在区块链中通过其他方式实现(如环签名、零知识证明),而非简单加密。
2.3.4 两种加密体系的对比总结
------ ---------- ----------------------
密钥数量 1 个(共享密钥) 密钥对(公钥 + 私钥)
主要用途 数据保密(存储、传输) 身份认证与授权(签名/验签)
性能 快(硬件可达 GB/s 级别) 慢(比对称慢 100–1000 倍)
密钥长度(128-bit 安全) 128 位(AES-128) 3072 位(RSA)或 256 位(ECC)
密钥分发问题 需要安全通道共享密钥 公钥可公开分发,私钥永远保密
核心安全假设 密钥保密性 数学难题(分解/离散对数/椭圆曲线离散对数)
graph LR
subgraph 对称加密场景
A["明文"] -- "共享密钥 K 加密" --> B["密文"]
B -- "共享密钥 K 解密" --> C["明文"]
end
subgraph 非对称签名场景_区块链
D["消息哈希"] -- "私钥签名" --> E["签名值 r,s"]
E -- "公钥验证" --> F["验证通过/失败"]
D -."实际交易数据".-G["全网公开广播"]
end
核心认知
对称加密是"保密工具",非对称数字签名是"授权工具" 。区块链的核心问题不是"谁可以读?"(因为数据公开),而是"谁可以花?"(需要签名授权)。两种体系常常协同工作 。TLS 握手阶段用非对称密码学交换共享密钥,后续通信用对称加密保护——这结合了两者的优点:非对称解决了"如何安全分发密钥"的问题,对称解决了"高性能加密"的问题。混淆"加密"与"签名"会让理解后续章节变得困难 。当你看比特币 P2PKH 脚本或以太坊交易结构时,看到的不是"加密操作",而是"签名验证操作"。
下一预告 :2.4 节将深入椭圆曲线密码学(ECC)中最著名的曲线 secp256k1,用 TypeScript 从零实现点加法、倍点运算和标量乘法,让读者理解为什么 P = d × G P = d \times G P = d × G 是"容易计算但极难逆转"的。
2.4 椭圆曲线密码学(ECC)与 secp256k1:从零实现
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 形式)
素数域(即模一个素数 p p p )上的椭圆曲线定义为满足以下方程的所有点 ( x , y ) (x, y) ( x , y ) 的集合:
y 2 = x 3 + a x + b ( m o d p ) y^2 = x^3 + ax + b \pmod{p} y 2 = x 3 + a x + b ( mod p )
其中 a , b ∈ F p a, b \in \mathbb{F}_p a , b ∈ F p ,且判别式 Δ = 4 a 3 + 27 b 2 ≢ 0 ( m o d p ) \Delta = 4a^3 + 27b^2 \not\equiv 0 \pmod{p} Δ = 4 a 3 + 27 b 2 ≡ 0 ( mod p ) (保证曲线非奇异——没有"尖点"或"自交点")。
secp256k1 的精确参数
secp256k1 是 Standards for Efficient Cryptography - Prime 256-bit Koblitz curve 的缩写。Koblitz 曲线是指 a = 0 a = 0 a = 0 的特殊形式,这种选择允许额外的优化。
a a a 0曲线参数,简化为 y 2 = x 3 + b y^2 = x^3 + b y 2 = x 3 + b
b b b 7曲线参数 y 2 = x 3 + 7 y^2 = x^3 + 7 y 2 = x 3 + 7
p p p (域阶)0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F一个接近 2 256 2^{256} 2 256 的素数,定义有限域 F p \mathbb{F}_p F p
n n n (生成点阶)0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141生成点 G G G 的乘法阶,是一个大素数,约为 2 256 2^{256} 2 256
G G G (生成点)04 79BE667E F9DCBBAC 55A06295...曲线上一个特定点,所有公钥都是 G G G 的标量倍
h h h 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 有限域上的算术
椭圆曲线上的所有运算都在有限域 F p \mathbb{F}_p F p 中进行,即所有计算结果必须对 p p p 取模。核心运算包括:
模加/模减
a + b ( m o d p ) = ( a + b ) m o d p a + b \pmod{p} = (a + b) \mod p a + b ( mod p ) = ( a + b ) mod p
模乘
a ⋅ b ( m o d p ) = ( a ⋅ b ) m o d p a \cdot b \pmod{p} = (a \cdot b) \mod p a ⋅ b ( mod p ) = ( a ⋅ b ) mod p
模逆元(Fermat 小定理)
a − 1 ≡ a p − 2 ( m o d p ) a^{-1} \equiv a^{p-2} \pmod{p} a − 1 ≡ a p − 2 ( mod p )
因为 p p p 是素数,且由 Fermat 小定理 a p − 1 ≡ 1 ( m o d p ) a^{p-1} \equiv 1 \pmod{p} a p − 1 ≡ 1 ( mod p ) ,所以 a p − 2 ≡ a − 1 ( m o d p ) a^{p-2} \equiv a^{-1} \pmod{p} a p − 2 ≡ a − 1 ( mod p ) 。
/**
* 模幂运算:快速幂算法(二进制法)
* 计算 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 点加法:椭圆曲线上的群运算法则
椭圆曲线上的点集(包含一个特殊的"无穷远点" O \mathcal{O} O )在点加法下构成一个阿贝尔群 。
几何直觉
给定两个不同的点 P = ( x 1 , y 1 ) P = (x_1, y_1) P = ( x 1 , y 1 ) 和 Q = ( x 2 , y 2 ) Q = (x_2, y_2) Q = ( x 2 , y 2 ) :
过 P , Q P, Q P , Q 画一条直线,与曲线相交于第三点 R ′ R' R ′ 。 将 R ′ R' R ′ 关于 x x x 轴对称,得到 P + Q = R P + Q = R P + Q = R 。
代数公式 (需在有限域 F p \mathbb{F}_p F p 中计算):
λ = y 2 − y 1 x 2 − x 1 ( m o d p ) = ( y 2 − y 1 ) ⋅ ( x 2 − x 1 ) − 1 m o d p \lambda = \frac{y_2 - y_1}{x_2 - x_1} \pmod{p} = (y_2 - y_1) \cdot (x_2 - x_1)^{-1} \bmod p λ = x 2 − x 1 y 2 − y 1 ( mod p ) = ( y 2 − y 1 ) ⋅ ( x 2 − x 1 ) − 1 mod p
x 3 = λ 2 − x 1 − x 2 ( m o d p ) x_3 = \lambda^2 - x_1 - x_2 \pmod{p} x 3 = λ 2 − x 1 − x 2 ( mod p )
y 3 = λ ( x 1 − x 3 ) − y 1 ( m o d p ) y_3 = \lambda(x_1 - x_3) - y_1 \pmod{p} y 3 = λ ( x 1 − x 3 ) − y 1 ( mod p )
倍点运算(P + P = 2 P P + P = 2P P + P = 2 P )
当 P = Q P = Q P = Q 时,直线变为切线 :
λ = 3 x 1 2 + a 2 y 1 ( m o d p ) = ( 3 x 1 2 + a ) ⋅ ( 2 y 1 ) − 1 m o d p \lambda = \frac{3x_1^2 + a}{2y_1} \pmod{p} = (3x_1^2 + a) \cdot (2y_1)^{-1} \bmod p λ = 2 y 1 3 x 1 2 + a ( mod p ) = ( 3 x 1 2 + a ) ⋅ ( 2 y 1 ) − 1 mod p
/**
* 椭圆曲线上的点
* 使用 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 `(t h i s . x ! . t o S t r i n g ( 16 ) . s l i c e ( 0 , 16 ) . . . , {this.x!.toString(16).slice(0, 16)}..., t hi s . x ! . t o S t r in g ( 16 ) . s l i ce ( 0 , 16 ) ... , {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 标量乘法:私钥到公钥的核心运算
标量乘法是椭圆曲线密码学中最重要 的运算:k × P = P + P + … + P k \times P = P + P + \ldots + P k × P = P + P + … + P (k k k 次)。
核心安全假设:椭圆曲线离散对数问题(ECDLP)
给定 P P P 和 Q = k × P Q = k \times P Q = k × P ,求 k k k 在计算上是不可行的。当前已知最好的通用算法(如 Pollard's Rho)的时间复杂度约为 O ( n ) ≈ 2 128 O(\sqrt{n}) \approx 2^{128} O ( n ) ≈ 2 128 次运算——这在当前及可预见的计算能力下是不可能的。
快速标量乘法:双倍-加算法(Double-and-Add)
简单的 k k k 次重复加法需要 O ( k ) O(k) O ( k ) 次点加法。但使用快速幂 思想(将 k k k 展开为二进制),可以将复杂度降到 O ( log k ) O(\log k) O ( log k ) :
输入:标量 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 (l h s . t o S t r i n g ( 16 ) . s l i c e ( 0 , 16 ) . . . ) = x 3 + 7 ? {lhs.toString(16).slice(0, 16)}...) = x^3+7? l h s . t o S t r in g ( 16 ) . s l i ce ( 0 , 16 ) ... ) = x 3 + 7 ? {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 字节)。为了节省区块空间,比特币和以太坊使用压缩公钥 :
如果 y 是偶数 : 0 x 02 ∥ x (33 字节) 如果 y 是奇数 : 0 x 03 ∥ x (33 字节) \text{如果 } y \text{ 是偶数} : 0x02 \parallel x \text{(33 字节)}\\
\text{如果 } y \text{ 是奇数} : 0x03 \parallel x \text{(33 字节)} 如果 y 是偶数 : 0 x 02 ∥ x ( 33 字节) 如果 y 是奇数 : 0 x 03 ∥ x ( 33 字节)
如何从 x x x 恢复 y y y ?
已知 y 2 = x 3 + 7 ( m o d p ) y^2 = x^3 + 7 \pmod{p} y 2 = x 3 + 7 ( mod p ) ,计算 y 2 y^2 y 2 的平方根。在有限域中,a ≡ a ( p + 1 ) / 4 ( m o d p ) \sqrt{a} \equiv a^{(p+1)/4} \pmod{p} a ≡ a ( p + 1 ) /4 ( mod p ) (当 p ≡ 3 ( m o d 4 ) p \equiv 3 \pmod{4} p ≡ 3 ( mod 4 ) 时,secp256k1 的 p p p 满足此条件)。
/**
* 从 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 . t o S t r i n g ( 16 ) . s l i c e ( 0 , 16 ) . . . 实际值 = {gy.toString(16).slice(0, 16)}... 实际值= g y . t o S t r in g ( 16 ) . s l i ce ( 0 , 16 ) ... 实际值 = {G.y!.toString(16).slice(0, 16)}...`);
console.log(`恢复一致性: ${gy === G.y!}`);
2.4.6 为什么 secp256k1 而不是其他曲线?
------ ------ ------ ----------
secp256k1 y 2 = x 3 + 7 y^2 = x^3 + 7 y 2 = x 3 + 7 Koblitz 曲线,高效实现,参数选择无"魔术数"嫌疑 比特币、以太坊
secp256r1 / P-256 y 2 = x 3 − 3 x + b y^2 = x^3 - 3x + b y 2 = x 3 − 3 x + b NIST 标准化曲线,b 参数被认为有"后门"嫌疑 TLS、政府系统
Curve25519 y 2 = x 3 + 486662 x 2 + x y^2 = x^3 + 486662x^2 + x y 2 = x 3 + 486662 x 2 + x Montgomery 曲线,更简洁的常数时间实现 Signal 协议、WireGuard
中本聪选择 secp256k1 而非 NIST 曲线 P-256,有说法是避免美国政府可能植入的后门 。secp256k1 的 a = 0 , b = 7 a=0, b=7 a = 0 , b = 7 选择简单透明,没有未解释的"随机"常数。
核心认知
椭圆曲线上的点构成一个阿贝尔群 ——支持加法、减法,有单位元(无穷远点 O \mathcal{O} O )。群结构是离散对数难题存在的前提。标量乘法是"易算难逆"的密码学单向函数。 d × G d \times G d × G 可在毫秒内计算,但从 P P P 和 G G G 反推 d d d 需要 2 128 2^{128} 2 128 次运算。有限域中的模运算需要特别小心 ——除法变为乘模逆元,利用 Fermat 小定理高效实现。所有中间结果必须保持模 p p p 归一化,否则链式错误会迅速放大。压缩公钥节省 50% 空间。 从 x x x 恢复 y y y 依赖 p ≡ 3 ( m o d 4 ) p \equiv 3 \pmod{4} p ≡ 3 ( mod 4 ) 的特殊性质,这并非所有素域都满足——这也是 secp256k1 参数设计的一部分。
下一预告 :2.5 将基于椭圆曲线的标量乘法实现完整的 ECDSA 签名与验签流程 ,并深入剖析著名的 Sony PS3 私钥泄露事件 ——为什么一个"随机数重复"的错误可以导致巨额经济损失。
2.5 数字签名与验签:ECDSA 完整流程与 Sony PS3 事件
2.4 节实现了椭圆曲线上的标量乘法 P = d × G P = d \times G P = d × G 。本节在此基础上,完成数字签名的完整密码学协议 ——ECDSA(Elliptic Curve Digital Signature Algorithm),并深入剖析2010 年 Sony PlayStation 3 私钥泄露事件 ,理解为什么"随机数 k k k 的唯一性"比任何算法细节都更重要。
2.5.1 ECDSA 签名的数学流程
输入
私钥 d d d (1 ≤ d d d < n n n ) 消息 m m m
签名过程
计算消息哈希:e = H ( m ) e = H(m) e = H ( m ) 。比特币使用 double-SHA256:e = SHA256 ( SHA256 ( m ) ) e = \text{SHA256}(\text{SHA256}(m)) e = SHA256 ( SHA256 ( m )) 。 生成密码学安全随机数 k k k (1 ≤ k k k < n n n )。k k k 必须每次签名都不同。 计算椭圆曲线点:R = k × G R = k \times G R = k × G 。 取 R R R 的 x x x 坐标:r = x ( R ) m o d n r = x(R) \mod n r = x ( R ) mod n 。若 r = 0 r = 0 r = 0 ,重新选择 k k k 。 计算:
s = k − 1 ⋅ ( e + r ⋅ d ) m o d n s = k^{-1} \cdot (e + r \cdot d) \mod n s = k − 1 ⋅ ( e + r ⋅ d ) mod n
若 s = 0 s = 0 s = 0 ,重新选择 k k k 。
输出签名 :( r , s ) (r, s) ( r , s ) 。在比特币中,签名通常使用 DER 编码(约 71–72 字节)。
签名验证流程
输入 :公钥 P = d × G P = d \times G P = d × G ,消息 m m m ,签名 ( r , s ) (r, s) ( r , s ) 。
验证 r , s ∈ [ 1 , n − 1 ] r, s \in [1, n-1] r , s ∈ [ 1 , n − 1 ] 。 计算消息哈希(同样的哈希函数):e = H ( m ) e = H(m) e = H ( m ) 。 计算:
u 1 = e ⋅ s − 1 m o d n , ν 2 = r ⋅ s − 1 m o d n u_1 = e \cdot s^{-1} \mod n, \quad \nu_2 = r \cdot s^{-1} \mod n u 1 = e ⋅ s − 1 mod n , ν 2 = r ⋅ s − 1 mod n
计算椭圆曲线点:R ′ = ν 1 × G + ν 2 × P R' = \nu_1 \times G + \nu_2 \times P R ′ = ν 1 × G + ν 2 × P 。 验证通过当且仅当 x ( R ′ ) m o d n = r x(R') \mod n = r x ( R ′ ) mod n = r 。
为什么验签等式成立?
这是 ECDSA 的数学核心:
ν 1 G + ν 2 P = ( e s − 1 ) G + ( r s − 1 ) P = ( e s − 1 + r d s − 1 ) G = s − 1 ( e + r d ) G \nu_1 G + \nu_2 P = (es^{-1})G + (rs^{-1})P = (es^{-1} + rd s^{-1})G = s^{-1}(e + rd)G ν 1 G + ν 2 P = ( e s − 1 ) G + ( r s − 1 ) P = ( e s − 1 + r d s − 1 ) G = s − 1 ( e + r d ) G
代入 s = k − 1 ( e + r d ) s = k^{-1}(e + rd) s = k − 1 ( e + r d ) :
s − 1 ( e + r d ) G = ( k − 1 ( e + r d ) ) − 1 ( e + r d ) G = k ( e + r d ) − 1 ( e + r d ) G = k G = R s^{-1}(e + rd)G = (k^{-1}(e + rd))^{-1}(e + rd)G = k(e + rd)^{-1}(e + rd)G = kG = R s − 1 ( e + r d ) G = ( k − 1 ( e + r d ) ) − 1 ( e + r d ) G = k ( e + r d ) − 1 ( e + r d ) G = k G = R
等式要求 x ( R ′ ) ≡ x ( R ) ( m o d n ) x(R') \equiv x(R) \pmod{n} x ( R ′ ) ≡ x ( R ) ( mod n ) ,这正是 r r r 的定义。所以验签等式等价于"这个签名只能由掌握 d d d (即 P = d G P = dG P = d G 的私钥持有者)的人"才能产生。
2.5.2 完整 TypeScript 实现
// secp256k1 参数(复用 2.4 节定义)
const { p, a, b, G, n } = SECP256K1;
/**
* 简化的 SHA-256 哈希(复用 2.2 节实现)
* 真实中应对消息先标准化
*/
function sha256Msg(msg: string): bigint {
const hash = sha256(msg); // 复用 2.2 节完整实现
return BigInt('0x' + hash);
}
/**
* CSPRNG 模拟:生成 [1, n-1] 范围内的密码学安全随机数
* 真实实现应使用 crypto.getRandomValues 并拒绝偏置值
*/
function randomK(): bigint {
const buf = new Uint8Array(32);
for (let i = 0; i < 32; i++) buf[i] = Math.floor(Math.random() * 256);
let k = 0n;
for (let i = 0; i < 32; i++) {
k = (k << 8n) | BigInt(buf[i]);
}
k = (k % (n - 1n)) + 1n; // 1..n-1
return k;
}
/**
* ECDSA 签名
* @param d 私钥
* @param m 消息
* @returns 签名 (r, s)
*/
function ecdsaSign(d: bigint, m: string): { r: bigint; s: bigint } {
const e = sha256Msg(m) % n;
let k: bigint, R: ECPoint, r: bigint, s: bigint;
do {
k = randomK();
R = scalarMultiply(k, new ECPoint(G.x, G.y), a, p);
r = R.x! % n;
} while (r === 0n);
const kInv = modInverse(k, n);
s = (kInv * (e + r * d)) % n;
// 低 s 值规范(BIP-62):如果 s > n/2,使用 n-s 以压缩签名空间
if (s > n / 2n) {
s = n - s;
}
if (s === 0n) throw new Error("s is zero, retry");
return { r, s };
}
/**
* ECDSA 验证
* @param P 公钥
* @param m 消息
* @param sig 签名 (r, s)
* @returns 是否有效
*/
function ecdsaVerify(P: ECPoint, m: string, sig: { r: bigint; s: bigint }): boolean {
const { r, s } = sig;
if (r <= 0n || r >= n || s <= 0n || s >= n) return false;
const e = sha256Msg(m) % n;
const sInv = modInverse(s, n);
const u1 = (e * sInv) % n;
const u2 = (r * sInv) % n;
const Gp = new ECPoint(G.x, G.y);
const u1G = scalarMultiply(u1, Gp, a, p);
const u2P = scalarMultiply(u2, new ECPoint(P.x, P.y), a, p);
const R = ecAdd(u1G, u2P, a, p);
if (R.isInfinity()) return false;
return (R.x! % n) === r;
}
// --- 完整流程验证 ---
console.log("=== ECDSA 签名验证完整演示 ===");
// 1. 生成密钥对
const d = 0xdeadbeef0123456789abcdef0123456789abcdef0123456789abcdef012345n; // 私钥
const pubKey = scalarMultiply(d, new ECPoint(G.x, G.y), a, p);
console.log(`私钥 d: 0x${d.toString(16).slice(0, 20)}...`);
console.log(`公钥 P: ${pubKey.toString()}`);
// 2. 签名
const message = "Transfer 1.0 BTC to Alice";
const sig = ecdsaSign(d, message);
console.log(`\n消息: "${message}"`);
console.log(`签名: (r=0xs i g . r . t o S t r i n g ( 16 ) . s l i c e ( 0 , 16 ) . . . , s = 0 x {sig.r.toString(16).slice(0, 16)}..., s=0x s i g . r . t o S t r in g ( 16 ) . s l i ce ( 0 , 16 ) ... , s = 0 x {sig.s.toString(16).slice(0, 16)}...)`);
// 3. 验证(通过)
const valid = ecdsaVerify(pubKey, message, sig);
console.log(`\n✅ 原始消息验签: ${valid}`);
// 4. 篡改后验证(应失败)
const tamperedMsg = "Transfer 100.0 BTC to Mallory";
const tamperedValid = ecdsaVerify(pubKey, tamperedMsg, sig);
console.log(`❌ 篡改消息验签: ${tamperedValid} (应为 false)`);
// 5. 用错误公钥验证(应失败)
const wrongKey = scalarMultiply(0x1234n, new ECPoint(G.x, G.y), a, p);
const wrongKeyValid = ecdsaVerify(wrongKey, message, sig);
console.log(`❌ 错误公钥验签: ${wrongKeyValid} (应为 false)`);
2.5.3 Sony PS3 事件:随机数 k k k 的致命秘密
事件背景
2010 年,黑客组织 fail0overflow 在著名的 CCC 黑客大会上展示了如何破解 Sony PlayStation 3 的安全系统。他们的核心发现令全场哗然:
Sony 在 PS3 固件的 ECDSA 实现中,对每一条消息使用了固定的 k k k 值。
攻击数学:为什么固定 k k k = 私钥泄露
假设用同一个 k k k 对两条不同消息 m 1 , m 2 m_1, m_2 m 1 , m 2 签名:
s 1 = k − 1 ( e 1 + r d ) m o d n s 2 = k − 1 ( e 2 + r d ) m o d n s_1 = k^{-1}(e_1 + r d) \mod n\\
s_2 = k^{-1}(e_2 + r d) \mod n s 1 = k − 1 ( e 1 + r d ) mod n s 2 = k − 1 ( e 2 + r d ) mod n
注意 r r r 也相同(因为 r = x ( k G ) r = x(kG) r = x ( k G ) ,k k k 相同 → R R R 相同 → r r r 相同)。
攻击者获得 ( r , s 1 ) , ( r , s 2 ) (r, s_1), (r, s_2) ( r , s 1 ) , ( r , s 2 ) 和公共的 e 1 = H ( m 1 ) , e 2 = H ( m 2 ) e_1 = H(m_1), e_2 = H(m_2) e 1 = H ( m 1 ) , e 2 = H ( m 2 ) :
s 1 − s 2 = k − 1 ( e 1 − e 2 ) m o d n s_1 - s_2 = k^{-1}(e_1 - e_2) \mod n s 1 − s 2 = k − 1 ( e 1 − e 2 ) mod n
⇒ k = ( e 1 − e 2 ) ⋅ ( s 1 − s 2 ) − 1 m o d n \Rightarrow k = (e_1 - e_2) \cdot (s_1 - s_2)^{-1} \mod n ⇒ k = ( e 1 − e 2 ) ⋅ ( s 1 − s 2 ) − 1 mod n
一旦得到 k k k ,私钥立即暴露:
d = ( s 1 k − e 1 ) ⋅ r − 1 m o d n d = (s_1 k - e_1) \cdot r^{-1} \mod n d = ( s 1 k − e 1 ) ⋅ r − 1 mod n
只需要两个使用相同 k k k 的签名,私钥就被完全破解。
历史后果
2010 年 12 月:fail0overflow 发布演示,展示从固定 k k k 恢复出 Sony 的主私钥 。 这意味着任何人都可以用 Sony 的私钥"签名"自定义固件,让 PS3 认为它是官方授权的。 Sony 紧急起诉多名黑客(包括 George Hotz / "GeoHot")。 但私钥已无法挽回——数学保证一旦泄露,没有任何技术方法可以"撤销"一个已公开的公钥对应私钥 。
/**
* Sony PS3 攻击演示:从两个使用相同 k 的签名中恢复私钥
*/
function sonyPs3Attack(
m1: string,
sig1: { r: bigint; s: bigint },
m2: string,
sig2: { r: bigint; s: bigint },
): bigint {
const e1 = sha256Msg(m1) % n;
const e2 = sha256Msg(m2) % n;
const { r, s: s1 } = sig1;
const { s: s2 } = sig2;
// 步骤 1: 恢复 k
const k = ((e1 - e2) * modInverse((s1 - s2), n)) % n;
if (k < 0n) k += n;
console.log(`攻击恢复 k: 0x${k.toString(16).slice(0, 16)}...`);
// 步骤 2: 恢复 d
let d = ((s1 * k - e1) * modInverse(r, n)) % n;
if (d < 0n) d += n;
return d;
}
// --- 模拟 Sony 的错误 ---
const fixedK = 0xbadc0de123456789abcdef0n; // Sony 使用的"固定随机数"
console.log("\n=== Sony PS3 攻击模拟 ===");
console.log(`Sony 错误: 固定使用 k = 0x${fixedK.toString(16).slice(0, 16)}...`);
// 用固定 k 签两条消息(模拟 Sony 的固件签名)
const msg1 = "Firmware_v3.41_signed_by_Sony";
const msg2 = "Custom_firmware_authorized"; // 攻击者控制的消息
// 签名 1: 使用"错误"的固定 k
const e1 = sha256Msg(msg1) % n;
const R1 = scalarMultiply(fixedK, new ECPoint(G.x, G.y), a, p);
const r = R1.x! % n; // r 相同!
const kInv = modInverse(fixedK, n);
const s1 = (kInv * (e1 + r * d)) % n;
// 签名 2: 攻击者可以构造任意消息
const e2 = sha256Msg(msg2) % n;
const s2 = (kInv * (e2 + r * d)) % n;
console.log(`签名1: (r, s1)`);
console.log(`签名2: (r, s2)`);
console.log(`注意: r 相同(因为 k 相同),这是致命线索!`);
// 攻击者执行攻击
const recoveredD = sonyPs3Attack(msg1, { r, s: s1 }, msg2, { r, s: s2 });
console.log(`\n🔓 恢复出的私钥: 0x${recoveredD.toString(16).slice(0, 20)}...`);
console.log(`原始私钥: 0x${d.toString(16).slice(0, 20)}...`);
console.log(`恢复正确? ${recoveredD === d}`);
console.log(`\n💀 一旦两个签名使用相同 k,私钥不可逆转地泄露!`);
如何正确生成 k k k ?
方案一:真随机(CSPRNG)
每次签名时从 crypto.getRandomValues 生成 256 位均匀随机数。风险:随机源可能污染/可预测(某些 IoT 设备的 /dev/urandom 可能熵不足)。
方案二:确定性 k k k (RFC 6979)—— 推荐
用私钥 d d d 和消息哈希 e e e 作为输入,通过 HMAC-SHA256 计算确定性 k k k :
k = HMAC-SHA256 ( key = d , data = e ∥ 0 x 00 (padding) ) k = \text{HMAC\text{-}SHA256}(\text{key}=d, \text{data}=e \parallel 0x00 \text{ (padding)}) k = HMAC - SHA256 ( key = d , data = e ∥ 0 x 00 (padding) )
优点 :
对同一 ( d , m ) (d, m) ( d , m ) 总是产生相同 k k k ,保证签名可复现。 不依赖外部随机源,避免熵不足问题。 不同消息产生不同的 k k k ,完全免疫"固定 k k k "攻击。
比特币的 libsecp256k1 默认使用 RFC 6979 确定性签名。
2.5.4 ECDSA vs Schnorr 签名
比特币在 2021 年的 Taproot 升级中引入了 Schnorr 签名 (BIP-340)。以下是两者对比:
签名大小 ~71 字节(DER 编码) 64 字节(r, s 各 32 字节)
可聚合性 ❌(签名不能数学合并) ✅(MuSig: k = k 1 + k 2 , s = s 1 + s 2 k = k_1 + k_2, s = s_1 + s_2 k = k 1 + k 2 , s = s 1 + s 2 ,得到联合签名)
公钥恢复 ✅(从签名恢复公钥,节省存储) ❌(BIP-340 中必须显式携带公钥)
标准安全性证明 较复杂 基于标准离散对数假设的简洁证明
Schnorr 签名的核心优势——线性 :
σ a g g = ( R 1 + R 2 , s 1 + s 2 ) = ( R a g g , s a g g ) \sigma_{agg} = (R_1 + R_2, s_1 + s_2) = (R_{agg}, s_{agg}) σ a g g = ( R 1 + R 2 , s 1 + s 2 ) = ( R a g g , s a g g )
两个参与方可以各自独立计算自己的 R i R_i R i 和 s i s_i s i ,然后简单相加得到一个"组合签名",验证方只需对组合公钥验证一次。这对多签钱包和闪电网络有巨大价值。
graph TD
P1["参与方1: s1, R1"] -->|s=s1+s2<br/>R=R1+R2| Agg["聚合签名<br/>64 字节"]
P2["参与方2: s2, R2"] -->|协同计算| Agg
P3["验证方"] -->|单次验签<br/>验证 P_agg| Agg
关键区别 :ECDSA 验签中需要计算 s − 1 s^{-1} s − 1 ,这使得签名之间不能线性组合。Schnorr 的设计避免了取逆,保留了线性结构。
核心认知
ECDSA 签名 = 两个数 ( r , s ) (r, s) ( r , s ) 。r r r 是随机点 R = k G R = kG R = k G 的 x x x 坐标,s s s 将消息哈希 e e e 、r r r 和私钥 d d d 绑定在一起。验证等式的本质是用公钥"解开"这个绑定。k k k 是签名的灵魂。 不是算法选择也不是性能参数——k k k 的一次重复 = 私钥泄露。 Sony 事件以数十亿美元的代价证明了这一点。确定性 k k k (RFC 6979)比纯随机更安全。 在密码学史上,随机源失败导致的漏洞(Debian OpenSSL 2008、Sony PS3 2010)远比确定性算法多。用 HMAC 从 ( d , m ) (d, m) ( d , m ) 派生 k k k 是最佳实践。Schnorr 是 ECDSA 的优雅进化。 更强的数学性质(线性可聚合、可批量验证、更简洁的形式化安全证明)使其成为比特币 Taproot 升级的基础。未来多签和跨链协议将大量使用 Schnorr/MuSig。
下一预告 :2.6 节将走出纯数学,进入实际应用——钱包的密钥管理 。从密码学安全随机数生成 128–256 位熵,到 BIP-39 助记词,再到 BIP-32/BIP-44 的层级确定性钱包,以及冷存储和硬件钱包的安全实践。
2.6 钱包与密钥管理:从熵到地址
2.5 节完成了数字签名的数学实现。本节转入工程实践 ——如何将密码学安全私钥安全地生成、存储和使用 ,这是用户与区块链交互的第一入口。钱包不是"存放币的地方",而是密钥的管理器 。
2.6.1 密钥生成的起点:密码学安全随机数
为什么需要 CSPRNG?
普通伪随机数生成器(如 Math.random())是可预测的。如果你用 Math.random() 生成私钥,攻击者可以通过观察少量输出推断出整个序列,从而推算出你的私钥。
密码学安全随机数生成器(CSPRNG) 的要求:
前向不可预测性 :即使攻击者知道前 n n n 个输出,也无法预测第 n + 1 n+1 n + 1 个。后向不可预测性 :即使攻击者知道后续输出,也无法推算出之前的内部状态(防止逆向推导种子)。
/**
* 密码学安全随机数生成(基于浏览器/Node crypto API)
*/
function secureRandomBytes(size: number): Uint8Array {
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
// 浏览器环境
return crypto.getRandomValues(new Uint8Array(size));
} else if (typeof require === 'function') {
// Node.js 环境
const nodeCrypto = require('crypto');
return nodeCrypto.randomBytes(size);
}
throw new Error('No CSPRNG available');
}
// 生成一个 secp256k1 私钥:256 位随机数,需满足 1 <= d < n
function generatePrivateKey(): bigint {
const n = SECP256K1.n;
while (true) {
const bytes = secureRandomBytes(32);
let d = 0n;
for (let i = 0; i < 32; i++) {
d = (d << 8n) | BigInt(bytes[i]);
}
if (d >= 1n && d < n) return d;
}
}
console.log("=== 安全私钥生成 ===");
const d = generatePrivateKey();
console.log(`私钥: 0x${d.toString(16).padStart(64, '0')}`);
console.log(`位数: ${d.toString(2).length}`);
console.log(`有效范围: ${d > 0n && d < SECP256K1.n ? '✅ 有效' : '❌ 无效'}`);
熵的物理来源
crypto.randomBytes 的核心是操作系统内核收集的真实熵源 :
硬件中断时间(键盘按键、磁盘 I/O、网络包到达时间) CPU 硬件随机数生成器(如 Intel RDRAND 指令) 其他不可预测的硬件事件
关键规则 :永远不要自己写随机数生成器。"密码学家唯一会自己实现的随机数生成器,就是另一个会被攻破的随机数生成器。"
2.6.2 BIP-39:从随机熵到人类可记忆的助记词
flowchart LR
RNG["密码学安全随机数<br/>熵 128-256 bit"] --> MSE["助记词<br/>12-24 个英文单词"]
MSE --> Seed["种子<br/>PBKDF2-HMAC-SHA512<br/>2000 轮迭代"]
Seed --> XK["主扩展密钥<br/>BIP-32"]
XK --> Chain["子密钥派生<br/>CKD: 父密钥 + 索引"]
Chain --> Tree["HD 派生树<br/>BIP-44: m/44'/0'/0'/i"]
Tree --> Addr["比特币/以太坊地址"]
style MSE fill:#fff3e0
style XK fill:#e3f2fd
style Addr fill:#c8e6c9
为什么需要助记词?
256 位的十六进制字符串(64 个字符)对人类极不友好。抄错一个字符 = 资产永久丢失。BIP-39 将 128–256 位的熵映射为 12–24 个英文单词 (从一个 2048 词的固定词表中选取),大幅降低抄写错误率。
BIP-39 的完整流程
128-256 位随机熵
│
▼
┌─────────────────┐
│ SHA-256 哈希 │ → 取前 (熵位数/32) 位作为"校验和"
└─────────────────┘
│
▼
熵 + 校验和 = 264-330 位
│
▼ 每 11 位 = 一个单词索引(0-2047)
12-24 个助记词
/**
* BIP-39 助记词简化实现
* 使用 128 位熵 = 12 词 + 4 位校验和
*/
const BIP39_WORD_LIST: string[] = [
"abandon", "ability", "able", // ... 完整词表有 2048 个词
// 教学演示:使用前 64 个词做简化版
...Array.from({ length: 2048 }, (_, i) => `word${i.toString(16).padStart(3, '0')}`) // 占位符
];
// 真实词表应使用标准英文词表(如 bitcoin/bips 仓库中的 bip-0039/english.txt)
class BIP39 {
private wordList: string[];
constructor(wordList: string[] = BIP39_WORD_LIST) {
if (wordList.length !== 2048) throw new Error('Word list must have 2048 entries');
this.wordList = wordList;
}
/**
* 熵 → 助记词
* @param entropy 128, 160, 192, 224, 或 256 位的 Uint8Array
*/
entropyToMnemonic(entropy: Uint8Array): string {
const ENT = entropy.length * 8; // 熵位数
const CS = ENT / 32; // 校验和位数
const totalBits = ENT + CS; // 总位数 = 11 的倍数
if (![128, 160, 192, 224, 256].includes(ENT)) {
throw new Error('Entropy must be 128/160/192/224/256 bits');
}
// 计算 SHA-256 并取前 CS 位作为校验和
// 教学简化:假设已有 sha256 函数
const hash = sha256(new TextDecoder().decode(entropy));
const hashBits = BigInt('0x' + hash).toString(2).padStart(256, '0');
const checksumBits = hashBits.slice(0, CS);
// 将完整数据转为二进制字符串
let dataBits = '';
for (const byte of entropy) {
dataBits += byte.toString(2).padStart(8, '0');
}
dataBits += checksumBits;
// 每 11 位 = 一个词
const words: string[] = [];
for (let i = 0; i < dataBits.length; i += 11) {
const index = parseInt(dataBits.slice(i, i + 11), 2);
words.push(this.wordList[index]);
}
return words.join(' ');
}
/**
* 助记词 → 种子(通过 PBKDF2 密钥派生)
* 真实现象中应使用该函数结合盐值 "mnemonic" 迭代 2048 次
*/
mnemonicToSeed(mnemonic: string, passphrase: string = ''): Uint8Array {
// 教学简化:实际应使用 PBKDF2-HMAC-SHA512
// 标准: seed = PBKDF2(mnemonic, "mnemonic" + passphrase, 2048, 512)
const normalized = mnemonic.normalize('NFKD');
const salt = ('mnemonic' + passphrase).normalize('NFKD');
// 返回 512 位种子
return secureRandomBytes(64); // 占位:真实需 PBKDF2 实现
}
}
// --- BIP-39 验证 ---
const bip39 = new BIP39();
const entropy = secureRandomBytes(16); // 128 位 = 12 词
console.log(`\n128 位熵: ${Array.from(entropy).map(b => b.toString(16).padStart(2, '0')).join('')}`);
// const mnemonic = bip39.entropyToMnemonic(entropy);
// console.log(`助记词: ${mnemonic}`);
助记词的安全强度
----------- ------- ------------ --------
12 词 128 位 2 128 2^{128} 2 128 极安全(当前算力不可行)
15 词 160 位 2 160 2^{160} 2 160 极高
18 词 192 位 2 192 2^{192} 2 192 后量子安全级别
21 词 224 位 2 224 2^{224} 2 224 过度安全
24 词 256 位 2 256 2^{256} 2 256 过度安全
标准推荐 :12 词(128 位)对大多数用户足够安全且便于记忆/抄写。24 词主要用于要求极致安全的场景(如机构冷存储)。
2.6.3 BIP-32/BIP-44:层级确定性钱包(HD Wallet)
为什么需要 HD 钱包?
传统钱包为每笔交易生成独立随机私钥。用户需要备份每一个私钥——使用 100 次 = 备份 100 个私钥。HD 钱包(BIP-32, 2012)解决了这个灾难:从一个主种子通过确定性算法派生无限多个子密钥 ,只需备份一次种子(助记词)。
核心思想:扩展密钥 + 子密钥派生
主种子 (64 字节, 512 位)
│
▼
┌──────────────┐
│ HMAC-SHA512 │
│ key="Bitcoin seed" │
└──────────────┘
│
├─ 主私钥 (256 位) → 主公钥
└─ 主链码 (256 位) → 用于子密钥派生
子密钥派生函数(CKD, Child Key Derivation) :
\text{child}_i = HMAC\text{-}SHA512(\text{parent_chain_code}, \text{parent_key} \parallel i)
输出分为两半:左半部分作为子私钥,右半部分作为子链码。通过递增索引 i i i ,可以生成无限多个独立的子密钥。
/**
* 简化的 BIP-32 层级派生(教学演示)
*/
class HDNode {
privateKey: bigint | null; // 仅"扩展私钥"节点持有
publicKey: ECPoint;
chainCode: Uint8Array;
depth: number;
index: number;
parentFingerprint: number;
constructor(
privateKey: bigint | null,
publicKey: ECPoint,
chainCode: Uint8Array,
depth: number = 0,
index: number = 0,
parentFingerprint: number = 0,
) {
this.privateKey = privateKey;
this.publicKey = publicKey;
this.chainCode = chainCode;
this.depth = depth;
this.index = index;
this.parentFingerprint = parentFingerprint;
}
/**
* 从种子创建 HD 钱包根节点
*/
static fromSeed(seed: Uint8Array): HDNode {
// 真实实现: I = HMAC-SHA512(key="Bitcoin seed", data=seed)
// 简化演示
const I = secureRandomBytes(64); // 占位
const IL = I.slice(0, 32); // 主私钥
const IR = I.slice(32, 64); // 主链码
let masterKey = 0n;
for (let i = 0; i < 32; i++) {
masterKey = (masterKey << 8n) | BigInt(IL[i]);
}
const pubKey = scalarMultiply(masterKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
return new HDNode(masterKey, pubKey, IR, 0, 0, 0);
}
/**
* 派生子节点(简化版,省略硬化派生的细节)
*/
derive(index: number): HDNode {
if (this.depth >= 255) throw new Error('Max depth exceeded');
// 真实实现需根据 hardened (index >= 2^31) 或 normal 模式选择不同的派生方式
const childKey = (this.privateKey! + BigInt(index)) % SECP256K1.n; // 教学简化
const childPubKey = scalarMultiply(childKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
return new HDNode(
childKey,
childPubKey,
this.chainCode, // 简化:实际需重新计算
this.depth + 1,
index,
0, // 简化:实际需计算父节点指纹
);
}
}
// --- 演示:从种子到无限地址 ---
const seed = secureRandomBytes(64);
const root = HDNode.fromSeed(seed);
console.log(`\n=== BIP-32 链式推导 ===`);
console.log(`根公钥: ${root.publicKey.toString()}`);
const child0 = root.derive(0);
console.log(`派生子 0: ${child0.publicKey.toString()}`);
const child1 = root.derive(1);
console.log(`派生子 1: ${child1.publicKey.toString()}`);
// 同样的种子总是产生同样的子密钥(确定性)
BIP-44:标准化的派生路径
BIP-44 定义了五层派生路径标准:
m / \text{purpose}' / \text{coin_type}' / \text{account}' / \text{change} / \text{address_index}
0'币种类型:0=比特币,60=以太坊,501=Solana
示例路径 :
比特币首个外部地址:m/44'/0'/0'/0/0 以太坊首个地址:m/44'/60'/0'/0/0 比特币第 10 个外部地址:m/44'/0'/0'/0/9
/**
* BIP-44 路径解析器
*/
function parseBIP44Path(path: string): number[] {
const parts = path.split('/');
if (parts[0] !== 'm') throw new Error('Path must start with m');
const indices: number[] = [];
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
const isHardened = part.endsWith("'");
const index = parseInt(isHardened ? part.slice(0, -1) : part, 10);
const finalIndex = isHardened ? index + 0x80000000 : index;
indices.push(finalIndex);
}
return indices;
}
console.log(`\nBIP-44 路径 m/44'/0'/0'/0/0 解析: [${parseBIP44Path("m/44'/0'/0'/0/0").join(', ')}]`);
// 硬化索引通过在第 31 位加 1 区分:0x80000000 = 2^31
2.6.4 从公钥到地址:比特币与以太坊的差异
比特币地址(P2PKH)
\text{地址} = \text{Base58Check}(\text{RIPEMD160}(\text{SHA256}(\text{pubkey})) \oplus \text{network_byte})
04 前缀(未压缩公钥)→ SHA256 → RIPEMD160(20 字节哈希)→ 加版本字节 0x00(主网)→ Base58Check 编码。以 "1" 开头的是主网 P2PKH 地址(如 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)。
以太坊地址
地址 = Keccak256 ( pubkey ) [ 12 : 32 ] \text{地址} = \text{Keccak256}(\text{pubkey})[12:32] 地址 = Keccak256 ( pubkey ) [ 12 : 32 ]
取公钥(64 字节未压缩,去掉 0x04)的 Keccak-256 哈希,取最后 20 字节。 以 0x 开头,40 个十六进制字符(如 0xdAC17F958D2ee523a2206206994597C13D831ec7)。 无 Base58Check ——原始十六进制,但包含 EIP-55 大小写校验。
/**
* 公钥到地址的两种路线
*/
function bitcoinAddressFromPubkey(pubKey: ECPoint): string {
// 简化:仅展示流程
const pubKeyBytes = new Uint8Array(65); // 0x04 + x + y
// ... 序列化公钥位为字节
// hash160 = RIPEMD160(SHA256(pubKeyBytes))
// address = Base58Check(0x00 || hash160)
return "1A1z...P1eP"; // 占位
}
function ethereumAddressFromPubkey(pubKey: ECPoint): string {
// 未压缩公钥去掉 0x04 后的 64 字节
const pubKeyBytes = new Uint8Array(64);
// ... 序列化 x, y 为字节
// address = Keccak256(pubKeyBytes).slice(-20)
return "0x..."; // 占位
}
------ ------------- --------
哈希函数 SHA256 → RIPEMD160 Keccak-256
输出大小 160 位 160 位(Keccak-256 的最后 160 位)
编码 Base58Check 十六进制(EIP-55 大小写校验)
2.6.5 冷存储与硬件钱包的安全实践
威胁模型
------ ------------------- ---------------
硬件钱包的核心机制
硬件钱包(如 Ledger、Trezor)的核心安全保证 是:
私钥从不出现在硬件设备的"易泄露区域"(RAM/CPU 缓存可能被侧信道攻击),且永远不暴露给连接的主机。
交易签名流程:
主机将待签名的交易数据发送给硬件设备。 硬件在屏幕上显示交易内容(如"发送 0.5 BTC 到 1A1z...")。 用户物理按下设备上的确认按钮。 设备使用芯片内安全元素的私钥执行签名。 只将签名结果 ( r , s ) (r, s) ( r , s ) 返回给主机,私钥从未离开设备。
助记词的安全备份
永远不要 :
将助记词存储在联网设备(手机照片、云盘、邮件)。 将助记词输入任何网站或应用(除非是首次在新设备上恢复钱包)。 只保留一份备份(单点故障)。
推荐做法 :
写在金属板 (防火防水)上,存放在两个不同物理位置。 或使用Shamir 秘密共享 (BIP-39 扩展):将 24 词分成 3 份,任意 2 份即可恢复,避免单点失窃/丢失。
核心认知
钱包 = 密钥管理器。 它不"存放"币,币永远在区块链上。钱包只是保存了授权花费这些币的私钥。BIP-39 助记词是人类可记忆的 128 位熵。 校验和机制保证 12 个词中抄错任意一个词可以立即被检测(约 1/256 的错误会被校验和捕获,约 255/256 的错误会被词表检查捕获)。BIP-32 推导 = 一次备份,无限密钥。 主种子派生子密钥的树状结构,使得企业可以批量管理数千个客户存款地址,个人可以在多账户间隔离隐私。冷存储的核心不是技术,而是物理隔离。 硬件钱包的价值在于"私钥永远不会暴露在联网环境中"。空气隔离的计算机 + 离线签名 + 二维码传输,是机构级冷存储的标准做法。
下一预告 :2.7 节将探索默克尔树(Merkle Tree)——如何将数百万笔交易压缩为一个 32 字节的根哈希,以及轻客户端(SPV)如何在不下载完整区块链的情况下验证交易存在性。
2.7 Merkle 树与 SPV 轻量验证
区块链需要存储海量交易数据。以太坊一个全节点的状态大小超过 700 GB,比特币区块链历史数据超过 600 GB。如果每个用户都需要下载全部数据才能验证一笔交易,去中心化的门槛将变得极高。本节介绍 Merkle 树(哈希树) 的巧妙结构,以及它如何使轻客户端(SPV)仅用数 KB 数据就可验证特定交易的存在性。
2.7.1 问题:如何高效证明"某个数据在集合中"
假设一个区块包含 1000 笔交易,全节点需要存储所有交易的完整列表。当 Alice 问"我的交易 tx_237 是否被包含在这个区块中?",传统方法是把 1000 笔交易全部发送给她,让她自己搜索。传输量 = 1000 × 平均交易大小(约 250 B)= 250 KB。
有没有更高效的方法?答案是:如果全节点只需证明"tx_237 的哈希在某个哈希值的集合中" ,而 Alice 信任自己已经知道的那个"集合的简洁表示",证明可以极短。
2.7.2 Merkle 树的结构
构建过程(自下而上)
假设 8 笔交易(N = 8 N=8 N = 8 ,扩展为 2 的幂次):
Level 3 (Root): H_{0-7}
/ \
Level 2: H_{0-3} H_{4-7}
/ \ / \
Level 1: H_0-1 H_2-3 H_4-5 H_6-7
/ \ / \ / \ / \
Level 0: tx0 tx1 tx2 tx3 tx4 tx5 tx6 tx7
(叶节点,每个都是数据的哈希)
每个内部节点存储其两个子节点的双重哈希 (防止长度扩展攻击):
H parent = H ( H left ∥ H right ) H_{\text{parent}} = H(H_{\text{left}} \parallel H_{\text{right}}) H parent = H ( H left ∥ H right )
在比特币中,H = double-SHA256 ( double-SHA256 ( left ∥ right ) ) H = \text{double-SHA256}(\text{double-SHA256}(\text{left} \parallel \text{right})) H = double-SHA256 ( double-SHA256 ( left ∥ right )) 。
关键性质:O ( log N ) O(\log N) O ( log N ) 的证明大小
要证明 tx3 在集合中,只需提供从 tx3 到根的一条路径上的兄弟节点 哈希:
-------------- -----------
Hash(tx2)与 Hash(tx3) 组合得 H_{2-3}
H_{0-1}与 H_{2-3} 组合得 H_{0-3}
H_{4-7}与 H_{0-3} 组合得 H_{0-7}(根)
证明大小 :O ( log N ) O(\log N) O ( log N ) 个兄弟节点。对于 1000 笔交易,需要 log 2 1000 ≈ 10 \log_2 1000 \approx 10 log 2 1000 ≈ 10 个哈希,每个 32 字节,总证明大小 ≈ 320 字节——比发送全部 250 KB 交易数据小了 800 倍 。
/**
* Merkle 树完整 TypeScript 实现
*/
class MerkleNode {
constructor(
public hash: Uint8Array,
public left: MerkleNode | null = null,
public right: MerkleNode | null = null,
public txIndex: number = -1, // 叶节点对应的交易索引
) {}
isLeaf(): boolean {
return this.left === null && this.right === null;
}
}
class MerkleTree {
root: MerkleNode | null = null;
private leaves: MerkleNode[] = [];
constructor(txHashes: Uint8Array[]) {
if (txHashes.length === 0) throw new Error('Cannot build tree with 0 leaves');
// 如果叶节点数量不是 2 的幂次,重复最后一个直到对齐
const padded = [...txHashes];
while ((padded.length & (padded.length - 1)) !== 0) {
padded.push(padded[padded.length - 1]);
}
// 构建叶节点
this.leaves = padded.map((h, i) => new MerkleNode(h, null, null, i < txHashes.length ? i : -1));
this.root = this._buildTree(this.leaves);
}
private _buildTree(nodes: MerkleNode[]): MerkleNode {
if (nodes.length === 1) return nodes[0];
const parents: MerkleNode[] = [];
for (let i = 0; i < nodes.length; i += 2) {
const left = nodes[i];
const right = nodes[i + 1] || nodes[i]; // 奇数时复制最后一项
const parentHash = this._hashPair(left.hash, right.hash);
parents.push(new MerkleNode(parentHash, left, right));
}
return this._buildTree(parents);
}
private _hashPair(a: Uint8Array, b: Uint8Array): Uint8Array {
// 比特币采用双重 SHA256:SHA256(SHA256(concat(a, b)))
const concat = new Uint8Array(a.length + b.length);
// 排序:较小的哈希在前(保证顺序确定性)
if (this._compare(a, b) < 0) {
concat.set(a, 0);
concat.set(b, a.length);
} else {
concat.set(b, 0);
concat.set(a, b.length);
}
// 简化:教学演示用 simple hash
return simpleDoubleHash(concat);
}
private _compare(a: Uint8Array, b: Uint8Array): number {
for (let i = 0; i < Math.min(a.length, b.length); i++) {
if (a[i] < b[i]) return -1;
if (a[i] > b[i]) return 1;
}
return a.length - b.length;
}
/**
* 生成某个叶节点的 Merkle 证明
* @param index 交易索引
* @returns 从叶到根路径上的兄弟节点列表
*/
getProof(index: number): Uint8Array[] {
if (index < 0 || index >= this.leaves.length) throw new Error('Invalid index');
const proof: Uint8Array[] = [];
let currentIndex = index;
let level = this.leaves;
while (level.length > 1) {
const isEven = currentIndex % 2 === 0;
const siblingIndex = isEven ? currentIndex + 1 : currentIndex - 1;
if (siblingIndex < level.length) {
proof.push(level[siblingIndex].hash);
}
// 构建父层
const parents: MerkleNode[] = [];
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = level[i + 1] || level[i];
const parentHash = this._hashPair(left.hash, right.hash);
parents.push(new MerkleNode(parentHash, left, right));
}
level = parents;
currentIndex = Math.floor(currentIndex / 2);
}
return proof;
}
getRoot(): Uint8Array | null {
return this.root?.hash ?? null;
}
}
/**
* 简化的双重哈希(教学用)
*/
function simpleDoubleHash(data: Uint8Array): Uint8Array {
const hash1 = new TextEncoder().encode(sha256(new TextDecoder().decode(data)));
const hash2 = new TextEncoder().encode(sha256(new TextDecoder().decode(hash1)));
return hash2.slice(0, 32);
}
// --- 完整流程验证 ---
console.log("=== Merkle 树验证 ===");
const txData = ["Alice->Bob: 1.0", "Bob->Charlie: 0.5", "Charlie->Dave: 0.3", "Dave->Eve: 0.2"];
const txHashes = txData.map(tx => {
const hash = sha256(tx);
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) bytes[i] = parseInt(hash.slice(i * 2, i * 2 + 2), 16);
return bytes;
});
const tree = new MerkleTree(txHashes);
console.log(`树根: ${Array.from(tree.getRoot()!).map(b => b.toString(16).padStart(2, '0')).slice(0, 8).join('')}...`);
// 为 tx[1] (Bob->Charlie) 生成证明
const proof = tree.getProof(1);
console.log(`\ntx[1] 的 Merkle 证明需要 ${proof.length} 个兄弟节点`);
proof.forEach((hash, i) => {
console.log(` 层级 i + 1 兄弟 : {i + 1} 兄弟: i + 1 兄弟 : {Array.from(hash).map(b => b.toString(16).padStart(2, '0')).slice(0, 4).join('')}...`);
});
2.7.3 证明验证算法
轻客户端(已拥有根哈希 H root H_{\text{root}} H root ,来自区块头):
输入:目标交易哈希 H(tx),证明 [s1, s2, ..., s_logN]
当前 = H(tx)
对每个兄弟哈希 s_i(从叶到根的顺序):
if 当前在左子树: 当前 = H(当前 || s_i)
else: 当前 = H(s_i || 当前)
返回 当前 == H_root
/**
* 验证 Merkle 证明
* @param targetHash 目标交易哈希
* @param proof 兄弟节点列表(从叶到根)
* @param root 预期的树根
* @param index 交易在原始列表中的索引
*/
function verifyMerkleProof(
targetHash: Uint8Array,
proof: Uint8Array[],
root: Uint8Array,
index: number,
): boolean {
let current = targetHash;
let idx = index;
for (const sibling of proof) {
// 判断当前节点在左还是右
if (idx % 2 === 0) {
// 当前在左,兄弟在右
current = simpleDoubleHash(concatBytes(current, sibling));
} else {
// 当前在右,兄弟在左
current = simpleDoubleHash(concatBytes(sibling, current));
}
idx = Math.floor(idx / 2);
}
return arraysEqual(current, root);
}
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
const result = new Uint8Array(a.length + b.length);
result.set(a, 0);
result.set(b, a.length);
return result;
}
function arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
// --- 验证演示 ---
const isValid = verifyMerkleProof(txHashes[1], proof, tree.getRoot()!, 1);
console.log(`\n✅ tx[1] 证明有效: ${isValid}`);
// 尝试验证错误索引(应失败)
const fakeIndex = 2;
const fakeValid = verifyMerkleProof(txHashes[1], proof, tree.getRoot()!, fakeIndex);
console.log(`❌ 错误索引(f a k e I n d e x ) 验证 : {fakeIndex}) 验证: f ak e I n d e x ) 验证 : {fakeValid} (应为 false)`);
// 尝试验证篡改的数据(应失败)
const tamperedHash = new Uint8Array(txHashes[1]);
tamperedHash[0] ^= 1;
const tamperedValid = verifyMerkleProof(tamperedHash, proof, tree.getRoot()!, 1);
console.log(`❌ 篡改数据验证: ${tamperedValid} (应为 false)`);
2.7.4 Simplified Payment Verification (SPV)
核心思想
SPV 客户端不下载完整区块,只下载区块头 (80 字节/块)。区块头包含:
前一区块哈希 Merkle 根 时间戳 难度目标 Nonce
SPV 客户端存储的数据量:
- 完整节点: 600+ GB (比特币全部交易历史)
- SPV 客户端: 80 bytes × ~800,000 区块 ≈ 64 MB (仅区块头)
- 验证单笔交易: + 320 bytes (Merkle 证明)
SPV 验证流程
graph LR
SPV["SPV 客户端<br/>存储: 64 MB 区块头"] -- "请求\ntx237 证明" --> FN["全节点<br/>存储: 600 GB 完整链"]
FN -- "提供 320 B Merkle 证明" --> SPV
SPV -- "验证: hash(tx237)+证明 == 区块头.Merkle根" --> OK{✅ 通过<br/>❌ 失败}
/**
* SPV 简化验证(教学演示)
*/
class SPVClient {
private blockHeaders: Array<{
blockHash: Uint8Array;
merkleRoot: Uint8Array;
timestamp: number;
// ... 其他字段
}> = [];
/**
* 仅下载区块头(高度轻量)
*/
addBlockHeader(header: { blockHash: Uint8Array; merkleRoot: Uint8Array; timestamp: number }): void {
this.blockHeaders.push(header);
}
/**
* 验证交易:需要全节点提供 Merkle 证明
*/
verifyTransaction(
txHash: Uint8Array,
blockHeight: number,
merkleProof: Uint8Array[],
txIndex: number,
): boolean {
if (blockHeight < 0 || blockHeight >= this.blockHeaders.length) {
throw new Error('Unknown block');
}
const header = this.blockHeaders[blockHeight];
return verifyMerkleProof(txHash, merkleProof, header.merkleRoot, txIndex);
}
}
const spv = new SPVClient();
spv.addBlockHeader({
blockHash: new Uint8Array(32), // 占位
merkleRoot: tree.getRoot()!,
timestamp: Date.now(),
});
const spvValid = spv.verifyTransaction(txHashes[1], 0, proof, 1);
console.log(`\n=== SPV 验证结果 ===`);
console.log(`SPV 验证 tx[1]: ${spvValid ? '✅ 交易确认包含在区块中' : '❌ 验证失败'}`);
SPV 的安全假设
SPV 客户端信任区块头 (通过 PoW 验证)和全节点提供的 Merkle 证明 。它不能验证 :
双花攻击(除非监听更多网络节点) 共识规则的完全合规(依赖全节点诚实) 当前余额(需追踪所有输入)
因此 SPV 适合验证特定交易是否被确认 (如"我收到的付款是否已上链"),但不适合作为完整节点的替代。
2.7.5 Patricia Merkle Tree(以太坊的状态树)
比特币的 Merkle 树仅用于交易列表 。以太坊使用更复杂的 Patricia Trie(前缀树 + 压缩) 和 Merkle Patricia Trie(MPT) 来存储:
状态树(State Trie) :账户地址 → 账户状态(nonce, balance, codeHash, storageRoot)存储树(Storage Trie) :每个合约的局部键值存储交易树(Transaction Trie) :当前区块的交易收据树(Receipt Trie) :交易收据(gas 使用、日志等)
以太坊的 MPT 使用三种节点类型:
叶子节点(Leaf) :存储键值对的最终值扩展节点(Extension) :压缩单一路径上的连续节点分支节点(Branch) :最多 16 个子节点 + 1 个值槽
存储少量地址的 MPT 示例:
[ , , , , [_extension: ,
0xe7] , , , ,...]
|
[branch: 0 → ac, 1 → 9b, ...]
/ \
[leaf: {address1}] [extension: 35]
|
[branch: 0 → leaf2, ...]
这种结构允许以太坊轻客户端用类似 SPV 的方式验证"某个地址的余额是否为 X X X ",通过提供从根到叶的路径证明。
核心认知
Merkle 树将证明大小从 O ( N ) O(N) O ( N ) 降到 O ( log N ) O(\log N) O ( log N ) 。 对于 4000 笔交易/区块的比特币,所需证明从 ≈ 1 MB 降到 ≈ 480 字节。哈希箱结构天然防止篡改。 如果攻击者修改任何一笔交易,计算出的根哈希将完全不同,与区块头中的存储值不符。这就是 Merkle 树作为"密码学累加器"的价值。SPV 是信任与效率的权衡。 64 MB 的区块头存储 vs 600 GB 的完整链——代价是信任全节点提供的证明,以及无法独立检测双花。以太坊的 MPT 是 Merkle 树的进化。 从有序列表的验证扩展到任意键值存储的验证,支撑了智能合约的复杂状态管理。
下一预告 :2.8 将跳出具体实现,从系统对比角度审视所有密码学原语——哈希、签名、对称/非对称加密——在区块链中的角色分工,并总结常见工程错误。
2.8 密码学原语选型对比与工程实践
2.1–2.7 深入实现了每个密码学原语。本节将视角从"单一算法"提升到"系统架构"层面,建立密码学组件选型决策框架 ,并总结区块链工程师最常犯的安全错误。
2.8.1 密码学原语在区块链中的角色分工
graph TB
subgraph 数据完整性层
H1["输入数据"] --> SHA["SHA-256<br/>双重哈希"] --> H2["固定长度摘要"]
H3["大量交易"] --> MT[Merkle Tree] --> H4["32 字节根哈希"]
end
subgraph 身份认证层
K1["私钥 d"] --> SM["ECDSA/Schnorr<br/>签名"] --> S1["签名 r,s"]
S1 --> V["公钥验证"] --> R{是否有效?}
end
subgraph 密钥管理层
E1["128-256 位熵"] --> BIP39["BIP-39<br/>助记词"] --> SE1["种子"]
SE1 --> BIP32["BIP-32<br/>HD 派生"] --> K2["无限子密钥"]
end
subgraph 保密层_应用层
M["消息"] --> AES[AES-CTR] --> C["密文"]
C --> AES --> M
end
H2 --> SM
K2 --> K1
------ ---------- ---------- ------------
哈希函数 数据压缩 + 完整性检查 区块引用(prevHash)、交易 ID、Merkle 根 碰撞阻力、雪崩效应、谜题友好
数字签名 身份认证 + 不可否认性 交易授权(ECDSA/Schnorr) ECDLP 困难性、k k k 唯一性
Merkle 树 集合成员证明 SPV 轻量验证 哈希链的防篡改性
对称加密 数据保密 钱包文件加密、P2P 层 TLS 密钥保密性
密钥派生 主种子 → 无限子密钥 HD 钱包地址批量生成 前向安全性(泄露子密钥不泄露父密钥)
2.8.2 选型决策树
需要的密码学能力是什么?
│
├── 确保"数据未被篡改" → 哈希函数
│ ├── 需要防长度扩展 → Keccak-256 (以太坊)
│ └── 追求最高性能 → SHA-256 (比特币)
│
├── 证明"操作被授权" → 数字签名
│ ├── 需要公钥恢复 → ECDSA (比特币传统)
│ ├── 需要批量验证/聚合 → Schnorr (BIP-340)
│ └── 机构级多签 → MuSig2 (Schnorr 聚合)
│
├── 证明"某数据在集合中" → Merkle 树/MPT
│ ├── 固定列表 → 二进制 Merkle 树
│ └── 动态键值存储 → Merkle Patricia Trie (以太坊)
│
├── 保护"密钥存储文件" → 对称加密 (AES-256-GCM)
│
└── 生成"无限可管理地址" → HD 钱包 (BIP-32/39/44)
2.8.3 安全强度对比表
安全级别 对称加密 (AES) 哈希 (SHA-256) 椭圆曲线 (secp256k1) RSA
--------- --------------- ---------------- --------------------- -----
80 bit (已 废 弃) 80 bit SHA-1 160 bit 1024 bit
112 bit (过渡) 112 bit SHA-224 224 bit 2048 bit
128 bit (当前标准) 128 bit SHA-256 256 bit 3072 bit
256 bit (后量子预备) 256 bit SHA-512 512 bit 15360 bit
关键洞察 :椭圆曲线提供"即用 256 位密钥实现 128 位安全",而 RSA 需要 3072 位。这就是为什么区块链优先选择 ECC 而非 RSA——在传输和存储受限的环境中,密钥尺寸直接决定效率。
2.8.4 十大常见工程安全错误
--- ------ ------ ----------
1 使用 Math.random() 生成密钥 私钥可预测,资产被盗 使用 crypto.getRandomValues
2 签名时重复使用 k k k 私钥泄露 RFC 6979 确定性签名
3 不验证曲线方程(输入不在曲线上) 离线签名攻击 验证 y 2 = x 3 + 7 m o d p y^2 = x^3 + 7 \mod p y 2 = x 3 + 7 mod p
4 未压缩点解析不验证 y y y 坐标 扭曲攻击(Twist Attack) 从 x x x 恢复 y y y 后验证曲线方程
5 使用 SHA-1 或 MD5 碰撞攻击可伪造 使用 SHA-256 或 Keccak-256
6 硬编码密钥/助记词在代码中 私钥暴露于版本控制 环境变量/硬件安全模块
7 未加盐哈希密码 彩虹表攻击 bcrypt/Argon2/PBKDF2
8 签名消息不包含上下文(如"Bitcoin Signed Message"前缀) 消息重放攻击 协议特定的消息前缀
9 忽略低 s s s 值规范(BIP-62) 签名可锻性(malleability) 规范化 s ≤ n / 2 s \leq n/2 s ≤ n /2
10 使用等价但不同的编码格式 哈希不一致导致验证失败 严格规定编码标准
错误 #3 详解:无效的曲线点攻击
假设攻击者提供一个伪造的公钥 P = ( x , y ) P = (x, y) P = ( x , y ) 但 y 2 ≢ x 3 + 7 ( m o d p ) y^2 \not\equiv x^3 + 7 \pmod{p} y 2 ≡ x 3 + 7 ( mod p ) 。如果签名验证代码不检查点是否在曲线上,攻击者可能构造"签名"骗过验证。
/**
* 曲线点验证——必须在使用任何传入点之前执行
*/
function isValidCurvePoint(P: ECPoint, a: bigint, b: bigint, p: bigint): boolean {
if (P.isInfinity()) return true; // 无穷远点是有效单位元
const lhs = (P.y! * P.y!) % p;
const rhs = (((P.x! * P.x! * P.x!) % p) + a * P.x! + b) % p;
return lhs === rhs || ((p + lhs) % p) === rhs; // 允许负坐标
}
// --- 攻击演示 ---
const maliciousX = 0xdeadbeefn; // 随机 x
const maliciousY = 0x13371337n; // 不合法的 y(很可能不在曲线上)
const maliciousPoint = new ECPoint(maliciousX, maliciousY);
console.log(`\n恶意点验证: ${isValidCurvePoint(maliciousPoint, 0n, 7n, SECP256K1.p)}`);
// 输出 false,应拒绝使用该点进行任何运算
2.8.5 后量子密码学:区块链的长期威胁
所有当前区块链密码学(ECC、RSA)都基于整数分解 和离散对数 问题,这两个问题在已知量子算法(Shor 算法)下是多项式时间可解的 。
Shor 算法的影响
------ ------------ ----------
ECDSA/ECDH 椭圆曲线离散对数 Shor 算法在量子计算机上 O ( n 3 ) O(n^3) O ( n 3 ) 可解
RSA 大数分解 Shor 算法在量子计算机上 O ( n 3 ) O(n^3) O ( n 3 ) 可解
SHA-256 无结构(理想哈希) 不受影响 (Grover 算法仅将 2 128 2^{128} 2 128 降到 2 64 2^{64} 2 64 ,仍计算不可行)
对称加密 (AES) 密钥穷举 Grover 算法将搜索空间减半,可用更长密钥补偿
量子安全签名候选 :
Lamport 签名 (一次性,基于哈希,193 KB/签名——过大但概念简单)SPHINCS+ (基于哈希,无状态,约 8 KB/签名)CRYSTALS-Dilithium (基于格,小签名,NIST 标准化)FALCON (基于格,更小签名,相同安全级别)
关键结论 :哈希函数(SHA-256, Keccak-256, 哈希树)天然免疫量子攻击 。这就是为什么比特币的 PoW(依赖哈希)比签名方案(依赖 ECC)更"量子健壮"——即使量子计算机攻破 ECC 签名,已上链的交易哈希仍然不可篡改。
核心认知
密码学是工具箱,不是万能药。 哈希保证完整性,签名保证授权,加密保证保密——没有单一原语可以替代另一个。区块链的"透明性"设计意味着加密使用最少,签名使用最多。密码学的安全边界是"计算不可行",不是"数学不可能"。 量子计算机的出现将打破 ECC 和 RSA 的安全假设,但哈希函数的抗碰撞性基于信息论(生日攻击的数学下限),不受影响。实现错误比算法被攻破更常见。 Sony PS3 使用正确的 ECDSA 算法,但因 k k k 重复而泄露私钥。Debian 2008 年因为 valgrind 工具清除了 OpenSSL 的熵源,导致两年内生成的所有密钥只有 15 位有效熵。工程纪律比算法选择更重要。"不要自己实现密码学"的例外是教学和理解。 本节和前几节通过从零实现让读者理解每个原语"为什么安全"。但在生产系统中,应使用经过数十亿次交易验证的库:libsecp256k1、OpenSSL、libsodium。
下一预告 :2.9 将整合全部密码学知识,构建本章的完整知识地图 和常见错误速查表 ,并设置一个动手实验:从零构建一个"密码学工具箱"验证库。
2.9 知识地图、概念关系与常见错误速查
2.1–2.8 从零实现了每个密码学原语的算法细节。本节跳出具体实现,从知识架构 视角构建第2章的完整概念网络,并建立工程速查表 ,帮助你在遇到实际问题时快速定位需要的工具。
2.9.1 第2章完整知识地图
graph TB
subgraph 基础原语_Foundation
HASH["哈希函数<br/>2.1, 2.2"]
HASH --> CR["碰撞阻力"]
HASH --> HID["隐藏性"]
HASH --> PF["谜题友好性"]
HASH --> SHA["SHA-256<br/>MD 结构"]
HASH --> KEC["Keccak-256<br/>海绵结构"]
end
subgraph 非对称密码_Asymmetric
ECC["椭圆曲线<br/>2.4"]
ECC --> SECP[secp256k1<br/>y² = x³ + 7]
ECC --> PADD["点加法"]
ECC --> SCM["标量乘法"]
ECC --> ECDLP["离散对数难题"]
SIG["数字签名<br/>2.5"]
SIG --> ECDSA[ECDSA: r, s]
SIG --> SCH["Schnorr: 线性可聚合"]
SIG --> KUNIQ["k 唯一性"]
ECDSA --> V["验证: s⁻¹·(e·G + r·P)"]
SCH --> MUSIG["MuSig2 多签"]
end
subgraph 密钥管理_Management
ENT["密码学熵<br/>2.6"]
ENT --> CSPRNG[CSPRNG]
CSPRNG --> BIP39["BIP-39 助记词"]
BIP39 --> BIP32["BIP-32 HD 钱包"]
BIP32 --> BIP44["BIP-44 路径"]
BIP32 --> ADDR["地址派生"]
end
subgraph 应用结构_Application
MT["Merkle 树<br/>2.7"]
MT --> PROOF["O(log N) 证明"]
MT --> SPV["SPV 轻量验证"]
MT --> MPT["Merkle Patricia<br/>Trie 以太坊"]
end
subgraph 安全工程_Security
COMP["原语对比<br/>2.8"]
COMP --> Q["后量子威胁"]
COMP --> ERR["十大常见错误"]
end
HASH --> MT
ECC --> SIG
BIP32 --> SIG
2.9.2 核心公式速查表
------ ---------- ----------
哈希碰撞概率 P ≈ 1 − e − k 2 / 2 n + 1 P \approx 1 - e^{-k^2 / 2^{n+1}} P ≈ 1 − e − k 2 / 2 n + 1 02.01
密码学承诺 c = H ( r ∥ v ) c = H(r \parallel v) c = H ( r ∥ v ) ,绑定+隐藏02.01
SHA-256 压缩 a , b , c , d , e , f , g , h a,b,c,d,e,f,g,h a , b , c , d , e , f , g , h 经 64 轮非线性更新02.02
Keccak 海绵 吸收阶段(异或+置换)+ 挤压阶段 02.02
模逆元 a − 1 ≡ a p − 2 ( m o d p ) a^{-1} \equiv a^{p-2} \pmod p a − 1 ≡ a p − 2 ( mod p ) (Fermat)02.04
点加法 λ = y 2 − y 1 x 2 − x 1 \lambda = \frac{y_2-y_1}{x_2-x_1} λ = x 2 − x 1 y 2 − y 1 ,x 3 = λ 2 − x 1 − x 2 x_3 = \lambda^2 - x_1 - x_2 x 3 = λ 2 − x 1 − x 2 02.04
标量乘法 二进制 Double-and-Add,O ( log k ) O(\log k) O ( log k ) 02.04
ECDSA 签名 s = k − 1 ( e + r d ) s = k^{-1}(e + rd) s = k − 1 ( e + r d ) ,r = x ( k G ) r = x(kG) r = x ( k G ) 02.05
ECDSA 验证 u 1 G + u 2 P = R ′ u_1 G + u_2 P = R' u 1 G + u 2 P = R ′ ,验证 x ( R ′ ) ≡ r x(R') \equiv r x ( R ′ ) ≡ r 02.05
固定 k 攻击 k = ( e 1 − e 2 ) ( s 1 − s 2 ) − 1 k = (e_1-e_2)(s_1-s_2)^{-1} k = ( e 1 − e 2 ) ( s 1 − s 2 ) − 1 ,恢复 d d d 02.05
BIP-39 助记词 熵 + SHA-256 校验和 → 每 11 位一个词 02.06
BIP-32 子密钥 I = H M A C ( " B i t c o i n s e e d " , seed ) I = HMAC("Bitcoin seed", \text{seed}) I = H M A C ( " B i t co in see d " , seed ) ,CKD 函数02.06
Merkle 证明大小 O ( log N ) O(\log N) O ( log N ) 个兄弟哈希02.07
Merkle 验证 沿路径逐层哈希,比对根值 02.07
2.9.3 常见错误诊断矩阵
---------- ------ ------ ----------
相同消息两次签名结果不同 正常(应有不同 k k k ) 02.05 使用 RFC 6979 确定性签名可复现
相同消息两次签名 r r r 相同 致命 :k k k 重复02.05 立即更换密钥,使用 HMAC-SHA256 生成 k k k
签名解析失败/长度可变 DER 编码问题 02.05 采用 BIP-66 严格 DER,或切换到 64 字节固定(Schnorr)
助记词校验失败 抄写错误或词表不匹配 02.06 重抄确认,使用标准 BIP-39 英文词表
派生地址与钱包不一致 路径/ hardened 索引错误 02.06 核对 m/44'/60'/0'/0/0 等完整路径
验证通过但交易被拒绝 链 ID/网络不匹配 02.08 在消息中包含链 ID 防止重放
哈希值与标准库输出不同 编码/字节序/填充差异 02.02 严格规定大端序、UTF-8、无 BOM
公钥验证通过但地址错误 哈希函数选错 02.06 比特币用 RIPEMD160(SHA256),以太坊用 Keccak-256
加密后还"能看懂" 流密码重复使用密钥流 02.03 流密码 nonce 不可重复(CTR 模式)
性能突然下降 1000 倍 使用非对称加密做大量数据加密 02.03 混合加密:非对称分发密钥 + 对称加密数据
2.9.4 密码学决策速查
"我该用什么哈希?"
需要 128 位安全 + 兼容性 → SHA-256 需要防长度扩展 + 新设计 → Keccak-256 需要密码存储 → Argon2 / bcrypt (不是 MD5/SHA-1)
"我该用什么签名?"
比特币传统 / 公钥恢复 → ECDSA 需要多签/批验证/新系统 → Schnorr (BIP-340) + MuSig2 需要量子安全(长期)→ SPHINCS+ / Dilithium (实验阶段)
"我该用什么加密?"
数据保密(大量数据)→ AES-256-GCM 密钥交换 → ECDH (Diffie-Hellman 在椭圆曲线上) 最高安全需求 → ChaCha20-Poly1305 (侧信道抗性优于 AES)
"我该用什么钱包?"
日常小额 → 软件热钱包 (方便,接受一定风险) 大额长期存储 → 硬件钱包 + 助记词金属备份 (物理隔离) 机构级 → Shamir 分片 + 多地离线存储 (消除单点)
2.9.5 数学安全强度对照
安全级别 (位) 暴力破解难度 适用场景
─────────────────────────────────────────────────────────
64 位 小时级 (GPU 农场) ❌ 玩具/测试
80 位 月级 (小国家) ❌ 过渡,寿命<5年
112 位 年级 (大国级算力) ⚠️ 短期敏感数据
128 位 世纪级 (全球算力) ✅ 当前标准(加密货币)
192 位 宇宙级 ✅ 后量子预备(State of the Art)
256 位 物理不可能 ✅ 国家机密级
核心结论 :当前区块链使用的 128 位安全级别(如 secp256k1 提供约 128 位,因为最好攻击是 Pollard's Rho,约 n ≈ 2 128 \sqrt{n} \approx 2^{128} n ≈ 2 128 次运算)在全球现有和可预见的计算能力下是计算不可行的 。量子计算机需要约 4000 个逻辑量子比特才能威胁 ECC,当前最大公开进展约数百物理量子比特(纠错后更少),预计 10–20 年内不会构成实际威胁。
2.9.6 延伸阅读与资源
下一节 :2.10 动手实验——从零构建一个密码学验证工具箱,将前面所有代码整合为可运行的验证库。
2.10 动手实验:密码学工具箱构建
本节将所有密码学原语整合为一个可运行的验证工具箱 。通过亲手组装这些组件,你将建立从"知道实现"到"能用密码学解决实际问题"的跨越。
2.10.1 工具箱架构
graph TD
Tool[Crypto Toolset]
Tool --> H["哈希层<br/>SHA-256 / Keccak-256"]
Tool --> ECC["ECC 层<br/>secp256k1 点运算"]
Tool --> K["密钥层<br/>BIP-39 助记词 + BIP-32 HD"]
Tool --> S["签名层<br/>ECDSA / Schnorr"]
Tool --> C["承诺层<br/>密码学承诺"]
H --> ECC
ECC --> K
K --> S
S --> C
style Tool fill:#bbdefb
style S fill:#c8e6c9
┌─────────────────────────────────────────────┐
│ 密码学工具箱 (Crypto Toolset) │
├─────────────────────────────────────────────┤
│ [哈希层] │
│ - SHA-256 从零实现 │
│ - 雪崩效应测试器 │
│ - 碰撞概率计算器 │
├─────────────────────────────────────────────┤
│ [椭圆曲线层] │
│ - secp256k1 点运算 │
│ - 标量乘法 (私钥→公钥) │
│ - 公钥压缩/解压 │
├─────────────────────────────────────────────┤
│ [签名层] │
│ - ECDSA 签名/验证 │
│ - RFC 6979 确定性 k │
│ - 签名 malleability 检测 │
├─────────────────────────────────────────────┤
│ [钱包层] │
│ - 熵生成 → 助记词 → 种子 → 子密钥 │
│ - BIP-44 路径解析 │
├─────────────────────────────────────────────┤
│ [Merkle 层] │
│ - 构建 Merkle 树 │
│ - 生成/验证 Merkle 证明 │
│ - 批量验证优化 │
└─────────────────────────────────────────────┘
2.10.2 完整整合代码
// =====================================================
// 密码学工具箱整合实现
// 所有底层实现复用 2.1-2.7 节代码,此处展示整合调用
// =====================================================
/**
* 工具箱:密码学验证套件
*/
class CryptoToolkit {
// --- 1. 哈希层 ---
sha256(message: string): string {
return sha256(message); // 复用 2.2 节完整实现
}
/**
* 雪崩效应测试:单 bit 变化导致多少输出位变化
*/
avalancheTest(input: string, rounds: number = 20): {
avgDistance: number;
avgRate: number;
results: number[];
} {
const baseHash = this.sha256(input);
const results: number[] = [];
for (let i = 0; i < rounds; i++) {
const modified = this._flipRandomBit(input);
const modifiedHash = this.sha256(modified);
results.push(this._hammingDistance(baseHash, modifiedHash));
}
const avgDist = results.reduce((a, b) => a + b, 0) / results.length;
return {
avgDistance: avgDist,
avgRate: (avgDist / 256) * 100,
results,
};
}
// --- 2. 密钥对生成层 ---
generateKeyPair(): { privateKey: bigint; publicKey: ECPoint } {
const d = generatePrivateKey(); // CSPRNG 生成
const pub = scalarMultiply(d, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
return { privateKey: d, publicKey: pub };
}
/**
* 公钥压缩:65 字节 → 33 字节
*/
compressPublicKey(pubKey: ECPoint): Uint8Array {
const isEven = (pubKey.y! % 2n) === 0n;
const prefix = isEven ? 0x02 : 0x03;
const bytes = new Uint8Array(33);
bytes[0] = prefix;
// ... 将 x 写入后 32 字节
return bytes;
}
// --- 3. 签名层 ---
sign(privateKey: bigint, message: string): { r: bigint; s: bigint } {
return ecdsaSign(privateKey, message); // 复用 2.5 节实现
}
verify(
publicKey: ECPoint,
message: string,
signature: { r: bigint; s: bigint },
): boolean {
return ecdsaVerify(publicKey, message, signature); // 复用 2.5 节实现
}
/**
* 检测签名可锻性(malleability)
* 如果 s > n/2,攻击者可用 n-s 构造等价但不同的签名
*/
isSignatureMalleable(signature: { r: bigint; s: bigint }): boolean {
return signature.s > SECP256K1.n / 2n;
}
normalizeSignature(signature: { r: bigint; s: bigint }): { r: bigint; s: bigint } {
if (this.isSignatureMalleable(signature)) {
return { r: signature.r, s: SECP256K1.n - signature.s };
}
return signature;
}
// --- 4. Merkle 层 ---
buildMerkleTree(txHashes: Uint8Array[]): MerkleTree {
return new MerkleTree(txHashes); // 复用 2.7 节实现
}
// --- 私有辅助 ---
private _flipRandomBit(s: string): string {
const chars = s.split('');
const pos = Math.floor(Math.random() * s.length);
const bit = Math.floor(Math.random() * 8);
const code = s.charCodeAt(pos);
chars[pos] = String.fromCharCode(code ^ (1 << bit));
return chars.join('');
}
private _hammingDistance(a: string, b: string): number {
let dist = 0;
for (let i = 0; i < a.length && i < b.length; i++) {
const x = parseInt(a[i], 16);
const y = parseInt(b[i], 16);
let diff = x ^ y;
while (diff) { dist++; diff &= diff - 1; }
}
return dist;
}
}
// =====================================================
// 验证测试套件
// =====================================================
function runTests() {
const kit = new CryptoToolkit();
console.log("╔════════════════════════════════════════╗");
console.log("║ 密码学工具箱验证套件 ║");
console.log("╚════════════════════════════════════════╝\n");
// Test 1: 哈希雪崩效应
console.log("[Test 1] 雪崩效应测试");
const avalanche = kit.avalancheTest("Blockchain Crypto Toolkit v1.0", 10);
console.log(` 平均翻转: ${avalanche.avgRate.toFixed(1)}% (目标: 50%)`);
console.log(` 结果: ${avalanche.avgRate > 45 && avalanche.avgRate < 55 ? '✅ 通过' : '❌ 异常'}`);
// Test 2: 密钥对生成
console.log("\n[Test 2] 密钥对生成与验证");
const { privateKey, publicKey } = kit.generateKeyPair();
console.log(` 私钥位数: ${privateKey.toString(2).length} (目标: 256)`);
console.log(` 公钥有效性: ${isValidCurvePoint(publicKey, 0n, 7n, SECP256K1.p) ? '✅ 在曲线上' : '❌ 无效'}`);
// Test 3: ECDSA 全流程
console.log("\n[Test 3] ECDSA 签名验证循环");
const msg = "Transfer 1.0 BTC to 1A1z...";
const sig = kit.sign(privateKey, msg);
const valid = kit.verify(publicKey, msg, sig);
console.log(` 原始消息: ${valid ? '✅ 验证通过' : '❌ 失败'}`);
const tampered = kit.verify(publicKey, msg + "_tampered", sig);
console.log(` 篡改消息: ${tampered ? '❌ 未检测出篡改' : '✅ 正确拒绝'}`);
// Test 4: 签名规范化
console.log("\n[Test 4] 签名可锻性检测");
const highS = { r: sig.r, s: SECP256K1.n - sig.s };
console.log(` 高 s 值: ${kit.isSignatureMalleable(highS) ? '✅ 检测到可锻性' : '❌ 未检测'}`);
const normalized = kit.normalizeSignature(highS);
console.log(` 规范化后: ${kit.isSignatureMalleable(normalized) ? '❌ 仍有问题' : '✅ 安全'}`);
console.log(` 等价验证: ${kit.verify(publicKey, msg, normalized) ? '✅ 仍有效' : '❌ 被破坏'}`);
// Test 5: Merkle 证明
console.log("\n[Test 5] Merkle 树构建与验证");
const txData = Array.from({ length: 8 }, (_, i) => `tx_${i}_data`);
const txHashes = txData.map(tx => {
const hex = kit.sha256(tx);
const bytes = new Uint8Array(32);
for (let j = 0; j < 32; j++) bytes[j] = parseInt(hex.slice(j * 2, j * 2 + 2), 16);
return bytes;
});
const tree = kit.buildMerkleTree(txHashes);
const root = tree.getRoot();
if (root) {
console.log(` 树根: ${Array.from(root).slice(0, 4).map(b => b.toString(16).padStart(2, '0')).join('')}...`);
const proof = tree.getProof(3);
const verified = verifyMerkleProof(txHashes[3], proof, root, 3);
console.log(` 证明大小: p r o o f . l e n g t h 个哈希 × 32 字节 = {proof.length} 个哈希 × 32 字节 = p r oo f . l e n g t h 个哈希 × 32 字节 = {proof.length * 32} 字节`);
console.log(` 验证结果: ${verified ? '✅ 通过' : '❌ 失败'}`);
}
console.log("\n╔════════════════════════════════════════╗");
console.log("║ 全部测试完成 ║");
console.log("╚════════════════════════════════════════╝");
}
// 执行测试
runTests();
2.10.3 扩展挑战
完成基础工具箱后,以下扩展项目可以加深理解:
挑战 1:实现 RFC 6979 确定性 k k k
替代随机 k k k ,实现基于 HMAC-SHA256 的确定性签名:
k = HMAC-SHA256 ( key = d , data = e ∥ 0 x 00 ) k = \text{HMAC\text{-}SHA256}(\text{key} = d, \text{data} = e \parallel 0x00) k = HMAC - SHA256 ( key = d , data = e ∥ 0 x 00 )
验证:同一 ( d , m ) (d, m) ( d , m ) 总是产生相同的 ( r , s ) (r, s) ( r , s ) ,且 Sony 攻击不再可能。
挑战 2:Schnorr 签名实现
实现 BIP-340 的 Schnorr 签名:
R = k G , e = H ( R ∥ P ∥ m ) , s = k + e d , 签名 = ( R , s ) R = kG, \quad e = H(R \parallel P \parallel m), \quad s = k + ed, \quad \text{签名} = (R, s) R = k G , e = H ( R ∥ P ∥ m ) , s = k + e d , 签名 = ( R , s )
验证等式:s G = ? R + e P sG \stackrel{?}{=} R + eP s G = ? R + e P 。
比较:签名大小降为 64 字节(R R R 和 s s s 各 32 字节,不需要编码 r r r 和 s s s ),无 DER 编码复杂性。
挑战 3:批量验证优化
Schnorr 的核心优势——线性 ——允许批量验证多个签名:
∑ i c i s i G = ? ∑ i c i R i + ∑ i c i e i P i \sum_i c_i s_i G \stackrel{?}{=} \sum_i c_i R_i + \sum_i c_i e_i P_i i ∑ c i s i G = ? i ∑ c i R i + i ∑ c i e i P i
其中 c i c_i c i 是随机挑战系数。将 n n n 次独立验证的 n × G n \times G n × G 运算合并为少量群运算。
挑战 4:Patricia Trie 简化实现
实现一个键值存储的 Patricia Trie(16 进制前缀压缩),并扩展为 Merkle Patricia Trie——在每次修改后重新计算到根的路径哈希。这是以太坊状态树的简化模型。
2.10.4 最佳安全实践清单
[ ] 所有随机数使用 crypto.getRandomValues 或 crypto.randomBytes [ ] ECDSA 使用 RFC 6979 确定性 k k k 或 CSPRNG + 防重放机制 [ ] 所有外部输入的公钥在使用前验证曲线方程 [ ] 签名后检查 s ≤ n / 2 s \leq n/2 s ≤ n /2 ,拒绝高 s 可锻性签名 [ ] 助记词不在任何联网设备存储 [ ] HD 钱包使用 BIP-44 标准路径,记录 hardened 索引 [ ] 消息签名前包含协议标识/链 ID 防止跨链重放 [ ] 密钥存储使用 AES-256-GCM 或 ChaCha20-Poly1305
本章小结 :从 SHA-256 的 64 轮压缩函数,到 secp256k1 的标量乘法,到 ECDSA 的灵魂 k k k ,到 BIP-39 的 12 个记忆词——每个密码学原语都是"信任机器"的基石。理解它们的数学、实现和安全边界,是理解区块链"为什么可信"的必经之路。
第2章 总结:密码学——区块链的信任基石
2.11 核心概念知识地图
graph TB
subgraph 第2章_密码学基础
direction TB
HASH["哈希函数<br/>2.1-2.2"] --> SEC["安全属性<br/>碰撞/隐藏/谜题友好"]
HASH --> SHA["SHA-256<br/>MD 结构"]
HASH --> KEC["Keccak-256<br/>海绵结构"]
SYM["对称加密"] --> AES[AES-256-CTR]
SYM --> CSPRNG["CSPRNG 熵源"]
ECC["椭圆曲线<br/>2.4"] --> SECP[secp256k1<br/>y²=x³+7]
ECC --> PADD["点加法/倍点"]
ECC --> SCM["标量乘法<br/>d×G=P"]
ECC --> ECDLP["离散对数难题"]
SIG["数字签名<br/>2.5"] --> ECDSA[ECDSA: r,s]
SIG --> SCHNORR["Schnorr: 可聚合"]
SIG --> K["k 唯一性
Sony 事件"]
WALLET["钱包<br/>2.6"] --> BIP39["BIP-39 助记词"]
BIP39 --> BIP32["BIP-32 HD 派生"]
BIP32 --> BIP44["BIP-44 路径"]
MT["Merkle 树<br/>2.7"] --> PROOF["O(log N) 证明"]
MT --> SPV["SPV 轻量验证"]
MT --> COMP["成员证明压缩"]
SYS["系统对比<br/>2.8"] --> CRYO["后量子安全分析"]
SYS --> ERR["十大工程错误"]
end
subgraph 与第1章_比特币篇的连接
TX["交易哈希"] -.-> HASH
ADDR["地址=公钥哈希"] -.-> ECC
SCRIPT["脚本签名验证"] -.-> SIG
SPVB["轻量节点"] -.-> MT
end
subgraph 向第3章_共识层的前瞻
SIG -.-> POW["工作量证明
挖矿签名"]
HASH -.-> DIFF["难度调整
谜题友好性"]
MT -.-> BLOCK["区块头 Merkle 根"]
end
2.12 密码学原语对比总表
原语 数学基础 输入→输出 核心安全假设 区块链角色 失效后果
------ ---------- ----------- ------------ ----------- ----------
SHA-256 位运算 + 模加 任意 → 256 bit 碰撞计算不可行 2 128 2^{128} 2 128 交易 ID、区块哈希、双重哈希 碰撞 → 伪造交易
Keccak-256 海绵结构 + f f f 置换 任意 → 256 bit 置换不可区分 以太坊地址、状态哈希 碰撞 → 地址碰撞
secp256k1 标量乘法 椭圆曲线群运算 私钥 d d d → 公钥 P P P ECDLP 困难 密钥对生成 离散对数破解 → 私钥暴露
ECDSA 模逆 + 曲线点加 ( d , m ) → ( r , s ) (d, m) \to (r, s) ( d , m ) → ( r , s ) k k k 唯一性 + ECDLP交易授权 k k k 重放 → 私钥泄露
Schnorr 线性方程 ( d , m ) → ( R , s ) (d, m) \to (R, s) ( d , m ) → ( R , s ) 离散对数 多签/聚合 (BIP-340) 未标准化实现风险
Merkle 树 二叉哈希树 N N N 叶节点 → 1 根哈希防篡改 SPV 轻验证 根伪造 → 假交易被"确认"
BIP-32 HD HMAC-SHA512 种子 → 无限子密钥 单向性/不可预测 地址批量管理 主种子泄露 → 所有地址暴露
2.13 第2章 → 第1章/第3章的知识衔接
与第1章的呼应
第1章介绍了比特币的交易模型和 UTXO 设计,但一直悬而未决的问题是:"为什么交易可以被信任?" 第2章的答案如下:
---------- --------------- ----------
UTXO 所有权 椭圆曲线标量乘法 "拥有 UTXO" = "知道控制该 UTXO 地址对应的私钥 d d d "
交易广播 ECDSA 数字签名 交易被私钥签名后广播,任何人可用公钥验证
PoW 谜题 哈希的谜题友好性 + 雪崩效应 nonce 搜索 = 穷举 H ( header ) < T H(\text{header}) < T H ( header ) < T ,无捷径可寻
区块链接 双重 SHA-256 哈希链 prevBlockHash 依赖前一区块全部内容的哈希
轻量节点 Merkle 树 + SPV 轻节点仅存储 80 字节区块头 + 320 字节证明
向第3章的铺垫
第3章将探讨共识机制 (PoW、PoS、BFT),而共识机制之所以能"工作",根本原因在于第2章的密码学保证:
PoW 的防作弊 :如果哈希不具有谜题友好性,矿工可以通过捷径绕过工作量计算。最长链规则 :改变历史区块需要重新计算所有后续区块的 PoW(因为每个区块头包含前一区块的哈希)——这在计算上不可行。交易不可伪造 :没有私钥 = 无法产生有效签名 = 无法花费他人的 UTXO。轻客户端信任 :轻节点信任区块头(通过工作量证明验证其难度),再通过 Merkle 证明验证交易——两者共同构成"无需信任全节点"的安全模型。
第2章密码学 ──→ 第3章共识的根本假设
│
├── 哈希(谜题友好) → PoW 的公平性
├── 哈希链 → 历史不可篡改
├── 数字签名 → 交易不可伪造
└── Merkle 树 → 轻节点的验证能力
2.14 关键思维模型总结
模型一:信任链的数学化
区块链用密码学将"信任问题"转化为可验证的数学问题 。不需要相信任何中介,只需要相信:
哈希的碰撞阻力和谜题友好性 椭圆曲线离散对数的困难性 这些假设在数学上经过数十年研究
模型二:安全强度的统一度量
所有密码学强度都可以用"暴力破解需要多少次尝试" 来度量:
128 位安全 = 2 128 2^{128} 2 128 次尝试 全球所有计算设备每秒 10 18 10^{18} 1 0 18 次运算 2 128 / 10 18 / ( 365 × 24 × 3600 ) ≈ 10 21 2^{128} / 10^{18} / (365 \times 24 \times 3600) \approx 10^{21} 2 128 /1 0 18 / ( 365 × 24 × 3600 ) ≈ 1 0 21 年
模型三:安全不是功能,而是设计属性
Sony PS3 使用正确的 ECDSA 算法——但因 k k k 的管理错误而泄露私钥。这揭示了一个核心原则:安全不是"功能正确"的附赠品,而是需要独立验证的设计属性。
2.15 本章学习路径建议
---------- ---------- --------
密码学初学者 2.1(安全属性概念)、2.3(体系分类)、2.6(钱包实践) 2.2(完整实现)、2.4(点运算细节)、2.5(验签推导)
应用开发者 2.1-2.3、2.5(签名安全)、2.6(钱包集成)、2.7(Merkle SPV) 2.4(点运算可从库调用)
协议/安全工程师 全部 + 验证 2.2 和 2.4 的实现与标准库输出对比 无
快速复习 2.9(速查)、2.10(工具箱)、本总结 2.2-2.8 按需追溯
2.15 前向阅读指南
第3章(共识机制) :需要理解 2.5.4(谜题友好性)和 2.1.4(谜题友好性与 PoW 的关系)。第4章(智能合约) :需要理解 2.4(公钥生成地址)和 2.5(签名授权)——所有合约调用本质上是「地址 + 签名」的封装。第5章(扩展性) :需要理解 2.7(Merkle 树)——状态通道、Rollup 的 Merkle 根验证是核心机制。
本章终点,亦是起点。 你已经拥有了理解区块链"为什么安全"的全部密码学工具。下一章,我们将用这些工具去分析"如何让分散的节点在没有指挥官的情况下达成一致"——这是区块链从密码学系统升华为社会共识机制的关键跃迁。
评论
0评论加载中…