教程区块链区块链技术第2章 密码学基础

本页目录

区块链一切安全承诺的数学基础。本章讲透哈希函数、对称/非对称密码、椭圆曲线 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}nH: \{0,1\}^* \to \{0,1\}^n

它将任意有限长度的二进制输入 {0,1}\{0,1\}^* 映射为固定长度nn 位输出。nn 称为摘要长度哈希位数,常见的取值有 256(SHA-256、Keccak-256)、384(SHA-384)、512(SHA-512)。

这一定义中有四个关键约束,缺一不可:

属性精确表述失效后果
--------------------------
确定性x:H(x)\forall x: H(x) 唯一确定同一输入产生不同输出导致验证失败
压缩性输出长度固定为 nn,与输入长度无关无法用于摘要
高效计算对任意 xxH(x)H(x) 可在多项式时间内计算系统吞吐量为零
原像抗性(单向性)给定 y=H(x)y = H(x),在计算上不可行地找到任意 x=H(x)=yx' = H(x') = y哈希可被"逆转",承诺机制失效

关键区分:"确定性"与"随机性"并不矛盾。HH 本身是确定性的函数,但对于攻击者来说,在没有额外信息时,H(x)H(x) 的行为必须如同随机函数(无法预测、无法逆推)。这就是密码学对"单向性"的严格表述。

2.1.2 碰撞阻力(Collision Resistance)

形式化定义

λ\lambda 为安全参数(通常 λ=128\lambda = 128),如果对于任何概率多项式时间(PPT)敌手 A\mathcal{A}

Pr[(x1,x2)A(1λ):x1x2H(x1)=H(x2)]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)

其中 negl(λ)\text{negl}(\lambda) 是一个关于 λ\lambda 的可忽略函数(比任何多项式的倒数下降得更快)。则称 HH 具有碰撞阻力

为什么碰撞阻力如此关键?

在数字签名中,我们不是对消息 mm 本身签名(因为 mm 可能很长),而是对 H(m)H(m) 签名。如果攻击者能找到 m1m2m_1 \neq m_2 使得 H(m1)=H(m2)H(m_1) = H(m_2),那么:

  1. Alice 对 H(m1)H(m_1) 签名(她认可 m1m_1)。
  2. 攻击者截获签名,与 m2m_2 组合。
  3. Bob 验证:H(m2)=H(m1)H(m_2) = H(m_1),签名有效,但消息内容却变成了 m2m_2

结论:签名伪造,信任体系崩塌。

生日攻击(Birthday Attack)与安全强度

寻找碰撞的直观暴力方法是:尝试随机输入 x1,x2,x_1, x_2, \ldots,记录所有 H(xi)H(x_i),直到发现重复。但这需要大约 2n2^n 次尝试——远比实际需要多。

生日悖论(Birthday Paradox) 告诉我们:在一个 N=2nN = 2^n 大小的输出空间中,只需约 k2Nln21.177N2n/2k \approx \sqrt{2N \ln 2} \approx 1.177\sqrt{N} \approx 2^{n/2} 次尝试,就能以 50% 概率找到碰撞。

推导:设 kk 次独立采样,没有碰撞的概率为:

P(no collision)=i=0k1(1i2n)ei=0k1i2nek(k1)22nek22n+1P(\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}}}

k=2n/2k = 2^{n/2} 时,Pe1/20.607P \approx e^{-1/2} \approx 0.607;碰撞概率 =10.607=0.393= 1 - 0.607 = 0.393(约 39%)。当 k1.1772n/2k \approx 1.177 \cdot 2^{n/2} 时,碰撞概率恰好为 50%。

