给状态树配一本直查账:NEAR 平坦存储的提速与它的协议代价 图 1
给状态树配一本直查账:NEAR 平坦存储的提速与它的协议代价 · 图 1

在 NEAR 节点里查一个账户的某个存储键,默认动作是从状态树的根出发,沿哈希指针一层层往下走。树深多少层,就要做多少次数据库查找。nearcore 的架构文档用一整篇的篇幅记录了给这个问题开捷径的全部代价:平坦存储(flat storage)本质上是给 trie 里的值复制一本按键直排的账,把查找从树深度次降为常数次,但这本账牵出协议改动、双副本一致性和读写不对称三个连锁问题。本文按这份文档的说法逐项展开。

起点:状态树为什么慢

文档交代了历史包袱:主网启动时整个状态树直接嵌在 RocksDB 的一个列族里,键是 trie 节点的 borsh 编码哈希。这种内容寻址的存储天然可自校验,也天然支持时间旅行——只要知道某个 chunk 的状态根,从根出发就能解析任意历史版本。平坦存储是像数据库索引那样的一份冗余拷贝,用最简单的方式说,就是用哈希表代替 trie,单次查找从深度 d 的对数级降到常数级。但文档紧接着说,魔鬼在细节:状态每个 chunk 都在变,节点既要能回看旧版本,又要能解析属于另一条分叉的 chunk,而这些能力在纯 trie 里几乎是免费的。

gas 计费被路径绑住

文档点名了一个容易被忽略的耦合:在 NEAR 上,状态查找走过的 trie 路径会影响 gas 消耗,也就是查找算法泄漏进了协议。这意味着不能哪天心血来潮就切换查找方式——改了查找路径就等于改了所有交易的 gas 结果,共识会直接分裂。可行的路线只剩一种:先做数据迁移、让平坦存储与 trie 并存并保持同步(这期间维护它是纯开销),然后在启用新行为的协议版本到达的那个纪元切换点,所有客户端同时改用新查找方式。没完成迁移的节点会因 gas 结果不一致而无法出块。文档也把根治方向写明了:让 gas 成本与树中位置脱钩,这个耦合才会消失。

示意:状态树路径查找与键直排查找两条路径

快从哪来:不是算法,是布局

一份诚实的工程文档难得的地方是它敢拆自己的台。平坦存储单次读取要做两次数据库请求:先找到值的引用,再解引用取值;trie 查找要做 d 次节点查找加一次解引用。听着是 d 比 2 大,但文档给出了实测视角:主网工作负载对 trie 节点的缓存命中率常年在九成九,d 的典型值在 10 到 20 之间,期望上 trie 查找的数据库请求反而更少。平坦存储真正的优势在数据布局:键有序带来 RocksDB 块缓存更高的命中率,列族也更小;文档记载单值读取观察到过百倍以上的加速,并明确注明这份收益来自布局而非算法。

写路径:为什么捷径帮不上

更新一个状态值时,默克尔树上所有祖先节点都要重算哈希,而新状态根必须及时算出来——它要写进 chunk 头。为了重算路径上的哈希,你还是得把从根到叶子的那些节点全部读一遍,恰好是平坦存储想省掉的开销。文档的结论因此很克制:对写路径来说平坦存储在算法层面没有意义,仍要做同样多次的树操作,能指望的只是键有序带来的局部性红利;真正吃红利的是只读场景,比如读密集的合约查询和视图调用。截至文档记录的 2023 年 3 月,实现是只读版:只对未最终化的前沿区块和最终区块生效,归档查询和视图调用仍直接走 trie。这份笔记式的坦白——双副本随时可能失同步、需要一个快速比对工具来判定哪本账是对的——比任何性能宣传都更能说明状态工程的分量。对普通用户,这一切最终只是体感差异:同样的节点,查余额快了多少毫秒;但对跑节点和写合约的人,状态访问结构是牵一发动全身的协议地层。