Merkle Patricia Tree
参考文献:
- Merkle Tree & Hash-based SIG
- Merkle Tree 实现
- 以太坊 Merkle Patricia Tree 全解析
- Merkle Patricia Tree (梅克尔帕特里夏树) 详解
文章目录
- MPT 结构
- 基本操作
- Get
- Insert
- Delete
- 其他
MPT 结构
Merkle Patricia Tree 是以太坊的关键算法之一。它是一种 16 进制的前缀树(key 编码在 Path 中),具有三种节点:
- Branch,
- 具有至少 2 个孩子,至多 16 个孩子
- 具有以该路径结束(key)的对应 value
- 附带字段
nodeFlag
,包括hash
, 本节点的 Hash 值gen
, 诞生标记,最近一次被写入/修改的时间dirty
,节点是否被修改,若是则hash
置空以更新
- Leaf,
- 和 Branch 类似,除了把
children
换成key
- 这个
key
用于处理一段后缀不存在相同前缀其他节点的情况
- 和 Branch 类似,除了把
- Extension,
- 与 Leaf 相同,除了把
value
替换为ptr/idx
- 这个
ptr/idx
用于持久化存储 MPT 时的检索
- 与 Leaf 相同,除了把
以太坊中 Leaf 和 Extension 共享定义,为了在持久化存储中做区分,需要将 Hex 编码替换为 Hex-Prefix 编码。
- 如果 Extension 的
key
为偶数长度 Hex 编码,那么在最前面补0x00
- 如果 Extension 的
key
为奇数长度 Hex 编码,那么在最前面补0x1-
- 如果 Leaf 的
key
为偶数长度 Hex 编码,那么在最前面补0x20
- 如果 Leaf 的
key
为奇数长度 Hex 编码,那么在最前面补0x3-
- 最终的
prefix || key
是偶数长度的 Hex 编码
MPT 的整体结构如下:
- 树根是一个 Extension 节点,它的
ptr/idx
是其 Branch(或者 Leaf,如果是树桩)子节点的 Hash 值 - Branch 节点的
children
存储 16 个孩子各自的 Hash 值 - Branch 节点的
children
可以是 Leaf 或者 Extension 节点 - Extension 节点的
ptr/idx
是其子节点(branch 或者 leaf)的 Hash 值 - 如果该路径是多个
key
的前缀,那么创建 Extersion 以及 Branch - 如果该路径只是单个
key
的前缀,那么创建 Leaf
换句话说,extension 的作用是存储 branch 或 leaf 的 Hash 值(也是索引),branch 的作用是存储最少 2 个、最多 16 个子树的 Hash 值(也是索引),通常 leaf 的父节点是 branch(除非只有一个 extension 作为树桩的根)
基本操作
Get
假设需要查找 inp_key
的节点,
- 当前是 leaf 节点,判断它的
path || key
是否等于inp_key
,输出它的value
或者false
- 当前是 extension 节点,判断它的
path || key
是否是inp_key
的前缀,决定是否递归检索ptr/idx
指向的子节点,或者输出false
- 当前是 branch 节点,如果
path
等于inp_key
,那么输出它的value
- 当前是 branch 节点,如果
path
是inp_key
的前缀,那么递归检索下一个 hex 所对应的children
节点(leaf, extension, empty)
Insert
假设需要插入 inp_key
的节点,
- 利用 Get 方法,获取
inp_key
所能匹配的最长前缀节点 - 如果它是 leaf 节点,判断
path || key
是否等于inp_key
,- 相等,那么更新它的
value
- 否则,新建一个 extension 节点和 branch 节点,前者的
key
是最长的公共前缀,后者的两个children
分别指向原始的 leaf(或者填入value
如果是前缀的话)和一个新建的 leaf
- 相等,那么更新它的
- 如果它是 extension 节点,过程和 leaf 的相似
- 如果它是 branch 节点,
- 如果
path
等于inp_key
,那么更新它的value
- 如果
path
是inp_key
的前缀,那么将对应的children
指向一个新建的 leaf
- 如果
- 最后,从下向上重建 Merkle Tree
Delete
假设需要删除 inp_key
的(单个)节点,
- 利用 Get 方法,获取
inp_key
所能匹配的最长前缀节点 - 如果它是 leaf 节点,判断
path || key
是否等于inp_key
,- 相等,那么删除它,并判断它的 branch 父节点是否仍具有至少 2 个孩子(包括它的
value
视为一个孩子),作相应的合并调整 - 否则,不存在
inp_key
节点,返回false
- 相等,那么删除它,并判断它的 branch 父节点是否仍具有至少 2 个孩子(包括它的
- 如果它是 extension 节点,那么
- 如果
path
等于inp_key
,这是一种非法操作(删除整个子树?) - 如果
path || key
不等于inp_key
,那么返回false
- 不会发生
path || key
是inp_key
前缀的情况,这一定会匹配到 extension 的孩子
- 如果
- 如果它是 branch 节点,
- 如果
path
等于inp_key
,那么删除value
,但是不要递归删除它的孩子(这个 branch 存储的完整 key 是其他 key 的前缀) - 否则,这意味着不存在
inp_key
节点(不然最长前缀节点不会是 branch 而应该是 extension 或 leaf),返回false
- 如果
- 最后,从下向上重建 Merkle Tree
其他
如果要求 {key: value}
具有相同长度的键,那么 branch 节点可以移除其 value
域(此时不存在某路径是其他路径的前缀),相应的 Get、Insert、Delete 可以被适当简化。
每个节点都是变长的连续数据块(均包含 key
和 value
域之一),节点的引用是 Hash 值。在内存中,可以利用 std::map
来管理;在磁盘中,如果树很大(无法一次性加载到内存)则使用数据库来管理。