1096 字
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 | 多棵树的链式排列,支持追加 | Tombchain、OpenTimestamps |
与其他完整性验证方案对比:
| 方案 | 验证代价 | 证明大小 | 动态更新 |
|---|---|---|---|
| 全量哈希 | O(n) | O(1) | 重新哈希全部 |
| Merkle 树 | O(log n) | O(log n) | O(log n) |
| 布隆过滤器 | O(k) | O(m) | 不支持删除 |
五、多语言实现
Go 实现
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 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}TypeScript 实现
function hash(data: string): string { let h = 0x811c9dc5; for (let i = 0; i < data.length; i++) { h ^= data.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0).toString(16).padStart(8, "0");}
class MerkleTree { private leaves: string[]; private layers: string[][];
constructor(data: string[]) { // 奇数个叶子时复制最后一个 if (data.length % 2 !== 0) data.push(data[data.length - 1]); this.leaves = data.map((d) => hash(d)); this.layers = [this.leaves]; this.build(); }
private build(): void { let current = this.leaves; while (current.length > 1) { const next: string[] = []; for (let i = 0; i < current.length; i += 2) { next.push(hash(current[i] + current[i + 1])); } this.layers.push(next); current = next; } }
getRoot(): string { const top = this.layers[this.layers.length - 1]; return top[0]; }
// 获取验证某叶子所需的兄弟哈希路径 getProof(index: number): { hash: string; isRight: boolean }[] { const proof: { hash: string; isRight: boolean }[] = []; let idx = index; for (let layer = 0; layer < this.layers.length - 1; layer++) { const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1; proof.push({ hash: this.layers[layer][siblingIdx], isRight: siblingIdx > idx, }); idx = Math.floor(idx / 2); } return proof; }
// 验证叶子属于这棵树 static verify( leafHash: string, proof: { hash: string; isRight: boolean }[], rootHash: string ): boolean { let current = leafHash; for (const step of proof) { current = step.isRight ? hash(current + step.hash) : hash(step.hash + current); } return current === rootHash; }}六、生产验证
| 项目 | 源码 | 用途 |
|---|---|---|
| Git | tree.c#L136-L171 | 每个 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
- Bitcoin Developer Guide - Merkle Trees - Bitcoin 中 Merkle 树的作用
- IPFS Merkle DAG - IPFS 中 Merkle DAG 的设计与实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
Merkle 树(Merkle Tree)
https://blog.souloss.com/posts/programming/data-structures/data-structures-merkle-tree/ 部分信息可能已经过时
相关文章 智能推荐






