1059 字
3 分钟
Merkle 树(Merkle Tree)
一、为什么需要 Merkle 树
假设你管理着一个包含 100 万条记录的数据库,如何确认其中某一条记录没有被篡改?
最直觉的做法是:把所有数据重新哈希一遍,跟已知的摘要做比对。但这样做代价太高,验证一条记录需要读取并哈希 100 万条数据。
Merkle 树解决了这个问题。它把数据组织成一棵哈希二叉树,每个内部节点存储子节点哈希的哈希。要验证单个叶子,只需要从叶子到根路径上的 O(log n) 个兄弟哈希,就像检查一排封条中哪一层被破坏了一样。
这个数据结构是 Git、ZFS、Bitcoin、IPFS 等系统的基石。
二、现实类比
想象一个防篡改的运输封条系统。每个箱子贴上独一无二的蜡封,箱子装入板条箱后,板条箱的封印由所有箱子的蜡封推导而来。板条箱再装入集装箱,集装箱有自己的总封印。如果其中一件物品被掉包,它上方的每一层封印都会破损,你只需检查 log(n) 个封印就能定位被篡改的箱子,而无需打开所有箱子。
三、核心思想
Merkle 树是一棵哈希二叉树。每个叶子节点包含数据块的哈希值,每个内部节点包含其两个子节点拼接后的哈希值。根哈希是整个数据集的指纹。
graph TB
R["Root Hash<br>H(H12 + H34)"]
R --> H12["H12<br>H(H1+H2)"]
R --> H34["H34<br>H(H3+H4)"]
H12 --> H1["H1<br>hash(Data A)"]
H12 --> H2["H2<br>hash(Data B)"]
H34 --> H3["H3<br>hash(Data C)"]
H34 --> H4["H4<br>hash(Data D)"]
H1 --- A["Data A"]
H2 --- B["Data B"]
H3 --- C["Data C"]
H4 --- D["Data D"]
style R fill:#f97316,stroke:#9a3412,color:#fff
style H12 fill:#fbbf24,stroke:#92400e
style H34 fill:#fbbf24,stroke:#92400e
验证 Data C 的过程:
- 计算 H3 = hash(Data C)
- 用兄弟 H4 计算 H34 = hash(H3 + H4)
- 用叔伯 H12 计算根 = hash(H12 + H34)
- 比较根哈希是否与已知的根哈希一致
只需要 H4 和 H12 两个兄弟节点,就能完成验证,这就是 O(log n) 的魅力。
| 属性 | 值 |
|---|---|
| 验证代价 | 每个叶子 O(log n) 次哈希 |
| 树构建 | O(n) 次哈希 |
| 证明空间 | O(log n) 个兄弟哈希 |
| 篡改检测 | 任何修改都会改变根哈希 |
四、变体与对比
| 变体 | 特点 | 适用场景 |
|---|---|---|
| 二叉 Merkle 树 | 每个节点两个子节点,最常见 | Bitcoin |
| Merkle Patricia Tree | 结合 Patricia 树和 Merkle 树,支持键值查找 | 以太坊状态树 |
| Merkle DAG | 有向无环图而非树,允许多父节点 | IPFS、Git |
| Merkle Mountain Range | 多棵树的链式排列,支持追加 | Grin、OpenTimestamps |
与其他完整性验证方案对比:
| 方案 | 验证代价 | 证明大小 | 动态更新 |
|---|---|---|---|
| 全量哈希 | O(n) | O(1) | 重新哈希全部 |
| Merkle 树 | O(log n) | O(log n) | O(log n) |
五、多语言实现
package merkle
import ( "crypto/sha256" "encoding/hex")
// MerkleTree 表示一棵 Merkle 树type MerkleTree struct { Root *Node Leaves []*Node}
// Node 表示树中的一个节点type Node struct { Hash string Left *Node Right *Node}
// NewMerkleTree 从数据块列表构建 Merkle 树func NewMerkleTree(data [][]byte) *MerkleTree { // 构建叶子节点 var leaves []*Node for _, d := range data { hash := sha256.Sum256(d) leaves = append(leaves, &Node{Hash: hex.EncodeToString(hash[:])}) }
// 奇数个叶子时复制最后一个 if len(leaves)%2 != 0 { leaves = append(leaves, leaves[len(leaves)-1]) }
// 自底向上构建树 currentLevel := leaves for len(currentLevel) > 1 { var nextLevel []*Node // 奇数个节点时复制最后一个,避免访问越界 if len(currentLevel)%2 != 0 { currentLevel = append(currentLevel, currentLevel[len(currentLevel)-1]) } for i := 0; i < len(currentLevel); i += 2 { left := currentLevel[i] right := currentLevel[i+1] combined := left.Hash + right.Hash hash := sha256.Sum256([]byte(combined)) parent := &Node{ Hash: hex.EncodeToString(hash[:]), Left: left, Right: right, } nextLevel = append(nextLevel, parent) } currentLevel = nextLevel }
return &MerkleTree{Root: currentLevel[0], Leaves: leaves}}
// VerifyProof 验证某个叶子属于这棵树func VerifyProof(leafHash string, proof []string, indices []bool, rootHash string) bool { current := leafHash for i, sibling := range proof { combined := sibling + current if indices[i] { combined = current + sibling } hash := sha256.Sum256([]byte(combined)) current = hex.EncodeToString(hash[:]) } return current == rootHash}六、生产验证
| 项目 | 源码 | 用途 |
|---|---|---|
| Git | tree.c#L167-L173 | 每个 commit、tree、blob 都通过 SHA-1 内容寻址,构成 Merkle DAG。修改任何文件中的一个字节都会改变到根 commit 的所有哈希,使 git fsck 能高效验证仓库完整性 |
| ZFS | blkptr.c | 每个块在父块指针中存储校验和,从数据块到 uberblock 形成 Merkle 树。zpool scrub 遍历此树检测静默数据损坏(bit rot) |
| Bitcoin | merkle.cpp | 区块头只存储 Merkle 根哈希,SPV 轻节点通过 Merkle 证明验证交易存在性,无需下载完整区块 |
七、小结
何时使用:
- 需要高效验证大数据集中单条记录的完整性
- 参与方之间互不信任,需要密码学证明
- 网络带宽有限,只传输必要的证明路径
- 数据需要追加写入并持续验证
何时不用:
- 数据量很小(直接全量哈希更简单)
- 所有参与方完全可信(不需要验证机制)
- 频繁的随机更新和删除(树重建代价高)
- 不需要完整性验证的场景
八、参考资料
- Merkle Tree - Wikipedia - Merkle 树概念与历史
- Battle-Tested Patterns: Merkle Tree - 交互式可视化与多语言实现
- Git Internals - Git Objects - Git 对象模型与 Merkle DAG
- IPFS Merkle DAG - IPFS 中 Merkle DAG 的设计与实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章智能推荐








