Skip to content

Commit f88fbc3

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 f88fbc3

File tree

1 file changed

+142
-0
lines changed

1 file changed

+142
-0
lines changed

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
27702912
async fn weak_subjectivity_sync_test(
27712913
slots: Vec<Slot>,
27722914
checkpoint_slot: Slot,

0 commit comments

Comments
 (0)