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

# consistency-checker

> 校验副本一致性、标记分叉块、维护节点状态，并向外部消费者发布确认通知

consistency-checker 不在 leafage-evm 的数据路径上——它消费与 leafage-evm 相同的 Kafka 通知，观察副本状态，然后做三件事：向外部消费者发布**已确认**的区块通知、在 S3 上标记分叉块、把节点健康状态写进 etcd。

| 项   | 值                                                                                   |
| --- | ----------------------------------------------------------------------------------- |
| 仓库  | [Chaintable/consistency-checker](https://github.com/Chaintable/consistency-checker) |
| 语言  | Go 1.23+                                                                            |
| 许可证 | Apache-2.0                                                                          |
| 依赖  | Kafka（内外各一）、etcd v3、S3、Pebble DB                                                    |

## 它解决什么问题

写节点发布通知的那一刻，副本还没有应用这个区块。外部消费者如果直接订阅内部 topic，就会在 leafage-evm 还查不到该区块时收到通知。

checker 在中间加了一道确认：等到足够比例的副本确实追上了这个高度，才向外部 topic 发布。外部消费者因此得到一个保证——收到通知时，去查询集群一定能读到这个区块。

顺带解决的两件事：分叉块在 S3 上没有标记（谁是规范链只有观察者知道），以及 nodex-proxy 需要知道每个节点当前是否健康。

## 处理流程

```text theme={null}
Inner Kafka（BlockChangeNotification）
  → 消息校验 & 去重
  → 并行预取 S3（BlockValidation + 同高度键列表）
  → 轮询副本直到 ready_ratio 达标
  → 写入 Pebble DB（双索引）
  → 标记 S3 中同高度的 fork 区块
  → 发布到 Outer Kafka（先 drop，再 new）
  → 对齐 singleton topic（仅 Leader，版本模式）
  → 发布后用新鲜 LIST 复核 fork 标记
```

核心实现在 `check/check.go` 的 `Process`。几个设计要点：

<AccordionGroup>
  <Accordion title="预取与等待并行">
    S3 预取和副本轮询同时进行。轮询通常要几十到几百毫秒，正好覆盖 S3 往返；失败路径不等预取结束，goroutine 自行收尾。
  </Accordion>

  <Accordion title="幂等与重试">
    整条 `Process` 失败时不提交 Kafka offset，下一轮重试同一条消息。重复消息、以及“已处理到发布步骤但整条未提交”的情况会短路到对齐后直接推进，避免死锁重试。已成功投递的目的地记入 `delivered`，重试时跳过，因此既不重复投递也不漏投递。
  </Accordion>

  <Accordion title="副本轮询的超时语义">
    以 `check_interval_ms`（默认 20ms）为间隔轮询所有副本的 `eth_blockNumber`，直到 `ready_ratio`（默认 0.8）的副本达到目标高度。总时长上限是 `check_timeout_ms`（默认 2000ms），单次 RPC 上限是 `rpc_node_timeout_ms`（默认 5000ms）。超时判失败，`Process` 整体重试。

    配置项 `check_num` 已废弃，不再生效。
  </Accordion>

  <Accordion title="发布后复核">
    预取的同高度键列表是 `Process` 开头的快照，等待副本期间新上传的对象不在其中。发布通知后会用一次新鲜的 LIST 再检查一遍。这一步不在关键路径上，失败只记日志，由周期巡检兜底。
  </Accordion>
</AccordionGroup>

## 分叉标记

S3 外部桶的 `BlockValidation` 对象带一个 `is_fork` 字段，写节点上传时恒为 `false`——因为写节点在上传的那一刻还不知道这个块最终是否在规范链上。checker 负责回写。

三条触发路径：

| 路径              | 触发时机                                                                 |
| --------------- | -------------------------------------------------------------------- |
| `dropBlocks` 重写 | 收到 `changeType: 2` 的重组通知                                             |
| 同高度扫描           | 每个新块处理时，列出该高度的所有对象，非规范的标记为 fork                                      |
| 周期巡检            | 每 `fork_scan_interval_sec`（默认 60 秒）回看 `fork_scan_lookback`（默认 64）个高度 |

`pipeline_fork_scan_rewrites_total` 不为零说明标记曾被覆盖或此前失败，值得关注。

<Info>
  leafage-evm 在 S3 追赶时依赖这个标记来把高度解析成规范哈希。checker 长时间停摆会让新节点的追赶变慢。
</Info>

## 节点状态维护

每轮轮询后，把结果写回 etcd（一个事务提交）：

| 键                                         | 内容                              |
| ----------------------------------------- | ------------------------------- |
| `{chainID}[/{version}]/nodes/{ip}_{port}` | 节点的 `stateType`：1 已追上、2 落后、3 离线 |
| `{chainID}[/{version}]/lastBlockNumber`   | 当前确认高度                          |

节点离线且没有租约时直接删除该键。高度只在变化时写入，避免无谓的 etcd 写放大。

这两个键是 nodex-proxy 路由的全部依据：节点池成员来自前者，State / Archive 的选择阈值来自后者。

## 双模式

由 `version` 和 `outer_version_new_block_topic` 两个配置共同决定。

<Tabs>
  <Tab title="版本模式">
    同时写 version topic 和 singleton topic。

    * etcd 键使用 `{chainID}/{version}/` 前缀
    * S3 路径包含 version 段
    * singleton topic 的写入权需要通过 etcd 分布式锁（`{chainID}/outer_block_notice`）选举
    * Leader 定期比对 `{chainID}/version`，版本不匹配时释放锁

    切换版本时，新 Leader 需要把 singleton topic 对齐到自己的进度：向前快进，或回退到共同祖先后重放。这段逻辑在 `AlignOuterSingleton` 和 `align`。
  </Tab>

  <Tab title="传统模式">
    只写 singleton topic，etcd 键使用 `{chainID}/` 前缀，不需要 Leader 选举。
  </Tab>
</Tabs>

## 本地存储

Pebble DB 保存已确认的区块，双索引：

```text theme={null}
h{hash}   → BlockInfo（RLP 编码）
n{number} → hash
```

用于对外提供区块查询，也用于分叉巡检时判断某个高度的规范哈希。

## JSON-RPC 接口

监听地址由 `listen` 配置（默认 `:8663`），同一端口提供 `GET /metrics`。

| 方法                 | 说明          |
| ------------------ | ----------- |
| `getLatestBlock`   | 最新已确认区块     |
| `getBlockByHeight` | 按高度查询       |
| `getBlockById`     | 按哈希查询       |
| `blockIsValid`     | 判断区块是否在规范链上 |

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

```json theme={null}
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": { "id": "0x...", "num": 12345, "validation_hash": 67890, "is_fork": false }
}
```

错误统一返回 `-39005`。

## 指标

| 指标                                                                      | 说明                           |
| ----------------------------------------------------------------------- | ---------------------------- |
| `pipeline_node_info`                                                    | 节点角色信息（标签 `chain_id`、`role`） |
| `pipeline_block_num` / `pipeline_block_time`                            | 最新已发布的区块高度与时间戳               |
| `pipeline_replica_ready_wait_seconds`                                   | 等待副本就绪的时长分布                  |
| `pipeline_replica_ready_timeouts_total`                                 | 副本未在超时内就绪的次数                 |
| `pipeline_process_publish_seconds`                                      | 从开始处理到外部通知写完的耗时              |
| `pipeline_block_ingress_to_outer_kafka_seconds`                         | 写节点收到区块到外部 Kafka 写入成功的端到端延迟  |
| `pipeline_fork_scan_rewrites_total` / `pipeline_fork_scan_errors_total` | 分叉巡检                         |
| `pipeline_drop_block_rewrite_failures_total`                            | drop block 标记最终失败次数          |

## 运行

```bash theme={null}
go build -o checker cmd/checker/*.go
./checker -config config.yml -listen :8663
```

配置项见[配置参考](/reference/configuration#consistency-checker)。

## 开发

```bash theme={null}
make build
make test
make race
make ci
```

`check/critical_path_test.go` 覆盖了关键路径的幂等与重试语义，改动 `Process` 流程时应先读它。
