Aptos 的状态根不是一棵逐层二分的二叉树算出来的。它的默克尔化结构叫 Jellyfish Merkle Tree,在以太坊的十六叉 Patricia 基础上又改了几处,把读一次状态要做的磁盘读次数压下来。本文按官方实现文档与 2021 年的设计论文说明它为什么长成水母形状、节点键为什么带版本号,以及证明为什么比原来短。
目标不是省空间,是省 IO 次数

实现文档开篇就写明,JMT 是为基于 Log-Structured Merge 树的键值存储优化的稀疏默克尔树,设计目标是同时省下计算和空间,服务于需要维护全局状态的链。它面对的矛盾很具体:二叉树每下一层都要多读一次节点,而磁盘读次数直接决定验一个键的延迟;十六叉树一次读能看十六个孩子,但空着的槽位仍要占结构和哈希成本。JMT 的答案是内部节点带十六个孩子,一个内部节点代表标准默克尔树的四层,读一次顶四层——论文的说法是把查询一棵三十一节点树所需的操作压进一个内部节点。
空子树整块跳过:稀疏性被认真使用
树的逻辑形态是两百五十六位键对应的稀疏默克尔树,但只要某个子树里没有叶子、或者只剩一个叶子,就不再逐层建节点,而是用占位哈希或那个叶子本身替掉整段。代价是键的前缀路径长度变得不固定:叶子密集的区域深,稀疏的区域浅。好处直接反映在读放大上——实现文档指出,在当前主网规模下假设树的多数层已在缓存中,一次读只按一到两个四千字节页计费;论文用同一口径给过数量级对照:两百五十六位键、十亿叶子的树平均高度约八个半字节,节点键平均约十二字节,明显小于三百二十六位哈希键。
物理结构上实现文档说明它与以太坊改写过的 Patricia 树相似,但没有扩展节点这个类型;只有内部节点与叶子节点两种,序列化用位图记录十六个槽中哪些存在、哪些直接指向叶子。
另一处改动在寻址方式。JMT 不用哈希后的键做节点键,而是把版本号编进节点键。实现文档给出的收益有三层:便于按版本分片、在 RocksDB 这类存储引擎上把整理开销压到接近零、平均键长更短。原理是键的字典序天然与版本先后一致,新版本产生的节点总是追加在当前键集之后,不必为一次插入在数千万键里随机落点。论文给过一个数量级对照:两百五十六位键、十亿叶子的树平均高度约八个半字节,节点键平均约十二字节,明显小于三百二十位哈希键定长三十六字节的方案。
分片体现在结构上:根之下的顶层把键空间按第一个半字节切成十六份,各自长成独立子树,顶层节点单独存在元数据库里。批量写入时可以逐分片算批、再合并出根,这也解释了为什么它适合同一高度大量写入的场景。
证明更短,验证要多算一步
稀疏优化直接改写了默克尔证明的形状。论文把证明分成三类:目标键的叶子在(存在证明);同前缀位置存在另一个键的叶子(间接证明不存在);路径上撞到占位空节点(证明不存在)。由于连续的空层被折叠,兄弟摘要个数平均随实际存在的叶子数增长,而不是随等价树的总高度增长——同一份论文对未优化的树给出的口径是随最大叶子数即树高增长。折中是验证逻辑更复杂:需要先按位图还原哪些层被折叠,再逐层哈希回根。论文的态度很明确,用算法复杂度换证明体积与验证计算量,且这层复杂度对终端用户透明。
边界
JMT 是存储与证明的优化,不改变共识,也不改变”谁能写状态”的权限模型;版本化的节点键意味着历史状态可按版本追溯,但能追溯到多远由节点保留策略与修剪决定,实现文档明确指出根哈希缺失(例如版本已被修剪)是一种正常错误。各版本实现的具体参数与是否分片属于工程细节,应以对应版本的源码与文档为准。
风险提示:本文仅为机制解释,不构成投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。