哈希算法输出长度 nn碰撞安全(n/2n/2生日攻击尝试次数(约)
--------------------------------------------------------------
SHA-256256 bit128 bit21283.4×10382^{128} \approx 3.4 \times 10^{38}
SHA-1160 bit80 bit2801.2×10242^{80} \approx 1.2 \times 10^{24}
MD5128 bit64 bit2641.8×10192^{64} \approx 1.8 \times 10^{19}(实际已被攻破)

实际案例:2017 年 Google 与 CWI 联合宣布了对 SHA-1 的实际碰撞攻击,用 2632^{63} 次计算找到了 PDF 碰撞。现代密码学标准已废弃 SHA-1,MD5 更已被完全攻破。

TypeScript:生日攻击概率计算器

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} 人中至少一对同生日的概率:{birthdayClassic(k).toFixed(4)}`);

});

预期输出

text

=== 生日攻击概率分析 ===

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

关键结论21282^{128} 次尝试(约等于全球所有沙粒的 101810^{18} 倍)在 SHA-256 上才有 ~39% 的碰撞概率。这在当前及可预见的计算能力下是计算不可行的。

2.1.3 隐藏性(Hiding)与承诺机制

形式化定义

隐藏性要求:给定 y=H(x)y = H(x),对于未知且均匀分布的 xx,任何 PPT 敌手在统计学意义上无法推断 xx 的任何部分信息。更严格地说,H(x)H(x) 必须"看起来"与随机函数不可区分。

注意:如果 xx 的分布是有偏的(例如 xx 是从一个极小的集合中选择的),仅靠 H(x)H(x) 的隐藏性不足以保证安全。此时需要引入盐值(Salt)c=H(rx)c = H(r \parallel x),其中 rr 是均匀随机数。

承诺机制(Commitment Scheme)

承诺机制是密码学的核心原语,应用于密封拍卖、区块链时间锁定交易、零知识证明等场景。它是一个两阶段协议:

  1. 承诺阶段(Commit Phase):承诺方选择值 vv,生成随机数 r{0,1}λr \gets \{0,1\}^\lambda,计算 c=H(rv)c = H(r \parallel v),将 cc 发送给验证方。
  2. 揭晓阶段(Reveal Phase):承诺方公开 (r,v)(r, v)。验证方验证 c=?H(rv)c \stackrel{?}{=} H(r \parallel v)

该机制需要同时满足两个属性:

属性保证依赖的哈希属性
---------------------------
绑定性(Binding)承诺方不能找到一个 vvv' \neq v 使得 H(rv)=cH(r \parallel v') = c碰撞阻力
隐藏性(Hiding)验证方在揭晓前无法推断 vv单向性 + 加盐随机性

TypeScript:密码学承诺机制实现

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=reveal!.salt,value={reveal!.salt}, value={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)

形式化定义

HH 为哈希函数,T{0,1}nT \subseteq \{0,1\}^n 为一个"谜题目标集"(如"前 dd 位为 0 的所有输出"),T2n|T| \ll 2^n。如果对于任何输入分布,不存在比穷举搜索(从 {0,1}m\{0,1\}^m 中随机选取 xx 计算 H(x)H(x))更高效的方法找到满足 H(x)TH(x) \in Txx,则称 HH 具有谜题友好性

与工作量证明的关系

比特币的 PoW 谜题可以精确表述为:

\text{找 } \text{nonce} \in \{0,1\}^{32} \text{ 使得 } H(\text{block_header} \parallel \text{nonce}) < T

其中 TT 是难度目标(一个远小于 22562^{256} 的数值)。满足条件的 expected 尝试次数为:

2256T=难度值(Difficulty)\frac{2^{256}}{T} = \text{难度值(Difficulty)}

如果 HH 不满足谜题友好性——即存在某种结构可以指导搜索(例如知道某些 nonce 范围更可能产生小输出),则矿工可以用远少于 "预期尝试次数" 的算力找到解,PoW 的安全性假设就崩塌了。

均匀分布假设

谜题友好性等价于要求:当输入 xx 是均匀随机的,H(x)H(x) 必须在输出空间中均匀分布。 没有任何区域被"偏爱"或"避免"。这是对密码学哈希的统计学检验标准之一。

性质核心含义应用场景
--------------------------
碰撞阻力找不到两个不同输入产生相同输出数字签名、数据完整性
原像抗性给定输出,找不到任何能生成它的输入密码存储、承诺隐藏
第二原像抗性给定 xx,找不到 xxx' \neq x 使得 H(x)=H(x)H(x) = H(x')防篡改保护
谜题友好性唯一策略是均匀穷举工作量证明、验证码

下节预告:2.2 将深入 SHA-256 和 Keccak-256 的内部结构,从零实现两种算法的完整流程,并通过雪崩效应(Avalanche Effect)实验验证"输入 1 位变化导致输出约 50% 位翻转"这一理想特性。

核心认知

  1. 哈希函数是密码学的瑞士军刀,碰撞阻力、隐藏性、谜题友好性分别从"防伪造""防泄露""防捷径"三个维度构建安全性。
  2. 安全强度由输出长度的一半决定。SHA-256 提供 128 位碰撞安全——不是 256 位,这是生日攻击的必然结果。
  3. 承诺机制是密码学中最优雅的协议之一。两阶段(承诺+揭晓)、两属性(绑定+隐藏),直接对应现实生活中的"密封信封"。
  4. 谜题友好性 = 均匀分布 + 无捷径。这是 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 摘 要。

text

消息 M

  └─ 填充 → 512-bit 对齐的消息 M' = M₁ || M₂ || ... || Mₙ

      ├─ M₁ ──→ [H₀=IV] + 压缩函数 ──→ H₁

      ├─ M₂ ──→ [H₁] + 压缩函数 ──→ H₂

      └─ Mₙ ──→ [Hₙ₋₁] + 压缩函数 ──→ Hₙ = 最终哈希

步骤一:消息填充(Padding)

SHA-256 的填充规则(Merkle-Damgård 标准填充):

  1. 在消息末尾追加一个 1 位(即 0x80 字节)。
  2. 追加 kk0 位,使得 len(message)+1+k+640(mod512)\text{len(message)} + 1 + k + 64 \equiv 0 \pmod{512}
  3. 追加原始消息长度的 64 位大端序二进制表示。
typescript

/**

 * 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)M|M| 后,可以计算 H(MpaddingX)H(M \parallel \text{padding} \parallel X) 而不需要知道 MM 本身。

步骤二:压缩函数核心逻辑(TypeScript 完整实现)

SHA-256 的压缩函数将 256-bit 的内部状态(8 个 32-bit 寄存器 A,B,C,D,E,F,G,HA, B, C, D, E, F, G, H)与 512-bit 的消息块结合,通过 64 轮非线性变换,更新内部状态。

typescript

// 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)(xy)(¬xz)(x \land y) \oplus (\lnot x \land z)条件函数:xx 为 1 选 yyxx 为 0 选 zz
Maj(x,y,z)(xy)(xz)(yz)(x \land y) \oplus (x \land z) \oplus (y \land z)多数投票:三个中至少两个为 1 时结果为 1
Σ₀(x)ROTR2(x)ROTR13(x)ROTR22(x)\text{ROTR}^2(x) \oplus \text{ROTR}^{13}(x) \oplus \text{ROTR}^{22}(x)高位扰动:让 xx 的高位充分混合
Σ₁(x)ROTR6(x)ROTR11(x)ROTR25(x)\text{ROTR}^6(x) \oplus \text{ROTR}^{11}(x) \oplus \text{ROTR}^{25}(x)低位扰动:让 xx 的低位充分混合

这些函数经过 64 轮迭代后,输入中任何 1 位的变化都会在全部 8 个寄存器中引起不可预测的连锁反应——这就是雪崩效应的微观机制。

SHA-256 的长度扩展攻击

SHA-256 作为 Merkle-Damgård 结构的典型代表,存在长度扩展攻击(Length Extension Attack)

攻击场景

  1. 已知 H(M)H(M)M|M|
  2. 攻击者(无需知道 MM 本身)可以计算 H(MpaddingX)H(M \parallel \text{padding} \parallel X),其中 XX 是攻击者控制的任意数据。
  3. 在一些 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))。双层哈希彻底阻断了长度扩展攻击。这也是所有安全协议使用HMAC而非简单。双层哈希彻底阻断了长度扩展攻击。这也是所有安全协议使用 HMAC 而非简单H(K \parallel m)$ 的原因。

2.2.2 Keccak-256:海绵结构

Keccak-256(以太坊选用的哈希函数)采用完全不同的设计哲学——海绵结构(Sponge Construction),天然免疫长度扩展攻击。

核心架构:吸收(Absorb)与挤压(Squeeze)

text

         ┌──────────────┐

消息块 1 ──→⊕──────┐   │

              │ f   ├──→⊕──────┐

消息块 2 ──→⊕──────┘   │      │

                        ...    │

消息块 n ──→⊕──────┐         │

              │ f   ├──→ 状态 ──→ 输出块 1

                     │         │     └──→ 额外输出块 ...

                     └─────────┘

                 [固定容量的内部状态]
  • 状态(State)5×5×64=16005 \times 5 \times 64 = 1600 bit(25 个 64-bit 字)。
  • 速率(Rate, rr:1088 bit(= 136 字节)——与消息块异或的部分。
  • 容量(Capacity, cc:512 bit——仅执行 ff 置换、不与消息直接交互的部分。
  • ff 置换:24 轮的非线性变换,每轮包含 θ,ρ,π,χ,ι\theta, \rho, \pi, \chi, \iota 五个步骤。

Keccak-256 核心实现(TypeScript 教学版)

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=512c = 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% 的概率翻转,且翻转位之间不应有统计相关性。

typescript

/**

 * 雪崩效应实验:测量 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:翻转1bitHamming距离={i + 1}: 翻转 1 bit → Hamming 距离 ={dist} / 256 (${(dist / 256 * 100).toFixed(1)}%)`);

}

console.log(`\n平均翻转率: ${(totalDist / 20 / 256 * 100).toFixed(1)}% (理想值: 50.0%)`);

预期输出

text

=== 雪崩效应实验 ===

基础哈希: 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%),说明某些输出位与某些输入位存在相关性,攻击者可以逐步构造碰撞,破坏碰撞阻力。

核心认知

  1. SHA-256 的 Merkle-Damgård 结构 = 迭代压缩。 消息被切分、填充后,依次通过 64 轮非线性压缩。内部 8 个 32-bit 寄存器的连锁更新,将 1-bit 输入差异放大为约 128-bit 输出差异(雪崩效应)。
  2. Keccak-256 的海绵结构 = 吸收 + 挤压。 消息与状态"速率"部分异或后,经过 24 轮 ff 置换,彻底混合。"容量"部分作为内部秘密,天然阻断了长度扩展攻击的路径。
  3. 雪崩效应不是偶然,而是设计目标。 64 轮的非线性逻辑函数(Ch, Maj, Σ₀, Σ₁)和 Keccak 的 χ\chi 非线性层,都是为了让 1 位输入变化以 50% 概率影响每一位输出。
  4. 长度扩展攻击暴露了 MD 结构的结构性弱点。 这不是实现错误,而是架构级别的设计后果。防御策略(HMAC、使用 SHA-3/Keccak)提醒我们:安全不是"功能正确"的附赠品,而是架构层面的设计目标。

下一预告:2.3 节将跳出"具体算法"层面,从系统分类角度审视密码学体系——对称加密 vs 非对称加密,以及一个关键澄清:区块链中"非对称加密"的真实含义是数字签名,而非消息加密。


2.3 密码学体系:对称加密、非对称加密与数字签名的角色

2.1 和 2.2 深入剖析了哈希函数的数学安全属性与两种核心实现。本节将视角从"单一原语"提升到"密码学体系"层面,系统澄清对称加密非对称加密的角色分工——尤其是区块链语境下"非对称密码学"的真实含义:它不是加密消息,而是数字签名

