Repository navigation
fix(blockchain): stop the missing-ancestor walk at an imported block - #720
MegaRedHand wants to merge 1 commit into
Conversation
pending_block_parents records each pending block's missing ancestor as it was when the block pended. When that ancestor later imports while a block between it and its descendants is still missing (one dropped for lack of an execution verdict after its own parent imported), every new arrival resolved to the imported ancestor. The DB walk re-queued it as a no-op and returned before requesting anything, so the dropped block was never retried and the head stayed put. Seen on Hoodi behind a snap-syncing geth: stuck at slot 4248640 for three hours. Stop the pointer walk at an ancestor that has a post-state, so the DB walk finds the real gap.
🤖 Kimi Code ReviewI'll review this PR diff focusing on the blockchain crate changes related to stale pending block parent resolution. Overall AssessmentThis PR fixes a legitimate consensus-critical bug where dropped blocks below an imported ancestor would never be retried, causing chain sync to stall. The fix is conceptually correct but has implementation concerns. Issues Found1. Critical:
|
| Priority | Location | Change |
|---|---|---|
| Critical | lib.rs:~2787 |
Replace .expect() with proper error handling |
| Medium | lib.rs:~2780 |
Clarify variable semantics after loop break |
| Low | mod tests |
Add DB error path test |
| Low | lib.rs:~2783 |
Verify comment accuracy on "no-op" behavior |
Summary
The fix is conceptually correct and addresses a real consensus stall bug, but the .expect() introduces a new liveness risk (crash on DB error) that could be exploited for DoS or cause node instability. This must be fixed before merge.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code ReviewLooks good overall. The fix in A few review notes:
No security issues jump out in this patch: it does not change signature verification, SSZ handling, fork-choice state, or consensus validation rules, and the new logic fails closed by continuing to pend/request blocks rather than admitting invalid data. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview of PR 720: stop the stale I found no blocking issues. I traced the logic by reading the diff and the surrounding code. I did not build the project or run the new test. Correctness
Performance
Style and tests
Nit, optional
LGTM. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🗒️ Description / Motivation
An
ethlambda beacon(v1.0.0-rc.2) paired with geth v1.17.10 on Hoodi stopped importing at slot 4248640 and stayed there for three hours. Found by ethrex's multisync (lambdaclass/ethrex#7461): the pair failed its 10-minute head-stall check while the same image passed with nethermind and ethrex.The trigger was geth: mid snap sync, it answered
engine_newPayloadV4past the 8 s engine ladder, so block 4248641 got no verdict and was dropped, as designed (ImportOutcome::Held). What should retry such a block is the next arrival:process_or_pend_blockresolves the arrival's missing ancestor throughpending_block_parents, and the DB walk then re-queues the first stored block whose parent has a state.That resolution is what went wrong.
pending_block_parentsstores the missing ancestor as it was when each block pended. Every descendant of 4248641 had pended while 4248639 was the gap, so all of them pointed at 4248639. Once 4248639 imported and 4248641 was dropped after it, every arrival resolved to 4248639. The DB walk re-queued that already-imported block (a no-op through the "already in the store" early return) and returned beforerequest_missing_block, so 4248641 was never retried or requested, and ~900 later blocks stayed parked. Only a re-delivery of 4248642, whose parent is 4248641 itself, could have recovered it, and none came.The same run's Sepolia pair dropped 11 blocks for lack of a verdict and recovered every time, because its pointers happened to land on the dropped block.
What Changed
process_or_pend_block(crates/blockchain/src/lib.rs): thepending_block_parentswalk stops at an ancestor that already has a post-state. The existing DB walk then steps through the gap block by block, finds the dropped block, re-queues it, and re-points each pending block it passes at its direct parent.ImportOutcome::Helddoc: a no-verdict block is re-driven by the next arrival that leads to it (from the DB, or by asking the network), not by a timer.Correctness / Behavior Guarantees
has_stateread per pointer step. Normally that is one step, since pointers jump straight to the top of a gap; while a gap opened this way is still open, later arrivals step through it a block at a time.Tests Added / Run
a_block_dropped_below_an_imported_ancestor_is_retried_by_the_next_arrival: an imported ancestor, its child dropped with no state, a grandchild still pointing at the ancestor, then one arrival. Asserts the dropped block is what gets queued. Without the guard it fails, queueing the imported ancestor instead.cargo test -p ethlambda-blockchain --lib --profile release-fast: 292 passed, 43 ignored.Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — onlyethlambda-blockchain --libwas run