From a76f934b6249828b3a29fdd01e8f10a80c31dceb Mon Sep 17 00:00:00 2001 From: Swanny Date: Fri, 3 Apr 2026 15:18:14 -0400 Subject: [PATCH] fix(host-rpc): prevent block gap when buffer exhaustion resets backfill When walk_chain exhausted the buffer, backfill_from was set to cached_finalized, which could be ahead of the last delivered block. This created a gap of undelivered blocks, causing "parent ru block not present in DB" crashes during initial sync. Now computes resume_from as min(chain_view.back + 1, finalized) to ensure continuity. Also adds a defensive gap check in the node's process_committed_chain to bail with a clear error message if a notification gap is ever detected. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/host-rpc/src/notifier.rs | 14 ++++++++++++-- crates/node/src/node.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/host-rpc/src/notifier.rs b/crates/host-rpc/src/notifier.rs index a8bc2ec4..eaa9909e 100644 --- a/crates/host-rpc/src/notifier.rs +++ b/crates/host-rpc/src/notifier.rs @@ -437,12 +437,22 @@ where } WalkResult::Exhausted => { let finalized = self.cached_finalized.ok_or(RpcHostError::NoFinalizedBlock)?; + + // Restart from the block after our last delivered block, or + // finalized — whichever is lower — so we never skip blocks + // between the old chain_view tip and finalized. + let resume_from = self + .chain_view + .back() + .map(|(n, _)| n + 1) + .map_or(finalized, |next| next.min(finalized)); + warn!( buffer_capacity = self.buffer_capacity, - finalized, "walk exhausted buffer, resetting to backfill from finalized" + finalized, resume_from, "walk exhausted buffer, resetting to backfill" ); self.chain_view.clear(); - self.backfill_from = Some(finalized); + self.backfill_from = Some(resume_from); self.last_tag_epoch = None; crate::metrics::record_handle_new_head_duration(start.elapsed()); return Ok(None); diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index ea045fc3..b1abd6fc 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -263,6 +263,18 @@ where let last_height = self.last_rollup_block()?; + // Detect gaps: if the first block to process is not contiguous with + // our last stored block, bail early so the notifier can re-backfill. + if let Some(first) = extracts.iter().find(|e| e.ru_height > last_height) { + let expected_next = last_height + 1; + eyre::ensure!( + first.ru_height == expected_next, + "notification gap: expected ru_height {expected_next}, got {}. \ + Last stored block is {last_height}.", + first.ru_height, + ); + } + let mut processed = false; for block_extracts in extracts.iter().filter(|e| e.ru_height > last_height) { // Constructed per-block: hardforks must be rechecked each block,