2.3.1 对称加密(Symmetric Encryption)

核心思想:一把钥匙开一把锁

Enck(m)=c,Deck(c)=m\text{Enc}_k(m) = c, \quad \text{Dec}_k(c) = m

加密和解密使用同一个密钥 kk。发送方和接收方必须在通信前以某种安全方式共享 kk。它的核心性质可以用信息论的一个基本问题来理解:如何用一个短密钥保护任意长度的消息?

流密码:XOR 的巧妙使用

最简单的流密码思想非常优雅:

ci=mikeystreamic_i = m_i \oplus \text{keystream}_i

其中 keystream\text{keystream} 是密钥 kk 通过一个伪随机数生成器(PRNG) 扩展得到的伪随机序列。只要 keystream 的长度与消息相同,就可以逐位异或。

安全性假设:密钥流对攻击者必须是"真正的随机"——即即使敌手拥有大量 (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 模式(计数器模式) 是最直观的一种:

ci=miEk(noncei)c_i = m_i \oplus E_k(\text{nonce} \parallel i)

将每个块序号 ii 和一个不可重复的随机初始化向量(nonce/IV)组合,用 EkE_k 加密,得到该块的密钥流,再与明文异或。

typescript

/**

 * 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)——一个函数正向计算容易,但逆向计算极难,除非拥有特定的"陷门信息"。

  • RSAf(x)=xemodNf(x) = x^e \mod N,其中 N=pqN = pqee 为公钥。逆向需知道 ppqq(即私钥 dd)。安全性基于大整数质因数分解难题。
  • ECCf(d)=d×G=Pf(d) = d \times G = P。正向是标量乘法(容易),逆向是离散对数(极难)。

消息加密场景

直观来说,非对称加密用于:

  1. Bob 生成密钥对,公开公钥 PKPK,保密私钥 SKSK
  2. Alice 用 PKPK 加密消息 mm 得到 cc
  3. Bob 用 SKSK 解密 cc 恢复 mm

但在区块链协议中,这个场景几乎不使用。原因如下:

2.3.3 关键澄清:区块链中的"非对称密码学"本质是数字签名

这是区块链密码学中最常被误解的一点。

为什么区块链不需要消息加密?

  1. 交易数据需要公开验证:如果交易被加密,如何验证它是否有效?
  2. 共识节点需要验证余额:如果账户余额是密文,节点无法执行共识规则。
  3. 区块链的透明度是设计意图:正是因为所有人都可以验证规则是否被遵循,去中心化才成立。

数字签名的核心角色

区块链中的非对称密码学只做一件事:证明"某个操作被私钥持有者授权"

text

┌─────────────┐     私钥签名      ┌─────────────┐

│  你的私钥    │ ──→ 签名(r, s) ──→│  广播到网络  │

│ (d)         │                 │             │

└─────────────┘                 └──────┬──────┘

                                     │

                                     ▼

                              任何人可用你的公钥验证签名

类比理解

现实类比对称加密非对称加密(数字签名)
------------------------------------------
物理世界保险箱密码手写笔迹 + 公证
核心属性"知道密码就能打开""只有你能写出这个笔迹"
区块链场景钱包文件保护资产转让授权
密钥泄露后果敌人能解密过去和未来的消息敌人能伪造授权,转移你的资产

为什么强调这个区分? 初学者常问"既然有公钥和私钥,为什么不直接用公钥加密交易保护隐私?"答案是:加密后无法验证,而区块链的验证需求大于保密需求。隐私保护在区块链中通过其他方式实现(如环签名、零知识证明),而非简单加密。

2.3.4 两种加密体系的对比总结

维度对称加密非对称加密(数字签名)
--------------------------------------
密钥数量1 个(共享密钥)密钥对(公钥 + 私钥)
主要用途数据保密(存储、传输)身份认证与授权(签名/验签)
性能快(硬件可达 GB/s 级别)慢(比对称慢 100–1000 倍)
密钥长度(128-bit 安全)128 位(AES-128)3072 位(RSA)或 256 位(ECC)
密钥分发问题需要安全通道共享密钥公钥可公开分发,私钥永远保密
区块链角色钱包文件加密、TLS交易签名、地址生成
核心安全假设密钥保密性数学难题(分解/离散对数/椭圆曲线离散对数)
graph LR

    subgraph 对称加密场景

        A["明文"] -- "共享密钥 K 加密" --> B["密文"]

        B -- "共享密钥 K 解密" --> C["明文"]

    end

    subgraph 非对称签名场景_区块链

        D["消息哈希"] -- "私钥签名" --> E["签名值 r,s"]

        E -- "公钥验证" --> F["验证通过/失败"]

        D -."实际交易数据".-G["全网公开广播"]

    end

核心认知

  1. 对称加密是"保密工具",非对称数字签名是"授权工具"。区块链的核心问题不是"谁可以读?"(因为数据公开),而是"谁可以花?"(需要签名授权)。
  2. 两种体系常常协同工作。TLS 握手阶段用非对称密码学交换共享密钥,后续通信用对称加密保护——这结合了两者的优点:非对称解决了"如何安全分发密钥"的问题,对称解决了"高性能加密"的问题。
  3. 混淆"加密"与"签名"会让理解后续章节变得困难。当你看比特币 P2PKH 脚本或以太坊交易结构时,看到的不是"加密操作",而是"签名验证操作"。

下一预告:2.4 节将深入椭圆曲线密码学(ECC)中最著名的曲线 secp256k1,用 TypeScript 从零实现点加法、倍点运算和标量乘法,让读者理解为什么 P=d×GP = d \times 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 形式)

素数域(即模一个素数 pp)上的椭圆曲线定义为满足以下方程的所有点 (x,y)(x, y) 的集合:

y2=x3+ax+b(modp)y^2 = x^3 + ax + b \pmod{p}

其中 a,bFpa, b \in \mathbb{F}_p,且判别式 Δ=4a3+27b2≢0(modp)\Delta = 4a^3 + 27b^2 \not\equiv 0 \pmod{p}(保证曲线非奇异——没有"尖点"或"自交点")。

secp256k1 的精确参数

secp256k1 是 Standards for Efficient Cryptography - Prime 256-bit Koblitz curve 的缩写。Koblitz 曲线是指 a=0a = 0 的特殊形式,这种选择允许额外的优化。

参数十六进制值意义
-----------------------
aa0曲线参数,简化为 y2=x3+by^2 = x^3 + b
bb7曲线参数 y2=x3+7y^2 = x^3 + 7
pp(域阶)0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F一个接近 22562^{256} 的素数,定义有限域 Fp\mathbb{F}_p
nn(生成点阶)0xFFFFFFFFFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141生成点 GG 的乘法阶,是一个大素数,约为 22562^{256}
GG(生成点)04 79BE667E F9DCBBAC 55A06295...曲线上一个特定点,所有公钥都是 GG 的标量倍
hh1余因子(cofactor),为 1 表示曲线没有小的子群
typescript

/**

 * secp256k1 参数定义

 */

const SECP256K1 = {

  a: 0n,

  b: 7n,

  p: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2Fn,

  n: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n,

  // 生成点 G 的坐标

  G: {

    x: 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798n,

    y: 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8n,

  },

  h: 1n,

} as const;

2.4.2 有限域上的算术

椭圆曲线上的所有运算都在有限域 Fp\mathbb{F}_p 中进行,即所有计算结果必须对 pp 取模。核心运算包括:

模加/模减

a+b(modp)=(a+b)modpa + b \pmod{p} = (a + b) \mod p

模乘

ab(modp)=(ab)modpa \cdot b \pmod{p} = (a \cdot b) \mod p

模逆元(Fermat 小定理)

a1ap2(modp)a^{-1} \equiv a^{p-2} \pmod{p}

因为 pp 是素数,且由 Fermat 小定理 ap11(modp)a^{p-1} \equiv 1 \pmod{p},所以 ap2a1(modp)a^{p-2} \equiv a^{-1} \pmod{p}

typescript

/**

 * 模幂运算:快速幂算法(二进制法)

 * 计算 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})在点加法下构成一个阿贝尔群

几何直觉

给定两个不同的点 P=(x1,y1)P = (x_1, y_1)Q=(x2,y2)Q = (x_2, y_2)

  1. P,QP, Q 画一条直线,与曲线相交于第三点 RR'
  2. RR' 关于 xx 轴对称,得到 P+Q=RP + Q = R

代数公式(需在有限域 Fp\mathbb{F}_p 中计算):

λ=y2y1x2x1(modp)=(y2y1)(x2x1)1modp\lambda = \frac{y_2 - y_1}{x_2 - x_1} \pmod{p} = (y_2 - y_1) \cdot (x_2 - x_1)^{-1} \bmod p
x3=λ2x1x2(modp)x_3 = \lambda^2 - x_1 - x_2 \pmod{p}
y3=λ(x1x3)y1(modp)y_3 = \lambda(x_1 - x_3) - y_1 \pmod{p}

倍点运算(P+P=2PP + P = 2P

P=QP = Q 时,直线变为切线

λ=3x12+a2y1(modp)=(3x12+a)(2y1)1modp\lambda = \frac{3x_1^2 + a}{2y_1} \pmod{p} = (3x_1^2 + a) \cdot (2y_1)^{-1} \bmod p
typescript

/**

 * 椭圆曲线上的点

 * 使用 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.x!.toString(16).slice(0,16)...,{this.x!.toString(16).slice(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++Pk \times P = P + P + \ldots + Pkk 次)。

核心安全假设:椭圆曲线离散对数问题(ECDLP)

给定 PPQ=k×PQ = k \times P,求 kk 在计算上是不可行的。当前已知最好的通用算法(如 Pollard's Rho)的时间复杂度约为 O(n)2128O(\sqrt{n}) \approx 2^{128} 次运算——这在当前及可预见的计算能力下是不可能的。

快速标量乘法:双倍-加算法(Double-and-Add)

简单的 kk 次重复加法需要 O(k)O(k) 次点加法。但使用快速幂思想(将 kk 展开为二进制),可以将复杂度降到 O(logk)O(\log k)

text

输入:标量 k(二进制:k_{n-1} ... k_1 k_0)

结果 = O

当前点 = P



对 i 从 0 到 n-1:

  如果 k_i == 1:结果 = 结果 + 当前点

  当前点 = 2 * 当前点



返回 结果
typescript

/**

 * 快速标量乘法: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.toString(16).slice(0,16)...)=x3+7?{lhs.toString(16).slice(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 是偶数:0x02x(33 字节)如果 y 是奇数:0x03x(33 字节)\text{如果 } y \text{ 是偶数} : 0x02 \parallel x \text{(33 字节)}\\ \text{如果 } y \text{ 是奇数} : 0x03 \parallel x \text{(33 字节)}

如何从 xx 恢复 yy

已知 y2=x3+7(modp)y^2 = x^3 + 7 \pmod{p},计算 y2y^2 的平方根。在有限域中,aa(p+1)/4(modp)\sqrt{a} \equiv a^{(p+1)/4} \pmod{p}(当 p3(mod4)p \equiv 3 \pmod{4} 时,secp256k1 的 pp 满足此条件)。

typescript

/**

 * 从 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: 计算值=gy.toString(16).slice(0,16)...实际值={gy.toString(16).slice(0, 16)}... 实际值={G.y!.toString(16).slice(0, 16)}...`);

console.log(`恢复一致性: ${gy === G.y!}`);

2.4.6 为什么 secp256k1 而不是其他曲线?

曲线方程特点主要使用方
----------------------------
secp256k1y2=x3+7y^2 = x^3 + 7Koblitz 曲线,高效实现,参数选择无"魔术数"嫌疑比特币、以太坊
secp256r1 / P-256y2=x33x+by^2 = x^3 - 3x + bNIST 标准化曲线,b 参数被认为有"后门"嫌疑TLS、政府系统
Curve25519y2=x3+486662x2+xy^2 = x^3 + 486662x^2 + xMontgomery 曲线,更简洁的常数时间实现Signal 协议、WireGuard

中本聪选择 secp256k1 而非 NIST 曲线 P-256,有说法是避免美国政府可能植入的后门。secp256k1 的 a=0,b=7a=0, b=7 选择简单透明,没有未解释的"随机"常数。

核心认知

  1. 椭圆曲线上的点构成一个阿贝尔群——支持加法、减法,有单位元(无穷远点 O\mathcal{O})。群结构是离散对数难题存在的前提。
  2. 标量乘法是"易算难逆"的密码学单向函数。 d×Gd \times G 可在毫秒内计算,但从 PPGG 反推 dd 需要 21282^{128} 次运算。
  3. 有限域中的模运算需要特别小心——除法变为乘模逆元,利用 Fermat 小定理高效实现。所有中间结果必须保持模 pp 归一化,否则链式错误会迅速放大。
  4. 压缩公钥节省 50% 空间。xx 恢复 yy 依赖 p3(mod4)p \equiv 3 \pmod{4} 的特殊性质,这并非所有素域都满足——这也是 secp256k1 参数设计的一部分。

下一预告:2.5 将基于椭圆曲线的标量乘法实现完整的 ECDSA 签名与验签流程,并深入剖析著名的 Sony PS3 私钥泄露事件——为什么一个"随机数重复"的错误可以导致巨额经济损失。


2.5 数字签名与验签:ECDSA 完整流程与 Sony PS3 事件

2.4 节实现了椭圆曲线上的标量乘法 P=d×GP = d \times G。本节在此基础上,完成数字签名的完整密码学协议——ECDSA(Elliptic Curve Digital Signature Algorithm),并深入剖析2010 年 Sony PlayStation 3 私钥泄露事件,理解为什么"随机数 kk 的唯一性"比任何算法细节都更重要。

2.5.1 ECDSA 签名的数学流程

输入

  • 私钥 dd(1 ≤ dd < nn
  • 消息 mm

签名过程

  1. 计算消息哈希:e=H(m)e = H(m)。比特币使用 double-SHA256:e=SHA256(SHA256(m))e = \text{SHA256}(\text{SHA256}(m))
  2. 生成密码学安全随机数 kk(1 ≤ kk < nn)。kk 必须每次签名都不同。
  3. 计算椭圆曲线点:R=k×GR = k \times G
  4. RRxx 坐标:r=x(R)modnr = x(R) \mod n。若 r=0r = 0,重新选择 kk
  5. 计算:
s=k1(e+rd)modns = k^{-1} \cdot (e + r \cdot d) \mod n

s=0s = 0,重新选择 kk

  1. 输出签名(r,s)(r, s)。在比特币中,签名通常使用 DER 编码(约 71–72 字节)。

签名验证流程

输入:公钥 P=d×GP = d \times G,消息 mm,签名 (r,s)(r, s)

  1. 验证 r,s[1,n1]r, s \in [1, n-1]
  2. 计算消息哈希(同样的哈希函数):e=H(m)e = H(m)
  3. 计算:
u1=es1modn,ν2=rs1modnu_1 = e \cdot s^{-1} \mod n, \quad \nu_2 = r \cdot s^{-1} \mod n
  1. 计算椭圆曲线点:R=ν1×G+ν2×PR' = \nu_1 \times G + \nu_2 \times P
  2. 验证通过当且仅当 x(R)modn=rx(R') \mod n = r

为什么验签等式成立?

这是 ECDSA 的数学核心:

ν1G+ν2P=(es1)G+(rs1)P=(es1+rds1)G=s1(e+rd)G\nu_1 G + \nu_2 P = (es^{-1})G + (rs^{-1})P = (es^{-1} + rd s^{-1})G = s^{-1}(e + rd)G

代入 s=k1(e+rd)s = k^{-1}(e + rd)

s1(e+rd)G=(k1(e+rd))1(e+rd)G=k(e+rd)1(e+rd)G=kG=Rs^{-1}(e + rd)G = (k^{-1}(e + rd))^{-1}(e + rd)G = k(e + rd)^{-1}(e + rd)G = kG = R

等式要求 x(R)x(R)(modn)x(R') \equiv x(R) \pmod{n},这正是 rr 的定义。所以验签等式等价于"这个签名只能由掌握 dd(即 P=dGP = dG 的私钥持有者)的人"才能产生。

2.5.2 完整 TypeScript 实现

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=0xsig.r.toString(16).slice(0,16)...,s=0x{sig.r.toString(16).slice(0, 16)}..., s=0x{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 事件:随机数 kk 的致命秘密

事件背景

2010 年,黑客组织 fail0overflow 在著名的 CCC 黑客大会上展示了如何破解 Sony PlayStation 3 的安全系统。他们的核心发现令全场哗然:

Sony 在 PS3 固件的 ECDSA 实现中,对每一条消息使用了固定的 kk 值。

攻击数学:为什么固定 kk = 私钥泄露

假设用同一个 kk 对两条不同消息 m1,m2m_1, m_2 签名:

s1=k1(e1+rd)modns2=k1(e2+rd)modns_1 = k^{-1}(e_1 + r d) \mod n\\ s_2 = k^{-1}(e_2 + r d) \mod n

注意 rr 也相同(因为 r=x(kG)r = x(kG)kk 相同 → RR 相同 → rr 相同)。

攻击者获得 (r,s1),(r,s2)(r, s_1), (r, s_2) 和公共的 e1=H(m1),e2=H(m2)e_1 = H(m_1), e_2 = H(m_2)

s1s2=k1(e1e2)modns_1 - s_2 = k^{-1}(e_1 - e_2) \mod n
k=(e1e2)(s1s2)1modn\Rightarrow k = (e_1 - e_2) \cdot (s_1 - s_2)^{-1} \mod n

一旦得到 kk,私钥立即暴露:

d=(s1ke1)r1modnd = (s_1 k - e_1) \cdot r^{-1} \mod n

只需要两个使用相同 kk 的签名,私钥就被完全破解。

历史后果

  • 2010 年 12 月:fail0overflow 发布演示,展示从固定 kk 恢复出 Sony 的主私钥
  • 这意味着任何人都可以用 Sony 的私钥"签名"自定义固件,让 PS3 认为它是官方授权的。
  • Sony 紧急起诉多名黑客(包括 George Hotz / "GeoHot")。
  • 但私钥已无法挽回——数学保证一旦泄露,没有任何技术方法可以"撤销"一个已公开的公钥对应私钥
typescript

/**

 * 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,私钥不可逆转地泄露!`);

如何正确生成 kk

方案一:真随机(CSPRNG)

每次签名时从 crypto.getRandomValues 生成 256 位均匀随机数。风险:随机源可能污染/可预测(某些 IoT 设备的 /dev/urandom 可能熵不足)。

方案二:确定性 kk(RFC 6979)—— 推荐

用私钥 dd 和消息哈希 ee 作为输入,通过 HMAC-SHA256 计算确定性 kk

k=HMAC-SHA256(key=d,data=e0x00 (padding))k = \text{HMAC\text{-}SHA256}(\text{key}=d, \text{data}=e \parallel 0x00 \text{ (padding)})

优点

  • 对同一 (d,m)(d, m) 总是产生相同 kk,保证签名可复现。
  • 不依赖外部随机源,避免熵不足问题。
  • 不同消息产生不同的 kk,完全免疫"固定 kk"攻击。

比特币的 libsecp256k1 默认使用 RFC 6979 确定性签名。

2.5.4 ECDSA vs Schnorr 签名

比特币在 2021 年的 Taproot 升级中引入了 Schnorr 签名(BIP-340)。以下是两者对比:

特性ECDSASchnorr
----------------------
签名大小~71 字节(DER 编码)64 字节(r, s 各 32 字节)
数学复杂度中等(需取模逆元)简化(基于线性点加)
可聚合性❌(签名不能数学合并)✅(MuSig: k=k1+k2,s=s1+s2k = k_1 + k_2, s = s_1 + s_2,得到联合签名)
线性验证✅(可批量验证多个签名)
公钥恢复✅(从签名恢复公钥,节省存储)❌(BIP-340 中必须显式携带公钥)
标准安全性证明较复杂基于标准离散对数假设的简洁证明

Schnorr 签名的核心优势——线性

σagg=(R1+R2,s1+s2)=(Ragg,sagg)\sigma_{agg} = (R_1 + R_2, s_1 + s_2) = (R_{agg}, s_{agg})

两个参与方可以各自独立计算自己的 RiR_isis_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 验签中需要计算 s1s^{-1},这使得签名之间不能线性组合。Schnorr 的设计避免了取逆,保留了线性结构。

核心认知

  1. ECDSA 签名 = 两个数 (r,s)(r, s)rr 是随机点 R=kGR = kGxx 坐标,ss 将消息哈希 eerr 和私钥 dd 绑定在一起。验证等式的本质是用公钥"解开"这个绑定。
  2. kk 是签名的灵魂。 不是算法选择也不是性能参数——kk 的一次重复 = 私钥泄露。 Sony 事件以数十亿美元的代价证明了这一点。
  3. 确定性 kk(RFC 6979)比纯随机更安全。 在密码学史上,随机源失败导致的漏洞(Debian OpenSSL 2008、Sony PS3 2010)远比确定性算法多。用 HMAC 从 (d,m)(d, m) 派生 kk 是最佳实践。
  4. 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) 的要求:

  1. 前向不可预测性:即使攻击者知道前 nn 个输出,也无法预测第 n+1n+1 个。
  2. 后向不可预测性:即使攻击者知道后续输出,也无法推算出之前的内部状态(防止逆向推导种子)。
typescript

/**

 * 密码学安全随机数生成(基于浏览器/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 的完整流程

text

128-256 位随机熵

    │

    ▼

┌─────────────────┐

│  SHA-256 哈希    │ → 取前 (熵位数/32) 位作为"校验和"

└─────────────────┘

    │

    ▼

熵 + 校验和 = 264-330 位

    │

    ▼ 每 11 位 = 一个单词索引(0-2047)

12-24 个助记词
typescript

/**

 * 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 位21282^{128}极安全(当前算力不可行)
15 词160 位21602^{160}极高
18 词192 位21922^{192}后量子安全级别
21 词224 位22242^{224}过度安全
24 词256 位22562^{256}过度安全

标准推荐:12 词(128 位)对大多数用户足够安全且便于记忆/抄写。24 词主要用于要求极致安全的场景(如机构冷存储)。

2.6.3 BIP-32/BIP-44:层级确定性钱包(HD Wallet)

为什么需要 HD 钱包?

传统钱包为每笔交易生成独立随机私钥。用户需要备份每一个私钥——使用 100 次 = 备份 100 个私钥。HD 钱包(BIP-32, 2012)解决了这个灾难:从一个主种子通过确定性算法派生无限多个子密钥,只需备份一次种子(助记词)。

核心思想:扩展密钥 + 子密钥派生

text

主种子 (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)

输出分为两半:左半部分作为子私钥,右半部分作为子链码。通过递增索引 ii,可以生成无限多个独立的子密钥。

typescript

/**

 * 简化的 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}
层级含义示例
------------------
m主密钥
44'目的:BIP-44 标准固定
0'币种类型:0=比特币,60=以太坊,501=Solana
0'账户编号:0 开始多账户管理
0外部链(0=收款,1=找零)比特币找零机制
0地址索引每用一次 +1

示例路径

  • 比特币首个外部地址:m/44'/0'/0'/0/0
  • 以太坊首个地址:m/44'/60'/0'/0/0
  • 比特币第 10 个外部地址:m/44'/0'/0'/0/9
typescript

/**

 * 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 前缀(未压缩公钥)→ SHA256RIPEMD160(20 字节哈希)→ 加版本字节 0x00(主网)→ Base58Check 编码。
  • 以 "1" 开头的是主网 P2PKH 地址(如 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)。

以太坊地址

地址=Keccak256(pubkey)[12:32]\text{地址} = \text{Keccak256}(\text{pubkey})[12:32]
  • 取公钥(64 字节未压缩,去掉 0x04)的 Keccak-256 哈希,取最后 20 字节。
  • 0x 开头,40 个十六进制字符(如 0xdAC17F958D2ee523a2206206994597C13D831ec7)。
  • 无 Base58Check——原始十六进制,但包含 EIP-55 大小写校验。
typescript

/**

 * 公钥到地址的两种路线

 */

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..."; // 占位

}
特性比特币 P2PKH以太坊
---------------------------
哈希函数SHA256 → RIPEMD160Keccak-256
输出大小160 位160 位(Keccak-256 的最后 160 位)
编码Base58Check十六进制(EIP-55 大小写校验)
前缀"1"(主网)"0x"
大小写敏感不敏感EIP-55 校验依赖大小写

