以太坊MPT构建(上)
字数 1551 2025-08-22 12:23:12
以太坊MPT(Merkle Patricia Tree)构建详解
一、MPT概述
MPT(Merkle Patricia Tree)是以太坊中用于存储任意键值对(key->value)的树形结构,主要用于存储:
- 用户状态信息
- 交易信息
- 交易收据
MPT是三种数据结构的组合:
- Trie(字典树)
- Patricia Trie(前缀树)
- Merkle Tree(哈希树)
特点:
- 插入、查找和删除效率都是O(Log(N))
- 可以存储任意长度的key-value键值对数据
- 提供交易状态快速回滚机制
- 提供快速计算数据集哈希标识的机制
- 支持默克尔证明,实现简单支付验证(SPV)
二、基础数据结构
1. Trie Tree(字典树)
特点:
- 多叉树结构(如英文字母是26叉树,数字是10叉树)
- 利用字符串公共前缀节约存储空间
- 无公共前缀时内存消耗大
特性:
- 根节点不包含字符
- 除根节点外每个节点只包含一个字符
- 每个节点的所有子节点包含的字符串不相同
- 节点对应字符串由根到该节点路径上的字符连接而成
2. Patricia Trie(前缀树)
与Trie的区别:
- Trie为每个字符串分配一个节点
- 前缀树将长且无公共节点的字符串Trie退化为数组
- 节点前缀相同时使用公共前缀,否则将剩余节点插入同一节点
3. Merkle Tree(哈希树)
特点:
- 叶子节点是数据块的哈希值
- 非叶节点是其子节点串联字符串的哈希
- 通过Top Hash可验证数据完整性
- 任何数据变动都会导致Top Hash变化
三、MPT在以太坊中的应用
每个区块头包含三棵MPT树:
- 交易树(Tx root)
- 收据树(Receipt root)
- 状态树(State root)
节点类型前缀:
0:扩展节点,偶数个半字节1:扩展节点,奇数个半字节2:叶子节点,偶数个半字节3:叶子节点,奇数个半字节
四、MPT节点类型
1. 叶子节点(Leaf)
- 包含两个字段:
- 剩余Key的半字节编码
- Key对应的Value
2. 扩展节点(Extension)
- 包含两个字段:
- 剩余Key的半字节编码
- 下一个节点的引用(n/J/j)
3. 分支节点(Branch)
- 包含17个字段:
- 前16个字段对应16个可能的半字节值
- 第17个字段存储在当前节点结束的值
五、源码实现分析
1. 关键文件
|- commiter.go 节点提交操作
|- database.go 内存中Trie操作
|- encoding.go 编码转换
|- hasher.go 计算子树哈希
|- iterator.go 枚举接口
|- node.go 节点类型和解析代码
|- sync.go SyncTrie实现
|- secure_trie.go SecureTrie实现
|- proof.go 为key构造merkle证明
|- trie.go Trie增删改查
2. 数据结构定义
type (
fullNode struct {
Children [17]node // 分支节点
flags nodeFlag
}
shortNode struct { // 扩展节点
Key []byte
Val node
flags nodeFlag
}
hashNode []byte // 哈希节点
valueNode []byte // 叶子节点
)
type Trie struct {
db *Database
root node // 根节点
unhashed int // 未哈希的叶子节点数
}
3. 树的创建
func New(root common.Hash, db *Database) (*Trie, error) {
if db == nil {
panic("trie.New called without a database")
}
trie := &Trie{db: db}
if root != (common.Hash{}) && root != emptyRoot {
rootnode, err := trie.resolveHash(root[:], nil)
if err != nil {
return nil, err
}
trie.root = rootnode
}
return trie, nil
}
4. 树的检索
检索流程:
- 调用
Get(key)方法 - 内部调用
TryGet(key) - 最终调用
tryGet(t.root, keybytesToHex(key), 0)
func (t *Trie) tryGet(origNode node, key []byte, pos int) (value []byte, newnode node, didResolve bool, err error) {
switch n := (origNode).(type) {
case nil:
return nil, nil, false, nil
case valueNode:
return n, n, false, nil
case *shortNode:
// 处理扩展节点...
case *fullNode:
// 处理分支节点...
case hashNode:
// 处理未加载的哈希节点...
default:
panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode))
}
}
5. 树的更新
更新流程:
- 调用
Update(key, value) - 内部调用
TryUpdate(key, value) - 根据value长度决定插入或删除操作
func (t *Trie) TryUpdate(key, value []byte) error {
t.unhashed++
k := keybytesToHex(key)
if len(value) != 0 {
_, n, err := t.insert(t.root, nil, k, valueNode(value))
if err != nil {
return err
}
t.root = n
} else {
_, n, err := t.delete(t.root, nil, k)
if err != nil {
return err
}
t.root = n
}
return nil
}
插入操作实现:
func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
if len(key) == 0 {
if v, ok := n.(valueNode); ok {
return !bytes.Equal(v, value.(valueNode)), value, nil
}
return true, value, nil
}
switch n := n.(type) {
case *shortNode:
// 处理扩展节点插入...
case *fullNode:
// 处理分支节点插入...
case nil:
return true, &shortNode{key, value, t.newFlag()}, nil
case hashNode:
// 处理未加载节点的插入...
default:
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
}
}
6. 树的删除
删除操作实现:
func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
switch n := n.(type) {
case *shortNode:
// 处理扩展节点删除...
case *fullNode:
// 处理分支节点删除...
case valueNode:
return true, nil, nil
case nil:
return false, nil, nil
case hashNode:
// 处理未加载节点的删除...
default:
panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key))
}
}
六、安全考虑
MPT设计中考虑了以下安全问题:
- 数据完整性:通过Merkle Tree特性保证
- 状态回滚:通过树结构实现快速回滚
- 轻节点验证:支持默克尔证明,实现SPV
- 内存安全:处理未加载节点时进行安全加载
七、性能优化
- 节点共享:相同前缀的节点共享存储
- 惰性加载:hashNode实现按需加载
- 内存优化:通过合并节点减少内存使用
- 快速哈希:子树哈希缓存和复用
八、总结
MPT是以太坊核心数据结构,它结合了:
- Trie的高效检索
- Patricia Trie的空间优化
- Merkle Tree的数据完整性验证
通过精心设计的节点类型和操作算法,MPT实现了:
- 高效的状态存储
- 快速的状态验证
- 便捷的状态回滚
- 安全的轻节点验证
理解MPT的实现机制对于深入理解以太坊状态管理和数据验证至关重要。