eth_call 及所有状态读取接口只依赖这四样东西,查询节点因此只存这四样东西。
为什么只需要账户状态
一个全节点的数据库里,账户状态只占一小部分。其余是交易体、收据、日志,以及为了验证和出块而维护的 Merkle Patricia Trie。
去掉 MPT 意味着查询节点可以用扁平的键值存储(RocksDB 或 MDBX):账户直接按地址查,存储槽直接按
address || slot 查。
状态差异(StateDiff)
状态差异是一个区块执行前后账户状态的变更集。写节点在OnCommit 钩子拿到 StateDB 的提交集后直接转换,不需要重放交易。
- 以 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 节点在这里分道:- State 节点
- Archive 节点
覆盖写,只保留最新值。
磁盘上不存在历史版本。请求的高度一旦离开内存窗口,节点返回
-39006 BlockNotFound。状态从哪里来
查询节点的状态有三个来源,对应三种场景:
全新节点的常规启动方式是先下载 RocksDB 快照,再从快照高度追赶到链头,通常分钟级完成,而不是从创世块重放。
小结
- 状态 = 账户的 balance、nonce、code、storage;查询节点只存这些,不存 MPT 和交易。
- StateDiff 是一个区块的状态变更集,以 state root 为键存在 S3 内部桶,空块不产生新对象。
- 最近的区块以差异层留在内存,窗口默认 64 层,既是 State 节点的可查深度也是 reorg 处理上限。
- 超出窗口的层被终结化到磁盘:State 节点覆盖写最新值,Archive 节点按高度保留历史。
- 区块通知与规范链:差异层按什么顺序、依据什么通知被推入状态树。
- 数据链路:摄入与终结化在代码中的位置。
- leafage-evm 组件文档:状态管理与两种节点模式的实现细节。