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.
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:
- Apply each
undoto the per-block state immediately, record in the same transaction that the aggregates need a rebuild, and acknowledge the batch as usual. - 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.
Built by Smart-Byte · Legal notice (Impressum) · Privacy