2.6.5 冷存储与硬件钱包的安全实践

威胁模型

威胁热钱包(联网软件)冷钱包(离线)
----------------------------------------
操作系统漏洞/恶意软件❌ 高风险✅ 免疫
网络钓鱼❌ 可能误签✅ 需物理确认
物理盗窃❓ 依赖设备密码❓ 依赖物理安全
用户误操作高(需确认多个步骤)

硬件钱包的核心机制

硬件钱包(如 Ledger、Trezor)的核心安全保证是:

私钥从不出现在硬件设备的"易泄露区域"(RAM/CPU 缓存可能被侧信道攻击),且永远不暴露给连接的主机。

交易签名流程:

  1. 主机将待签名的交易数据发送给硬件设备。
  2. 硬件在屏幕上显示交易内容(如"发送 0.5 BTC 到 1A1z...")。
  3. 用户物理按下设备上的确认按钮。
  4. 设备使用芯片内安全元素的私钥执行签名。
  5. 只将签名结果 (r,s)(r, s) 返回给主机,私钥从未离开设备。

助记词的安全备份

永远不要

  • 将助记词存储在联网设备(手机照片、云盘、邮件)。
  • 将助记词输入任何网站或应用(除非是首次在新设备上恢复钱包)。
  • 只保留一份备份(单点故障)。

