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

# 区块通知与规范链

> 区块变更通知的语义、重组如何表达为 dropBlocks、分叉块如何被标记，以及“已发布”与“已确认”两种通知的区别。

Leafage 的所有组件都围绕同一条**区块流**工作：写节点按执行顺序发布区块变更通知，查询节点和 consistency-checker 按同样的顺序消费。这条流里有三个需要分清的概念：通知、规范链与分叉块、已发布与已确认。

## 区块变更通知

写节点每确定一次新的 canonical head，就向 Kafka 内部 topic 发布一条 `BlockChangeNotification`：

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

通知里只有元信息，没有数据体。消费者拿到哈希后自己去 S3 取 Header 和 StateDiff。Kafka 因此只承担顺序和低延迟，大对象走 S3 可以并行拉取。

| 属性   | 值                                          | 含义                   |
| ---- | ------------------------------------------ | -------------------- |
| 分区   | 单分区                                        | 全序区块流，所有消费者看到同样的顺序   |
| 发布者  | 仅 Leader 写节点                               | 多个写节点不会发出重复或交错的通知    |
| 消费方式 | 每个查询节点独立 `assign` 分区并自管 offset             | 每个副本都收到全部消息，不是消费组内分摊 |
| 发布时机 | `writeBlockAndSetHead` 确定 canonical head 后 | 此前的区块可能还会被重组         |

<Warning>
  S3 上传与 Kafka 发布不是原子操作。消费者收到通知时对应的 S3 对象通常已就绪，但必须容忍短暂的 404 并重试。
</Warning>

## 规范链与分叉块

**规范链**是写节点当前 canonical head 所在的那条链。写节点发布通知前，把上一条已发布的区块与新 head 做共同祖先比较：

```text theme={null}
上次发布的 head: A ─ B ─ C
当前 head:       A ─ B ─ D ─ E

共同祖先 B
dropBlocks = [C]        changeType = 2
newBlocks  = [D, E]
```

只有新块时 `changeType` 为 `1`，`newBlocks` 按高度升序；存在被丢弃的分支时 `changeType` 为 `2`，同时带 `dropBlocks` 和 `newBlocks`。

被丢弃的块就是**分叉块**。它们不会从系统里消失：

| 位置                  | 分叉块的去向                                                                             |
| ------------------- | ---------------------------------------------------------------------------------- |
| S3                  | 对象保留。写节点上传时不知道它会不会被丢弃，`BlockValidation` 的 `is_fork` 上传时恒为 `false`                  |
| 查询节点内存              | 差异层保留在 `hash_diff_map`，从 `num_diff_map` 摘除；按哈希仍可查，按高度走新的规范链                        |
| consistency-checker | 把 S3 上对应的 `BlockValidation` 重写为 `is_fork: true`，并向外部 topic 发出带 `is_fork: true` 的通知 |

同一高度在 S3 外部桶的前缀 `{chainID}[/{version}]/{height}/` 下可能有多个对象，`is_fork` 是区分它们的唯一依据。查询节点冷启动追赶时靠它把高度解析成规范哈希。

## 已发布与已确认

写节点发布通知的那一刻，查询节点还没有应用这个区块。直接订阅内部 topic 的消费者会在“去查还查不到”的时候收到通知。consistency-checker 在中间加了一道确认：

|            | 已发布                                 | 已确认                                 |
| ---------- | ----------------------------------- | ----------------------------------- |
| 通知所在 topic | 内部 topic `nodex_pipeline_{chainID}` | 外部 topic `pipeline_{chainID}`       |
| 发布者        | 写节点 Leader                          | consistency-checker                 |
| 含义         | 写节点已执行并上传                           | 达到 `ready_ratio`（默认 0.8）的查询节点已追上该高度 |
| 消费者        | leafage-evm、consistency-checker     | 索引器、分析平台等外部系统                       |
| 消息结构       | `BlockChangeNotification`           | `OuterBlockChangeNotification`      |

```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 先发 drop 通知，再发新块通知。

外部消费者因此得到一个保证：**收到通知时，去查询集群一定能读到这个区块**。等待副本超过 `check_timeout_ms`（默认 2000 毫秒）仍未达标时，checker 不发布该区块并整体重试。

## 同一个区块，三处记录

一个区块被确认后，链路上有三个地方各自记着它，服务不同的读取者：

| 位置                                           | 记录内容                              | 读取者                                     |
| -------------------------------------------- | --------------------------------- | --------------------------------------- |
| 查询节点状态树                                      | 差异层，按哈希与高度索引                      | `eth_call` 等状态查询                        |
| consistency-checker 的 Pebble DB              | `h{hash}` → 区块信息，`n{number}` → 哈希 | `getLatestBlock`、`blockIsValid` 等确认状态查询 |
| etcd `{chainID}[/{version}]/lastBlockNumber` | 已确认的最新高度                          | nodex-proxy 的 State / Archive 路由        |

leafage-evm 和 consistency-checker 都提供 `getLatestBlock` / `getBlockByHeight` / `getBlockById` / `blockIsValid`，方法名相同但语义不同：前者返回本节点当前视图，后者返回已确认的区块。

## 小结

* **区块变更通知**只含哈希、高度等元信息，单分区全序，仅 Leader 发布；数据体在 S3。
* 重组表达为 `changeType: 2` 加 `dropBlocks`；被丢弃的**分叉块**在 S3 和查询节点内存中都保留，由 consistency-checker 回写 `is_fork`。
* **已发布**（内部 topic）与**已确认**（外部 topic）是两条不同的流，外部消费者应当只订阅后者。
* 已确认高度写在 etcd 的 `lastBlockNumber`，是 nodex-proxy 判断 State / Archive 的依据。

继续阅读：

* [区块上下文与路由](/concepts/block-context)：请求如何表达“要哪个区块”，以及已确认高度如何参与路由。
* [数据链路](/architecture/data-flow)：reorg 在写节点、leafage-evm 与 checker 三处的处理细节。
* [接口契约](/architecture/interfaces#kafka)：两个 topic 的命名、编码与生产者配置。
