fix: skip finalization advance for stale finalized sources#405
Conversation
Port leanSpec #802. A supermajority attestation whose source sits at or
behind the finalized boundary may justify a newer target, but must not
enter the finalization-advance scan: doing so would rewind or re-finalize
below the boundary.
Gate finalization advancement on `source.slot > finalized_slot` in two
mirrored spots:
- `try_finalize` (state transition): the actual advance. ethlambda did not
panic like the spec (its `slot_is_justifiable_after` returns false on
underflow), but the guard removes a misleading `inc_finalizations("error")`
for stale sources and a spurious re-finalize in the empty-range boundary
case.
- `score_entry` (block builder): projects/scores finalization while packing.
Without the guard, a boundary source with `target == finalized + 1` yields
an empty scan range, making `.all(...)` vacuously true and mis-tiering the
entry as `Finalize` even though it advances nothing.
Add regression tests for both paths.
🤖 Kimi Code ReviewOverall Assessment: Correct and critical safety fix for 3SF-mini finalization. The PR prevents a supermajority from rewinding finalization by voting with a stale source at or behind the finalized boundary. Code Correctness & Security
Performance
Testing
Nitpicks
Verdict: Ship it. This is a precise, minimal fix for a consensus safety bug with excellent test coverage. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewHere is the review: PR #405 — fix: skip finalization advance for stale finalized sourcesOverview: This ports leanSpec #802 — the correct fix for spurious re-finalization from stale/boundary sources. Rather than rejecting such attestations (the approach in #402), the spec now accepts the justification and only guards the finalization advancement. Two symmetric guards are added: one in CorrectnessThe root bug (boundary case, In if ((source.slot + 1)..target.slot).any(|slot| justifiable(slot, finalized)) {
return; // gaps remain — don't finalize
}
// else: advance finalizationWith The same vacuous-truth hazard appeared in Both guards use strict The stale case ( Symmetry: The two guards mirror each other precisely, and the comment in Tests
Suggestion: Add a companion test in // source = finalized = slot 4, target = slot 5
// Empty scan range would spuriously re-finalize without the guard.
// After fix: justified advances to 5, finalized stays at 4.
fn boundary_source_justifies_without_re_finalizing() { ... }This directly exercises the code path described as "the genuine behavioral fix" in the PR body. Minor Points
SummaryThe fix is correct, minimal, and well-reasoned. The only gap is the missing boundary-case test in Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code ReviewNo blocking findings in the reviewed diff. The core fix looks correct: lib.rs now rejects finalization attempts when Residual risk: this was a static review only. I could not run the new tests in this sandbox because Cargo needs to fetch/write external dependencies ( Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryPorts leanSpec #802: gates finalization advancement on
Confidence Score: 5/5Safe to merge — the change is a targeted spec-alignment fix with no regressions in the surrounding logic. Both changed sites (state transition and block builder) receive the identical guard, the boundary-case vacuous-truth bug is directly reproduced and fixed by the new tests, and the pre-existing stale-source path now exits cleanly instead of incrementing a misleading error metric. The fix is additive (an early-return and a short-circuit &&), leaving all downstream finalization logic untouched. No files require special attention.
|
| Filename | Overview |
|---|---|
| crates/blockchain/state_transition/src/lib.rs | Adds a 6-line early-return guard to try_finalize plus a well-constructed regression test; guard logic is correct and matches the spec change. |
| crates/blockchain/src/block_builder.rs | Adds source.slot > projected_finalized_slot to the finalizes conjunction and a regression test for the boundary mis-tier; mirrors the state-transition guard correctly. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Supermajority attestation crosses 2/3 threshold] --> B{source.slot <= finalized_slot?}
B -- Yes: stale or boundary source --> C[Return early / score as Justify\nNo finalization advance]
B -- No: source is past finalized boundary --> D{Any slot in source+1..target\nis justifiable?}
D -- Yes: gap exists --> E[Block finalization\ninc_finalizations error]
D -- No: source and target are consecutive --> F[Advance finalized to source\ninc_finalizations success\nShift justified_slots window\nPrune stale justifications]
Reviews (1): Last reviewed commit: "fix: skip finalization advance for stale..." | Re-trigger Greptile
Summary
Ports leanSpec #802 to ethlambda.
A supermajority attestation whose source checkpoint sits at or behind the finalized boundary is a valid justification anchor (
is_slot_justifiedtreats slots<= finalized.slotas already justified), so it may justify a newer target. But once it crosses the supermajority threshold, the finalization logic scannedsource.slot + 1 .. target.slotand could try to advance (rewind/re-finalize) below the finalized boundary.The fix gates finalization advancement on
source.slot > finalized_slot. Stale or boundary sources may still justify newer targets; they no longer try to rewind or scan below the finalized boundary.Relationship to #402
This supersedes #402 (
fix(stf): reject blocks justifying a target whose source is below finalized), which was the pre-#802 attempt that fixed the issue by rejecting such blocks. The spec instead accepts the justification and only skips the finalization advance, so this PR follows the merged spec approach. #402 can be closed in favor of this.Changes
Two mirrored spots get the
source.slot > finalized_slotguard:try_finalize(state transition)score_entry(block builder)Tests
state_transition:stale_finalized_source_justifies_without_rewinding_finalization— supermajority from a stale source justifies the target while finalization stays pinned.block_builder:score_entry_does_not_finalize_source_at_boundary— a boundary source is scoredJustify, notFinalize.fmt + clippy (
-D warnings) clean on both crates.