推荐做法

  • 写在金属板(防火防水)上,存放在两个不同物理位置。
  • 或使用Shamir 秘密共享(BIP-39 扩展):将 24 词分成 3 份,任意 2 份即可恢复,避免单点失窃/丢失。

核心认知

  1. 钱包 = 密钥管理器。 它不"存放"币,币永远在区块链上。钱包只是保存了授权花费这些币的私钥。
  2. BIP-39 助记词是人类可记忆的 128 位熵。 校验和机制保证 12 个词中抄错任意一个词可以立即被检测(约 1/256 的错误会被校验和捕获,约 255/256 的错误会被词表检查捕获)。
  3. BIP-32 推导 = 一次备份,无限密钥。 主种子派生子密钥的树状结构,使得企业可以批量管理数千个客户存款地址,个人可以在多账户间隔离隐私。
  4. 冷存储的核心不是技术,而是物理隔离。 硬件钱包的价值在于"私钥永远不会暴露在联网环境中"。空气隔离的计算机 + 离线签名 + 二维码传输,是机构级冷存储的标准做法。

下一预告: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=8N=8,扩展为 2 的幂次):

text

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

           (叶节点,每个都是数据的哈希)

每个内部节点存储其两个子节点的双重哈希(防止长度扩展攻击):

Hparent=H(HleftHright)H_{\text{parent}} = H(H_{\text{left}} \parallel H_{\text{right}})

