十六进制前缀编码是什么?状态树节点的路径与类型怎么塞进第一个半字节 图 1
十六进制前缀编码是什么?状态树节点的路径与类型怎么塞进第一个半字节 · 图 1

用区块浏览器或 eth_getProof 拿回一份状态证明时,你会看到一串奇怪的十六进制串,每个节点开头的第一个半字节往往藏着解读整个节点的钥匙。这就是十六进制前缀编码:默克尔帕特里夏树用它来压缩路径、标注节点类型。看明白第一个半字节,状态树节点就不再是一团乱码。本文以以太坊官方执行规范仓库对 merkle_patricia_trie 模块的记载为据,按先看字段、再谈含义的顺序拆解。

先讲直觉:树为什么需要半字节

帕特里夏树按位走。状态键是一个 32 字节哈希,展开是 64 个半字节(每个半字节 0 到 15 的一个十六进制数字),每个节点最多 16 个孩子,正好对应下一位的所有取值。分支节点存 16 个哈希指针;叶子节点在路径终点挂着值;还有一种扩展节点,专门把一段所有键共享的前缀压成一个节点,避免一串只有一个孩子的分支。规范文档把这三种节点讲得很直白:LeafNode 终止路径并携值,ExtensionNode 压缩公共半字节段,BranchNode 挂最多 16 个孩子并可选携带恰好终止于此的键值。

第一个半字节里的两位开关

十六进制前缀编码是什么的抽象机制示意

在序列化时,叶子的剩余路径和扩展节点的共享前缀都不按原始半字节存,而是先套一层紧凑编码。规范给出的格式是:首字节高位放一个旗标半字节,它的低两位各有分工——最低位编码路径长度的奇偶性(偶数为 0,奇数为 1),次低位区分叶子与扩展。剩下的高两位不用。奇偶位解决的是一个真实歧义:偶数长度的路径可以两两配对成整字节,奇数长度会多出半个字节,编码规则是奇数时把路径的第一个半字节直接塞进旗标所在的字节,剩下的半字节再成对排下去;偶数长度则在旗标后面垫一个 0 半字节凑对。所以解码器的判断顺序是:读旗标,判类型,看奇偶决定是否从首字节低半字节捡起路径的第一个数字,然后逐字节展开。判断链到此闭合,节点后面的 RLP 载荷该按哪两种结构解析也就定了。

为什么键要先哈希:安全树的深意

顺带一个容易忽略的事实:状态树并不直接拿地址当键,而是拿 keccak256 哈希后的值,存储树对槽号做同样的事,规范把这叫安全树。先哈希的作用有二:把键均匀撒进半字节空间,让树的深度贴着以 16 为底的对数走;同时堵住攻击者精心构造共享长前缀的地址、制造超深路径去撑爆证明体积的通道。对读证明的人来说,这意味着路径上出现意外规整的公共前缀本身就是异常信号。

核对一份证明时的读法

把上面拼成操作顺序:从可信的状态根出发,逐节点取回并本地哈希比对,哈希必须和上一层指针一致;解开 RLP,先看载荷是几项、首元素首半字节的旗标位是什么,确定它是分支、扩展还是叶子;扩展节点要求共享段与当前已消耗的路径吻合,否则这条路径在树里根本不存在——键不在树上的否定证明正是靠在某一层发现对应孩子指针为空来完成的;到达叶子后,剩余路径必须精确等于键未消耗的半字节,再解出账户或存储值。如果某个节点的哈希对不上指针,说明提供的节点序列被篡改过,后面的字段再漂亮也不用看;如果旗标位组合出高两位非零这类不该出现的形态,基本可以断定是解码实现出的问题而不是链的问题。规范对每个字段的定义都在执行规范仓库里,遇到边角案例以当版本源码为准。本文是数据结构的核验方法说明,不构成投资建议。