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

# Architecture overview

> How Leafage is divided into components, its runtime topology, and the responsibility boundary between data plane and control plane

Leafage is an architecture for EVM state queries and block data distribution. It separates four things that are coupled together in a traditional full node: **sync and execution**, **state storage**, **RPC serving**, and **data export**.

## Why split the full node

At production scale, scaling query capacity by "deploying a few more Geth instances" runs into several hard problems:

| Problem                 | Symptom                                                                                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Storage redundancy      | Every full node needs 1.3TB+ (Archive 2–6.5TB); 100 nodes means 130TB+ of identical data                                                             |
| Bandwidth amplification | Every node joins P2P gossip independently, so the same block is pulled from the network hundreds of times                                            |
| Replica inconsistency   | P2P sync is non-deterministic; different nodes behind a load balancer can sit at different heights, so the same `eth_call` returns different results |
| Workload interference   | Sync (CPU + disk writes) and queries share one process; query peaks slow down sync, and vice versa                                                   |
| Slow recovery           | After a node failure, resync takes hours to days                                                                                                     |
| No data export          | Extracting transactions/traces/events relies on `debug_traceBlock`, which competes with production traffic for resources                             |

Leafage's answer: **let only one node execute blocks**; every other node consumes the execution results.

## Component responsibilities

<Columns cols={2}>
  <Card title="Write node" icon="server" href="/en/components/go-ethereum-x">
    The execution client plus the pipeline integration; exports state changes, call traces, and events while executing blocks.
  </Card>

  <Card title="pipeline" icon="share-2" href="/en/components/pipeline">
    Distribution library. Serializes execution data, writes to two S3 buckets, and on the Leader node publishes block change notifications to Kafka.
  </Card>

  <Card title="leafage-evm" icon="cpu" href="/en/components/leafage-evm">
    Query node. Consumes Kafka + S3 to rebuild state and runs `eth_call` with revm; no P2P, no transaction storage.
  </Card>

  <Card title="consistency-checker" icon="shield-check" href="/en/components/consistency-checker">
    Verification layer. Waits for replicas to converge, marks fork blocks, maintains node health state in etcd, and publishes confirmed notifications to the external Kafka.
  </Card>

  <Card title="nodex-proxy" icon="route" href="/en/components/nodex-proxy">
    Gateway. Discovers nodes through etcd and routes by block context to the State / Archive / Native node pools.
  </Card>
</Columns>

## Runtime topology

```mermaid theme={null}
flowchart TB
    P2P["Blockchain P2P network"] -->|"Sync blocks"| GETH

    subgraph WRITE["Write side (1 primary + N standby per chain)"]
        GETH["Write node<br/>execution client + pipeline tracer"]
    end

    GETH -->|"Header + StateDiff"| S3IN["S3 internal bucket<br/>NodeX Bucket"]
    GETH -->|"BlockFile + Validation"| S3OUT["S3 external bucket<br/>ChainTable Bucket"]
    GETH -->|"BlockChangeNotification<br/>Leader only"| KIN["Kafka internal topic"]

    KIN --> LE1["leafage-evm<br/>State node"]
    KIN --> LE2["leafage-evm<br/>Archive node"]
    KIN --> CC["consistency-checker"]

    S3IN --> LE1
    S3IN --> LE2

    CC -->|"Poll eth_blockNumber"| LE1
    CC -->|"Poll eth_blockNumber"| LE2
    CC -->|"Mark is_fork"| S3OUT
    CC -->|"Confirmed block notifications"| KOUT["Kafka external topic"]
    KOUT --> EXT["External consumers<br/>indexers / analytics platforms"]
    S3OUT --> EXT

    CC -->|"Node state + chain height"| ETCD[("etcd")]
    LE1 -.->|"Self-registration"| ETCD
    LE2 -.->|"Self-registration"| ETCD
    ETCD -->|"watch"| PROXY["nodex-proxy"]

    PROXY --> LE1
    PROXY --> LE2
    CLIENT["JSON-RPC clients"] --> PROXY
```

## Data plane and control plane

The key to understanding Leafage is to look at the two paths separately. They use different middleware, and a failure in each affects a different scope.

### Data plane: Kafka + S3

Block data flows in one direction only, from the write node to consumers.

* **Kafka (internal topic)** carries notifications only, not data payloads. A message holds the block hash, parent hash, height, timestamp, and the list of blocks dropped during a reorg.
* **S3 (internal bucket)** stores Header and StateDiff; it is the data source leafage-evm rebuilds state from.
* **S3 (external bucket)** stores BlockFile (transactions, traces, events) and BlockValidation for external consumers and analytics platforms.
* **Kafka (external topic)** is published by consistency-checker; it is the entry point external consumers should subscribe to, and it comes with consistency guarantees.

When the data plane fails, leafage-evm stops catching up with blocks but can still serve queries against the state it already has.

### Control plane: etcd

etcd is the source of truth for all runtime state; three components coordinate through it:

| Writer                  | Key                           | Reader                           | Purpose                                                             |
| ----------------------- | ----------------------------- | -------------------------------- | ------------------------------------------------------------------- |
| leafage-evm             | `{chainID}/nodes/{ip}_{port}` | consistency-checker, nodex-proxy | Node self-registration                                              |
| consistency-checker     | `{chainID}/nodes/{ip}_{port}` | nodex-proxy                      | Updates node health state; deletes the key when the node is offline |
| consistency-checker     | `{chainID}/lastBlockNumber`   | nodex-proxy                      | Current chain height, decides State/Archive routing                 |
| pipeline                | `{chainID}/writers/{nodeID}`  | nodex-proxy                      | Write node registration (with lease)                                |
| pipeline                | `{chainID}/writers/leader`    | all pipeline instances           | Write node Leader election                                          |
| Operators / nodex-proxy | `{chainID}/gateway`           | nodex-proxy                      | Node weights and method routing                                     |
| Operators               | `{chainID}/version`           | consistency-checker, nodex-proxy | Version switching                                                   |

See [Interface contracts](/en/architecture/interfaces#etcd-key-space) for the full key space.

<Tip>
  This control-plane loop is easy to overlook: leafage-evm only registers itself in etcd and marks itself as lagging (`StateType::Delay`); it is consistency-checker that actually decides whether the node is healthy and whether it has caught up with the chain head. nodex-proxy is merely a consumer of that state.

  To understand "why a node is getting no traffic", start from consistency-checker's polling results.
</Tip>

## Node types

| Type         | Storage (ETH mainnet)                | Queryable range                                                  | Typical use                                                       |
| ------------ | ------------------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------- |
| State node   | \~90 GB                              | Latest state (diffs of the most recent 64 blocks kept in memory) | The vast majority of RPC queries                                  |
| Archive node | \~360 GB                             | Any historical height                                            | Historical `eth_call`, analytics queries                          |
| Native node  | Depends on the original chain client | Full-node capabilities of the original chain                     | Calls that cannot be executed locally, such as Cosmos precompiles |

nodex-proxy routes between the three node pools based on the block parameter carried by the request; see the [nodex-proxy component documentation](/en/components/nodex-proxy#node-selection) for the rules.

## Deployment unit

One chain is one independent deployment unit; chains do not share Kafka topics, S3 prefixes, or etcd prefixes (all of which start with `chainID`). A typical single-chain deployment includes:

```text theme={null}
1 × consensus client (such as lighthouse, only needed for PoS chains)
1 × write node (standby nodes can be added; the pipeline Leader election decides who publishes to Kafka)
1 × consistency-checker
N × leafage-evm (mostly State nodes, with Archive nodes added as needed)
1 × nodex-proxy (multiple instances allowed, sharing the same etcd)
Shared infrastructure: Kafka, S3, etcd
```

nodex-proxy is multi-chain: one process manages the node pools of multiple chains according to the `chainId` in the path, so usually the whole cluster deploys only one set of proxies.

## Multi-chain support

The write side and the read side are adapted separately:

* **Write side**: pipeline must be embedded in the target chain's execution client. Client forks for 40+ chains have already been adapted; see the [leafage-evm README](https://github.com/Chaintable/leafage-evm#supported-write-node-repositories) for the list. See [Adding a new chain](/en/guides/new-chain) for how to adapt one.
* **Read side**: leafage-evm selects the executor through `--evm-type`; it currently supports `mainnet`, `arbitrum`, `op`, `base`, `bsc`, `cosmos`, `mantlev2`, `tempo`, `citrea`, `iotex`, `moonbeam`, `moonriver`, `polygon`, `hemi`.

## Design trade-offs

<AccordionGroup>
  <Accordion title="leafage-evm does not store transaction data">
    `eth_call` only needs account state (balance, nonce, code, storage), so transaction bodies, receipts, and logs can all be discarded. The cost is that the block query APIs (`eth_getBlockByNumber` and so on) return only the header, and `transactions` and `uncles` are always empty. Consumers that need transaction data should read the S3 external bucket or subscribe to the external Kafka topic.
  </Accordion>

  <Accordion title="Only 64 blocks of diffs are kept in memory">
    The state of the most recent 64 blocks stays in memory as a linked list of diff layers; layers beyond that depth are flushed to RocksDB. This window is also the upper bound on reorg handling: deeper reorgs must go through the S3 backfill path. The window size is controlled by `--diff-depth-limit`.
  </Accordion>

  <Accordion title="Consistency is not guaranteed by leafage-evm itself">
    All query nodes consume the same Kafka partition and apply the same blocks in the same order, so there is no P2P non-determinism. But "have all replicas caught up" is decided by consistency-checker, which notifies external consumers only after `ready_ratio` (default 0.8) is reached.
  </Accordion>

  <Accordion title="Kafka carries notifications only; payloads go through S3">
    Notification messages are small, so Kafka is responsible only for ordering and low latency; the large StateDiff and BlockFile payloads go through S3, where consumers can fetch in parallel and backfill history on demand during a cold start.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Data flow" icon="git-branch" href="/en/architecture/data-flow">
    Follow one block through the five stages: execution, distribution, ingestion, finalization, and query.
  </Card>

  <Card title="Interface contracts" icon="plug" href="/en/architecture/interfaces">
    Exact format definitions for Kafka, S3, etcd, and RPC.
  </Card>
</Columns>
