@@ -2767,6 +2767,158 @@ async fn weak_subjectivity_sync_from_genesis() {
27672767 weak_subjectivity_sync_test ( slots, checkpoint_slot, None ) . await
27682768}
27692769
2770+ // Ensures that an unaligned checkpoint sync (the block is older than the state)
2771+ // works correctly even when `prune_payloads` is enabled.
2772+ //
2773+ // Previously, the `HotColdDB` would refuse to load the execution payload for the
2774+ // anchor block because it was considered "pruned", causing the node to fail startup.
2775+ #[ tokio:: test]
2776+ async fn reproduction_unaligned_checkpoint_sync_pruned_payload ( ) {
2777+ let spec = test_spec :: < E > ( ) ;
2778+
2779+ // Requires Execution Payloads.
2780+ let Some ( _) = spec. deneb_fork_epoch else {
2781+ return ;
2782+ } ;
2783+
2784+ // Create an unaligned checkpoint with a gap of 3 slots.
2785+ let num_initial_slots = E :: slots_per_epoch ( ) * 11 ;
2786+ let checkpoint_slot = Slot :: new ( E :: slots_per_epoch ( ) * 9 - 3 ) ;
2787+
2788+ let slots = ( 1 ..num_initial_slots)
2789+ . map ( Slot :: new)
2790+ . filter ( |& slot| slot <= checkpoint_slot || slot > checkpoint_slot + 3 )
2791+ . collect :: < Vec < _ > > ( ) ;
2792+
2793+ let temp1 = tempdir ( ) . unwrap ( ) ;
2794+ let full_store = get_store_generic ( & temp1, StoreConfig :: default ( ) , spec. clone ( ) ) ;
2795+
2796+ let harness = get_harness_import_all_data_columns ( full_store. clone ( ) , LOW_VALIDATOR_COUNT ) ;
2797+ let all_validators = ( 0 ..LOW_VALIDATOR_COUNT ) . collect :: < Vec < _ > > ( ) ;
2798+
2799+ let ( genesis_state, genesis_state_root) = harness. get_current_state_and_root ( ) ;
2800+ harness
2801+ . add_attested_blocks_at_slots (
2802+ genesis_state. clone ( ) ,
2803+ genesis_state_root,
2804+ & slots,
2805+ & all_validators,
2806+ )
2807+ . await ;
2808+
2809+ // Extract snapshot data from the harness.
2810+ let wss_block_root = harness
2811+ . chain
2812+ . block_root_at_slot ( checkpoint_slot, WhenSlotSkipped :: Prev )
2813+ . unwrap ( )
2814+ . unwrap ( ) ;
2815+ let wss_state_root = harness
2816+ . chain
2817+ . state_root_at_slot ( checkpoint_slot)
2818+ . unwrap ( )
2819+ . unwrap ( ) ;
2820+
2821+ let wss_block = harness
2822+ . chain
2823+ . store
2824+ . get_full_block ( & wss_block_root)
2825+ . unwrap ( )
2826+ . unwrap ( ) ;
2827+
2828+ // The test premise requires the anchor block to have a payload.
2829+ assert ! ( wss_block. message( ) . execution_payload( ) . is_ok( ) ) ;
2830+
2831+ let wss_blobs_opt = harness
2832+ . chain
2833+ . get_or_reconstruct_blobs ( & wss_block_root)
2834+ . unwrap ( ) ;
2835+
2836+ let wss_state = full_store
2837+ . get_state ( & wss_state_root, Some ( checkpoint_slot) , CACHE_STATE_IN_TESTS )
2838+ . unwrap ( )
2839+ . unwrap ( ) ;
2840+
2841+ // Configure the client with `prune_payloads = true`.
2842+ // This triggers the path where `try_get_full_block` must explicitly handle the anchor block.
2843+ let temp2 = tempdir ( ) . unwrap ( ) ;
2844+ let store_config = StoreConfig {
2845+ prune_payloads : true ,
2846+ ..StoreConfig :: default ( )
2847+ } ;
2848+
2849+ let store = get_store_generic ( & temp2, store_config, spec. clone ( ) ) ;
2850+
2851+ let slot_clock = TestingSlotClock :: new (
2852+ Slot :: new ( 0 ) ,
2853+ Duration :: from_secs ( harness. chain . genesis_time ) ,
2854+ Duration :: from_secs ( spec. seconds_per_slot ) ,
2855+ ) ;
2856+ slot_clock. set_slot ( harness. get_current_slot ( ) . as_u64 ( ) ) ;
2857+
2858+ let chain_config = ChainConfig {
2859+ reconstruct_historic_states : true ,
2860+ ..ChainConfig :: default ( )
2861+ } ;
2862+
2863+ let trusted_setup = get_kzg ( & spec) ;
2864+ let ( shutdown_tx, _shutdown_rx) = futures:: channel:: mpsc:: channel ( 1 ) ;
2865+ let mock = mock_execution_layer_from_parts (
2866+ harness. spec . clone ( ) ,
2867+ harness. runtime . task_executor . clone ( ) ,
2868+ ) ;
2869+ let all_custody_columns = ( 0 ..spec. number_of_custody_groups ) . collect :: < Vec < _ > > ( ) ;
2870+
2871+ // Attempt to build the BeaconChain.
2872+ // If the bug is present, this will panic with `MissingFullBlockExecutionPayloadPruned`.
2873+ let beacon_chain = BeaconChainBuilder :: < DiskHarnessType < E > > :: new ( MinimalEthSpec , trusted_setup)
2874+ . chain_config ( chain_config)
2875+ . store ( store. clone ( ) )
2876+ . custom_spec ( spec. clone ( ) . into ( ) )
2877+ . task_executor ( harness. chain . task_executor . clone ( ) )
2878+ . weak_subjectivity_state (
2879+ wss_state,
2880+ wss_block. clone ( ) ,
2881+ wss_blobs_opt. clone ( ) ,
2882+ genesis_state,
2883+ )
2884+ . unwrap ( )
2885+ . store_migrator_config ( MigratorConfig :: default ( ) . blocking ( ) )
2886+ . slot_clock ( slot_clock)
2887+ . shutdown_sender ( shutdown_tx)
2888+ . event_handler ( Some ( ServerSentEventHandler :: new_with_capacity ( 1 ) ) )
2889+ . execution_layer ( Some ( mock. el ) )
2890+ . ordered_custody_column_indices ( all_custody_columns)
2891+ . rng ( Box :: new ( StdRng :: seed_from_u64 ( 42 ) ) )
2892+ . build ( ) ;
2893+
2894+ assert ! (
2895+ beacon_chain. is_ok( ) ,
2896+ "Beacon Chain failed to build. The anchor payload may have been incorrectly pruned. Error: {:?}" ,
2897+ beacon_chain. err( )
2898+ ) ;
2899+
2900+ let chain = beacon_chain. as_ref ( ) . unwrap ( ) ;
2901+ let wss_block_slot = wss_block. slot ( ) ;
2902+
2903+ assert_ne ! (
2904+ wss_block_slot,
2905+ chain. head_snapshot( ) . beacon_state. slot( ) ,
2906+ "Test invalid: Checkpoint was aligned (Slot {} == Slot {}). The test did not trigger the unaligned edge case." ,
2907+ wss_block_slot,
2908+ chain. head_snapshot( ) . beacon_state. slot( )
2909+ ) ;
2910+
2911+ let payload_exists = chain
2912+ . store
2913+ . execution_payload_exists ( & wss_block_root)
2914+ . unwrap_or ( false ) ;
2915+
2916+ assert ! (
2917+ payload_exists,
2918+ "Split block payload must exist in the new node's store after checkpoint sync"
2919+ ) ;
2920+ }
2921+
27702922async fn weak_subjectivity_sync_test (
27712923 slots : Vec < Slot > ,
27722924 checkpoint_slot : Slot ,
0 commit comments