Skip to content

Coverage, finality, and reorgs

Coverage answers “which block intervals did this processor commit?” Finality answers “what chain-confidence status does each committed interval have?” They are related but deliberately not collapsed into one boolean.

Finality levels and publish modes

Every record carries one level. A block only ever rises: unattested, attested, finalized. It is never published twice; each rise is a marker.

Level Evidence Typical delay after the slot starts Can be undone
unattested At least two execution peers served the same header, it passes Leani’s header rules (base fee, gas limit, blob gas, slot timing, fork fields), and it extends the delivered chain within two slots of the newest attested head ~2–5 s today; ~5–14 s after Glamsterdam Yes: by a reorg, by an attested head on another block, or after 60 s without an attestation
attested At least 342 of 512 sync-committee members signed it (or a descendant), verified locally from a light-client update. After Glamsterdam the committee signs the next beacon block, which builds on this payload ~15 s today; ~25–29 s after Glamsterdam Yes, by a reorg
finalized Casper FFG finality, verified from a light-client finality update ~13–19 min No

publish sets the first level a processor’s consumers see:

publish First level Markers Query-then-follow
unattested (default) unattested attested, then finalized Yes
attested attested finalized No: its state holds unpublished unattested blocks
finalized finalized none No

An Ethereum RPC’s latest corresponds to unattested; Leani’s JSON-RPC latest is the delivered tip at any level, and safe and finalized name the finalized head.

Choose attested when a consumer must never act on a block that is later undone for want of an attestation. Late blocks — first served more than 7 s into their slot, the ones the network most often orphans — wait for their child or their attestation before they are published unattested.

A bounded history job that only needs its final aggregate sets [processors.delivery] emit = "job_result" instead of a level.

finalized reflects the node’s configured finality source. Embedded and native-P2P runs verify it locally through light-client proofs. Sources that serve finalized material under dataset trust (Xatu, EraE) carry the label under that source’s trust model; subscribing to them does not turn the label into an independent proof.

Post-merge execution headers carry no seal, so any peer could serve a self-consistent block of its own after the chain tip. An unattested block therefore rests on several peers, Leani’s header rules and its link to the delivered chain, and stays within two slots of the newest attested head: an execution header that at least two thirds of the sync committee signed in a light-client optimistic update, which the finality source verifies with the header’s execution branch within the slot it appears. When a processor on the lane requires attested frames, the lane includes a block only when its header chain leads to an attested head. Without verified finality there is no attested head, so live following requires a finality source.

If verified finality ever names another block at a height than the chain the node followed, neither block is finalized. When the node’s blocks there are unfinalized, for example because the lane stalled across a reorg, the network lanes restart: retained unfinalized blocks that neither link to the new finalized anchor nor are proven to be its ancestors are undone as a reorg and never finalized, as a branch reorged away while the node was down is at startup. Where parent links do not reach the anchor, execution headers from peers that link by parent hash down from it prove which retained blocks are its ancestors, and those are kept and finalized; the runbook’s finality contradiction section explains the proof and its fallback. The lanes halt and report not ready instead when the contradicted block is itself finalized, or when restarting twice did not resolve the contradiction.

Requested blocks 100 to 140. Coverage is 100 to 112 and 120 to 140, so 113 to 119 are missing. Finality is separate: finalized 100 to 128, attested 129 to 140.Requested blocks 100 to 140. Coverage is 100 to 112 and 120 to 140, so 113 to 119 are missing. Finality is separate: finalized 100 to 128, attested 129 to 140.

In that example, complete is false because 113..119 is absent. A high processedThrough value does not hide the internal gap, and finalized coverage does not imply the unfinalized tail is absent.

Filling history gaps

Leani resumes from committed coverage, not from the last downloaded object. Retained material and configured archive sources are preferred when capable. Verified execution P2P can recover the remaining finalized gaps while the live lane continues following head.

By default, omission of sources.live.history_fallback_blocks makes every gap at or after the processor start block eligible. Setting it creates an explicit suffix boundary. It is a policy limit, not a protocol limitation or a new default.

A block-local live lane can open a gap of its own: after downtime it resumes at the newest finalized anchor, past blocks it never processed. Automatic history refills them. With history_mode = "on_demand", the processor’s live stream announces them in a system.live_gap.put change instead, and the application requests them, as Keep your application in sync shows. A processor with [processors.live_gap_fill] fills a short range itself, from finalized history before it applies the next block, and announces only what it did not fill; see Live gap fill.

Reorgs are delivery events

For attested publication (publish = "attested"), downstream consumers can see apply, undo, and later finalized changes. Processor state, undo records, and published changes commit together. A consumer must implement these operations idempotently and must not infer finality from age alone.

With publish = "finalized" the change stream carries only finalized apply records and system.finality markers. Unfinalized blocks are applied to processor state and journaled for undo, but their changes stay in the store until verified finality promotes them. Their bytes count against the delivery budget from the moment they are applied, so a slow consumer pauses the processor exactly as it would with unattested publication. For a processor with the default canonical delivery ordering, a block that is already finalized when it is applied, for example after gap recovery, first publishes the older blocks still waiting, so the stream stays in block order. A reorg of an unpublished block emits nothing downstream.

Use processor status for exact intervals and /v1/network/status for the independent live/finality lane state. Readiness can be healthy while an older backfill gap is still being filled.

Undo runs

A reorg undoes its reverted blocks newest first, and so does the revert at a restart after a long outage. A consumer’s live stream delivers each reverted block’s undo changes as a batch of its own, so several reverted blocks arrive as a run of undo batches, newest first, before any replacement block’s apply batch. An application that keeps derived aggregates, such as leaderboards or rolling totals, beside its per-block state can rebuild them once per run instead of once per batch:

  1. Apply each undo to the per-block state immediately, record in the same transaction that the aggregates need a rebuild, and acknowledge the batch as usual.
  2. Rebuild the aggregates, and clear that record, when the run ends: before the first batch that is not an undo, at a heartbeat, or when the session closes.

A run of N reverted blocks then costs one rebuild, not N. Because the record commits with the per-block state, an application that restarts in the middle of a run still finds the rebuild due.