Skip to content

Commit 75772e0

Browse files
committed
Add regression test for unaligned checkpoint sync with payload pruning
This adds a regression test for #8426. When `prune_payloads` is enabled, unaligned checkpoint syncs (where the anchor block is older than the checkpoint state) were previously failing to import the anchor block's execution payload. The new test `reproduction_unaligned_checkpoint_sync_pruned_payload` reproduces this scenario and asserts that the Beacon Chain initializes successfully (which confirms the anchor payload was correctly loaded).
1 parent 74b8c02 commit 75772e0

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
27702922
async fn weak_subjectivity_sync_test(
27712923
slots: Vec<Slot>,
27722924
checkpoint_slot: Slot,

0 commit comments

Comments
 (0)