> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leafage.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 状态与状态差异

> 查询节点只维护账户状态：状态差异是什么、为什么以 state root 为键、差异层如何在内存中组织并在超出窗口后终结化到磁盘。

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

## 为什么只需要账户状态

一个全节点的数据库里，账户状态只占一小部分。其余是交易体、收据、日志，以及为了验证和出块而维护的 Merkle Patricia Trie。

| 数据                   | 全节点需要它做什么                                | 查询节点是否需要              |
| -------------------- | ---------------------------------------- | --------------------- |
| 账户状态                 | 执行交易与 `eth_call`                         | 需要                    |
| 区块头                  | 提供 `number`、`timestamp`、`baseFee` 等执行环境  | 需要                    |
| Merkle Patricia Trie | 计算并验证 state root                         | 不需要，state root 由写节点算好 |
| 交易体、收据、日志            | `eth_getTransactionByHash`、`eth_getLogs` | 不需要，这些数据在 S3 外部桶      |

去掉 MPT 意味着查询节点可以用扁平的键值存储（RocksDB 或 MDBX）：账户直接按地址查，存储槽直接按 `address || slot` 查。

## 状态差异（StateDiff）

状态差异是**一个区块执行前后账户状态的变更集**。写节点在 `OnCommit` 钩子拿到 StateDB 的提交集后直接转换，不需要重放交易。

```go theme={null}
type BlockStorageDiff struct {
    Hash            common.Hash          // 当前区块的 state root
    ParentHash      common.Hash          // 父区块的 state root
    NewAccounts     []NewAccount         // 新增或更新的账户
    DeletedAccounts []common.Hash        // 被删除的账户
    StorageDiff     []AccountStorageDiff // 存储槽变更
    NewCodes        []NewCode            // 新部署的合约代码
}
```

两个设计点决定了它在 S3 上的形态：

* **以 state root 而不是区块哈希为键**：`{chainID}[/{version}]/{stateRoot}/stateDiff`。state root 相同的相邻区块（典型是空块）共享同一个对象，查询节点发现父块与当前块 state root 相同时直接视为空差异，不拉取。
* **用 RLP 而不是 JSON 编码**：它只被 Leafage 内部组件消费，不需要可读性，体积优先。

区块头单独存为 `{chainID}[/{version}]/{blockHash}/block`。查询节点按哈希取头、按 state root 取差异，两者合起来就是应用一个区块所需的全部输入。

## 差异层与状态树

查询节点不把每个区块的差异直接写进数据库，而是先在内存里以\*\*差异层（DiffLayer）\*\*的形式保留一段时间。所有差异层组成一条链表，叫状态树（`StateTree`）：

```text theme={null}
Block N ──► Block N-1 ──► ... ──► Block N-63 ──► CacheDiskLayer ──► RocksDB
(DiffLayer)  (DiffLayer)          (DiffLayer)      (读缓存)         (已终结状态)
```

| 结构               | 作用                                                                    |
| ---------------- | --------------------------------------------------------------------- |
| `DiffLayer`      | 一个区块相对父层的变更集，只含被改动的账户和槽位                                              |
| `StateTree`      | 持有 `latest` 指针，以及两张索引：`hash_diff_map`（所有块，含分叉块）和 `num_diff_map`（仅规范链） |
| `CacheDiskLayer` | 磁盘之上的读缓存                                                              |

**读取**：从目标区块对应的层开始向下遍历，命中即返回；所有层都未命中，落到磁盘。绝大多数请求指向 `latest`，因此通常是纯内存操作。

**窗口**：内存中最多保留 `--diff-depth-limit`（默认 64）层。这个窗口有双重含义：它既是 State 节点能直接回答的历史深度，也是能在内存中处理的最大 reorg 深度，更深的重组需要走 S3 回补。

**分叉**：分叉块的差异层进入 `hash_diff_map` 但不进入 `num_diff_map`。按哈希查询仍能访问分叉状态，按高度查询始终走规范链。

## 终结化

区块深度超过窗口时，最老的一层被刷入数据库并从内存移除，这一步叫**终结化**。State 节点和 Archive 节点在这里分道：

<Tabs>
  <Tab title="State 节点">
    覆盖写，只保留最新值。

    | 数据 | 键                   |
    | -- | ------------------- |
    | 账户 | `address`           |
    | 存储 | `address \|\| slot` |

    磁盘上不存在历史版本。请求的高度一旦离开内存窗口，节点返回 `-39006 BlockNotFound`。
  </Tab>

  <Tab title="Archive 节点">
    双写，同时保留历史版本和最新值快速路径。

    | 数据     | 键                                  |
    | ------ | ---------------------------------- |
    | 账户（历史） | `address \|\| block_num`           |
    | 存储（历史） | `address \|\| slot \|\| block_num` |
    | 最新值    | 同上，`block_num` 取 `u64::MAX`        |

    历史查询用 `seek_for_prev` 定位不大于目标高度的最近版本。
  </Tab>
</Tabs>

## 状态从哪里来

查询节点的状态有三个来源，对应三种场景：

| 来源          | 场景                   | 机制                                                          |
| ----------- | -------------------- | ----------------------------------------------------------- |
| Kafka + S3  | 生产环境的持续追块            | 收到通知后按哈希取 Header、按 state root 取 StateDiff，推入状态树头部           |
| S3 追赶       | 冷启动或 Kafka offset 过期 | 按高度逐块回补，用外部桶 `{chainID}[/{version}]/{height}/` 前缀把高度解析为规范哈希 |
| 写节点 HTTP 轮询 | 本地开发、没有 Kafka 的环境    | 轮询 `trace_debankBlock`，串行应用                                 |

全新节点的常规启动方式是先下载 RocksDB 快照，再从快照高度追赶到链头，通常分钟级完成，而不是从创世块重放。

<Tip>
  追赶靠近链头时可能落到错误的分支上。`--catchup-safe-depth` 让链头附近这些区块改为沿通知里的 parent-hash 链回补，取值应大于该链的最大 reorg 深度。
</Tip>

## 小结

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

继续阅读：

* [区块通知与规范链](/concepts/blocks)：差异层按什么顺序、依据什么通知被推入状态树。
* [数据链路](/architecture/data-flow)：摄入与终结化在代码中的位置。
* [leafage-evm 组件文档](/components/leafage-evm#状态管理)：状态管理与两种节点模式的实现细节。
