diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index ae2c595..aabf803 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -506,6 +506,12 @@ where let drained = self.storage.drain_above(target).await?; + // Immediately cap block tags to the common ancestor so that + // `latest` never references a block that no longer exists in + // storage. This must happen before the reorg notification so + // that RPC consumers see consistent tags. + self.chain.tags().rewind_to(target); + // The early return above guards against no-op reverts, so drained // should always contain at least one block. Guard defensively. debug_assert!(!drained.is_empty(), "drain_above returned empty after host revert"); diff --git a/crates/rpc/src/config/resolve.rs b/crates/rpc/src/config/resolve.rs index 47d4a1f..465b7fd 100644 --- a/crates/rpc/src/config/resolve.rs +++ b/crates/rpc/src/config/resolve.rs @@ -109,6 +109,33 @@ impl BlockTags { self.latest.store(latest, Ordering::Release); } + /// Cap all three tags to at most `ancestor`. + /// + /// Used during reorgs to ensure tags never reference blocks that + /// have been removed from storage. Stores are ordered + /// finalized → safe → latest (the same as [`update_all`]) so + /// that readers never observe `latest < finalized` while the + /// values are being decreased. + /// + /// [`update_all`]: Self::update_all + /// + /// # Example + /// + /// ``` + /// use signet_rpc::BlockTags; + /// + /// let tags = BlockTags::new(100, 95, 90); + /// tags.rewind_to(92); + /// assert_eq!(tags.latest(), 92); + /// assert_eq!(tags.safe(), 92); + /// assert_eq!(tags.finalized(), 90); // already below ancestor + /// ``` + pub fn rewind_to(&self, ancestor: u64) { + self.finalized.fetch_min(ancestor, Ordering::Release); + self.safe.fetch_min(ancestor, Ordering::Release); + self.latest.fetch_min(ancestor, Ordering::Release); + } + /// Returns `true` if the node is currently syncing. pub fn is_syncing(&self) -> bool { self.sync_status.read().expect("sync status lock poisoned").is_some() @@ -130,6 +157,38 @@ impl BlockTags { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rewind_to_caps_all_tags() { + let tags = BlockTags::new(100, 95, 90); + tags.rewind_to(92); + assert_eq!(tags.latest(), 92); + assert_eq!(tags.safe(), 92); + assert_eq!(tags.finalized(), 90); + } + + #[test] + fn rewind_to_caps_all_above_ancestor() { + let tags = BlockTags::new(100, 95, 90); + tags.rewind_to(50); + assert_eq!(tags.latest(), 50); + assert_eq!(tags.safe(), 50); + assert_eq!(tags.finalized(), 50); + } + + #[test] + fn rewind_to_noop_when_all_below() { + let tags = BlockTags::new(100, 95, 90); + tags.rewind_to(200); + assert_eq!(tags.latest(), 100); + assert_eq!(tags.safe(), 95); + assert_eq!(tags.finalized(), 90); + } +} + /// Error resolving a block identifier. #[derive(Debug, thiserror::Error)] pub enum ResolveError {