From d9cd6a657b290f43c0dcb98e12807336232f9905 Mon Sep 17 00:00:00 2001 From: Snehendu Roy Date: Thu, 6 Aug 2026 04:22:54 +0530 Subject: [PATCH] fix(core): announce slot lifecycle to geyser plugins --- crates/core/src/runloops/mod.rs | 15 ++++++ crates/core/src/surfnet/mod.rs | 6 +++ crates/core/src/surfnet/svm.rs | 82 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 4eae10571..2d67bc7e5 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -304,6 +304,18 @@ pub async fn start_local_surfnet_runloop( // Notify geyser plugins that startup is complete let _ = svm_locker.with_svm_reader(|svm| svm.geyser_events_tx.send(GeyserEvent::EndOfStartup)); + // Announce the genesis slot so plugins begin tracking it. Block production + // announces slot N+1 while closing slot N, so without this the first slot is + // never created from a plugin's perspective and its block data gets dropped. + let _ = svm_locker.with_svm_reader(|svm| { + let slot = svm.get_latest_absolute_slot(); + svm.geyser_events_tx.send(GeyserEvent::UpdateSlotStatus { + slot, + parent: slot.checked_sub(1), + status: crate::surfnet::GeyserSlotStatus::CreatedBank, + }) + }); + start_block_production_runloop( clock_event_rx, clock_command_tx, @@ -733,6 +745,9 @@ fn start_geyser_runloop( crate::surfnet::GeyserSlotStatus::Processed => SlotStatus::Processed, crate::surfnet::GeyserSlotStatus::Confirmed => SlotStatus::Confirmed, crate::surfnet::GeyserSlotStatus::Rooted => SlotStatus::Rooted, + crate::surfnet::GeyserSlotStatus::FirstShredReceived => SlotStatus::FirstShredReceived, + crate::surfnet::GeyserSlotStatus::CreatedBank => SlotStatus::CreatedBank, + crate::surfnet::GeyserSlotStatus::Completed => SlotStatus::Completed, }; for plugin in managed_plugins.iter().map(|p| &*p.plugin) { diff --git a/crates/core/src/surfnet/mod.rs b/crates/core/src/surfnet/mod.rs index 55fc53204..edb83e846 100644 --- a/crates/core/src/surfnet/mod.rs +++ b/crates/core/src/surfnet/mod.rs @@ -48,6 +48,12 @@ pub enum GeyserSlotStatus { Rooted, /// Slot has been confirmed Confirmed, + /// First Shred Received + FirstShredReceived, + /// All shreds for the slot have been received. + Completed, + /// A new bank fork is created with the slot + CreatedBank, } /// Block metadata for geyser plugin notifications. diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index d10bd46fd..da80e83aa 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -2477,8 +2477,25 @@ impl SurfnetSvm { timestamp: slots_update_ts, }); + self.geyser_events_tx + .send(GeyserEvent::UpdateSlotStatus { + slot: new_slot, + parent: Some(parent_slot), + status: GeyserSlotStatus::CreatedBank, + }) + .ok(); + let geyser_parent_slot = slot.saturating_sub(1); + // Emit `Processed` for the slot that just executed + self.geyser_events_tx + .send(GeyserEvent::UpdateSlotStatus { + slot, + parent: slot.checked_sub(1), + status: GeyserSlotStatus::Processed, + }) + .ok(); + // Emit confirmation for the same slot used by processed account/transaction updates. self.geyser_events_tx .send(GeyserEvent::UpdateSlotStatus { @@ -6756,4 +6773,69 @@ mod tests { .expect("Valid account should be restored"); assert_eq!(restored_account.lamports, 1_000_000); } + + /// Geyser consumers that reconstruct blocks (yellowstone-grpc, for one) start + /// tracking a slot only once a lifecycle status announces it, and discard block + /// data that arrives for a slot they are not tracking. So every slot must be + /// announced with `CreatedBank` before any of its block data is emitted. + /// + /// The genesis slot is announced by the runloop rather than here, since block + /// production only ever announces the *next* slot — see the `CreatedBank` send + /// after `GeyserEvent::EndOfStartup` in `runloops::start_local_surfnet_runloop`. + /// That first slot is therefore excluded below. + #[test] + fn test_slot_is_announced_before_its_block_data_is_emitted() { + let (mut svm, _events_rx, geyser_rx) = SurfnetSvm::default(); + let genesis_slot = svm.get_latest_absolute_slot(); + + for _ in 0..5 { + svm.confirm_current_block() + .expect("block confirmation should succeed"); + } + + let mut announced = HashSet::new(); + let mut slots_with_block_data = HashSet::new(); + + while let Ok(event) = geyser_rx.try_recv() { + match event { + GeyserEvent::UpdateSlotStatus { + slot, + status: GeyserSlotStatus::CreatedBank, + .. + } => { + assert!( + announced.insert(slot), + "slot {slot} was announced more than once" + ); + } + GeyserEvent::NotifyBlockMetadata(metadata) => { + if metadata.slot != genesis_slot { + assert!( + announced.contains(&metadata.slot), + "block metadata for slot {} was emitted before the slot was announced", + metadata.slot + ); + } + slots_with_block_data.insert(metadata.slot); + } + GeyserEvent::NotifyEntry(entry) => { + if entry.slot != genesis_slot { + assert!( + announced.contains(&entry.slot), + "entry for slot {} was emitted before the slot was announced", + entry.slot + ); + } + slots_with_block_data.insert(entry.slot); + } + _ => {} + } + } + + assert!( + slots_with_block_data.len() > 1, + "expected block data for several slots, got {}", + slots_with_block_data.len() + ); + } }