fix(startup): resume from existing DB without a checkpoint URL - #554
fix(startup): resume from existing DB without a checkpoint URL#554MegaRedHand wants to merge 4 commits into
Conversation
`fetch_initial_state` gated the on-disk state lookup on `--checkpoint-sync-url`, so restarting a node without that flag wrote a slot-0 genesis anchor over a populated RocksDB and discarded a current chain. Operators had to pass a checkpoint URL purely as a fallback trigger, even on a fresh DB where it was never used. Try the persisted state first and treat the URL as a fallback for when nothing on disk is resumable. When the DB is stale and no URL was given, resume anyway instead of re-initializing: P2P forward-sync can close the gap, a genesis re-init cannot. The staleness threshold now only decides whether a checkpoint is preferable to what we already have. Also drop the `Starting checkpoint sync` line emitted before the DB was consulted; it fired on every successful resume, so grepping the boot log for it falsely reported a resync.
🤖 Kimi Code ReviewOverall Assessment: This is a well-structured PR that correctly prioritizes resuming from local disk over checkpoint sync to prevent accidental chain re-initialization on restart. The logic is sound and the tests cover the critical edge cases. A few minor issues need attention. Issues Found1. Silent suppression of DB read errorsFile: The code silently ignores errors from if let Ok(Some(store)) = Store::from_db_state(backend.clone(), genesis.genesis_time) {
// ...
}
// Err(e)
---
*Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt* |
🤖 Codex Code ReviewNo findings. The changed control flow in bin/ethlambda/src/main.rs looks correct: it now preserves an existing same-network DB when Residual gap: I did not find a regression, but there is no direct test asserting that a stale DB still falls through to checkpoint sync when Verification: I could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview:
|
Greptile SummaryThe PR makes persisted database state the first startup choice and treats checkpoint URLs as a fallback.
Confidence Score: 4/5The storage-error fallback must be fixed before merging because a restart can still replace an existing chain with genesis state.
Files Needing Attention: bin/ethlambda/src/main.rs
|
| Filename | Overview |
|---|---|
| bin/ethlambda/src/main.rs | Reorders initial-state selection to prefer persisted state, but still conflates storage read failures with an empty database. |
| bin/ethlambda/src/cli.rs | Updates checkpoint URL help text to describe its new fallback semantics. |
| docs/checkpoint_sync.md | Documents restart precedence, stale-state behavior, and operational recovery guidance. |
| CLAUDE.md | Updates unrelated RPC listener documentation without introducing a review finding. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
Start[Start node] --> ReadDB[Read persisted DB state]
ReadDB -->|Matching state| Age{Within resume window?}
Age -->|Yes| Resume[Resume from DB]
Age -->|No, URLs absent| ResumeStale[Resume stale DB with warning]
Age -->|No, URLs present| Sync[Checkpoint sync]
ReadDB -->|No state| URLs{Checkpoint URLs configured?}
ReadDB -->|Read error currently discarded| URLs
URLs -->|Yes| Sync
URLs -->|No| Genesis[Initialize from genesis]
Prompt To Fix All With AI
### Issue 1
bin/ethlambda/src/main.rs:684
**Storage errors trigger genesis fallback**
When a populated database returns an I/O, decoding, or consistency error from `Store::from_db_state` and no checkpoint URL is configured, `if let Ok(Some(store))` discards the error and proceeds to initialize the same backend from genesis, causing an unreadable existing chain to be overwritten instead of failing safely.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "docs: fix docs" | Re-trigger Greptile
🗒️ Description / Motivation
Restarting a node without
--checkpoint-sync-urldestroyed its chain.fetch_initial_stategated the on-disk state lookup on that flag:So a redeploy against a populated RocksDB wrote a slot-0 genesis anchor over a perfectly current chain, and operators had to pass a checkpoint URL purely as a fallback trigger even when the DB was fresh and the URL was never fetched.
This makes on-disk state authoritative:
--checkpoint-sync-urlbecomes a fallback for when there is nothing resumable, not a precondition for reading what is there.What Changed
bin/ethlambda/src/main.rs—fetch_initial_statetriesStore::from_db_statebefore the empty-URL early return:Also removes the
info!(url_count, "Starting checkpoint sync")that was emitted before the DB was consulted. It fired on every successful resume, so grepping a boot log for"Starting checkpoint sync"false-positived on nodes that never synced. Each outcome now has exactly one unambiguous log line.bin/ethlambda/src/cli.rs—--checkpoint-sync-urlhelp text no longer claims it "skips genesis initialization"; it is documented as a fallback.docs/checkpoint_sync.md— new Restarts and Existing State section: precedence table, the resume window and why it is measured against the head rather than the finalized checkpoint, the P2P-catch-up caveat, and the boot-log table.Correctness / Behavior Guarantees
GENESIS_TIME)--checkpoint-sync-urlMAX_RESUMABLE_DB_STATE_AGEkeeps its value and its meaning in the URL-present case; it now only decides whether a checkpoint is preferable to what we already have, never whether the DB is readable.current_slot - head_slot), so a node whose head is current resumes during a finality stall.SIGNATURE_PRUNING_RANGEhorizon (~1 day)BlocksByRangecannot serve the missing history, so a node that far behind needs a checkpoint URL. The alternative (refuse to start) would break unattended restarts after a routine 31-minute outage.--checkpoint-sync-urlno longer means "start from genesis" when a DB exists; to deliberately start over, remove the data directory. That is already the documented idiom for a clean checkpoint sync, and an--ignore-existing-dbflag would only reintroduce the write-genesis-over-live-data footgun behind a flag.GENESIS_TIMEmismatch still degrades silently (from_db_statelogs"Persisted DB has a different genesis_time; treating as empty"), so with no URL the node writes genesis over a foreign-network DB. Pre-existing behavior, flagged for a separate change.Tests Added / Run
Four unit tests in
bin/ethlambda/src/main.rsdrivingfetch_initial_stateagainstInMemoryBackend(no new dev-dependency; the resume and genesis paths need no network):initializes_from_genesis_when_db_is_emptyresumes_from_fresh_db_without_checkpoint_urlresumes_from_stale_db_without_checkpoint_urlgap > MAX_RESUMABLE_DB_STATE_AGEinitializes_from_genesis_when_db_genesis_time_differsThe seeded anchor sits above slot 0 because a genesis re-init also yields head slot 0; that is what makes "resumed" distinguishable from "started over". Staleness is induced purely by choosing
genesis_time(current_slotderives from the wall clock against it), so no clock injection. Both resume tests fail onmainwithleft: 0, right: 12.Commands run:
Local multi-client devnet verification is in progress; I'll post the boot logs showing a keep-DB restart with no
--checkpoint-sync-urlas a comment.Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — all passing