以太坊MPT构建(上)
字数 1551 2025-08-22 12:23:12

以太坊MPT(Merkle Patricia Tree)构建详解

一、MPT概述

MPT(Merkle Patricia Tree)是以太坊中用于存储任意键值对(key->value)的树形结构,主要用于存储:

  • 用户状态信息
  • 交易信息
  • 交易收据

MPT是三种数据结构的组合:

  1. Trie(字典树)
  2. Patricia Trie(前缀树)
  3. 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树:

  1. 交易树(Tx root)
  2. 收据树(Receipt root)
  3. 状态树(State root)

节点类型前缀:

  • 0:扩展节点,偶数个半字节
  • 1:扩展节点,奇数个半字节
  • 2:叶子节点,偶数个半字节
  • 3:叶子节点,奇数个半字节

四、MPT节点类型

1. 叶子节点(Leaf)

  • 包含两个字段:
    1. 剩余Key的半字节编码
    2. Key对应的Value

2. 扩展节点(Extension)

  • 包含两个字段:
    1. 剩余Key的半字节编码
    2. 下一个节点的引用(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. 树的检索

检索流程:

  1. 调用Get(key)方法
  2. 内部调用TryGet(key)
  3. 最终调用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. 树的更新

更新流程:

  1. 调用Update(key, value)
  2. 内部调用TryUpdate(key, value)
  3. 根据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设计中考虑了以下安全问题:

  1. 数据完整性:通过Merkle Tree特性保证
  2. 状态回滚:通过树结构实现快速回滚
  3. 轻节点验证:支持默克尔证明,实现SPV
  4. 内存安全:处理未加载节点时进行安全加载

七、性能优化

  1. 节点共享:相同前缀的节点共享存储
  2. 惰性加载:hashNode实现按需加载
  3. 内存优化:通过合并节点减少内存使用
  4. 快速哈希:子树哈希缓存和复用

八、总结

MPT是以太坊核心数据结构,它结合了:

  • Trie的高效检索
  • Patricia Trie的空间优化
  • Merkle Tree的数据完整性验证

通过精心设计的节点类型和操作算法,MPT实现了:

  1. 高效的状态存储
  2. 快速的状态验证
  3. 便捷的状态回滚
  4. 安全的轻节点验证

理解MPT的实现机制对于深入理解以太坊状态管理和数据验证至关重要。

以太坊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. 关键文件 2. 数据结构定义 3. 树的创建 4. 树的检索 检索流程: 调用 Get(key) 方法 内部调用 TryGet(key) 最终调用 tryGet(t.root, keybytesToHex(key), 0) 5. 树的更新 更新流程: 调用 Update(key, value) 内部调用 TryUpdate(key, value) 根据value长度决定插入或删除操作 插入操作实现: 6. 树的删除 删除操作实现: 六、安全考虑 MPT设计中考虑了以下安全问题: 数据完整性 :通过Merkle Tree特性保证 状态回滚 :通过树结构实现快速回滚 轻节点验证 :支持默克尔证明,实现SPV 内存安全 :处理未加载节点时进行安全加载 七、性能优化 节点共享 :相同前缀的节点共享存储 惰性加载 :hashNode实现按需加载 内存优化 :通过合并节点减少内存使用 快速哈希 :子树哈希缓存和复用 八、总结 MPT是以太坊核心数据结构,它结合了: Trie的高效检索 Patricia Trie的空间优化 Merkle Tree的数据完整性验证 通过精心设计的节点类型和操作算法,MPT实现了: 高效的状态存储 快速的状态验证 便捷的状态回滚 安全的轻节点验证 理解MPT的实现机制对于深入理解以太坊状态管理和数据验证至关重要。