Resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-03-09 19:33:22 +09:00
231 changed files with 12305 additions and 7272 deletions

View File

@@ -1,4 +1,5 @@
#![cfg(not(debug_assertions))]
#![allow(clippy::result_large_err)]
use beacon_chain::attestation_verification::Error as AttnError;
use beacon_chain::block_verification_types::RpcBlock;
@@ -100,7 +101,7 @@ fn get_harness(
) -> TestHarness {
// Most tests expect to retain historic states, so we use this as the default.
let chain_config = ChainConfig {
reconstruct_historic_states: true,
archive: true,
..ChainConfig::default()
};
get_harness_generic(
@@ -117,7 +118,8 @@ fn get_harness_import_all_data_columns(
) -> TestHarness {
// Most tests expect to retain historic states, so we use this as the default.
let chain_config = ChainConfig {
reconstruct_historic_states: true,
ignore_ws_check: true,
archive: true,
..ChainConfig::default()
};
get_harness_generic(
@@ -2875,7 +2877,7 @@ async fn reproduction_unaligned_checkpoint_sync_pruned_payload() {
slot_clock.set_slot(harness.get_current_slot().as_u64());
let chain_config = ChainConfig {
reconstruct_historic_states: true,
archive: true,
..ChainConfig::default()
};
@@ -3029,9 +3031,9 @@ async fn weak_subjectivity_sync_test(
slot_clock.set_slot(harness.get_current_slot().as_u64());
let chain_config = ChainConfig {
// Set reconstruct_historic_states to true from the start in the genesis case. This makes
// Set archive to true from the start in the genesis case. This makes
// some of the later checks more uniform across the genesis/non-genesis cases.
reconstruct_historic_states: checkpoint_slot == 0,
archive: checkpoint_slot == 0,
..ChainConfig::default()
};
@@ -3684,7 +3686,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() {
let temp = tempdir().unwrap();
let store = get_store(&temp);
let chain_config = ChainConfig {
reconstruct_historic_states: false,
archive: false,
..ChainConfig::default()
};
let harness = get_harness_generic(
@@ -3923,202 +3925,17 @@ async fn finalizes_after_resuming_from_db() {
);
}
#[allow(clippy::large_stack_frames)]
#[tokio::test]
async fn revert_minority_fork_on_resume() {
let validator_count = 16;
let slots_per_epoch = MinimalEthSpec::slots_per_epoch();
let fork_epoch = Epoch::new(4);
let fork_slot = fork_epoch.start_slot(slots_per_epoch);
let initial_blocks = slots_per_epoch * fork_epoch.as_u64() - 1;
let post_fork_blocks = slots_per_epoch * 3;
let mut spec1 = MinimalEthSpec::default_spec();
spec1.altair_fork_epoch = None;
let mut spec2 = MinimalEthSpec::default_spec();
spec2.altair_fork_epoch = Some(fork_epoch);
let all_validators = (0..validator_count).collect::<Vec<usize>>();
// Chain with no fork epoch configured.
let db_path1 = tempdir().unwrap();
let store1 = get_store_generic(&db_path1, StoreConfig::default(), spec1.clone());
let harness1 = BeaconChainHarness::builder(MinimalEthSpec)
.spec(spec1.clone().into())
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.fresh_disk_store(store1)
.mock_execution_layer()
.build();
// Chain with fork epoch configured.
let db_path2 = tempdir().unwrap();
let store2 = get_store_generic(&db_path2, StoreConfig::default(), spec2.clone());
let harness2 = BeaconChainHarness::builder(MinimalEthSpec)
.spec(spec2.clone().into())
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.fresh_disk_store(store2)
.mock_execution_layer()
.build();
// Apply the same blocks to both chains initially.
let mut state = harness1.get_current_state();
let mut block_root = harness1.chain.genesis_block_root;
for slot in (1..=initial_blocks).map(Slot::new) {
let state_root = state.update_tree_hash_cache().unwrap();
let attestations = harness1.make_attestations(
&all_validators,
&state,
state_root,
block_root.into(),
slot,
);
harness1.set_current_slot(slot);
harness2.set_current_slot(slot);
harness1.process_attestations(attestations.clone(), &state);
harness2.process_attestations(attestations, &state);
let ((block, blobs), new_state) = harness1.make_block(state, slot).await;
harness1
.process_block(slot, block.canonical_root(), (block.clone(), blobs.clone()))
.await
.unwrap();
harness2
.process_block(slot, block.canonical_root(), (block.clone(), blobs.clone()))
.await
.unwrap();
state = new_state;
block_root = block.canonical_root();
}
assert_eq!(harness1.head_slot(), fork_slot - 1);
assert_eq!(harness2.head_slot(), fork_slot - 1);
// Fork the two chains.
let mut state1 = state.clone();
let mut state2 = state.clone();
let mut majority_blocks = vec![];
for i in 0..post_fork_blocks {
let slot = fork_slot + i;
// Attestations on majority chain.
let state_root = state.update_tree_hash_cache().unwrap();
let attestations = harness2.make_attestations(
&all_validators,
&state2,
state_root,
block_root.into(),
slot,
);
harness2.set_current_slot(slot);
harness2.process_attestations(attestations, &state2);
// Minority chain block (no attesters).
let ((block1, blobs1), new_state1) = harness1.make_block(state1, slot).await;
harness1
.process_block(slot, block1.canonical_root(), (block1, blobs1))
.await
.unwrap();
state1 = new_state1;
// Majority chain block (all attesters).
let ((block2, blobs2), new_state2) = harness2.make_block(state2, slot).await;
harness2
.process_block(slot, block2.canonical_root(), (block2.clone(), blobs2))
.await
.unwrap();
state2 = new_state2;
block_root = block2.canonical_root();
majority_blocks.push(block2);
}
let end_slot = fork_slot + post_fork_blocks - 1;
assert_eq!(harness1.head_slot(), end_slot);
assert_eq!(harness2.head_slot(), end_slot);
// Resume from disk with the hard-fork activated: this should revert the post-fork blocks.
// We have to do some hackery with the `slot_clock` so that the correct slot is set when
// the beacon chain builder loads the head block.
drop(harness1);
let resume_store = get_store_generic(&db_path1, StoreConfig::default(), spec2.clone());
let resumed_harness = TestHarness::builder(MinimalEthSpec)
.spec(spec2.clone().into())
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.resumed_disk_store(resume_store)
.override_store_mutator(Box::new(move |mut builder| {
builder = builder
.resume_from_db()
.unwrap()
.testing_slot_clock(spec2.get_slot_duration())
.unwrap();
builder
.get_slot_clock()
.unwrap()
.set_slot(end_slot.as_u64());
builder
}))
.mock_execution_layer()
.build();
// Head should now be just before the fork.
resumed_harness.chain.recompute_head_at_current_slot().await;
assert_eq!(resumed_harness.head_slot(), fork_slot - 1);
// Fork choice should only know the canonical head. When we reverted the head we also should
// have called `reset_fork_choice_to_finalization` which rebuilds fork choice from scratch
// without the reverted block.
assert_eq!(
resumed_harness.chain.heads(),
vec![(resumed_harness.head_block_root(), fork_slot - 1)]
);
// Apply blocks from the majority chain and trigger finalization.
let initial_split_slot = resumed_harness.chain.store.get_split_slot();
for block in &majority_blocks {
resumed_harness
.process_block_result((block.clone(), None))
.await
.unwrap();
// The canonical head should be the block from the majority chain.
resumed_harness.chain.recompute_head_at_current_slot().await;
assert_eq!(resumed_harness.head_slot(), block.slot());
assert_eq!(resumed_harness.head_block_root(), block.canonical_root());
}
let advanced_split_slot = resumed_harness.chain.store.get_split_slot();
// Check that the migration ran successfully.
assert!(advanced_split_slot > initial_split_slot);
// Check that there is only a single head now matching harness2 (the minority chain is gone).
let heads = resumed_harness.chain.heads();
assert_eq!(heads, harness2.chain.heads());
assert_eq!(heads.len(), 1);
}
// This test checks whether the schema downgrade from the latest version to some minimum supported
// version is correct. This is the easiest schema test to write without historic versions of
// Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually
// as old downgrades are deprecated.
async fn schema_downgrade_to_min_version(
store_config: StoreConfig,
reconstruct_historic_states: bool,
) {
async fn schema_downgrade_to_min_version(store_config: StoreConfig, archive: bool) {
let num_blocks_produced = E::slots_per_epoch() * 4;
let db_path = tempdir().unwrap();
let spec = test_spec::<E>();
let chain_config = ChainConfig {
reconstruct_historic_states,
archive,
..ChainConfig::default()
};
@@ -4173,7 +3990,7 @@ async fn schema_downgrade_to_min_version(
.build();
// Check chain dump for appropriate range depending on whether this is an archive node.
let chain_dump_start_slot = if reconstruct_historic_states {
let chain_dump_start_slot = if archive {
Slot::new(0)
} else {
store.get_split_slot()
@@ -5153,7 +4970,7 @@ async fn ancestor_state_root_prior_to_split() {
..StoreConfig::default()
};
let chain_config = ChainConfig {
reconstruct_historic_states: false,
archive: false,
..ChainConfig::default()
};
@@ -5246,7 +5063,7 @@ async fn replay_from_split_state() {
..StoreConfig::default()
};
let chain_config = ChainConfig {
reconstruct_historic_states: false,
archive: false,
..ChainConfig::default()
};
@@ -5742,6 +5559,226 @@ fn check_iterators_from_slot(harness: &TestHarness, slot: Slot) {
);
}
/// Test that blocks with default (pre-merge) execution payloads and non-default (post-merge)
/// execution payloads can be produced, stored, and retrieved correctly through a merge transition.
///
/// Spec (see .claude/plans/8658.md):
/// - Bellatrix at epoch 0 (genesis), genesis has default execution payload header
/// - Slots 1-9: blocks have default (zeroed) execution payloads
/// - Slot 10: first block with a non-default execution payload (merge transition block)
/// - Slots 11-32+: non-default payloads, each with parent_hash == prev payload block_hash
/// - Chain must finalize past genesis
#[tokio::test]
async fn bellatrix_produce_and_store_payloads() {
use beacon_chain::test_utils::{
DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, InteropGenesisBuilder,
};
use safe_arith::SafeArith;
use state_processing::per_block_processing::is_merge_transition_complete;
use tree_hash::TreeHash;
let merge_slot = 10u64;
let total_slots = 48u64;
let spec = ForkName::Bellatrix.make_genesis_spec(E::default_spec());
// Build genesis state with a default (zeroed) execution payload header so that
// is_merge_transition_complete = false at genesis.
let keypairs = KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec();
let genesis_state = InteropGenesisBuilder::default()
.set_alternating_eth1_withdrawal_credentials()
.set_opt_execution_payload_header(None)
.build_genesis_state(
&keypairs,
HARNESS_GENESIS_TIME,
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
&spec,
)
.unwrap();
assert!(
!is_merge_transition_complete(&genesis_state),
"genesis should NOT have merge complete"
);
let db_path = tempdir().unwrap();
let store = get_store_generic(
&db_path,
StoreConfig {
prune_payloads: false,
..StoreConfig::default()
},
spec.clone(),
);
let chain_config = ChainConfig {
archive: true,
..ChainConfig::default()
};
let harness = TestHarness::builder(MinimalEthSpec)
.spec(store.get_chain_spec().clone())
.keypairs(keypairs.clone())
.fresh_disk_store(store.clone())
.override_store_mutator(Box::new(move |builder: BeaconChainBuilder<_>| {
builder
.genesis_state(genesis_state)
.expect("should set genesis state")
}))
.mock_execution_layer()
.chain_config(chain_config)
.build();
harness
.mock_execution_layer
.as_ref()
.unwrap()
.server
.all_payloads_valid();
harness.advance_slot();
// Phase 1: slots 1 to merge_slot-1 — blocks with default execution payloads.
let mut state = harness.get_current_state();
for slot_num in 1..merge_slot {
let slot = Slot::new(slot_num);
harness.advance_slot();
harness
.build_and_import_block_with_payload(
&mut state,
slot,
ExecutionPayloadBellatrix::default(),
)
.await;
state = harness.get_current_state();
}
// Phase 2: slot merge_slot — the merge transition block with a real payload.
{
let slot = Slot::new(merge_slot);
harness.advance_slot();
// Advance state to compute correct timestamp and randao.
let mut pre_state = state.clone();
complete_state_advance(&mut pre_state, None, slot, &harness.spec)
.expect("should advance state");
pre_state
.build_caches(&harness.spec)
.expect("should build caches");
let timestamp = pre_state
.genesis_time()
.safe_add(
slot.as_u64()
.safe_mul(harness.spec.seconds_per_slot)
.unwrap(),
)
.unwrap();
let prev_randao = *pre_state.get_randao_mix(pre_state.current_epoch()).unwrap();
let mut transition_payload = ExecutionPayloadBellatrix {
parent_hash: ExecutionBlockHash::zero(),
fee_recipient: Address::repeat_byte(42),
receipts_root: Hash256::repeat_byte(42),
state_root: Hash256::repeat_byte(43),
logs_bloom: vec![0; 256].try_into().unwrap(),
prev_randao,
block_number: 1,
gas_limit: 30_000_000,
gas_used: 0,
timestamp,
extra_data: VariableList::empty(),
base_fee_per_gas: Uint256::from(1u64),
block_hash: ExecutionBlockHash::zero(),
transactions: VariableList::empty(),
};
transition_payload.block_hash =
ExecutionBlockHash::from_root(transition_payload.tree_hash_root());
// Insert the transition payload into the mock EL so subsequent blocks can chain.
{
let mock_el = harness.mock_execution_layer.as_ref().unwrap();
let mut block_gen = mock_el.server.execution_block_generator();
block_gen.insert_block_without_checks(execution_layer::test_utils::Block::PoS(
ExecutionPayload::Bellatrix(transition_payload.clone()),
));
}
harness
.build_and_import_block_with_payload(&mut state, slot, transition_payload)
.await;
state = harness.get_current_state();
assert!(
is_merge_transition_complete(&state),
"merge should be complete after slot {merge_slot}"
);
}
// Phase 3: slots merge_slot+1 to total_slots — use harness with attestations.
let post_merge_slots = (total_slots - merge_slot) as usize;
harness.extend_slots(post_merge_slots).await;
// ---- Verification: check all blocks in the store against plan invariants ----
let mut prev_payload_block_hash: Option<ExecutionBlockHash> = None;
for slot_num in 1..=total_slots {
let slot = Slot::new(slot_num);
let block_root = harness
.chain
.block_root_at_slot(slot, WhenSlotSkipped::Prev)
.unwrap()
.unwrap_or_else(|| panic!("missing block at slot {slot_num}"));
let block = store
.get_blinded_block(&block_root)
.unwrap()
.unwrap_or_else(|| panic!("block not in store at slot {slot_num}"));
let payload = block
.message()
.body()
.execution_payload()
.expect("bellatrix block should have execution payload");
if slot_num < merge_slot {
// Slots 1 to merge_slot-1: payload must be default.
assert!(
payload.is_default_with_empty_roots(),
"slot {slot_num} should have default payload"
);
} else if slot_num == merge_slot {
// Merge transition block: first non-default payload.
assert!(
!payload.is_default_with_empty_roots(),
"slot {slot_num} (merge) should have non-default payload"
);
prev_payload_block_hash = Some(payload.block_hash());
} else {
// Post-merge: non-default payload with valid parent_hash chain.
assert!(
!payload.is_default_with_empty_roots(),
"slot {slot_num} should have non-default payload"
);
assert_eq!(
payload.parent_hash(),
prev_payload_block_hash.unwrap(),
"slot {slot_num} payload parent_hash should chain from previous payload"
);
prev_payload_block_hash = Some(payload.block_hash());
}
}
// Verify finalization.
let finalized_epoch = harness
.chain
.canonical_head
.cached_head()
.finalized_checkpoint()
.epoch;
assert!(
finalized_epoch > 0,
"chain should have finalized past genesis"
);
}
fn get_finalized_epoch_boundary_blocks(
dump: &[BeaconSnapshot<MinimalEthSpec, BlindedPayload<MinimalEthSpec>>],
) -> HashSet<SignedBeaconBlockHash> {