> ## 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.

# leafage-evm

> Lightweight EVM executor: consumes state changes and serves state queries such as eth_call

leafage-evm is the query node. It does no P2P sync, does not execute blocks, does not maintain a Merkle Patricia Trie, and does not store transaction data — it only maintains account state and uses revm to execute read-only calls.

| Item              | Value                                                                                                            |
| ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| Repository        | [Chaintable/leafage-evm](https://github.com/Chaintable/leafage-evm)                                              |
| Language          | Rust 1.79+                                                                                                       |
| License           | Apache-2.0                                                                                                       |
| Core dependencies | [revm](https://github.com/bluealloy/revm), [alloy](https://github.com/alloy-rs/alloy), RocksDB / MDBX, jsonrpsee |

## Crate layout

```text theme={null}
leafage-evm/
├── bin/leafage-evm/           # CLI and runtime assembly
│   ├── runner.rs              # Subcommand definitions
│   ├── standalone.rs          # Service startup, chain config parsing
│   ├── updater/               # kafka_updater.rs / http_updater.rs
│   ├── initializer/           # State initialization at startup
│   ├── register/              # etcd self-registration
│   ├── warm/                  # Startup warmup
│   └── utils.rs               # S3 key construction and reads
└── crates/
    ├── leafage-evm-types/     # Base types, BlockStorageDiff, RPC types
    ├── leafage-evm-storage/   # StateDB trait, StateTree, RocksDB/MDBX backends
    ├── leafage-evm-rpc/       # JSON-RPC definitions and per-chain executor implementations
    └── leafage-evm-chains/    # Chain-specific precompiles and hard fork rules
```

## State management

State is kept in two tiers: recent blocks are held in memory as a linked list of diff layers, and older blocks are flushed to disk.

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

| Structure        | Role                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `StateTree`      | Holds the `latest` pointer, `hash_diff_map` (includes fork blocks), and `num_diff_map` (canonical chain only) |
| `DiffLayer`      | The change set of a single block relative to its parent layer                                                 |
| `CacheDiskLayer` | Read cache above the disk (accounts / storage / code)                                                         |
| `HybridStateDB`  | Combines the in-memory layers and the disk layer into a `DatabaseRef` usable by revm                          |

**Queries**: walk down the diff layers from the latest layer and return on the first hit; if every layer misses, read from disk. The vast majority of requests target `latest` or blocks near the chain head, so they are usually pure in-memory operations.

**Finalization**: when a block's depth exceeds `--diff-depth-limit` (default 64), the oldest layer is flushed to the database and removed from memory.

**Forks**: fork blocks enter `hash_diff_map` but not `num_diff_map`. Queries by hash can reach fork state; queries by height always follow the canonical chain.

See the repository's [`docs/StateManage.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/StateManage.md) for details.

## Two node modes

<Tabs>
  <Tab title="State node (default)">
    Keeps only the latest state; about 90GB for ETH mainnet.

    | Data    | RocksDB key         |
    | ------- | ------------------- |
    | Account | `address`           |
    | Storage | `address \|\| slot` |

    Queries use a direct `get()`. When the requested height falls outside the in-memory window, it returns `-39006`, and nodex-proxy forwards the request to an Archive node.
  </Tab>

  <Tab title="Archive node (--archive)">
    Keeps all historical state; about 360GB for ETH mainnet.

    | Data    | RocksDB key                        |
    | ------- | ---------------------------------- |
    | Account | `address \|\| block_num`           |
    | Storage | `address \|\| slot \|\| block_num` |

    Uses dual writes:

    * Historical versions are written with a height suffix.
    * The latest value is written again with a `u64::MAX` suffix as the fast path.

    Historical queries use `seek_for_prev` to locate the most recent version not greater than the target height, together with a prefix extractor tuned per column family (32-byte prefix for accounts, 64-byte for storage).

    Iterators have a timeout tracking mechanism (`--iterator-timeout-secs`) to prevent long-held iterators from blocking compaction.
  </Tab>
</Tabs>

See [`docs/Database.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/Database.md) for the column family layout and tuning parameters.

## State updates

The updater is chosen by parameters at startup: if `--kafka-s3-config` is set, Kafka + S3 is used; otherwise, if `--rpc-addr` is set, HTTP polling is used; if neither is set, state stays static.

### Kafka + S3 (production)

```json theme={null}
{
  "topic": "nodex_pipeline_1",
  "brokers": "kafka1:9092,kafka2:9092",
  "partition": 0,
  "bucket_name": "nodex-internal",
  "outer_bucket_name": "chaintable-pipeline",
  "offset_dir": "/nodex-eth/offset",
  "s3_chain_id": "1",
  "version": ""
}
```

* `bucket_name` is the internal bucket (Header + StateDiff); `outer_bucket_name` is the external bucket (BlockFile, used for indexing by height and warmup)
* The partition is set with an explicit `assign` and auto-commit is disabled; the offset is written to `offset_dir`, so every replica consumes the full message stream independently
* Diff fetching is skipped when the parent block and the current block have the same state root

**Catch-up logic**: at startup, read the local offset; if it is earlier than Kafka's low watermark, backfill block by block from S3 first, then consume from the latest position. During backfill, the external bucket's `{chainID}/{height}/` prefix is used to resolve a height to the canonical hash.

`--catchup-safe-depth` suppresses fork misjudgment during catch-up: this many blocks near the chain head are instead backfilled block by block along the parent-hash chain in the Kafka notifications, rather than looked up by height index, to avoid selecting the wrong branch. The value should be greater than the target chain's maximum reorg depth; `0` disables it.

### HTTP polling (development / fallback)

Polls the write node's `trace_debankBlock`. When the parent block is not in the StateTree, it walks backwards to find the common ancestor, then applies blocks in order. Suitable for local development and environments without Kafka.

See [`docs/StateUpdater.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/StateUpdater.md) for details.

## RPC API

### `eth` namespace

`call`, `multiCall`, `blockNumber`, `getBalance`, `getCode`, `getStorageAt`, `getTransactionCount`, `getBlockByNumber`, `getBlockByHash`, `chainId`, `baseFee`.

<Warning>
  Block queries return only the header; `transactions` and `uncles` are always empty arrays — leafage-evm does not store transaction data. Consumers that need transactions should read the S3 external bucket.

  Also, the gas estimation method is called `estimateGas` (no namespace prefix), not `eth_estimateGas`.
</Warning>

### DeBank namespace (no prefix)

`version`, `getAddressNonce`, `getAddressBalance`, `getAddressCode`, `getStorageAt`, `contractMultiCall`, `simulateTransactions`, `estimateGas`, `getLatestBlock`, `getBlockByHeight`, `getBlockById`, `blockIsValid`.

### Others

| Method                            | Description                                                             |
| --------------------------------- | ----------------------------------------------------------------------- |
| `pre_traceCall` / `pre_traceMany` | Call tracing without needing a full node's debug API                    |
| `blockx_stateReadBatch`           | Internal batch state reads with a binary payload, for internal services |

See the [RPC reference](/en/reference/rpc) for the full parameters.

## Multi-chain executors

`--evm-type` selects the executor implementation:

| Value                                                                        | Description                                              |
| ---------------------------------------------------------------------------- | -------------------------------------------------------- |
| `mainnet`                                                                    | Standard Ethereum                                        |
| `op` / `base` / `mantlev2`                                                   | OP Stack family; L2 gas calculation and OVM precompiles  |
| `arbitrum`                                                                   | Nitro                                                    |
| `bsc`                                                                        | Parlia validators, tendermint / IAVL precompiles         |
| `cosmos`                                                                     | bech32 addresses, p256 signatures, native token handling |
| `polygon` / `moonbeam` / `moonriver` / `iotex` / `citrea` / `tempo` / `hemi` | Chain-specific hard forks and precompiles                |

Adding a new chain requires implementing the `EvmExecutor` trait, with the chain-specific logic placed in the `leafage-evm-chains` crate.

`--historical-rpc` and `--historical-height` are for historical ranges without block diffs (such as OP pre-bedrock): requests below the threshold are forwarded to an external RPC.

## Command line

```bash theme={null}
RUST_LOG=info ./target/release/leafage-evm standalone \
  --db-path /nodex-eth \
  --listen-addr 0.0.0.0:8659 \
  --chain-cfg 1 \
  --evm-type mainnet \
  --kafka-s3-config /etc/leafage/kafka_s3.json
```

| Subcommand                  | Purpose                                                                                            |
| --------------------------- | -------------------------------------------------------------------------------------------------- |
| `standalone`                | Start the service                                                                                  |
| `archive-init`              | Initialize the Archive database from S3 + RPC                                                      |
| `db-migrate`                | Database migration (RocksDB ↔ MDBX, Archive → State)                                               |
| `compact` / `force-compact` | Compact the database; `force-compact` repairs bulk-imported SSTs that are missing blooms / indexes |
| `rewind`                    | Roll the committed chain head back to an earlier block and resync from S3                          |
| `archive-scan`              | Read-only scan of a column family, for troubleshooting                                             |

See the [configuration reference](/en/reference/configuration#leafage-evm) for common parameters.

## Service registration

At startup, the node writes itself into etcd at `{chain_id}[/{version}]/nodes/{ip}_{port}` with an initial `stateType` of `2` (lagging). After that, consistency-checker rewrites the state based on polling results, and nodex-proxy watches these keys to update the node pool.

Registration uses a periodic transaction (writing only when the key does not exist), so it does not overwrite the state written by the checker; the node deletes its own key on process exit.

## Metrics

Exposed when `--prometheus-addr` is enabled:

| Metric                                                             | Description                   |
| ------------------------------------------------------------------ | ----------------------------- |
| `leafage_rpc_call_time` / `leafage_rpc_call_status`                | RPC latency and status        |
| `leafage_storage_read_account_latency` etc.                        | Read latency by type          |
| `leafage_storage_latest_commit_block`                              | Latest height flushed to disk |
| `leafage_storage_active_iterators` / `timed_out_iterators`         | Archive iterator state        |
| `leafage_state_batch_latency_seconds` / `leafage_state_batch_size` | Batch reads                   |
| `pipeline_block_num` / `pipeline_block_time`                       | Latest block in memory        |

## Development

```bash theme={null}
cargo build --release
cargo test
cargo clippy --all-targets -- -D warnings
```

The benchmarking tool `leafage-bench` compares `eth_call` performance between leafage-evm and geth:

```bash theme={null}
cargo build --release -p leafage-bench
git lfs pull   # the test corpus is managed through Git LFS

./target/release/leafage-bench run \
  --corpus bin/leafage-bench/corpus/corpus.json \
  --target http://leafage-evm:8545 \
  --compare http://geth:8545
```

## Related documents

Design documents in the repository:

* [`Architecture.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/Architecture.md) — overall architecture
* [`StateManage.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/StateManage.md) — state management: diff layers, finalization, forks
* [`StateUpdater.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/StateUpdater.md) — state updater: Kafka + S3 and HTTP polling
* [`Database.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/Database.md) — database storage: column family layout and tuning parameters
* [`DataSpec.md`](https://github.com/Chaintable/leafage-evm/blob/main/docs/DataSpec.md) — data specification
