> ## 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 and state diffs

> Query nodes keep account state only: what a state diff is, why it is keyed by state root, and how diff layers are organized in memory and finalized to disk once they leave the window.

In Leafage, "state" means EVM account state: the **balance, nonce, and code** of every address, plus the **storage** slots of every contract. `eth_call` and every state-read API depend on these four things only, so a query node stores these four things only.

## Why account state is enough

Account state is a small part of a full node's database. The rest is transaction bodies, receipts, logs, and the Merkle Patricia Trie maintained for validation and block production.

| Data                               | What a full node needs it for                                  | Needed by a query node                                 |
| ---------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
| Account state                      | Executing transactions and `eth_call`                          | Yes                                                    |
| Block header                       | Execution environment such as `number`, `timestamp`, `baseFee` | Yes                                                    |
| Merkle Patricia Trie               | Computing and verifying the state root                         | No; the write node has already computed the state root |
| Transaction bodies, receipts, logs | `eth_getTransactionByHash`, `eth_getLogs`                      | No; this data lives in the S3 external bucket          |

Dropping the MPT lets a query node use a flat key-value store (RocksDB or MDBX): accounts are looked up by address, storage slots by `address || slot`.

## State diff (StateDiff)

A state diff is **the set of account state changes between before and after executing one block**. The write node converts it directly from the StateDB commit set it receives in the `OnCommit` hook; no transaction replay is involved.

```go theme={null}
type BlockStorageDiff struct {
    Hash            common.Hash          // state root of this block
    ParentHash      common.Hash          // state root of the parent block
    NewAccounts     []NewAccount         // created or updated accounts
    DeletedAccounts []common.Hash        // deleted accounts
    StorageDiff     []AccountStorageDiff // storage slot changes
    NewCodes        []NewCode            // newly deployed contract code
}
```

Two design decisions shape how it lives on S3:

* **Keyed by state root, not block hash**: `{chainID}[/{version}]/{stateRoot}/stateDiff`. Adjacent blocks with the same state root (typically empty blocks) share one object; when a query node sees that the parent and current block have the same state root, it treats the diff as empty and skips the fetch.
* **Encoded with RLP, not JSON**: it is consumed only by Leafage's internal components, so readability does not matter and size does.

The block header is stored separately as `{chainID}[/{version}]/{blockHash}/block`. A query node fetches the header by hash and the diff by state root; together they are all the input needed to apply one block.

## Diff layers and the state tree

A query node does not write each block's diff straight into the database. It first keeps it in memory for a while as a **diff layer (DiffLayer)**. All diff layers form a linked list called the state tree (`StateTree`):

```text theme={null}
Block N ──► Block N-1 ──► ... ──► Block N-63 ──► CacheDiskLayer ──► RocksDB
(DiffLayer)  (DiffLayer)          (DiffLayer)      (read cache)     (finalized state)
```

| Structure        | Role                                                                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `DiffLayer`      | One block's changes relative to its parent layer; contains only the touched accounts and slots                                            |
| `StateTree`      | Holds the `latest` pointer and two indexes: `hash_diff_map` (all blocks, including fork blocks) and `num_diff_map` (canonical chain only) |
| `CacheDiskLayer` | Read cache in front of the disk                                                                                                           |

**Reads** start at the layer of the target block and walk downwards, returning on the first hit; if no layer has the key, the read falls through to disk. Most requests target `latest`, so reads are usually pure in-memory operations.

**Window**: at most `--diff-depth-limit` (default 64) layers stay in memory. The window has two meanings at once: it is the historical depth a State node can answer directly, and it is the deepest reorg that can be handled in memory. Deeper reorgs go through the S3 backfill path.

**Forks**: a fork block's diff layer enters `hash_diff_map` but not `num_diff_map`. Queries by hash can still reach fork state; queries by height always follow the canonical chain.

## Finalization

When a block's depth exceeds the window, the oldest layer is flushed to the database and removed from memory. This step is called **finalization**, and it is where State and Archive nodes part ways:

<Tabs>
  <Tab title="State node">
    Overwrites in place, keeping only the latest value.

    | Data    | Key                 |
    | ------- | ------------------- |
    | Account | `address`           |
    | Storage | `address \|\| slot` |

    No historical versions exist on disk. Once a requested height leaves the in-memory window, the node returns `-39006 BlockNotFound`.
  </Tab>

  <Tab title="Archive node">
    Double-writes, keeping both historical versions and a fast path to the latest value.

    | Data              | Key                                          |
    | ----------------- | -------------------------------------------- |
    | Account (history) | `address \|\| block_num`                     |
    | Storage (history) | `address \|\| slot \|\| block_num`           |
    | Latest value      | Same keys with `block_num` set to `u64::MAX` |

    Historical reads use `seek_for_prev` to find the nearest version at or below the target height.
  </Tab>
</Tabs>

## Where state comes from

A query node's state has three sources, matching three scenarios:

| Source                  | Scenario                                      | Mechanism                                                                                                                                       |
| ----------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Kafka + S3              | Continuous block following in production      | On each notification, fetch the Header by hash and the StateDiff by state root, then push onto the head of the state tree                       |
| S3 catch-up             | Cold start, or an expired Kafka offset        | Backfill block by block by height, resolving each height to its canonical hash via the external bucket prefix `{chainID}[/{version}]/{height}/` |
| Write node HTTP polling | Local development, environments without Kafka | Poll `trace_debankBlock` and apply serially                                                                                                     |

The usual way to bring up a brand-new node is to download a RocksDB snapshot and catch up from the snapshot height to the chain head, which typically takes minutes rather than replaying from genesis.

<Tip>
  Catch-up near the chain head can land on the wrong branch. `--catchup-safe-depth` makes those blocks near the head backfill along the parent-hash chain from the notifications instead; set it above the chain's maximum reorg depth.
</Tip>

## Summary

* State = an account's **balance, nonce, code, storage**; query nodes store only these, no MPT and no transactions.
* A **StateDiff** is one block's set of state changes, stored in the S3 internal bucket keyed by **state root**; empty blocks produce no new object.
* Recent blocks stay in memory as **diff layers**; the window defaults to 64 layers and is both a State node's queryable depth and the reorg handling limit.
* Layers leaving the window are **finalized** to disk: a State node overwrites the latest value, an Archive node keeps history by height.

Continue reading:

* [Block notifications and the canonical chain](/en/concepts/blocks): in what order, and driven by which notifications, diff layers are pushed onto the state tree.
* [Data flow](/en/architecture/data-flow): where ingestion and finalization live in the code.
* [leafage-evm component documentation](/en/components/leafage-evm#state-management): implementation details of state management and the two node modes.
