Skip to main content
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.

Crate layout

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.
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 for details.

Two node modes

Keeps only the latest state; about 90GB for ETH mainnet.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.
See 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)

  • 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 for details.

RPC API

eth namespace

call, multiCall, blockNumber, getBalance, getCode, getStorageAt, getTransactionCount, getBlockByNumber, getBlockByHash, chainId, baseFee.
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.

DeBank namespace (no prefix)

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

Others

See the RPC reference for the full parameters.

Multi-chain executors

--evm-type selects the executor implementation: 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

See the configuration reference 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:

Development

The benchmarking tool leafage-bench compares eth_call performance between leafage-evm and geth:
Design documents in the repository: