Skip to main content
Leafage 里的“状态”专指 EVM 账户状态:每个地址的 balance、nonce、code,以及每个合约的 storage 槽位。eth_call 及所有状态读取接口只依赖这四样东西,查询节点因此只存这四样东西。

为什么只需要账户状态

一个全节点的数据库里,账户状态只占一小部分。其余是交易体、收据、日志,以及为了验证和出块而维护的 Merkle Patricia Trie。 去掉 MPT 意味着查询节点可以用扁平的键值存储(RocksDB 或 MDBX):账户直接按地址查,存储槽直接按 address || slot 查。

状态差异(StateDiff)

状态差异是一个区块执行前后账户状态的变更集。写节点在 OnCommit 钩子拿到 StateDB 的提交集后直接转换,不需要重放交易。
两个设计点决定了它在 S3 上的形态:
  • 以 state root 而不是区块哈希为键{chainID}[/{version}]/{stateRoot}/stateDiff。state root 相同的相邻区块(典型是空块)共享同一个对象,查询节点发现父块与当前块 state root 相同时直接视为空差异,不拉取。
  • 用 RLP 而不是 JSON 编码:它只被 Leafage 内部组件消费,不需要可读性,体积优先。
区块头单独存为 {chainID}[/{version}]/{blockHash}/block。查询节点按哈希取头、按 state root 取差异,两者合起来就是应用一个区块所需的全部输入。

差异层与状态树

查询节点不把每个区块的差异直接写进数据库,而是先在内存里以**差异层(DiffLayer)**的形式保留一段时间。所有差异层组成一条链表,叫状态树(StateTree):
读取:从目标区块对应的层开始向下遍历,命中即返回;所有层都未命中,落到磁盘。绝大多数请求指向 latest,因此通常是纯内存操作。 窗口:内存中最多保留 --diff-depth-limit(默认 64)层。这个窗口有双重含义:它既是 State 节点能直接回答的历史深度,也是能在内存中处理的最大 reorg 深度,更深的重组需要走 S3 回补。 分叉:分叉块的差异层进入 hash_diff_map 但不进入 num_diff_map。按哈希查询仍能访问分叉状态,按高度查询始终走规范链。

终结化

区块深度超过窗口时,最老的一层被刷入数据库并从内存移除,这一步叫终结化。State 节点和 Archive 节点在这里分道:
覆盖写,只保留最新值。磁盘上不存在历史版本。请求的高度一旦离开内存窗口,节点返回 -39006 BlockNotFound

状态从哪里来

查询节点的状态有三个来源,对应三种场景: 全新节点的常规启动方式是先下载 RocksDB 快照,再从快照高度追赶到链头,通常分钟级完成,而不是从创世块重放。
追赶靠近链头时可能落到错误的分支上。--catchup-safe-depth 让链头附近这些区块改为沿通知里的 parent-hash 链回补,取值应大于该链的最大 reorg 深度。

小结

  • 状态 = 账户的 balance、nonce、code、storage;查询节点只存这些,不存 MPT 和交易。
  • StateDiff 是一个区块的状态变更集,以 state root 为键存在 S3 内部桶,空块不产生新对象。
  • 最近的区块以差异层留在内存,窗口默认 64 层,既是 State 节点的可查深度也是 reorg 处理上限。
  • 超出窗口的层被终结化到磁盘:State 节点覆盖写最新值,Archive 节点按高度保留历史。
继续阅读: