Conversation
🟡 Heimdall Review Status
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| } else if let (Ok(Some(head)), Some(unsafe_head_tx)) = | ||
| (reth_head, self.unsafe_head_tx.as_ref()) | ||
| { | ||
| unsafe_head_tx | ||
| .send_if_modified(|val| (*val != head).then(|| *val = head).is_some()); | ||
| info!( | ||
| target: "engine", | ||
| head = %head.block_info.number, | ||
| "Bootstrap: skipped reset (beyond genesis), seeded unsafe head from reth" | ||
| ); | ||
| } |
There was a problem hiding this comment.
The else if arm silently does nothing when unsafe_head_tx is None (validator mode) and reth is beyond genesis. In that case:
engine.reset()is skipped (correct — avoids reorging below the state pivot)el_sync_finishedstaysfalse(itsDefault)- No watch channel is seeded
The validator path doesn't call schedule_initial_reset, so this isn't a sequencer-style deadlock. But consider logging at info! level when we're beyond genesis in validator mode so operators can distinguish "skipped because validator" from "skipped because watch channel missing":
| } else if let (Ok(Some(head)), Some(unsafe_head_tx)) = | |
| (reth_head, self.unsafe_head_tx.as_ref()) | |
| { | |
| unsafe_head_tx | |
| .send_if_modified(|val| (*val != head).then(|| *val = head).is_some()); | |
| info!( | |
| target: "engine", | |
| head = %head.block_info.number, | |
| "Bootstrap: skipped reset (beyond genesis), seeded unsafe head from reth" | |
| ); | |
| } | |
| } else if let Ok(Some(head)) = reth_head { | |
| if let Some(unsafe_head_tx) = self.unsafe_head_tx.as_ref() { | |
| unsafe_head_tx | |
| .send_if_modified(|val| (*val != head).then(|| *val = head).is_some()); | |
| } | |
| info!( | |
| target: "engine", | |
| head = %head.block_info.number, | |
| "Bootstrap: skipped reset (beyond genesis), seeded unsafe head from reth" | |
| ); | |
| } |
This way the log line fires for validators too, confirming the bootstrap took the "beyond genesis" branch.
| let confirmed = | ||
| self.check_forkchoice_updated_status(state, &valid_response.payload_status.status)?; | ||
|
|
||
| // Apply the new sync state to the engine state. | ||
| state.sync_state = new_sync_state; | ||
| // Only apply the sync-state update when the EL confirmed the forkchoice | ||
| // (`Valid`). When the EL returns `Syncing` the block is merely stored — | ||
| // advancing `sync_state` here would move `unsafe_head` beyond what the EL | ||
| // can serve, creating a gap that breaks derivation consolidation. | ||
| if confirmed { | ||
| state.sync_state = new_sync_state; | ||
| } |
There was a problem hiding this comment.
Consider the interaction with the early-return at line 112: when the EL returns Syncing, sync_state is not updated (confirmed = false). If the same state_update is retried, new_sync_state equals state.sync_state (both still at the old value), the early return at line 112 fires, no FCU is sent, and el_sync_finished never flips to true.
In practice this may not trigger because callers typically advance to a new block. But it may be worth guarding the early return on el_sync_finished being true, so that a retry of the same update while the EL is still syncing re-sends the FCU rather than no-oping:
if state.el_sync_finished && state.sync_state != Default::default() && state.sync_state == new_sync_state {
Review SummaryThe PR correctly addresses the core problem: when the EL returns Syncing from engine_forkchoiceUpdated, the node should not advance sync_state (and thus unsafe_head) beyond what the EL can actually serve. The approach -- returning a bool from check_forkchoice_updated_status and gating the state update -- is clean and well-tested. Findings1. Early-return short-circuit vs Syncing retry (task.rs line 112) -- When the EL returns Syncing, sync_state is intentionally not updated. If the same state_update is retried, new_sync_state == state.sync_state triggers the early return, skipping the FCU entirely. This means the EL is never re-queried and el_sync_finished stays false. Guarding the early return on state.el_sync_finished would allow the FCU to be re-sent on retry while the EL is still syncing. Looks Good
|
Summary
Fixes the
base-consensussequencer to respect el sync on startup