导语:密码学是区块链的信任地基
区块链的"不可篡改"“不可伪造"“所有权证明"三大承诺,全部建立在密码学之上。没有密码学,区块链只是一台昂贵的广播服务器。
本篇文章从数学和工程两个层面,拆解区块链依赖的四大密码学构件:
- 哈希函数(SHA-256、Keccak-256):数据的指纹,保证完整性
- 椭圆曲线密码(secp256k1):公钥推导与密钥对的数学基础
- ECDSA 数字签名:所有权证明,保证交易的不可否认性
- Merkle Tree + SPV:把整块数据压缩成 32 字节根哈希,让轻节点能验证
一句话总结:哈希函数负责"压缩与锁定”,椭圆曲线负责"生成密钥对”,ECDSA 负责"签名证明所有权",Merkle Tree 负责"把海量交易压成一个根值"——四者组合,区块链才有了可信的历史。
1. 哈希函数:区块链的"指纹机"
1.1 哈希函数的性质
密码学哈希函数 H(m) 接受任意长度的输入,输出固定长度的摘要(如 SHA-256 输出 256 位)。它必须满足三个核心性质:
| 性质 | 定义 | 不满足的后果 |
|---|---|---|
| 抗原像性 | 已知 H(m),难以反推出 m | 攻击者可反推原始数据 |
| 抗第二原像 | 已知 m1,难以找到 m2 ≠ m1 使 H(m2) = H(m1) | 攻击者可伪造数据 |
| 抗碰撞性 | 难以找到任意两个 m1 ≠ m2 使哈希相同 | 攻击者可制造"合法"的伪造数据 |
第三个性质在区块链中尤其关键。SHA-256 抗碰撞强度约为 128 位(生日攻击下的有效强度),意味着约需 2^128 次计算才能以可忽略概率找到碰撞——这在可预见范围内是不可行的。
1.2 SHA-256 与 Keccak-256 的区别
比特币使用 SHA-256,以太坊使用 Keccak-256(注意:不是 SHA3-256 标准,而是原始 Keccak 参数)。二者输出都是 256 位,但内部填充规则不同,同一输入产生不同输出:
import hashlib
def sha256(data: bytes) -> bytes:
return hashlib.sha256(data).digest()
# Bitcoin 对区块头使用 double-SHA256(哈希两次,防长度扩展攻击)
def double_sha256(data: bytes) -> bytes:
return sha256(sha256(data))
# 以太坊使用 Keccak-256(Web3.py / eth_utils 提供)
from eth_hash.auto import keccak # 或 eth_utils.keccak
print(sha256(b"birdor").hex())
print(keccak(b"birdor").hex())
1.3 哈希在区块链中的四个用途
用途 1:区块头哈希(Chaining)
prev_hash 作为下一个区块头的一部分 → 形成哈希链
用途 2:Merkle Root
所有交易逐层哈希压缩 → 32 字节根值
用途 3:地址生成
Keccak-256(公钥) 取后 20 字节 → 以太坊地址
用途 4:PoW 难度目标
矿工寻找 nonce 使 block_hash < target
一句话总结:哈希函数是区块链的"基础油"——它把任意长度的交易数据压缩成定长指纹,任何一位数据的改变都会让指纹面目全非,这正是不可篡改性的数学来源。
2. 椭圆曲线密码:secp256k1
2.1 什么是椭圆曲线
比特币与以太坊的密钥对基于 secp256k1 椭圆曲线,其方程为:
y² = x³ + 7 (定义在有限域 GF(p) 上,p = 2^256 - 2^32 - 977)
参数选择 a = 0, b = 7 使曲线方程简洁。这个曲线的阶(curve order)为:
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
2.2 椭圆曲线点运算
椭圆曲线上定义两种基本运算:
P + Q(点加法):过 P、Q 的直线与曲线交于第三点 R',关于 x 轴对称得到 R = P + Q
2P = P + P(点倍乘):过 P 的切线与曲线交于 R',对称得到 2P
标量乘法:kP = P + P + ... + P(k 次)
椭圆曲线离散对数问题(ECDLP):已知基点 G 和公钥点 Q = kG,求私钥 k 在计算上不可行。这就是安全性根基。
# 使用纯 Python 演示 secp256k1 点运算(教学简化版)
# 实际实现必须使用经过审计的库,如 ecdsa、coincurve、libsecp256k1
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
def point_add(p1, p2):
if p1 is None:
return p2
if p2 is None:
return p1
x1, y1 = p1
x2, y2 = p2
if x1 == x2 and (y1 + y2) % P == 0:
return None # 无穷远点
if p1 == p2:
lam = (3 * x1 * x1) * pow(2 * y1, -1, P) % P
else:
lam = (y2 - y1) * pow(x2 - x1, -1, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def scalar_mult(k, point):
result = None
addend = point
while k:
if k & 1:
result = point_add(result, addend)
addend = point_add(addend, addend)
k >>= 1
return result
G = (Gx, Gy)
# 随机私钥 k,公钥 Q = kG
# Q = scalar_mult(k, G)
2.3 从私钥到地址
私钥 d(256 位随机数,1 ≤ d < n)
│ dG(椭圆曲线标量乘法)
▼
公钥 Q(压缩:02/03 前缀 + x 坐标;未压缩:04 + x + y)
│ Keccak-256(Q) 取后 20 字节(以太坊)
▼
地址(十六进制,0x 前缀 + EIP-55 校验和)
比特币路径:SHA-256 + RIPEMD-160 → 160 位 Hash160 → Base58Check(含版本前缀与校验)
| 属性 | 比特币 | 以太坊 |
|---|---|---|
| 曲线 | secp256k1 | secp256k1 |
| 地址算法 | Hash160 + Base58Check | Keccak-256 后 20 字节 + hex |
| 地址前缀 | 1 / bc1(SegWit) | 0x |
| 校验机制 | 4 字节校验和 | EIP-55 大小写校验 |
一句话总结:椭圆曲线给了我们一个"单向门"——从私钥(随机数)出发可以轻松算出公钥,但反过来从公钥推私钥要面对
2^128量级的计算量,这就是区块链账户安全的数学根基。
3. ECDSA 数字签名:所有权证明
3.1 签名与验证的数学流程
ECDSA 签名为 (r, s) 对。签名者持有私钥 d,选择随机 nonce k:
签名:
z = 交易数据的哈希(截断为曲线阶 n 的位长)
(x1, y1) = kG
r = x1 mod n
s = k⁻¹ (z + r·d) mod n
输出签名 (r, s)
验证:
w = s⁻¹ mod n
u1 = z·w mod n, u2 = r·w mod n
(x1, y1) = u1·G + u2·Q
校验:x1 mod n == r
关键点:随机 nonce k 必须真正随机且绝不重用。如果两个签名共用同一个 k,攻击者可以直接解出私钥:
已知两次签名 (r, s1) 与 (r, s2) 共用 k:
s1 - s2 ≡ k⁻¹(z1 - z2) (mod n) → 可解出 k → 私钥 d = (s·k - z)·r⁻¹
2013 年 PlayStation 3 的 ECDSA 签名漏洞正是因此被攻破。
3.2 Python 签名与验证
from ecdsa import SECP256k1, SigningKey, VerifyingKey
# 生成密钥对
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
# 私钥与公钥(字节)
priv_bytes = sk.to_string()
pub_bytes = vk.to_string() # 未压缩 65 字节;.to_string(compressed=True) 为 33 字节
# 签名(对交易哈希签)
message = b"transfer 0.5 BTC to Bob"
signature = sk.sign(message) # 默认使用哈希算法,输出 DER 编码 (r, s)
# 验证
assert vk.verify(signature, message)
3.3 可延展性(Malleability)与签名规范
s 与 n - s 是同一个签名的两种表示(验证公式中都成立)。比特币的 BIP-62 / 低位 s 值 规范强制 s < n/2,避免第三方无密钥改写签名导致交易哈希变化。这是 BIP-146 最终落地的规则:
低 s 规范:若 s > n/2,则令 s' = n - s
好处:交易 ID 不被第三方篡改,为二层协议(如闪电网络)提供确定性
以太坊同样要求低位 s 值(v 值域约束),并在 EIP-2 中强制 0 ≤ s ≤ secp256k1n/2。
3.4 哈希类型(Hash Type)与签名的边界
| 哈希类型 | 覆盖内容 | 用途 |
|---|---|---|
SIGHASH_ALL | 全部输入输出,签名后不可修改 | 标准转账 |
SIGHASH_NONE | 仅输入,输出可被他人更改 | 见证人式签名 |
SIGHASH_SINGLE | 本输入与同索引输出 | 部分签名 |
| `SIGHASH_ALL | SIGHASH_ANYONECANPAY` | 仅本输入被锁定 |
一句话总结:ECDSA 让私钥持有者可以对任意交易数据出具"数学签名",验证方只需公钥就能确权——nonce 复用、s 值延展性、哈希覆盖范围,是实际工程中三个最容易被忽视的坑。
4. Merkle Tree:交易的压缩指纹
4.1 构造与节点
Merkle Tree 是满二叉树(叶子不足时复制最后一个叶子——比特币对奇数节点做双哈希):
Root
H(AB + CD)
┌────┴────┐
H_AB H_CD
┌──┴──┐ ┌──┴──┐
H_A H_B H_C H_D
│ │ │ │
TxA TxB TxC TxD
每个内部节点 H(X + Y)。最终 100 万笔交易的区块,根哈希只有 32 字节。
4.2 存在性证明(Inclusion Proof)
证明某笔交易在树中,只需提供从叶子到根的兄弟路径(sibling path):
import hashlib
def leaf_hash(tx: bytes) -> bytes:
return hashlib.sha256(hashlib.sha256(tx).digest()).digest() # double-SHA256
def verify_inclusion(tx_hash: bytes, merkle_root: bytes, siblings: list) -> bool:
"""
siblings: [(哈希, 方向)],方向 True 表示兄弟在左,False 表示兄弟在右
"""
current = tx_hash
for sibling_hash, is_left in siblings:
if is_left:
current = leaf_hash(sibling_hash + current)
else:
current = leaf_hash(current + sibling_hash)
return current == merkle_root
证明大小 = log2(N) 个哈希。N = 1,000,000 时只需约 20 个 32 字节哈希(640 字节)——这是轻客户端能工作的关键。
4.3 验证流程:Merkle Path 图示
要验证 TxB 在树中:
Root ← 比对最终结果
┌────┴────┐
H_AB H_CD ← 需要 H_AB 的兄弟 H_CD
↑ 提供 H_AB = H(H_A + H_B),其中 H_A 是 TxB 的兄弟
验证方只需要:TxB 的哈希 + H_A + H_CD。三个值即可从叶子推到根,比对根哈希即完成验证——O(log N) 存储、O(log N) 计算。
一句话总结:Merkle Tree 把"验证交易是否存在"的存储成本从"下载全部交易"降为"下载 log 个兄弟哈希",这是 SPV 轻钱包能够跑在手机上的前提。
5. SPV 轻客户端验证
5.1 完整节点 vs 轻节点
| 维度 | 全节点(Full Node) | 轻节点(SPV) |
|---|---|---|
| 存储 | 全部区块(比特币 ~600GB+) | 仅区块头(~100MB,2024 年量级) |
| 验证交易 | 执行全部脚本/状态 | Merkle 存在性证明 |
| 验证规则 | 完整共识规则 | 最长链 + 难度检查(简化) |
| 隐私 | 自己扫描全部交易 | 需向对方暴露请求的过滤条件(Bloom 过滤) |
| 典型场景 | 矿池、交易所、基础设施 | 钱包、浏览器插件 |
5.2 SPV 的工作流程
1. 轻节点下载所有区块头(80 字节/个),构建区块头链
2. 需要验证某笔交易时:
a. 向对等节点发起 Bloom filter / compact filter (BIP-157)
b. 对方返回包含该交易的区块 + 对应 Merkle 证明路径
c. 轻节点:用自己的区块头链,验证该区块的 prev_hash 链条有效
d. 轻节点:用 Merkle 证明验证交易哈希存在于该区块的 Merkle Root 中
e. 轻节点:等待 N 个确认(6 个确认 ≈ 99.99% 不可回滚)
// BIP-157 Compact Block Filters(getcfilters)伪代码流程
// 轻节点请求 "基本过滤器"(SipHash 编码的交易输出脚本集合)
async function spvVerify(txId, cfHeaders, blockHeaders) {
// 1. 校验过滤器头部链与区块头链的锚定关系
assert(anchorBlockHeaders(cfHeaders, blockHeaders));
// 2. 获取包含交易的区块及其 Merkle 证明
const { block, proof } = await peer.fetchFilteredBlock(txId);
// 3. 用区块头校验工作量证明与 prev_hash
assert(validPow(block.header));
// 4. 用 Merkle 路径校验交易存在
assert(verifyInclusion(txId, block.header.merkleRoot, proof));
// 5. 确认数达标(概率终局)
return currentHeight - block.header.height >= 6;
}
5.3 SPV 的信任边界
SPV 不是无条件安全的。攻击者可以伪造区块头链(如低难度假链)欺骗轻节点,对策是**检查链上累计难度(chainwork)**而非仅看长度。BIP-157 过滤器相比旧式 Bloom filter 更好的隐私性,也降低了"过滤器误报污染"问题。
一句话总结:SPV 用"只信区块头 + Merkle 证明"换来了千分之几的存储量,代价是放弃了对共识规则全量的独立验证——这是移动钱包在实用性与安全性之间的经典折中。
6. 密码学在区块链中的安全边界
6.1 量子威胁
Shor 算法可在多项式时间内破解椭圆曲线离散对数,但需要数千逻辑量子比特。当前量子计算进展下,secp256k1 仍是安全的,但生态已在为量子安全地址做准备:
| 方案 | 思路 | 代表 |
|---|---|---|
| 延迟签名 | 只在签名时揭示公钥 | 匿名/隐私协议 |
| 一次性地址 | 每笔交易新地址 | 隐私币(Monero 风格) |
| 格密码签名 | 抗量子 LWE/Dilithium | 未来的以太坊 EIP |
| Lamport 签名 | 哈希基一次性签名 | 比特币 BIP-xxx 讨论 |
6.2 密码学被破坏的现实案例
| 事故 | 原因 | 教训 |
|---|---|---|
| PlayStation 3(2010) | ECDSA nonce 复用 | 随机数必须真随机且唯一 |
| 安卓 SecureRandom 缺陷(2013) | 熵不足导致私钥可预测 | 钱包必须用 CSPRNG |
| Heartbleed(2014) | OpenSSL 内存泄漏 | 基础设施安全影响一切上层 |
| 已知曲线弱点 | 曲线迁移 | 不要自定义曲线参数 |
一句话总结:区块链的安全性建立在密码学算法参数被正确实现之上——“数学安全"不等于"工程安全”,随机数生成器、侧信道、协议级错误(nonce 复用)才是真正的高发风险。
7. 总结
区块链密码学四件套构成完整的安全闭环:
- 哈希函数:不可逆、抗碰撞,锁住历史数据(SHA-256 / Keccak-256)
- 椭圆曲线 secp256k1:单向标量乘法,提供密钥对的数学基础
- ECDSA 签名:不可伪造的所有权证明,配合 nonce 唯一性约束
- Merkle Tree + SPV:把整区块压缩成 32 字节根,让轻客户端低成本验证
这四层之上,才谈得上共识机制与智能合约。相关深入阅读:
- 区块链基础原理与共识机制 — 密码学之上,谁有记账权
- 比特币 UTXO 模型与隔离见证 — Script 脚本与 UTXO 生命周期
- 以太坊 EVM 与状态模型 — 账户模型与 gas 机制
- 区块链-web3 专题 — 从密码学走向 DApp 应用开发
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。