在比特币中,H=double-SHA256(double-SHA256(leftright))H = \text{double-SHA256}(\text{double-SHA256}(\text{left} \parallel \text{right}))

关键性质:O(logN)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(logN)O(\log N) 个兄弟节点。对于 1000 笔交易,需要 log2100010\log_2 1000 \approx 10 个哈希,每个 32 字节,总证明大小 ≈ 320 字节——比发送全部 250 KB 交易数据小了 800 倍

typescript

/**

 * 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} 兄弟:{Array.from(hash).map(b => b.toString(16).padStart(2, '0')).slice(0, 4).join('')}...`);

});

2.7.3 证明验证算法

轻客户端(已拥有根哈希 HrootH_{\text{root}},来自区块头):

text

输入:目标交易哈希 H(tx),证明 [s1, s2, ..., s_logN]



当前 = H(tx)

对每个兄弟哈希 s_i(从叶到根的顺序):

  if 当前在左子树: 当前 = H(当前 || s_i)

  else: 当前 = H(s_i || 当前)



返回 当前 == H_root
typescript

/**

 * 验证 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(`❌ 错误索引(fakeIndex)验证:{fakeIndex}) 验证:{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
text

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/>❌ 失败}
typescript

/**

 * 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 使用三种节点类型:

  1. 叶子节点(Leaf):存储键值对的最终值
  2. 扩展节点(Extension):压缩单一路径上的连续节点
  3. 分支节点(Branch):最多 16 个子节点 + 1 个值槽
text

存储少量地址的 MPT 示例:



                    [ , , , , [_extension: , 

                   0xe7] , , , ,...]

                       |

             [branch: 0 → ac, 1 → 9b, ...]

             /                           \

    [leaf: {address1}]              [extension: 35]

                                       |

                            [branch: 0 → leaf2, ...]

这种结构允许以太坊轻客户端用类似 SPV 的方式验证"某个地址的余额是否为 XX",通过提供从根到叶的路径证明。

核心认知

  1. Merkle 树将证明大小从 O(N)O(N) 降到 O(logN)O(\log N) 对于 4000 笔交易/区块的比特币,所需证明从 ≈ 1 MB 降到 ≈ 480 字节。
  2. 哈希箱结构天然防止篡改。 如果攻击者修改任何一笔交易,计算出的根哈希将完全不同,与区块头中的存储值不符。这就是 Merkle 树作为"密码学累加器"的价值。
  3. SPV 是信任与效率的权衡。 64 MB 的区块头存储 vs 600 GB 的完整链——代价是信任全节点提供的证明,以及无法独立检测双花。
  4. 以太坊的 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 困难性、kk 唯一性
Merkle 树集合成员证明SPV 轻量验证哈希链的防篡改性
对称加密数据保密钱包文件加密、P2P 层 TLS密钥保密性
密钥派生主种子 → 无限子密钥HD 钱包地址批量生成前向安全性(泄露子密钥不泄露父密钥)

2.8.2 选型决策树

text

需要的密码学能力是什么?

    │

    ├── 确保"数据未被篡改" → 哈希函数

    │       ├── 需要防长度扩展 → 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 bitSHA-1160 bit1024 bit
112 bit (过渡)112 bitSHA-224224 bit2048 bit
128 bit (当前标准)128 bitSHA-256256 bit3072 bit
256 bit (后量子预备)256 bitSHA-512512 bit15360 bit

关键洞察:椭圆曲线提供"即用 256 位密钥实现 128 位安全",而 RSA 需要 3072 位。这就是为什么区块链优先选择 ECC 而非 RSA——在传输和存储受限的环境中,密钥尺寸直接决定效率。

2.8.4 十大常见工程安全错误

#错误后果正确做法
-------------------------
1使用 Math.random() 生成密钥私钥可预测,资产被盗使用 crypto.getRandomValues
2签名时重复使用 kk私钥泄露RFC 6979 确定性签名
3不验证曲线方程(输入不在曲线上)离线签名攻击验证 y2=x3+7modpy^2 = x^3 + 7 \mod p
4未压缩点解析不验证 yy 坐标扭曲攻击(Twist Attack)xx 恢复 yy 后验证曲线方程
5使用 SHA-1 或 MD5碰撞攻击可伪造使用 SHA-256 或 Keccak-256
6硬编码密钥/助记词在代码中私钥暴露于版本控制环境变量/硬件安全模块
7未加盐哈希密码彩虹表攻击bcrypt/Argon2/PBKDF2
8签名消息不包含上下文(如"Bitcoin Signed Message"前缀)消息重放攻击协议特定的消息前缀
9忽略低 ss 值规范(BIP-62)签名可锻性(malleability)规范化 sn/2s \leq n/2
10使用等价但不同的编码格式哈希不一致导致验证失败严格规定编码标准

错误 #3 详解:无效的曲线点攻击

假设攻击者提供一个伪造的公钥 P=(x,y)P = (x, y)y2≢x3+7(modp)y^2 \not\equiv x^3 + 7 \pmod{p}。如果签名验证代码不检查点是否在曲线上,攻击者可能构造"签名"骗过验证。

typescript

/**

 * 曲线点验证——必须在使用任何传入点之前执行

 */

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(n3)O(n^3) 可解
RSA大数分解Shor 算法在量子计算机上 O(n3)O(n^3) 可解
SHA-256无结构(理想哈希)不受影响(Grover 算法仅将 21282^{128} 降到 2642^{64},仍计算不可行)
对称加密 (AES)密钥穷举Grover 算法将搜索空间减半,可用更长密钥补偿

