mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-31 21:27:12 +00:00
Resolve merge conflicts
This commit is contained in:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user