@@ -20,27 +20,33 @@ use crate::{
2020 GOSSIP_DISPARITY_INTERVALS , INTERVALS_PER_SLOT , MAX_ATTESTATIONS_DATA ,
2121 MILLISECONDS_PER_INTERVAL , MILLISECONDS_PER_SLOT ,
2222 block_builder:: { PostBlockCheckpoints , build_block} ,
23+ events:: { ChainEvent , ChainEventTx } ,
2324 metrics,
2425} ;
2526
2627const JUSTIFICATION_LOOKBACK_SLOTS : u64 = 3 ;
2728
2829/// Accept new aggregated payloads, promoting them to known for fork choice.
29- fn accept_new_attestations ( store : & mut Store , log_tree : bool ) {
30+ fn accept_new_attestations ( store : & mut Store , log_tree : bool , events : Option < & ChainEventTx > ) {
3031 store. promote_new_aggregated_payloads ( ) ;
3132 metrics:: update_latest_new_aggregated_payloads ( store. new_aggregated_payloads_count ( ) ) ;
3233 metrics:: update_latest_known_aggregated_payloads ( store. known_aggregated_payloads_count ( ) ) ;
33- update_head ( store, log_tree) ;
34+ update_head ( store, log_tree, events ) ;
3435}
3536
3637/// Update the head based on the fork choice rule.
3738///
3839/// When `log_tree` is true, also computes block weights and logs an ASCII
3940/// fork choice tree to the terminal.
40- pub fn update_head ( store : & mut Store , log_tree : bool ) {
41+ ///
42+ /// When `events` is `Some`, emits a [`ChainEvent::Head`] whenever the head
43+ /// changes and a [`ChainEvent::FinalizedCheckpoint`] whenever finalization
44+ /// advances. Send errors (no subscribers) are ignored.
45+ pub fn update_head ( store : & mut Store , log_tree : bool , events : Option < & ChainEventTx > ) {
4146 let blocks = store. get_live_chain ( ) ;
4247 let attestations = store. extract_latest_known_attestations ( ) ;
4348 let old_head = store. head ( ) ;
49+ let old_finalized = store. latest_finalized ( ) ;
4450 let ( new_head, weights) = ethlambda_fork_choice:: compute_lmd_ghost_head (
4551 store. latest_justified ( ) . root ,
4652 & blocks,
@@ -60,6 +66,30 @@ pub fn update_head(store: &mut Store, log_tree: bool) {
6066 . filter ( |finalized| store. get_block_header ( & finalized. root ) . is_some ( ) ) ;
6167 store. update_checkpoints ( ForkCheckpoints :: new ( new_head, None , finalized) ) ;
6268
69+ if let Some ( events) = events {
70+ // Emit the new head whenever fork choice moved it.
71+ if old_head != new_head {
72+ let parent_root = store
73+ . get_block_header ( & new_head)
74+ . map ( |h| h. parent_root )
75+ . unwrap_or ( H256 :: ZERO ) ;
76+ let _ = events. send ( ChainEvent :: Head {
77+ slot : store. head_slot ( ) ,
78+ root : new_head,
79+ parent_root,
80+ } ) ;
81+ }
82+
83+ // Emit a finalized-checkpoint event only when finalization advanced.
84+ let new_finalized = store. latest_finalized ( ) ;
85+ if new_finalized. slot > old_finalized. slot || new_finalized. root != old_finalized. root {
86+ let _ = events. send ( ChainEvent :: FinalizedCheckpoint {
87+ slot : new_finalized. slot ,
88+ root : new_finalized. root ,
89+ } ) ;
90+ }
91+ }
92+
6393 if old_head != new_head {
6494 let old_slot = store
6595 . get_block_header ( & old_head)
@@ -254,7 +284,12 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<()
254284/// 800ms interval. Slot and interval-within-slot are derived as:
255285/// slot = store.time() / INTERVALS_PER_SLOT
256286/// interval = store.time() % INTERVALS_PER_SLOT
257- pub fn on_tick ( store : & mut Store , timestamp_ms : u64 , has_proposal : bool ) {
287+ pub fn on_tick (
288+ store : & mut Store ,
289+ timestamp_ms : u64 ,
290+ has_proposal : bool ,
291+ events : Option < & ChainEventTx > ,
292+ ) {
258293 // Convert UNIX timestamp (ms) to interval count since genesis
259294 let genesis_time_ms = store. config ( ) . genesis_time * 1000 ;
260295 let time_delta_ms = timestamp_ms. saturating_sub ( genesis_time_ms) ;
@@ -287,7 +322,7 @@ pub fn on_tick(store: &mut Store, timestamp_ms: u64, has_proposal: bool) {
287322 0 => {
288323 // Start of slot - process attestations if proposal exists
289324 if should_signal_proposal {
290- accept_new_attestations ( store, false ) ;
325+ accept_new_attestations ( store, false , events ) ;
291326 }
292327 }
293328 1 => {
@@ -302,7 +337,7 @@ pub fn on_tick(store: &mut Store, timestamp_ms: u64, has_proposal: bool) {
302337 }
303338 4 => {
304339 // End of slot - accept accumulated attestations and log tree
305- accept_new_attestations ( store, true ) ;
340+ accept_new_attestations ( store, true , events ) ;
306341 }
307342 _ => unreachable ! ( "slots only have 5 intervals" ) ,
308343 }
@@ -481,8 +516,12 @@ fn on_gossip_aggregated_attestation_core(
481516///
482517/// This is the safe default: it always verifies cryptographic signatures
483518/// and stores them for future block building. Use this for all production paths.
484- pub fn on_block ( store : & mut Store , signed_block : SignedBlock ) -> Result < ( ) , StoreError > {
485- on_block_core ( store, signed_block, true )
519+ pub fn on_block (
520+ store : & mut Store ,
521+ signed_block : SignedBlock ,
522+ events : Option < & ChainEventTx > ,
523+ ) -> Result < ( ) , StoreError > {
524+ on_block_core ( store, signed_block, true , events)
486525}
487526
488527/// Process a new block without signature verification.
@@ -493,7 +532,7 @@ pub fn on_block_without_verification(
493532 store : & mut Store ,
494533 signed_block : SignedBlock ,
495534) -> Result < ( ) , StoreError > {
496- on_block_core ( store, signed_block, false )
535+ on_block_core ( store, signed_block, false , None )
497536}
498537
499538/// Core block processing logic.
@@ -504,6 +543,7 @@ fn on_block_core(
504543 store : & mut Store ,
505544 signed_block : SignedBlock ,
506545 verify : bool ,
546+ events : Option < & ChainEventTx > ,
507547) -> Result < ( ) , StoreError > {
508548 let _timing = metrics:: time_fork_choice_block_processing ( ) ;
509549 let block_start = std:: time:: Instant :: now ( ) ;
@@ -586,8 +626,17 @@ fn on_block_core(
586626 metrics:: inc_attestations_valid ( count) ;
587627 }
588628
629+ // Emit the imported block before fork choice runs, so subscribers see the
630+ // `block` event ahead of any `head` move it triggers.
631+ if let Some ( events) = events {
632+ let _ = events. send ( ChainEvent :: Block {
633+ slot,
634+ root : block_root,
635+ } ) ;
636+ }
637+
589638 // Update forkchoice head based on new block and attestations
590- update_head ( store, false ) ;
639+ update_head ( store, false , events ) ;
591640
592641 let block_total = block_start. elapsed ( ) ;
593642 info ! (
@@ -758,11 +807,16 @@ fn get_proposal_head(store: &mut Store, slot: u64) -> H256 {
758807 // Calculate time corresponding to this slot
759808 let slot_time_ms = store. config ( ) . genesis_time * 1000 + slot * MILLISECONDS_PER_SLOT ;
760809
761- // Advance time to current slot (ticking intervals)
762- on_tick ( store, slot_time_ms, true ) ;
810+ // Advance time to current slot (ticking intervals).
811+ //
812+ // No event sender here: this is the proposer's pre-build catch-up, and the
813+ // block it produces is imported via `on_block` (which emits the resulting
814+ // `Block`/`Head`). Emitting from here would surface a head move before the
815+ // block exists.
816+ on_tick ( store, slot_time_ms, true , None ) ;
763817
764818 // Process any pending attestations before proposal
765- accept_new_attestations ( store, false ) ;
819+ accept_new_attestations ( store, false , None ) ;
766820
767821 store. head ( )
768822}
0 commit comments