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.
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 theOnCommit hook; no transaction replay is involved.
- 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.
{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):
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:- State node
- Archive node
Overwrites in place, keeping only the latest value.
No historical versions exist on disk. Once a requested height leaves the in-memory window, the node returns
-39006 BlockNotFound.Where state comes from
A query node’s state has three sources, matching three scenarios:
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.
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.
- Block notifications and the canonical chain: in what order, and driven by which notifications, diff layers are pushed onto the state tree.
- Data flow: where ingestion and finalization live in the code.
- leafage-evm component documentation: implementation details of state management and the two node modes.