量子安全签名候选

  • Lamport 签名(一次性,基于哈希,193 KB/签名——过大但概念简单)
  • SPHINCS+(基于哈希,无状态,约 8 KB/签名)
  • CRYSTALS-Dilithium(基于格,小签名,NIST 标准化)
  • FALCON(基于格,更小签名,相同安全级别)

关键结论:哈希函数(SHA-256, Keccak-256, 哈希树)天然免疫量子攻击。这就是为什么比特币的 PoW(依赖哈希)比签名方案(依赖 ECC)更"量子健壮"——即使量子计算机攻破 ECC 签名,已上链的交易哈希仍然不可篡改。

核心认知

  1. 密码学是工具箱,不是万能药。 哈希保证完整性,签名保证授权,加密保证保密——没有单一原语可以替代另一个。区块链的"透明性"设计意味着加密使用最少,签名使用最多。
  2. 密码学的安全边界是"计算不可行",不是"数学不可能"。 量子计算机的出现将打破 ECC 和 RSA 的安全假设,但哈希函数的抗碰撞性基于信息论(生日攻击的数学下限),不受影响。
  3. 实现错误比算法被攻破更常见。 Sony PS3 使用正确的 ECDSA 算法,但因 kk 重复而泄露私钥。Debian 2008 年因为 valgrind 工具清除了 OpenSSL 的熵源,导致两年内生成的所有密钥只有 15 位有效熵。工程纪律比算法选择更重要。
  4. "不要自己实现密码学"的例外是教学和理解。 本节和前几节通过从零实现让读者理解每个原语"为什么安全"。但在生产系统中,应使用经过数十亿次交易验证的库: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 核心公式速查表

概念数学/公式文件位置
--------------------------
哈希碰撞概率P1ek2/2n+1P \approx 1 - e^{-k^2 / 2^{n+1}}02.01
密码学承诺c=H(rv)c = H(r \parallel v),绑定+隐藏02.01
SHA-256 压缩a,b,c,d,e,f,g,ha,b,c,d,e,f,g,h 经 64 轮非线性更新02.02
Keccak 海绵吸收阶段(异或+置换)+ 挤压阶段02.02
模逆元a1ap2(modp)a^{-1} \equiv a^{p-2} \pmod p(Fermat)02.04
点加法λ=y2y1x2x1\lambda = \frac{y_2-y_1}{x_2-x_1}x3=λ2x1x2x_3 = \lambda^2 - x_1 - x_202.04
标量乘法二进制 Double-and-Add,O(logk)O(\log k)02.04
ECDSA 签名s=k1(e+rd)s = k^{-1}(e + rd)r=x(kG)r = x(kG)02.05
ECDSA 验证u1G+u2P=Ru_1 G + u_2 P = R',验证 x(R)rx(R') \equiv r02.05
固定 k 攻击k=(e1e2)(s1s2)1k = (e_1-e_2)(s_1-s_2)^{-1},恢复 dd02.05
BIP-39 助记词熵 + SHA-256 校验和 → 每 11 位一个词02.06
BIP-32 子密钥I=HMAC("Bitcoinseed",seed)I = HMAC("Bitcoin seed", \text{seed}),CKD 函数02.06
Merkle 证明大小O(logN)O(\log N) 个兄弟哈希02.07
Merkle 验证沿路径逐层哈希,比对根值02.07

2.9.3 常见错误诊断矩阵

症状/场景根因章节解决方案
--------------------------------
相同消息两次签名结果不同正常(应有不同 kk02.05使用 RFC 6979 确定性签名可复现
相同消息两次签名 rr 相同致命kk 重复02.05立即更换密钥,使用 HMAC-SHA256 生成 kk
签名解析失败/长度可变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 数学安全强度对照

text

安全级别 (位)    暴力破解难度          适用场景

─────────────────────────────────────────────────────────

  64 位          小时级 (GPU 农场)     ❌ 玩具/测试

  80 位          月级 (小国家)         ❌ 过渡,寿命<5年

 112 位          年级 (大国级算力)      ⚠️ 短期敏感数据

 128 位          世纪级 (全球算力)      ✅ 当前标准(加密货币)

 192 位          宇宙级                ✅ 后量子预备(State of the Art)

 256 位          物理不可能             ✅ 国家机密级

核心结论:当前区块链使用的 128 位安全级别(如 secp256k1 提供约 128 位,因为最好攻击是 Pollard's Rho,约 n2128\sqrt{n} \approx 2^{128} 次运算)在全球现有和可预见的计算能力下是计算不可行的。量子计算机需要约 4000 个逻辑量子比特才能威胁 ECC,当前最大公开进展约数百物理量子比特(纠错后更少),预计 10–20 年内不会构成实际威胁。

2.9.6 延伸阅读与资源

资源类型推荐理由
----------------------
bitcoin/bips标准文档BIP-32/39/44/66/340 的权威定义
SEC2: Recommended Elliptic Curve标准文档secp256k1 的精确参数定义
RFC 6979标准文档确定性 ECDSA 的权威规范
Keccak Reference论文海绵结构和 Keccak-f 置换的设计理论
Daniel J. Bernstein: Curve25519论文对比 NIST 曲线,展示更简洁的 ECC 设计
Rosetta Code: SHA-256示例代码多语言的 SHA-256 从零实现,适合对照学习

下一节: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
text

┌─────────────────────────────────────────────┐

│          密码学工具箱 (Crypto Toolset)         │

├─────────────────────────────────────────────┤

│  [哈希层]                                    │

│    - SHA-256 从零实现                        │

│    - 雪崩效应测试器                          │

│    - 碰撞概率计算器                          │

├─────────────────────────────────────────────┤

│  [椭圆曲线层]                                │

│    - secp256k1 点运算                       │

│    - 标量乘法 (私钥→公钥)                   │

│    - 公钥压缩/解压                           │

├─────────────────────────────────────────────┤

│  [签名层]                                    │

│    - ECDSA 签名/验证                         │

│    - RFC 6979 确定性 k                      │

│    - 签名 malleability 检测                  │

├─────────────────────────────────────────────┤

│  [钱包层]                                    │

│    - 熵生成 → 助记词 → 种子 → 子密钥        │

│    - BIP-44 路径解析                         │

├─────────────────────────────────────────────┤

│  [Merkle 层]                               │

│    - 构建 Merkle 树                         │

│    - 生成/验证 Merkle 证明                   │

│    - 批量验证优化                           │

└─────────────────────────────────────────────┘

2.10.2 完整整合代码

typescript

// =====================================================

// 密码学工具箱整合实现

// 所有底层实现复用 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(`  证明大小: proof.length个哈希×32字节={proof.length} 个哈希 × 32 字节 ={proof.length * 32} 字节`);

    console.log(`  验证结果: ${verified ? '✅ 通过' : '❌ 失败'}`);

  }

  

  console.log("\n╔════════════════════════════════════════╗");

  console.log("║    全部测试完成                       ║");

  console.log("╚════════════════════════════════════════╝");

}



