Skip to content

fix: use the head slot for the DB staleness check#506

Open
MegaRedHand wants to merge 1 commit into
mainfrom
fix-sync-gate
Open

fix: use the head slot for the DB staleness check#506
MegaRedHand wants to merge 1 commit into
mainfrom
fix-sync-gate

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

The current DB staleness check only takes into account the latest finalized slot. We should check against the head slot instead.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the DB staleness check in fetch_initial_state to compare the current slot against the head slot instead of the latest finalized slot, which can lag up to 2 epochs (~5 min) behind head. The fix makes the resume window more accurate and avoids unnecessary checkpoint syncs when the node has recently processed blocks.

  • Replaces store.latest_finalized().slot with store.head_slot() in the gap calculation; since head >= finalized, the computed gap is now ≤ the old gap, so the node is less likely to be incorrectly classified as stale.
  • Logging fields (finalized_slothead_slot) are updated consistently in both the resume and warn paths.

Confidence Score: 5/5

Safe to merge — the change tightens the freshness measurement to the actual chain tip rather than the lagging finalized checkpoint, reducing unnecessary re-syncs.

The fix is a two-line swap that corrects which slot is used as the freshness baseline. head_slot() is always at or ahead of finalized_slot, so the computed gap can only stay the same or shrink, correctly keeping the node resumable when it has recently seen blocks. The head_slot() implementation is straightforward and panics only if the head block is missing, which cannot happen when Store::from_db_state returns Some. Log fields are updated consistently.

No files require special attention.

Important Files Changed

Filename Overview
bin/ethlambda/src/main.rs Switches DB staleness check from latest_finalized().slot to head_slot(), giving a more accurate freshness window; log fields updated to match.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[fetch_initial_state called] --> B{checkpoint_urls empty?}
    B -- Yes --> C[Init from genesis state]
    B -- No --> D{Store::from_db_state returns Some?}
    D -- No --> G[Start checkpoint sync]
    D -- Yes --> E[Compute current_slot from wall clock]
    E --> F[Compute gap = current_slot - head_slot\n  previously: finalized_slot]
    F --> H{gap <= MAX_RESUMABLE_DB_STATE_AGE\n  450 slots / ~30 min}
    H -- Yes --> I[Resume from existing DB state]
    H -- No --> J[Warn: DB state is stale]
    J --> G
    G --> K[fetch_anchor_block_and_state]
    K --> L[Build Store from checkpoint]
    L --> M[Return Ok store]
    I --> M
    C --> M
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[fetch_initial_state called] --> B{checkpoint_urls empty?}
    B -- Yes --> C[Init from genesis state]
    B -- No --> D{Store::from_db_state returns Some?}
    D -- No --> G[Start checkpoint sync]
    D -- Yes --> E[Compute current_slot from wall clock]
    E --> F[Compute gap = current_slot - head_slot\n  previously: finalized_slot]
    F --> H{gap <= MAX_RESUMABLE_DB_STATE_AGE\n  450 slots / ~30 min}
    H -- Yes --> I[Resume from existing DB state]
    H -- No --> J[Warn: DB state is stale]
    J --> G
    G --> K[fetch_anchor_block_and_state]
    K --> L[Build Store from checkpoint]
    L --> M[Return Ok store]
    I --> M
    C --> M
Loading

Reviews (1): Last reviewed commit: "fix: use the head slot for the DB stalen..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Good — KEY_HEAD is always written alongside KEY_LATEST_FINALIZED in init_store (line 669/672), so head_slot()'s .expect("head block exists") is safe in this code path; no new panic risk introduced. The change is small, correct, and well-targeted.

Review: PR 506 — fix: use the head slot for the DB staleness check

Summary: Changes the DB-resume staleness gate in fetch_initial_state (bin/ethlambda/src/main.rs:652-663) to compare current_slot against store.head_slot() instead of store.latest_finalized().slot.

Correctness

This is a genuine bug fix, not just a rename. latest_finalized_slot structurally lags current_slot even on a perfectly healthy, live-synced node — finalization requires justifying two consecutive checkpoints under 3SF-mini, so it trails the head by multiple slots as a matter of protocol design. Gating "is this DB stale" on that lag means the old check could trip MAX_RESUMABLE_DB_STATE_AGE (450 slots) and force an unnecessary checkpoint sync even when the node's actual head was fully caught up. head_slot() is the correct proxy for "how fresh is the tip of this DB," which is what this check is actually trying to measure. Good fix.

Safety of head_slot() in this path

Store::head_slot() (crates/storage/src/store.rs:1494-1498) does self.get_block_header(&self.head()).expect("head block exists"), so it panics if KEY_HEAD isn't populated. Checked whether from_db_state (line 596) — which gates entry into this branch by only probing KEY_CONFIG and KEY_LATEST_FINALIZED — could return Some in a state where KEY_HEAD is absent. It can't: KEY_HEAD and KEY_LATEST_FINALIZED are always written together in init_store (store.rs:669 and 672), so any DB that passes the from_db_state probe has a head too. No new panic surface introduced.

Minor / non-blocking

  • Logging: the finalized_slot field is dropped from these two log lines in favor of head_slot. That's consistent with the new comparison, but if finalized_slot was useful operational context for diagnosing sync gaps (e.g., "head looks fresh but finality is stuck"), consider keeping both fields rather than swapping one for the other. Not a blocker — just a possible ops-visibility trade-off worth a second thought.
  • No test coverage changes accompany this — worth checking if fetch_initial_state/staleness-gap logic has existing unit tests that should be updated to assert against head_slot rather than latest_finalized. If none exist today, that's a pre-existing gap, not something this PR needs to fix, but flagging in case there's an easy add.

Verdict

Small, correct, low-risk fix. No blocking issues found.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. bin/ethlambda/src/main.rs:652 introduces a startup panic path on partially populated or schema-skewed DBs. Store::from_db_state() only probes KEY_CONFIG and KEY_LATEST_FINALIZED before returning Some (crates/storage/src/store.rs:600-606), but store.head_slot() immediately requires both KEY_HEAD and the corresponding block header to exist and will expect(...) otherwise (crates/storage/src/store.rs:730-736, 1494-1497). Before this change, the freshness gate only touched latest_finalized, so the same DB shape would fall through more gracefully. I’d either strengthen from_db_state() to validate KEY_HEAD plus its header, or make the freshness check fallible and checkpoint-sync on invalid resume state.

No other substantive issues stood out in this diff; using head_slot instead of latest_finalized.slot for the staleness heuristic is directionally correct.

I couldn’t run the Rust tests in this environment because cargo/rustup tried to write under a read-only home directory.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Consensus Safety Concern

File: bin/ethlambda/src/main.rs
Lines: 652–653, 656, 663

Switching from finalized_slot to head_slot for the resumption gap calculation changes the safety semantics of the checkpoint sync fallback:

  1. Non-finalized head risk: The head is not guaranteed to be on the canonical finalized chain. If the node was offline during a reorg that finalized a different branch, resuming from head_slot may cause the client to temporarily follow a stale fork until it realizes the finality mismatch. Resuming from latest_finalized() is the only safe way to guarantee the node starts on the canonical chain.

  2. More permissive resumption: Since head_slot >= finalized_slot (in consistent states), the gap calculation yields a smaller value, making the client more likely to resume from a potentially stale DB rather than fetching a fresh checkpoint. If MAX_RESUMABLE_DB_STATE_AGE is intended to bound finality drift, this weakens that invariant.

Recommendation:
If the intent is to avoid re-syncing when the node was recently active (even if finality is stalled), ensure the fork choice store can handle reorgs from the non-finalized head on startup. Otherwise, consider using max(head_slot, finalized_slot) or documenting why head is preferred here.

Minor: Ensure store.head_slot() cannot return a slot from a pruned state that would cause state regeneration failures when the node resumes block processing.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant