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

# Block notifications and the canonical chain

> The semantics of block change notifications, how a reorg is expressed as dropBlocks, how fork blocks are marked, and the difference between “published” and “confirmed” notifications.

Every Leafage component works around the same **block stream**: the write node publishes block change notifications in execution order, and query nodes and consistency-checker consume them in that same order. Three things in this stream need to be kept apart: the notification itself, the canonical chain versus fork blocks, and published versus confirmed.

## Block change notification

Each time the write node sets a new canonical head, it publishes one `BlockChangeNotification` to the Kafka internal topic:

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

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

The notification carries metadata only, no payload. Consumers take the hash and fetch the Header and StateDiff from S3 themselves. Kafka is therefore responsible only for ordering and low latency, while large objects go through S3 and can be fetched in parallel.

| Property      | Value                                                                     | Meaning                                                                   |
| ------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Partitions    | One                                                                       | A totally ordered block stream; every consumer sees the same order        |
| Publisher     | The Leader write node only                                                | Multiple write nodes never produce duplicate or interleaved notifications |
| Consumption   | Each query node `assign`s the partition itself and manages its own offset | Every replica receives every message; there is no consumer-group sharing  |
| Publish point | After `writeBlockAndSetHead` sets the canonical head                      | Blocks before this point may still be reorged                             |

<Warning>
  The S3 upload and the Kafka publish are not one atomic operation. By the time a consumer receives the notification the S3 objects are usually there, but consumers must tolerate a brief 404 and retry.
</Warning>

## Canonical chain and fork blocks

The **canonical chain** is the chain that contains the write node's current canonical head. Before publishing, the write node compares the last published block with the new head by finding their common ancestor:

```text theme={null}
Last published head: A ─ B ─ C
Current head:        A ─ B ─ D ─ E

Common ancestor B
dropBlocks = [C]        changeType = 2
newBlocks  = [D, E]
```

With only new blocks, `changeType` is `1` and `newBlocks` is ascending by height; when a branch is dropped, `changeType` is `2` and both `dropBlocks` and `newBlocks` are present.

The dropped blocks are **fork blocks**. They do not vanish from the system:

| Location            | What happens to a fork block                                                                                                                                      |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| S3                  | The objects stay. The write node cannot know at upload time whether a block will be dropped, so `is_fork` in `BlockValidation` is always `false` when uploaded    |
| Query node memory   | Its diff layer stays in `hash_diff_map` and is removed from `num_diff_map`; it is still reachable by hash, while lookups by height follow the new canonical chain |
| consistency-checker | Rewrites the matching `BlockValidation` on S3 to `is_fork: true` and publishes a notification with `is_fork: true` to the external topic                          |

The same height may have several objects under the external bucket prefix `{chainID}[/{version}]/{height}/`; `is_fork` is the only way to tell them apart. A query node catching up from a cold start relies on it to resolve a height to its canonical hash.

## Published versus confirmed

At the moment the write node publishes a notification, query nodes have not applied the block yet. A consumer subscribed directly to the internal topic would receive the notification while the block is "not yet queryable". consistency-checker adds a confirmation step in between:

|              | Published                                          | Confirmed                                                                       |
| ------------ | -------------------------------------------------- | ------------------------------------------------------------------------------- |
| Topic        | Internal topic `nodex_pipeline_{chainID}`          | External topic `pipeline_{chainID}`                                             |
| Publisher    | Write node Leader                                  | consistency-checker                                                             |
| Meaning      | The write node has executed and uploaded the block | A `ready_ratio` (default 0.8) share of query nodes has caught up to this height |
| Consumers    | leafage-evm, consistency-checker                   | External systems such as indexers and analytics platforms                       |
| Message type | `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"`
}
```

External notifications use one structure for both new and dropped blocks, distinguished by `is_fork`. On a reorg, the checker publishes the drop notifications first, then the new-block notifications.

External consumers therefore get one guarantee: **when the notification arrives, the query cluster can already serve that block**. If replicas are still not ready after `check_timeout_ms` (default 2000 milliseconds), the checker does not publish the block and retries the whole step.

## One block, three records

Once a block is confirmed, three places along the pipeline each keep a record of it, serving different readers:

| Location                                     | Record                                     | Reader                                                              |
| -------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------- |
| Query node state tree                        | Diff layer, indexed by hash and height     | `eth_call` and other state queries                                  |
| consistency-checker's Pebble DB              | `h{hash}` → block info, `n{number}` → hash | Confirmed-state queries such as `getLatestBlock` and `blockIsValid` |
| etcd `{chainID}[/{version}]/lastBlockNumber` | Latest confirmed height                    | nodex-proxy's State / Archive routing                               |

Both leafage-evm and consistency-checker expose `getLatestBlock` / `getBlockByHeight` / `getBlockById` / `blockIsValid`. The method names match but the semantics differ: the former returns the node's own current view, the latter returns confirmed blocks.

## Summary

* A **block change notification** carries hashes and heights only, on a single totally ordered partition, published by the Leader alone; payloads live in S3.
* A reorg is expressed as `changeType: 2` plus `dropBlocks`; dropped **fork blocks** stay in both S3 and query node memory, and consistency-checker writes back `is_fork`.
* **Published** (internal topic) and **confirmed** (external topic) are two different streams; external consumers should subscribe only to the latter.
* The confirmed height is written to etcd's `lastBlockNumber`, which is what nodex-proxy uses to choose between State and Archive.

Continue reading:

* [Block context and routing](/en/concepts/block-context): how a request says "which block", and how the confirmed height feeds into routing.
* [Data flow](/en/architecture/data-flow): reorg handling details in the write node, leafage-evm, and the checker.
* [Interface contracts](/en/architecture/interfaces#kafka): naming, encoding, and producer settings of the two topics.