// 执行测试

runTests();

2.10.3 扩展挑战

完成基础工具箱后,以下扩展项目可以加深理解:

挑战 1:实现 RFC 6979 确定性 kk

替代随机 kk,实现基于 HMAC-SHA256 的确定性签名:

k=HMAC-SHA256(key=d,data=e0x00)k = \text{HMAC\text{-}SHA256}(\text{key} = d, \text{data} = e \parallel 0x00)

验证:同一 (d,m)(d, m) 总是产生相同的 (r,s)(r, s),且 Sony 攻击不再可能。

挑战 2:Schnorr 签名实现

实现 BIP-340 的 Schnorr 签名:

R=kG,e=H(RPm),s=k+ed,签名=(R,s)R = kG, \quad e = H(R \parallel P \parallel m), \quad s = k + ed, \quad \text{签名} = (R, s)

验证等式:sG=?R+ePsG \stackrel{?}{=} R + eP

比较:签名大小降为 64 字节(RRss 各 32 字节,不需要编码 rrss),无 DER 编码复杂性。

挑战 3:批量验证优化

Schnorr 的核心优势——线性——允许批量验证多个签名:

icisiG=?iciRi+icieiPi\sum_i c_i s_i G \stackrel{?}{=} \sum_i c_i R_i + \sum_i c_i e_i P_i

其中 cic_i 是随机挑战系数。将 nn 次独立验证的 n×Gn \times G 运算合并为少量群运算。

挑战 4:Patricia Trie 简化实现

实现一个键值存储的 Patricia Trie(16 进制前缀压缩),并扩展为 Merkle Patricia Trie——在每次修改后重新计算到根的路径哈希。这是以太坊状态树的简化模型。

2.10.4 最佳安全实践清单

  • [ ] 所有随机数使用 crypto.getRandomValuescrypto.randomBytes
  • [ ] ECDSA 使用 RFC 6979 确定性 kk 或 CSPRNG + 防重放机制
  • [ ] 所有外部输入的公钥在使用前验证曲线方程
  • [ ] 签名后检查 sn/2s \leq n/2,拒绝高 s 可锻性签名
  • [ ] 助记词不在任何联网设备存储
  • [ ] HD 钱包使用 BIP-44 标准路径,记录 hardened 索引
  • [ ] 消息签名前包含协议标识/链 ID 防止跨链重放
  • [ ] 密钥存储使用 AES-256-GCM 或 ChaCha20-Poly1305

本章小结:从 SHA-256 的 64 轮压缩函数,到 secp256k1 的标量乘法,到 ECDSA 的灵魂 kk,到 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碰撞计算不可行 21282^{128}交易 ID、区块哈希、双重哈希碰撞 → 伪造交易
Keccak-256海绵结构 + ff 置换任意 → 256 bit置换不可区分以太坊地址、状态哈希碰撞 → 地址碰撞
secp256k1 标量乘法椭圆曲线群运算私钥 dd → 公钥 PPECDLP 困难密钥对生成离散对数破解 → 私钥暴露
ECDSA模逆 + 曲线点加(d,m)(r,s)(d, m) \to (r, s)kk 唯一性 + ECDLP交易授权kk 重放 → 私钥泄露
Schnorr线性方程(d,m)(R,s)(d, m) \to (R, s)离散对数多签/聚合 (BIP-340)未标准化实现风险
Merkle 树二叉哈希树NN 叶节点 → 1 根哈希防篡改SPV 轻验证根伪造 → 假交易被"确认"
BIP-32 HDHMAC-SHA512种子 → 无限子密钥单向性/不可预测地址批量管理主种子泄露 → 所有地址暴露

2.13 第2章 → 第1章/第3章的知识衔接

与第1章的呼应

第1章介绍了比特币的交易模型和 UTXO 设计,但一直悬而未决的问题是:"为什么交易可以被信任?" 第2章的答案如下:

第1章概念第2章密码学基础具体机制
-----------------------------------
UTXO 所有权椭圆曲线标量乘法"拥有 UTXO" = "知道控制该 UTXO 地址对应的私钥 dd"
交易广播ECDSA 数字签名交易被私钥签名后广播,任何人可用公钥验证
PoW 谜题哈希的谜题友好性 + 雪崩效应nonce 搜索 = 穷举 H(header)<TH(\text{header}) < T,无捷径可寻
区块链接双重 SHA-256 哈希链prevBlockHash 依赖前一区块全部内容的哈希
轻量节点Merkle 树 + SPV轻节点仅存储 80 字节区块头 + 320 字节证明

向第3章的铺垫

第3章将探讨共识机制(PoW、PoS、BFT),而共识机制之所以能"工作",根本原因在于第2章的密码学保证:

  1. PoW 的防作弊:如果哈希不具有谜题友好性,矿工可以通过捷径绕过工作量计算。
  2. 最长链规则:改变历史区块需要重新计算所有后续区块的 PoW(因为每个区块头包含前一区块的哈希)——这在计算上不可行。
  3. 交易不可伪造:没有私钥 = 无法产生有效签名 = 无法花费他人的 UTXO。
  4. 轻客户端信任:轻节点信任区块头(通过工作量证明验证其难度),再通过 Merkle 证明验证交易——两者共同构成"无需信任全节点"的安全模型。
text

第2章密码学 ──→ 第3章共识的根本假设

   │

   ├── 哈希(谜题友好) → PoW 的公平性

   ├── 哈希链 → 历史不可篡改

   ├── 数字签名 → 交易不可伪造

   └── Merkle 树 → 轻节点的验证能力

2.14 关键思维模型总结

模型一:信任链的数学化

区块链用密码学将"信任问题"转化为可验证的数学问题。不需要相信任何中介,只需要相信:

  • 哈希的碰撞阻力和谜题友好性
  • 椭圆曲线离散对数的困难性
  • 这些假设在数学上经过数十年研究

模型二:安全强度的统一度量

所有密码学强度都可以用"暴力破解需要多少次尝试"来度量:

  • 128 位安全 = 21282^{128} 次尝试
  • 全球所有计算设备每秒 101810^{18} 次运算
  • 2128/1018/(365×24×3600)10212^{128} / 10^{18} / (365 \times 24 \times 3600) \approx 10^{21}

模型三:安全不是功能,而是设计属性

Sony PS3 使用正确的 ECDSA 算法——但因 kk 的管理错误而泄露私钥。这揭示了一个核心原则:安全不是"功能正确"的附赠品,而是需要独立验证的设计属性。

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

评论加载中…

发表评论

0/2000