@@ -2767,6 +2767,148 @@ 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 (where 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+ // Force Deneb from Epoch 0 so that blocks have Execution Payloads.
2778+ let mut spec = test_spec :: < E > ( ) ;
2779+ spec. altair_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
2780+ spec. bellatrix_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
2781+ spec. capella_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
2782+ spec. deneb_fork_epoch = Some ( Epoch :: new ( 0 ) ) ;
2783+ spec. fulu_fork_epoch = None ;
2784+
2785+ // Create an unaligned checkpoint with a gap of 3 slots.
2786+ let num_initial_slots = E :: slots_per_epoch ( ) * 11 ;
2787+ let checkpoint_slot = Slot :: new ( E :: slots_per_epoch ( ) * 9 - 3 ) ;
2788+
2789+ let slots = ( 1 ..num_initial_slots)
2790+ . map ( Slot :: new)
2791+ . filter ( |& slot| slot <= checkpoint_slot || slot > checkpoint_slot + 3 )
2792+ . collect :: < Vec < _ > > ( ) ;
2793+
2794+ let temp1 = tempdir ( ) . unwrap ( ) ;
2795+ let full_store = get_store_generic ( & temp1, StoreConfig :: default ( ) , spec. clone ( ) ) ;
2796+
2797+ let harness = get_harness ( full_store. clone ( ) , LOW_VALIDATOR_COUNT ) ;
2798+ let all_validators = ( 0 ..LOW_VALIDATOR_COUNT ) . collect :: < Vec < _ > > ( ) ;
2799+
2800+ let ( genesis_state, genesis_state_root) = harness. get_current_state_and_root ( ) ;
2801+ harness
2802+ . add_attested_blocks_at_slots (
2803+ genesis_state. clone ( ) ,
2804+ genesis_state_root,
2805+ & slots,
2806+ & all_validators,
2807+ )
2808+ . await ;
2809+
2810+ // Extract snapshot data from the harness.
2811+ let wss_block_root = harness
2812+ . chain
2813+ . block_root_at_slot ( checkpoint_slot, WhenSlotSkipped :: Prev )
2814+ . unwrap ( )
2815+ . unwrap ( ) ;
2816+ let wss_state_root = harness
2817+ . chain
2818+ . state_root_at_slot ( checkpoint_slot)
2819+ . unwrap ( )
2820+ . unwrap ( ) ;
2821+
2822+ let wss_block = harness
2823+ . chain
2824+ . store
2825+ . get_full_block ( & wss_block_root)
2826+ . unwrap ( )
2827+ . unwrap ( ) ;
2828+
2829+ // The test premise requires the anchor block to have a payload.
2830+ assert ! ( wss_block. message( ) . execution_payload( ) . is_ok( ) ) ;
2831+
2832+ let wss_blobs_opt = harness
2833+ . chain
2834+ . get_or_reconstruct_blobs ( & wss_block_root)
2835+ . unwrap ( ) ;
2836+
2837+ let wss_state = full_store
2838+ . get_state ( & wss_state_root, Some ( checkpoint_slot) , CACHE_STATE_IN_TESTS )
2839+ . unwrap ( )
2840+ . unwrap ( ) ;
2841+
2842+ // Configure the client with `prune_payloads = true`.
2843+ // This triggers the code path where `try_get_full_block` must explicitly handle the anchor block.
2844+ let temp2 = tempdir ( ) . unwrap ( ) ;
2845+ let mut store_config = StoreConfig :: default ( ) ;
2846+ store_config. prune_payloads = true ;
2847+
2848+ let store = get_store_generic ( & temp2, store_config, spec. clone ( ) ) ;
2849+
2850+ let slot_clock = TestingSlotClock :: new (
2851+ Slot :: new ( 0 ) ,
2852+ Duration :: from_secs ( harness. chain . genesis_time ) ,
2853+ Duration :: from_secs ( spec. seconds_per_slot ) ,
2854+ ) ;
2855+ slot_clock. set_slot ( harness. get_current_slot ( ) . as_u64 ( ) ) ;
2856+
2857+ let chain_config = ChainConfig {
2858+ reconstruct_historic_states : true ,
2859+ ..ChainConfig :: default ( )
2860+ } ;
2861+
2862+ let trusted_setup = get_kzg ( & spec) ;
2863+ let ( shutdown_tx, _shutdown_rx) = futures:: channel:: mpsc:: channel ( 1 ) ;
2864+ let mock = mock_execution_layer_from_parts (
2865+ harness. spec . clone ( ) ,
2866+ harness. runtime . task_executor . clone ( ) ,
2867+ ) ;
2868+ let all_custody_columns = ( 0 ..spec. number_of_custody_groups ) . collect :: < Vec < _ > > ( ) ;
2869+
2870+ // Attempt to build the BeaconChain.
2871+ // If the bug is present, this will panic with `MissingFullBlockExecutionPayloadPruned`.
2872+ let beacon_chain = BeaconChainBuilder :: < DiskHarnessType < E > > :: new ( MinimalEthSpec , trusted_setup)
2873+ . chain_config ( chain_config)
2874+ . store ( store. clone ( ) )
2875+ . custom_spec ( spec. clone ( ) . into ( ) )
2876+ . task_executor ( harness. chain . task_executor . clone ( ) )
2877+ . weak_subjectivity_state (
2878+ wss_state,
2879+ wss_block. clone ( ) ,
2880+ wss_blobs_opt. clone ( ) ,
2881+ genesis_state,
2882+ )
2883+ . unwrap ( )
2884+ . store_migrator_config ( MigratorConfig :: default ( ) . blocking ( ) )
2885+ . slot_clock ( slot_clock)
2886+ . shutdown_sender ( shutdown_tx)
2887+ . event_handler ( Some ( ServerSentEventHandler :: new_with_capacity ( 1 ) ) )
2888+ . execution_layer ( Some ( mock. el ) )
2889+ . ordered_custody_column_indices ( all_custody_columns)
2890+ . rng ( Box :: new ( StdRng :: seed_from_u64 ( 42 ) ) )
2891+ . build ( ) ;
2892+
2893+ assert ! (
2894+ beacon_chain. is_ok( ) ,
2895+ "Beacon Chain failed to build. The anchor payload may have been incorrectly pruned. Error: {:?}" ,
2896+ beacon_chain. err( )
2897+ ) ;
2898+
2899+ // Ensure the test setup actually created an unaligned checkpoint.
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+
27702912async fn weak_subjectivity_sync_test (
27712913 slots : Vec < Slot > ,
27722914 checkpoint_slot : Slot ,
0 commit comments