@@ -9,7 +9,7 @@ use ream_network_state_lean::NetworkState;
99use ream_storage:: tables:: { field:: REDBField , table:: REDBTable } ;
1010use tree_hash:: TreeHash ;
1111
12- use crate :: sync:: job:: queue:: JobQueue ;
12+ use crate :: { slot :: get_current_slot , sync:: job:: queue:: JobQueue } ;
1313
1414pub struct ForwardBackgroundSyncer {
1515 pub store : Arc < LeanStoreWriter > ,
@@ -107,9 +107,23 @@ impl ForwardBackgroundSyncer {
107107 "Failed to find block with root {root:?} in pending blocks during insertion"
108108 )
109109 } ) ?;
110- let time = lean_network_spec ( ) . genesis_time
111- + ( block. block . slot * lean_network_spec ( ) . seconds_per_slot ) ;
112110 let block_slot = block. block . slot ;
111+
112+ // Reject blocks that are ahead of wall-clock by more than one slot. This
113+ // is a safety check to prevent importing blocks from malicious peers.
114+ let wall_clock_slot = get_current_slot ( ) ;
115+ if block_slot > wall_clock_slot + 1 {
116+ return Ok ( ForwardSyncResults :: BlockAheadOfWallClock {
117+ previous_queue : self . job_queue . clone ( ) ,
118+ bad_root : root,
119+ bad_slot : block_slot,
120+ wall_clock_slot,
121+ } ) ;
122+ }
123+
124+ let spec = lean_network_spec ( ) ;
125+ let time = spec. genesis_time + block_slot * spec. seconds_per_slot ;
126+
113127 store_writer. on_tick ( time, false , false ) . await ?;
114128 store_writer. on_block ( & block, true ) . await ?;
115129 blocks_synced += 1 ;
@@ -155,6 +169,12 @@ pub enum ForwardSyncResults {
155169 actual_root : B256 ,
156170 network_finalized_slot : u64 ,
157171 } ,
172+ BlockAheadOfWallClock {
173+ previous_queue : JobQueue ,
174+ bad_root : B256 ,
175+ bad_slot : u64 ,
176+ wall_clock_slot : u64 ,
177+ } ,
158178}
159179
160180#[ cfg( test) ]
@@ -164,7 +184,7 @@ mod tests {
164184 use libp2p_identity:: PeerId ;
165185 #[ cfg( feature = "devnet5" ) ]
166186 use ream_consensus_lean:: attestation:: MultiMessageAggregate ;
167- use ream_consensus_lean:: block:: SignedBlock ;
187+ use ream_consensus_lean:: block:: { Block , SignedBlock } ;
168188 use ream_fork_choice_lean:: store:: Store ;
169189 use ream_peer:: { ConnectionState , Direction } ;
170190 use ream_sync:: rwlock:: Writer ;
@@ -373,4 +393,91 @@ mod tests {
373393 other => panic ! ( "expected completed result, got {other:?}" ) ,
374394 }
375395 }
396+
397+ async fn run_crafted_high_slot_sync (
398+ crafted_slot : u64 ,
399+ ) -> ( anyhow:: Result < ForwardSyncResults > , u64 ) {
400+ let store = sample_store ( 10 ) . await ;
401+
402+ let ( head_root, genesis_block) = {
403+ let db = store. store . lock ( ) . await ;
404+ let head_root = db. head_provider ( ) . get ( ) . unwrap ( ) ;
405+ let genesis_block = db. block_provider ( ) . get ( head_root) . unwrap ( ) . unwrap ( ) ;
406+ ( head_root, genesis_block)
407+ } ;
408+
409+ let crafted_block = SignedBlock {
410+ block : Block {
411+ slot : crafted_slot,
412+ proposer_index : 0 ,
413+ parent_root : head_root,
414+ state_root : B256 :: ZERO ,
415+ body : genesis_block. block . body . clone ( ) ,
416+ } ,
417+ #[ cfg( feature = "devnet5" ) ]
418+ proof : MultiMessageAggregate {
419+ proof : ssz_types:: VariableList :: default ( ) ,
420+ } ,
421+ } ;
422+ let crafted_root = crafted_block. block . tree_hash_root ( ) ;
423+
424+ store
425+ . store
426+ . lock ( )
427+ . await
428+ . pending_blocks_provider ( )
429+ . insert ( crafted_root, crafted_block)
430+ . unwrap ( ) ;
431+
432+ let ( writer, _reader) = Writer :: new ( store) ;
433+ let writer = Arc :: new ( writer) ;
434+ let network_state = writer. read ( ) . await . network_state . clone ( ) ;
435+ let mut queue = JobQueue :: new ( crafted_root, crafted_slot, crafted_slot) ;
436+ queue. is_complete = true ;
437+
438+ let mut syncer = ForwardBackgroundSyncer :: new ( writer. clone ( ) , network_state, queue) ;
439+ let result = syncer. start ( ) . await ;
440+ let final_clock = writer
441+ . read ( )
442+ . await
443+ . store
444+ . lock ( )
445+ . await
446+ . time_provider ( )
447+ . get ( )
448+ . unwrap ( ) ;
449+ ( result, final_clock)
450+ }
451+
452+ #[ tokio:: test]
453+ async fn test_b2_high_slot_block_does_not_freeze_or_poison_clock ( ) {
454+ let ( result, final_clock) = run_crafted_high_slot_sync ( 1_000_000_000_000 ) . await ;
455+
456+ match result. unwrap ( ) {
457+ ForwardSyncResults :: BlockAheadOfWallClock { bad_slot, .. } => {
458+ assert_eq ! ( bad_slot, 1_000_000_000_000 ) ;
459+ }
460+ other => panic ! ( "expected BlockAheadOfWallClock, got {other:?}" ) ,
461+ }
462+ assert ! (
463+ final_clock < 1_000 ,
464+ "clock must not be poisoned toward the crafted slot"
465+ ) ;
466+ }
467+
468+ #[ tokio:: test]
469+ async fn test_b2_near_max_slot_overflow_is_defended ( ) {
470+ let ( result, final_clock) = run_crafted_high_slot_sync ( u64:: MAX - 5 ) . await ;
471+
472+ match result. unwrap ( ) {
473+ ForwardSyncResults :: BlockAheadOfWallClock { bad_slot, .. } => {
474+ assert_eq ! ( bad_slot, u64 :: MAX - 5 ) ;
475+ }
476+ other => panic ! ( "expected BlockAheadOfWallClock, got {other:?}" ) ,
477+ }
478+ assert ! (
479+ final_clock < 1_000 ,
480+ "clock must not be poisoned toward the crafted slot"
481+ ) ;
482+ }
376483}
0 commit comments