> ## 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 组件之间的 Kafka 消息、S3 对象、etcd 键空间和 RPC 约定

组件之间不直接调用，只通过 Kafka、S3、etcd 和少量 RPC 协作。这些是跨仓库改动时需要保持兼容的部分。

<Info>
  下文中 `{version}` 段是可选的。启用版本模式后，Kafka topic、S3 键和 etcd 键都会带上版本命名空间，用于在同一套基础设施上并行运行多个数据版本。
</Info>

## Kafka

### 内部 topic

写节点发布，leafage-evm 和 consistency-checker 消费。

| 项         | 值                                                                 |
| --------- | ----------------------------------------------------------------- |
| 默认命名      | `nodex_pipeline_{chainID}` / `nodex_pipeline_{chainID}_{version}` |
| 消息 Key    | `NewBlock`                                                        |
| 消息 Value  | `BlockChangeNotification`，JSON + gzip                             |
| 消息 Header | 区块首次可见时间戳，用于端到端延迟打点                                               |
| 生产者配置     | `RequiredAcks=all`，`BatchSize=1`（低延迟优先）                           |

```go theme={null}
type BlockChangeNotification struct {
    ChangeType uint64         `json:"changeType"` // 1=新块, 2=重组
    NewBlocks  []BlockContext `json:"newBlocks"`  // 按高度升序
    DropBlocks []BlockContext `json:"dropBlocks"` // 重组时被丢弃的块
}

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

<Warning>
  内部 topic 承载的是全序区块流，必须使用**单分区**。leafage-evm 通过显式 `assign` 固定分区并自行持久化 offset（关闭自动提交），因此所有副本都能收到全部消息，而不是消费组内分摊。partition 由 `--kafka-s3-config` 里的 `partition` 字段指定。
</Warning>

### 外部 topic

consistency-checker 发布，外部消费者订阅。leafage-evm 不消费这个 topic。

| 项        | 值                                                                                 |
| -------- | --------------------------------------------------------------------------------- |
| 惯例命名     | singleton topic `pipeline_{chainID}`，version topic `pipeline_{chainID}_{version}` |
| 消息 Key   | `NewBlock`                                                                        |
| 消息 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"`
}
```

新块和被丢弃的块用同一个结构表达，靠 `is_fork` 区分。版本模式下 checker 同时写 version topic 和 singleton topic，后者只有持有 etcd 锁的 Leader 才写。

## S3

两个桶按消费者划分，键都以 `chainID` 开头，因此多条链可以共用同一个桶。

### 内部桶（NodeX Bucket）

| 对象        | 键                                             | 编码          |
| --------- | --------------------------------------------- | ----------- |
| Header    | `{chainID}[/{version}]/{blockHash}/block`     | JSON + gzip |
| StateDiff | `{chainID}[/{version}]/{stateRoot}/stateDiff` | RLP         |

StateDiff 以 **state root** 而非区块哈希为键：state root 相同的相邻区块共享同一个对象，空块因此不产生新对象。leafage-evm 在父块与当前块 state root 相同时会跳过拉取，直接视为空差异。

```go theme={null}
type BlockStorageDiff struct {
    Hash            common.Hash          // 当前 state root
    ParentHash      common.Hash          // 父 state root
    NewAccounts     []NewAccount         // 新增或更新的账户
    DeletedAccounts []common.Hash        // 被删除的账户
    StorageDiff     []AccountStorageDiff // 存储变更
    NewCodes        []NewCode            // 新部署的合约代码
}
```

### 外部桶（ChainTable Bucket）

| 对象              | 键                                            | 编码          |
| --------------- | -------------------------------------------- | ----------- |
| BlockFile       | `{chainID}[/{version}]/{blockHash}`          | JSON + gzip |
| BlockValidation | `{chainID}[/{version}]/{height}/{blockHash}` | JSON + gzip |

`BlockFile` 包含区块、交易、调用追踪和事件日志，是外部消费者的主要数据来源。`BlockValidation` 是同一份数据的校验摘要，同时充当**按高度的索引**：

```go theme={null}
type BlockValidation struct {
    ValidationHash        int64 // 校验和
    IsFork                bool  // 由 consistency-checker 回写
    TxsCount              int
    EventsCount           int
    TracesCount           int
    ErrorEventsCount      int
    ErrorTracesCount      int
    StorageContractsCount int
}
```

前缀 `{chainID}[/{version}]/{height}/` 下列出的对象即该高度的所有候选区块。两个组件依赖这个索引：

* **consistency-checker** 把非规范的对象重写为 `is_fork: true`
* **leafage-evm** 在 S3 追赶时用它把高度解析为规范区块哈希

字段完整定义见 pipeline 仓库的 [`docs/protocol.md`](https://github.com/Chaintable/pipeline/blob/main/docs/protocol.md)。

## etcd 键空间

所有键以 `chainID` 开头（可选 `version` 段），不同链天然隔离。

| 键                                         | 写入方                                      | 读取方                             | 值                               |
| ----------------------------------------- | ---------------------------------------- | ------------------------------- | ------------------------------- |
| `{chainID}[/{version}]/nodes/{ip}_{port}` | leafage-evm 自注册；consistency-checker 更新状态 | consistency-checker、nodex-proxy | 节点信息 JSON                       |
| `{chainID}[/{version}]/lastBlockNumber`   | consistency-checker                      | nodex-proxy                     | `{"latestBlockNumber":"0x..."}` |
| `{chainID}/nativeNodes/{nodeKey}`         | 运维                                       | nodex-proxy                     | 节点信息 JSON                       |
| `{chainID}/gateway`                       | 运维 / nodex-proxy 管理 API                  | nodex-proxy                     | 权重与方法路由                         |
| `{chainID}/mirror/{addrKey}`              | 运维 / nodex-proxy 管理 API                  | nodex-proxy                     | 镜像目标                            |
| `{chainID}/version`                       | 运维                                       | consistency-checker、nodex-proxy | 当前生效的版本号                        |
| `{chainID}[/{version}]/writers/{nodeID}`  | pipeline（带租约）                            | nodex-proxy                     | 写节点注册信息                         |
| `{chainID}[/{version}]/writers/leader`    | pipeline                                 | pipeline 各实例                    | 当前 Leader 的 nodeID              |
| `{chainID}/outer_block_notice`            | consistency-checker                      | consistency-checker             | singleton 发布权的分布式锁              |

节点信息的结构：

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

| 字段          | 取值                              |
| ----------- | ------------------------------- |
| `nodeType`  | `1` = State 节点，`2` = Archive 节点 |
| `stateType` | `1` = 已追上链头，`2` = 落后，`3` = 离线   |
| `weight`    | 负载均衡权重，缺省 `100`                 |

<Tip>
  节点生命周期的分工：leafage-evm 启动时把自己写成 `stateType: 2`（落后），之后由 consistency-checker 每轮轮询后改写为 `1` 或 `2`；节点离线且没有租约时，consistency-checker 直接删除该键。nodex-proxy 只 watch 这些键，自己不写入健康状态。
</Tip>

### 版本切换

`{chainID}/version` 是版本模式的控制点，两个组件同时读它：

* **consistency-checker** 比较自身 `version` 配置与 etcd 中的值，一致时争抢 `{chainID}/outer_block_notice` 锁，成为向 singleton topic 发布的 Leader。
* **nodex-proxy** 把请求里的基础 `chainId` 改写为 `{chainId}-{version}`，路由到版本化的节点池。

## RPC 契约

### `trace_debankBlock`

写节点提供，用于在没有 Kafka 的场景下获取单个区块的完整执行输出。leafage-evm 的 HTTP 回退模式依赖它。

```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}'
```

返回 `DebankOutPut`，包含 `BlockFile`、`Header`、`StateDiff` 和 `ValidationHash`——与 S3 上四类对象的内容一一对应。

### 健康检查

| 调用方                 | 目标                 | 方法                |
| ------------------- | ------------------ | ----------------- |
| consistency-checker | leafage-evm        | `eth_blockNumber` |
| nodex-proxy         | State / Archive 节点 | `getLatestBlock`  |
| nodex-proxy         | Native 节点          | `eth_blockNumber` |

nodex-proxy 的健康检查每 5 秒重试一次，直到 `node_health_check_max_wait`（默认 300 秒）。只有通过检查的节点才会进入负载均衡池。

### 路由相关错误码

leafage-evm 返回这些错误码时，nodex-proxy 会自动改选节点池重试一次。

| 错误码      | 名称                   | 含义                     | 重试目标       |
| -------- | -------------------- | ---------------------- | ---------- |
| `-39006` | `StateBlockNotFound` | 请求的高度不在 State 节点的状态范围内 | Archive 节点 |
| `-39008` | `CosmosPrecompile`   | 调用触及需要原链能力的预编译         | Native 节点  |

## 兼容性约定

改动跨组件接口时需要注意：

* **Kafka 消息结构**只增字段、不改语义。消费者用 JSON 解码，多余字段会被忽略，但缺字段会导致零值误判。
* **S3 键格式**是 leafage-evm、consistency-checker 和外部消费者共同依赖的硬编码约定，改动需要同步三方并考虑存量对象。
* **etcd 键格式**同样硬编码在三个组件里，`{chainID}` 与 `{version}` 段的拼接方式必须一致。
* **错误码**是路由行为的一部分，新增需要在 nodex-proxy 侧同步处理逻辑。
