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

# Interface contracts

> Kafka messages, S3 objects, etcd key space, and RPC conventions between Leafage components

Components do not call each other directly; they cooperate only through Kafka, S3, etcd, and a small amount of RPC. These are the parts that must stay compatible when a change spans repositories.

<Info>
  The `{version}` segment below is optional. With version mode enabled, Kafka topics, S3 keys, and etcd keys all carry a version namespace, which lets multiple data versions run in parallel on the same infrastructure.
</Info>

## Kafka

### Internal topic

Published by the write node, consumed by leafage-evm and consistency-checker.

| Item            | Value                                                                                   |
| --------------- | --------------------------------------------------------------------------------------- |
| Default naming  | `nodex_pipeline_{chainID}` / `nodex_pipeline_{chainID}_{version}`                       |
| Message key     | `NewBlock`                                                                              |
| Message value   | `BlockChangeNotification`, JSON + gzip                                                  |
| Message header  | Timestamp of when the block was first seen, used for end-to-end latency instrumentation |
| Producer config | `RequiredAcks=all`, `BatchSize=1` (low latency first)                                   |

```go theme={null}
type BlockChangeNotification struct {
    ChangeType uint64         `json:"changeType"` // 1=new block, 2=reorg
    NewBlocks  []BlockContext `json:"newBlocks"`  // ascending by height
    DropBlocks []BlockContext `json:"dropBlocks"` // blocks dropped in the reorg
}

type BlockContext struct {
    Hash        common.Hash `json:"hash"`
    ParentHash  common.Hash `json:"parentHash"`
    BlockNumber uint64      `json:"blockNumber"`
    Timestamp   uint64      `json:"timestamp"`
}
```

<Warning>
  The internal topic carries a totally ordered block stream and must use a **single partition**. The partition is specified by the `partition` field in `--kafka-s3-config`.

  leafage-evm pins the partition with an explicit `assign` and persists the offset itself (auto-commit disabled), so every replica receives all messages instead of sharing them within a consumer group.
</Warning>

### External topic

Published by consistency-checker, subscribed to by external consumers. leafage-evm does not consume this topic.

| Item                | Value                                                                              |
| ------------------- | ---------------------------------------------------------------------------------- |
| Conventional naming | singleton topic `pipeline_{chainID}`, version topic `pipeline_{chainID}_{version}` |
| Message key         | `NewBlock`                                                                         |
| Message value       | `OuterBlockChangeNotification`, JSON + gzip                                        |

```go theme={null}
type OuterBlockChangeNotification struct {
    ChainID     int64       `json:"chain_id"`
    Hash        common.Hash `json:"block_id"`
    BlockNumber uint64      `json:"block_height"`
    Timestamp   uint64      `json:"block_timestamp"`
    IsFork      bool        `json:"is_fork"`
}
```

New blocks and dropped blocks use the same structure, distinguished by `is_fork`.

In version mode, the checker writes to both the version topic and the singleton topic; only the Leader holding the etcd lock writes to the latter.

## S3

The two buckets are split by consumer. All keys start with `chainID`, so multiple chains can share the same bucket.

### Internal bucket (NodeX bucket)

| Object    | Key                                           | Encoding    |
| --------- | --------------------------------------------- | ----------- |
| Header    | `{chainID}[/{version}]/{blockHash}/block`     | JSON + gzip |
| StateDiff | `{chainID}[/{version}]/{stateRoot}/stateDiff` | RLP         |

StateDiff is keyed by **state root** rather than block hash: adjacent blocks with the same state root share one object, so empty blocks produce no new objects. When the parent and current block have the same state root, leafage-evm skips the fetch and treats it as an empty diff.

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

### External bucket (ChainTable bucket)

| Object          | Key                                          | Encoding    |
| --------------- | -------------------------------------------- | ----------- |
| BlockFile       | `{chainID}[/{version}]/{blockHash}`          | JSON + gzip |
| BlockValidation | `{chainID}[/{version}]/{height}/{blockHash}` | JSON + gzip |

`BlockFile` contains the block, transactions, call traces, and event logs, and is the main data source for external consumers. `BlockValidation` is a validation digest of the same data and also serves as the **index by height**:

```go theme={null}
type BlockValidation struct {
    ValidationHash        int64 // checksum
    IsFork                bool  // written back by consistency-checker
    TxsCount              int
    EventsCount           int
    TracesCount           int
    ErrorEventsCount      int
    ErrorTracesCount      int
    StorageContractsCount int
}
```

The objects listed under the prefix `{chainID}[/{version}]/{height}/` are all the candidate blocks at that height. Two components rely on this index:

* **consistency-checker** rewrites non-canonical objects as `is_fork: true`
* **leafage-evm** uses it during S3 catch-up to resolve a height to the canonical block hash

See [`docs/protocol.md`](https://github.com/Chaintable/pipeline/blob/main/docs/protocol.md) in the pipeline repository for the full field definitions.

## etcd key space

All keys start with `chainID` (with an optional `version` segment), so different chains are naturally isolated.

| Key                                       | Writer                                                              | Reader                           | Value                                            |
| ----------------------------------------- | ------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------ |
| `{chainID}[/{version}]/nodes/{ip}_{port}` | leafage-evm registers itself; consistency-checker updates the state | consistency-checker, nodex-proxy | Node info JSON                                   |
| `{chainID}[/{version}]/lastBlockNumber`   | consistency-checker                                                 | nodex-proxy                      | `{"latestBlockNumber":"0x..."}`                  |
| `{chainID}/nativeNodes/{nodeKey}`         | Operators                                                           | nodex-proxy                      | Node info JSON                                   |
| `{chainID}/gateway`                       | Operators / nodex-proxy admin API                                   | nodex-proxy                      | Weights and method routing                       |
| `{chainID}/mirror/{addrKey}`              | Operators / nodex-proxy admin API                                   | nodex-proxy                      | Mirror targets                                   |
| `{chainID}/version`                       | Operators                                                           | consistency-checker, nodex-proxy | Currently active version number                  |
| `{chainID}[/{version}]/writers/{nodeID}`  | pipeline (with lease)                                               | nodex-proxy                      | Write node registration info                     |
| `{chainID}[/{version}]/writers/leader`    | pipeline                                                            | pipeline instances               | nodeID of the current Leader                     |
| `{chainID}/outer_block_notice`            | consistency-checker                                                 | consistency-checker              | Distributed lock for the singleton publish right |

Structure of the node info:

```json theme={null}
{
  "address": "10.0.90.11",
  "port": 8659,
  "nodeType": 1,
  "stateType": 1,
  "weight": 100,
  "source": "manual"
}
```

| Field       | Values                                                            |
| ----------- | ----------------------------------------------------------------- |
| `nodeType`  | `1` = State node, `2` = Archive node                              |
| `stateType` | `1` = caught up with the chain head, `2` = lagging, `3` = offline |
| `weight`    | Load balancing weight, default `100`                              |

<Tip>
  The node lifecycle is maintained by three components, each with its own role:

  * leafage-evm writes itself as `stateType: 2` (lagging) on startup.
  * consistency-checker rewrites it to `1` or `2` after each polling round; when a node is offline and has no lease, it deletes the key outright.
  * nodex-proxy only watches these keys and does not write health state itself.
</Tip>

### Version switching

`{chainID}/version` is the control point for version mode; two components read it:

* **consistency-checker** compares its own `version` config with the value in etcd; when they match, it contends for the `{chainID}/outer_block_notice` lock to become the Leader that publishes to the singleton topic.
* **nodex-proxy** rewrites the base `chainId` in the request to `{chainId}-{version}` and routes to the versioned node pool.

## RPC contracts

### `trace_debankBlock`

Provided by the write node, used to obtain the complete execution output of a single block in setups without Kafka. leafage-evm's HTTP fallback mode depends on it.

```bash theme={null}
curl -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"trace_debankBlock","params":["0x1"],"id":1}'
```

Returns `DebankOutPut`, which contains `BlockFile`, `Header`, `StateDiff`, and `ValidationHash` — corresponding one-to-one with the contents of the four object types on S3.

### Health checks

| Caller              | Target                | Method            |
| ------------------- | --------------------- | ----------------- |
| consistency-checker | leafage-evm           | `eth_blockNumber` |
| nodex-proxy         | State / Archive nodes | `getLatestBlock`  |
| nodex-proxy         | Native nodes          | `eth_blockNumber` |

nodex-proxy's health check retries every 5 seconds, up to `node_health_check_max_wait` (default 300 seconds). Only nodes that pass the check enter the load balancing pool.

### Routing-related error codes

When leafage-evm returns these error codes, nodex-proxy automatically switches node pools and retries once.

| Error code | Name                 | Meaning                                                               | Retry target |
| ---------- | -------------------- | --------------------------------------------------------------------- | ------------ |
| `-39006`   | `StateBlockNotFound` | The requested height is outside the State node's state range          | Archive node |
| `-39008`   | `CosmosPrecompile`   | The call touches a precompile that requires native chain capabilities | Native node  |

## Compatibility rules

When changing cross-component interfaces, keep in mind:

* **Kafka message structures** only add fields and never change semantics. Consumers decode JSON; extra fields are ignored, but missing fields lead to zero values being misinterpreted.
* **S3 key formats** are hard-coded conventions shared by leafage-evm, consistency-checker, and external consumers; changing them requires updating all three and accounting for existing objects.
* **etcd key formats** are likewise hard-coded in three components; the way the `{chainID}` and `{version}` segments are joined must be consistent.
* **Error codes** are part of routing behavior; adding one requires matching handling logic on the nodex-proxy side.
