Re-issue ForkchoiceUpdate based on updated PayloadStatus (#9102)

Co-Authored-By: hopinheimer <knmanas6@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
hopinheimer
2026-04-25 04:04:09 -04:00
committed by GitHub
parent 8a384ff445
commit df764ffa9a
14 changed files with 808 additions and 41 deletions

View File

@@ -117,8 +117,8 @@ use state_processing::{
epoch_cache::initialize_epoch_cache,
per_block_processing,
per_block_processing::{
VerifySignatures, errors::AttestationValidationError, get_expected_withdrawals,
verify_attestation_for_block_inclusion,
VerifySignatures, apply_parent_execution_payload, errors::AttestationValidationError,
get_expected_withdrawals, verify_attestation_for_block_inclusion,
},
per_slot_processing,
state_advance::{complete_state_advance, partial_state_advance},
@@ -4858,16 +4858,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
proposal_slot: Slot,
) -> Result<Withdrawals<T::EthSpec>, Error> {
let cached_head = self.canonical_head.cached_head();
let head_block = &cached_head.snapshot.beacon_block;
let head_block_root = cached_head.head_block_root();
let head_state = &cached_head.snapshot.beacon_state;
let parent_block_root = forkchoice_update_params.head_root;
let (unadvanced_state, unadvanced_state_root) =
if cached_head.head_block_root() == parent_block_root {
(Cow::Borrowed(head_state), cached_head.head_state_root())
let (unadvanced_state, unadvanced_state_root, parent_bid_block_hash) =
if parent_block_root == head_block_root {
(
Cow::Borrowed(head_state),
cached_head.head_state_root(),
head_block.payload_bid_block_hash().ok(),
)
} else {
// TODO(gloas): this function needs updating to be envelope-aware
// See: https://github.com/sigp/lighthouse/issues/8957
let block = self
.get_blinded_block(&parent_block_root)?
.ok_or(Error::MissingBeaconBlock(parent_block_root))?;
@@ -4875,20 +4879,27 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.store
.get_advanced_hot_state(parent_block_root, proposal_slot, block.state_root())?
.ok_or(Error::MissingBeaconState(block.state_root()))?;
(Cow::Owned(state), state_root)
(
Cow::Owned(state),
state_root,
block.payload_bid_block_hash().ok(),
)
};
// Parent state epoch is the same as the proposal, we don't need to advance because the
// list of expected withdrawals can only change after an epoch advance or a
// block application.
let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch());
if head_state.current_epoch() == proposal_epoch {
return get_expected_withdrawals(&unadvanced_state, &self.spec)
.map(Into::into)
.map_err(Error::PrepareProposerFailed);
}
let parent_payload_status = if let Some(block_hash) = parent_bid_block_hash
&& block_hash != ExecutionBlockHash::default()
&& forkchoice_update_params.head_hash == Some(block_hash)
{
fork_choice::PayloadStatus::Full
} else {
fork_choice::PayloadStatus::Empty
};
// Advance the state using the partial method.
// TODO(gloas): we might want to optimise this further by using:
// - `get_advanced_hot_state` instead of the cached head
// - restoring the pre-Gloas optimisation to avoid advancing further than the epoch
// boundary
debug!(
%proposal_slot,
?parent_block_root,
@@ -4898,9 +4909,33 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
partial_state_advance(
&mut advanced_state,
Some(unadvanced_state_root),
proposal_epoch.start_slot(T::EthSpec::slots_per_epoch()),
proposal_slot,
&self.spec,
)?;
// For Gloas, when the head payload is Full, we need to apply the parent's
// execution requests to the state to get the correct withdrawals.
if parent_payload_status == fork_choice::PayloadStatus::Full {
let envelope = if parent_block_root == head_block_root {
cached_head.snapshot.execution_envelope.clone()
} else {
self.store
.get_payload_envelope(&parent_block_root)?
.map(Arc::new)
}
.ok_or(Error::MissingExecutionPayloadEnvelope(parent_block_root))?;
let parent_bid = advanced_state.latest_execution_payload_bid()?.clone();
apply_parent_execution_payload(
&mut advanced_state,
&parent_bid,
&envelope.message.execution_requests,
&self.spec,
)
.map_err(Error::PrepareProposerFailed)?;
}
get_expected_withdrawals(&advanced_state, &self.spec)
.map(Into::into)
.map_err(Error::PrepareProposerFailed)
@@ -6112,13 +6147,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
fcu_params.head_root,
&cached_head,
)?;
Ok::<_, Error>(Some((fcu_params, pre_payload_attributes)))
let head_payload_status = cached_head.head_payload_status();
Ok::<_, Error>(Some((
fcu_params,
pre_payload_attributes,
head_payload_status,
)))
},
"prepare_beacon_proposer_head_read",
)
.await??;
let Some((forkchoice_update_params, Some(pre_payload_attributes))) = maybe_prep_data else {
let Some((forkchoice_update_params, Some(pre_payload_attributes), head_payload_status)) =
maybe_prep_data
else {
// Appropriate log messages have already been logged above and in
// `get_pre_payload_attributes`.
return Ok(None);
@@ -6140,7 +6182,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// considerable time to compute if a state load is required.
let head_root = forkchoice_update_params.head_root;
let payload_attributes = if let Some(payload_attributes) = execution_layer
.payload_attributes(prepare_slot, head_root)
.payload_attributes(prepare_slot, head_root, head_payload_status)
.await
{
payload_attributes
@@ -6187,6 +6229,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.insert_proposer(
prepare_slot,
head_root,
head_payload_status,
proposer,
payload_attributes.clone(),
)
@@ -6198,6 +6241,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
%prepare_slot,
validator = proposer,
parent_root = ?head_root,
payload_status = ?head_payload_status,
"Prepared beacon proposer"
);
payload_attributes
@@ -6250,6 +6294,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.update_execution_engine_forkchoice(
current_slot,
forkchoice_update_params,
head_payload_status,
OverrideForkchoiceUpdate::AlreadyApplied,
)
.await?;
@@ -6262,6 +6307,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self: &Arc<Self>,
current_slot: Slot,
input_params: ForkchoiceUpdateParameters,
head_payload_status: fork_choice::PayloadStatus,
override_forkchoice_update: OverrideForkchoiceUpdate,
) -> Result<(), Error> {
let execution_layer = self
@@ -6322,6 +6368,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
finalized_hash,
current_slot,
head_block_root,
head_payload_status,
)
.await
.map_err(Error::ExecutionForkChoiceUpdateFailed);

View File

@@ -827,8 +827,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// The execution layer updates might attempt to take a write-lock on fork choice, so it's
// important to ensure the fork-choice lock isn't being held.
let el_update_handle =
spawn_execution_layer_updates(self.clone(), new_forkchoice_update_parameters)?;
let el_update_handle = spawn_execution_layer_updates(
self.clone(),
new_forkchoice_update_parameters,
new_payload_status,
)?;
// We have completed recomputing the head and it's now valid for another process to do the
// same.
@@ -1186,6 +1189,7 @@ fn perform_debug_logging<T: BeaconChainTypes>(
fn spawn_execution_layer_updates<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
forkchoice_update_params: ForkchoiceUpdateParameters,
head_payload_status: PayloadStatus,
) -> Result<JoinHandle<Option<()>>, Error> {
let current_slot = chain
.slot_clock
@@ -1208,6 +1212,7 @@ fn spawn_execution_layer_updates<T: BeaconChainTypes>(
.update_execution_engine_forkchoice(
current_slot,
forkchoice_update_params,
head_payload_status,
OverrideForkchoiceUpdate::Yes,
)
.await

View File

@@ -771,6 +771,36 @@ where
.execution_block_generator()
}
/// Create a switch-to-compounding `ConsolidationRequest` for the given validator.
///
/// Panics if the validator doesn't exist, doesn't have eth1 withdrawal credentials,
/// or doesn't have an execution withdrawal address.
pub fn make_switch_to_compounding_request(
&self,
validator_index: usize,
) -> ConsolidationRequest {
let head = self.chain.canonical_head.cached_head();
let head_state = &head.snapshot.beacon_state;
let validator = head_state
.get_validator(validator_index)
.expect("validator should exist");
assert!(
validator.has_eth1_withdrawal_credential(&self.spec),
"validator {validator_index} should have eth1 withdrawal credentials"
);
let source_address = validator
.get_execution_withdrawal_address(&self.spec)
.expect("validator should have execution withdrawal address");
ConsolidationRequest {
source_address,
source_pubkey: validator.pubkey,
target_pubkey: validator.pubkey,
}
}
pub fn set_mock_builder(
&mut self,
beacon_url: SensitiveUrl,

View File

@@ -6,6 +6,7 @@ mod column_verification;
mod events;
mod op_verification;
mod payload_invalidation;
mod prepare_payload;
mod rewards;
mod schema_stability;
mod store_tests;

View File

@@ -0,0 +1,575 @@
#![cfg(not(debug_assertions))]
#![allow(clippy::result_large_err)]
use beacon_chain::test_utils::{
AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, test_spec,
};
use beacon_chain::{ChainConfig, custody_context::NodeCustodyType};
use bls::Keypair;
use eth2::types::ProposerPreparationData;
use fork_choice::PayloadStatus;
use logging::create_test_tracing_subscriber;
use ssz_types::VariableList;
use state_processing::{
per_block_processing::{apply_parent_execution_payload, withdrawals::get_expected_withdrawals},
state_advance::complete_state_advance,
};
use std::sync::{Arc, LazyLock};
use store::database::interface::BeaconNodeBackend;
use store::{HotColdDB, StoreConfig};
use tempfile::{TempDir, tempdir};
use types::*;
// Should ideally be divisible by 3.
pub const LOW_VALIDATOR_COUNT: usize = 32;
pub const HIGH_VALIDATOR_COUNT: usize = 64;
/// A cached set of keys.
static KEYPAIRS: LazyLock<Vec<Keypair>> =
LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(HIGH_VALIDATOR_COUNT));
type E = MinimalEthSpec;
type TestHarness = BeaconChainHarness<DiskHarnessType<E>>;
fn get_store(
db_path: &TempDir,
spec: Arc<ChainSpec>,
) -> Arc<HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>> {
let store_config = StoreConfig {
prune_payloads: false,
..StoreConfig::default()
};
get_store_generic(db_path, store_config, spec)
}
fn get_store_generic(
db_path: &TempDir,
config: StoreConfig,
spec: Arc<ChainSpec>,
) -> Arc<HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>> {
create_test_tracing_subscriber();
let hot_path = db_path.path().join("chain_db");
let cold_path = db_path.path().join("freezer_db");
let blobs_path = db_path.path().join("blobs_db");
HotColdDB::open(
&hot_path,
&cold_path,
&blobs_path,
|_, _, _| Ok(()),
config,
spec,
)
.expect("disk store should initialize")
}
fn get_harness(
store: Arc<HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>,
validator_count: usize,
) -> TestHarness {
// Most tests expect to retain historic states, so we use this as the default.
let chain_config = ChainConfig {
archive: true,
..ChainConfig::default()
};
get_harness_generic(
store,
validator_count,
chain_config,
NodeCustodyType::Fullnode,
)
}
fn get_harness_generic(
store: Arc<HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>,
validator_count: usize,
chain_config: ChainConfig,
node_custody_type: NodeCustodyType,
) -> TestHarness {
let harness = TestHarness::builder(MinimalEthSpec)
.spec(store.get_chain_spec().clone())
.keypairs(KEYPAIRS[0..validator_count].to_vec())
.fresh_disk_store(store)
.mock_execution_layer()
.chain_config(chain_config)
.node_custody_type(node_custody_type)
.build();
harness.advance_slot();
harness
}
#[tokio::test]
async fn prepare_payload_on_full_parent_next_slot() {
prepare_payload_generic(
PayloadStatus::Full,
Slot::new(3 * E::slots_per_epoch() + 1),
Slot::new(3 * E::slots_per_epoch() + 2),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_full_parent_one_epoch_skip() {
prepare_payload_generic(
PayloadStatus::Full,
Slot::new(3 * E::slots_per_epoch() + 1),
Slot::new(4 * E::slots_per_epoch()),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_full_parent_uneven_one_epoch_skip() {
prepare_payload_generic(
PayloadStatus::Full,
Slot::new(3 * E::slots_per_epoch() + 1),
Slot::new(5 * E::slots_per_epoch() - 1),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_empty_parent_next_slot() {
prepare_payload_generic(
PayloadStatus::Empty,
Slot::new(3 * E::slots_per_epoch() + 1),
Slot::new(3 * E::slots_per_epoch() + 2),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_empty_parent_one_epoch_skip() {
prepare_payload_generic(
PayloadStatus::Empty,
Slot::new(3 * E::slots_per_epoch() + 1),
Slot::new(4 * E::slots_per_epoch()),
)
.await;
}
async fn prepare_payload_generic(
parent_payload_status: PayloadStatus,
parent_block_slot: Slot,
prepare_slot: Slot,
) {
assert!(parent_block_slot > 0);
// Post-Gloas test.
let spec = Arc::new(test_spec::<E>());
if !spec.fork_name_at_slot::<E>(Slot::new(0)).gloas_enabled() {
return;
}
let num_blocks_produced = parent_block_slot.as_u64() - 1;
let db_path = tempdir().unwrap();
let store = get_store(&db_path, spec.clone());
let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT);
harness
.extend_chain(
num_blocks_produced as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Advance the slot so the next extend_chain produces at a fresh slot.
harness.advance_slot();
// Produce a block with a payload that affects withdrawals for the next slot.
// A switch-to-compounding consolidation changes withdrawal credentials from 0x01 to 0x02,
// which queues the validator's excess balance as a pending deposit and removes it from the
// partial withdrawal sweep. We target an odd-indexed validator since odd validators are
// created with eth1 withdrawal credentials in the interop genesis builder.
let consolidation_request = harness.make_switch_to_compounding_request(1);
let execution_requests = ExecutionRequests::<E> {
deposits: VariableList::empty(),
withdrawals: VariableList::empty(),
consolidations: VariableList::new(vec![consolidation_request]).unwrap(),
};
// Inject the execution requests into the mock EL so the next payload includes them.
harness
.execution_block_generator()
.set_next_execution_requests(execution_requests);
// Produce and import one more block. Its envelope will contain the consolidation request.
// TODO(gloas): all this ugly plumbing could be avoided with some more "implicit" context
// methods
let state = harness.get_current_state();
let (block_contents, opt_envelope, parent_block_state) = harness
.make_block_with_envelope(state, parent_block_slot)
.await;
let envelope = opt_envelope.unwrap();
let block_root = harness
.process_block(
parent_block_slot,
block_contents.0.canonical_root(),
block_contents.clone(),
)
.await
.unwrap();
// TODO(gloas): try a case where head is empty even though envelope is processed
if parent_payload_status == PayloadStatus::Full {
harness
.process_envelope(
block_root.into(),
envelope.clone(),
&parent_block_state,
block_contents.0.state_root(),
)
.await;
}
// Verify that the withdrawals computed from the block's state differ from the withdrawals
// computed from the block's state with its payload applied by
// `apply_parent_execution_payload`.
let cached_head = harness.chain.canonical_head.cached_head();
let unadvanced_empty_state = &cached_head.snapshot.beacon_state;
let parent_bid = unadvanced_empty_state
.latest_execution_payload_bid()
.unwrap();
let mut advanced_empty_state = unadvanced_empty_state.clone();
complete_state_advance(&mut advanced_empty_state, None, prepare_slot, &spec).unwrap();
let mut unadvanced_full_state = unadvanced_empty_state.clone();
apply_parent_execution_payload(
&mut unadvanced_full_state,
parent_bid,
&envelope.message.execution_requests,
&spec,
)
.unwrap();
let mut advanced_full_state = advanced_empty_state.clone();
apply_parent_execution_payload(
&mut advanced_full_state,
parent_bid,
&envelope.message.execution_requests,
&spec,
)
.unwrap();
let withdrawals_unadvanced_empty: Withdrawals<E> =
get_expected_withdrawals(unadvanced_empty_state, &spec)
.unwrap()
.into();
let withdrawals_advanced_empty: Withdrawals<E> =
get_expected_withdrawals(&advanced_empty_state, &spec)
.unwrap()
.into();
let withdrawals_unadvanced_full: Withdrawals<E> =
get_expected_withdrawals(&unadvanced_full_state, &spec)
.unwrap()
.into();
let withdrawals_advanced_full: Withdrawals<E> =
get_expected_withdrawals(&advanced_full_state, &spec)
.unwrap()
.into();
assert_ne!(
withdrawals_advanced_empty, withdrawals_advanced_full,
"Applying execution requests should change the expected withdrawals"
);
let expect_state_advance_to_change_withdrawals =
prepare_slot.epoch(E::slots_per_epoch()) > parent_block_slot.epoch(E::slots_per_epoch());
if expect_state_advance_to_change_withdrawals {
if parent_payload_status == fork_choice::PayloadStatus::Full {
assert_ne!(
withdrawals_unadvanced_full, withdrawals_advanced_full,
"Advancing the state should change the withdrawals"
);
} else {
assert_ne!(
withdrawals_unadvanced_empty, withdrawals_advanced_empty,
"Advancing the state should change the withdrawals"
);
}
}
// Call `prepare_beacon_proposer` for the next slot and ensure that it primes the execution
// layer payload attributes cache with the correct withdrawals (the ones taking into account
// the applied execution_requests).
let current_slot = prepare_slot - 1;
let proposer_index = advanced_empty_state
.get_beacon_proposer_index(prepare_slot, &spec)
.expect("should get proposer index");
// Register the proposer so prepare_beacon_proposer doesn't skip it.
let el = harness.chain.execution_layer.as_ref().unwrap();
el.update_proposer_preparation(
prepare_slot.epoch(E::slots_per_epoch()),
[(
&ProposerPreparationData {
validator_index: proposer_index as u64,
fee_recipient: Address::repeat_byte(42),
},
&None,
)],
)
.await;
// Advance the slot clock to just before the prepare slot so the lookahead check passes.
harness.advance_to_slot_lookahead(prepare_slot, harness.chain.config.prepare_payload_lookahead);
harness
.chain
.prepare_beacon_proposer(current_slot)
.await
.expect("prepare_beacon_proposer should succeed");
// Read the payload attributes from the EL cache and verify the withdrawals.
let el = harness.chain.execution_layer.as_ref().unwrap();
let head_root = harness.head_block_root();
let attributes = el
.payload_attributes(prepare_slot, head_root, parent_payload_status)
.await
.expect("should have cached payload attributes for prepare_slot");
let actual_withdrawals = attributes.withdrawals().unwrap();
let expected_withdrawals: Vec<Withdrawal> = if parent_payload_status == PayloadStatus::Full {
withdrawals_advanced_full.to_vec()
} else {
withdrawals_advanced_empty.to_vec()
};
assert_eq!(
actual_withdrawals, &expected_withdrawals,
"prepare_beacon_proposer should use withdrawals computed from the \
{parent_payload_status:?} state"
);
}
#[tokio::test]
async fn prepare_payload_on_genesis_next_slot() {
prepare_payload_on_genesis_generic(Slot::new(1)).await;
}
#[tokio::test]
async fn prepare_payload_on_genesis_skip_two_epochs() {
prepare_payload_on_genesis_generic(Slot::new(2 * E::slots_per_epoch())).await;
}
async fn prepare_payload_on_genesis_generic(prepare_slot: Slot) {
// Post-Gloas test.
let spec = Arc::new(test_spec::<E>());
if !spec.fork_name_at_slot::<E>(Slot::new(0)).gloas_enabled() {
return;
}
// Genesis is always considered Empty.
let parent_payload_status = PayloadStatus::Empty;
let db_path = tempdir().unwrap();
let store = get_store(&db_path, spec.clone());
let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT);
// At genesis withdrawals are empty (because nothing has happened yet), so we don't assert
// anything about the advanced vs unadvanced state. This test just exists to test that
// calculating payload attributes at genesis works and doesn't error.
let cached_head = harness.chain.canonical_head.cached_head();
let unadvanced_state = &cached_head.snapshot.beacon_state;
let mut advanced_state = unadvanced_state.clone();
complete_state_advance(&mut advanced_state, None, prepare_slot, &spec).unwrap();
let withdrawals_advanced: Withdrawals<E> = get_expected_withdrawals(&advanced_state, &spec)
.unwrap()
.into();
// Call `prepare_beacon_proposer` for the next slot and ensure that it primes the execution
// layer payload attributes cache with the correct withdrawals (the ones taking into account
// the state advance).
let current_slot = prepare_slot - 1;
let proposer_index = advanced_state
.get_beacon_proposer_index(prepare_slot, &spec)
.unwrap();
// Register the proposer so prepare_beacon_proposer doesn't skip it.
let el = harness.chain.execution_layer.as_ref().unwrap();
el.update_proposer_preparation(
prepare_slot.epoch(E::slots_per_epoch()),
[(
&ProposerPreparationData {
validator_index: proposer_index as u64,
fee_recipient: Address::repeat_byte(42),
},
&None,
)],
)
.await;
// Advance the slot clock to just before the prepare slot so the lookahead check passes.
harness.advance_to_slot_lookahead(prepare_slot, harness.chain.config.prepare_payload_lookahead);
harness
.chain
.prepare_beacon_proposer(current_slot)
.await
.unwrap();
// Read the payload attributes from the EL cache and verify the withdrawals.
let el = harness.chain.execution_layer.as_ref().unwrap();
let head_root = harness.head_block_root();
let attributes = el
.payload_attributes(prepare_slot, head_root, parent_payload_status)
.await
.unwrap();
let actual_withdrawals = attributes.withdrawals().unwrap();
let expected_withdrawals: Vec<Withdrawal> = withdrawals_advanced.to_vec();
assert_eq!(
actual_withdrawals, &expected_withdrawals,
"prepare_beacon_proposer should use withdrawals computed from the \
{parent_payload_status:?} advanced genesis state"
);
assert!(actual_withdrawals.is_empty());
}
#[tokio::test]
async fn prepare_payload_on_fork_boundary_no_skip() {
prepare_payload_on_fork_boundary(
Slot::new(2 * E::slots_per_epoch()) - 1,
Slot::new(2 * E::slots_per_epoch()),
Epoch::new(2),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_fork_boundary_skip_one_prior() {
prepare_payload_on_fork_boundary(
Slot::new(2 * E::slots_per_epoch()) - 2,
Slot::new(2 * E::slots_per_epoch()),
Epoch::new(2),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_fork_boundary_skip_one_after() {
prepare_payload_on_fork_boundary(
Slot::new(2 * E::slots_per_epoch()) - 1,
Slot::new(2 * E::slots_per_epoch()) + 1,
Epoch::new(2),
)
.await;
}
#[tokio::test]
async fn prepare_payload_on_fork_boundary_skip_whole_epoch() {
prepare_payload_on_fork_boundary(
Slot::new(E::slots_per_epoch()),
Slot::new(2 * E::slots_per_epoch()),
Epoch::new(2),
)
.await;
}
async fn prepare_payload_on_fork_boundary(
parent_block_slot: Slot,
prepare_slot: Slot,
gloas_fork_epoch: Epoch,
) {
// Post-Gloas test.
let mut spec = test_spec::<E>();
if !spec.fork_name_at_slot::<E>(Slot::new(0)).gloas_enabled() {
return;
}
spec.gloas_fork_epoch = Some(gloas_fork_epoch);
let spec = Arc::new(spec);
// Pre-Gloas blocks are always considered Empty.
let parent_payload_status = PayloadStatus::Empty;
let num_blocks_produced = parent_block_slot.as_u64();
let db_path = tempdir().unwrap();
let store = get_store(&db_path, spec.clone());
let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT);
harness
.extend_chain(
num_blocks_produced as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Verify that the withdrawals computed from the block's state differ from the withdrawals
// computed from the block's state with its payload applied by
// `apply_parent_execution_payload`.
let cached_head = harness.chain.canonical_head.cached_head();
let unadvanced_state = &cached_head.snapshot.beacon_state;
let mut advanced_state = unadvanced_state.clone();
complete_state_advance(&mut advanced_state, None, prepare_slot, &spec).unwrap();
let withdrawals_unadvanced: Withdrawals<E> = get_expected_withdrawals(unadvanced_state, &spec)
.unwrap()
.into();
let withdrawals_advanced: Withdrawals<E> = get_expected_withdrawals(&advanced_state, &spec)
.unwrap()
.into();
let expect_state_advance_to_change_withdrawals = prepare_slot.epoch(E::slots_per_epoch()) > 0;
if expect_state_advance_to_change_withdrawals {
assert_ne!(
withdrawals_unadvanced, withdrawals_advanced,
"Advancing the state should change the withdrawals"
);
}
// Call `prepare_beacon_proposer` for the next slot and ensure that it primes the execution
// layer payload attributes cache with the correct withdrawals (the ones taking into account
// the applied execution_requests).
let current_slot = prepare_slot - 1;
let proposer_index = advanced_state
.get_beacon_proposer_index(prepare_slot, &spec)
.unwrap();
// Register the proposer so prepare_beacon_proposer doesn't skip it.
let el = harness.chain.execution_layer.as_ref().unwrap();
el.update_proposer_preparation(
prepare_slot.epoch(E::slots_per_epoch()),
[(
&ProposerPreparationData {
validator_index: proposer_index as u64,
fee_recipient: Address::repeat_byte(42),
},
&None,
)],
)
.await;
// Advance the slot clock to just before the prepare slot so the lookahead check passes.
harness.advance_to_slot_lookahead(prepare_slot, harness.chain.config.prepare_payload_lookahead);
harness
.chain
.prepare_beacon_proposer(current_slot)
.await
.unwrap();
// Read the payload attributes from the EL cache and verify the withdrawals.
let el = harness.chain.execution_layer.as_ref().unwrap();
let head_root = harness.head_block_root();
let attributes = el
.payload_attributes(prepare_slot, head_root, parent_payload_status)
.await
.unwrap();
let actual_withdrawals = attributes.withdrawals().unwrap();
let expected_withdrawals: Vec<Withdrawal> = withdrawals_advanced.to_vec();
assert_eq!(
actual_withdrawals, &expected_withdrawals,
"prepare_beacon_proposer should use withdrawals computed from the \
advanced state"
);
}

View File

@@ -721,10 +721,9 @@ where
if let Some(execution_layer) = beacon_chain.execution_layer.as_ref() {
// Only send a head update *after* genesis.
if let Ok(current_slot) = beacon_chain.slot() {
let params = beacon_chain
.canonical_head
.cached_head()
.forkchoice_update_parameters();
let cached_head = beacon_chain.canonical_head.cached_head();
let head_payload_status = cached_head.head_payload_status();
let params = cached_head.forkchoice_update_parameters();
if params
.head_hash
.is_some_and(|hash| hash != ExecutionBlockHash::zero())
@@ -737,6 +736,7 @@ where
.update_execution_engine_forkchoice(
current_slot,
params,
head_payload_status,
Default::default(),
)
.await;

View File

@@ -1,7 +1,7 @@
use super::*;
use alloy_rlp::RlpEncodable;
use serde::{Deserialize, Serialize};
use ssz::{Decode, TryFromIter};
use ssz::{Decode, Encode, TryFromIter};
use ssz_types::{FixedVector, VariableList, typenum::Unsigned};
use strum::EnumString;
use superstruct::superstruct;
@@ -481,6 +481,34 @@ pub enum RequestsError {
#[serde(transparent)]
pub struct JsonExecutionRequests(pub Vec<String>);
impl<E: EthSpec> From<ExecutionRequests<E>> for JsonExecutionRequests {
fn from(requests: ExecutionRequests<E>) -> Self {
let mut result = Vec::new();
if !requests.deposits.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Deposit.to_u8(),
hex::encode(requests.deposits.as_ssz_bytes())
));
}
if !requests.withdrawals.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Withdrawal.to_u8(),
hex::encode(requests.withdrawals.as_ssz_bytes())
));
}
if !requests.consolidations.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Consolidation.to_u8(),
hex::encode(requests.consolidations.as_ssz_bytes())
));
}
JsonExecutionRequests(result)
}
}
impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequests<E> {
type Error = RequestsError;

View File

@@ -403,6 +403,7 @@ impl ProposerPreparationDataEntry {
pub struct ProposerKey {
slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
}
#[derive(PartialEq, Clone)]
@@ -1461,12 +1462,14 @@ impl<E: EthSpec> ExecutionLayer<E> {
&self,
slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
validator_index: u64,
payload_attributes: PayloadAttributes,
) -> bool {
let proposers_key = ProposerKey {
slot,
head_block_root,
head_payload_status,
};
let existing = self.proposers().write().await.insert(
@@ -1485,16 +1488,18 @@ impl<E: EthSpec> ExecutionLayer<E> {
}
/// If there has been a proposer registered via `Self::insert_proposer` with a matching `slot`
/// `head_block_root`, then return the appropriate `PayloadAttributes` for inclusion in
/// `forkchoiceUpdated` calls.
/// `head_block_root`, and `head_payload_status` then return the appropriate `PayloadAttributes`
/// for inclusion in `forkchoiceUpdated` calls.
pub async fn payload_attributes(
&self,
current_slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
) -> Option<PayloadAttributes> {
let proposers_key = ProposerKey {
slot: current_slot,
head_block_root,
head_payload_status,
};
let proposer = self.proposers().read().await.get(&proposers_key).cloned()?;
@@ -1518,6 +1523,7 @@ impl<E: EthSpec> ExecutionLayer<E> {
finalized_block_hash: ExecutionBlockHash,
current_slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
) -> Result<PayloadStatus, Error> {
let _timer = metrics::start_timer_vec(
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
@@ -1534,7 +1540,9 @@ impl<E: EthSpec> ExecutionLayer<E> {
);
let next_slot = current_slot + 1;
let payload_attributes = self.payload_attributes(next_slot, head_block_root).await;
let payload_attributes = self
.payload_attributes(next_slot, head_block_root, head_payload_status)
.await;
// Compute the "lookahead", the time between when the payload will be produced and now.
if let Some(ref payload_attributes) = payload_attributes

View File

@@ -26,8 +26,8 @@ use tree_hash_derive::TreeHash;
use types::{
Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix,
ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu,
ExecutionPayloadGloas, ExecutionPayloadHeader, ForkName, Hash256, KzgProofs, Transaction,
Transactions, Uint256,
ExecutionPayloadGloas, ExecutionPayloadHeader, ExecutionRequests, ForkName, Hash256, KzgProofs,
Transaction, Transactions, Uint256,
};
const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz");
@@ -161,6 +161,14 @@ pub struct ExecutionBlockGenerator<E: EthSpec> {
pub blobs_bundles: HashMap<PayloadId, BlobsBundle<E>>,
pub kzg: Option<Arc<Kzg>>,
rng: Arc<Mutex<StdRng>>,
/*
* Execution requests (electra+)
*/
/// Per-payload execution requests returned by `getPayload`.
execution_requests: HashMap<PayloadId, ExecutionRequests<E>>,
/// If set, the next call to `build_new_execution_payload` will associate these
/// execution requests with the generated payload ID.
next_execution_requests: Option<ExecutionRequests<E>>,
}
fn make_rng() -> Arc<Mutex<StdRng>> {
@@ -199,6 +207,8 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
blobs_bundles: <_>::default(),
kzg,
rng: make_rng(),
execution_requests: <_>::default(),
next_execution_requests: None,
};
generator.insert_pow_block(0).unwrap();
@@ -458,6 +468,15 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
self.blobs_bundles.get(id).cloned()
}
pub fn get_execution_requests(&self, id: &PayloadId) -> Option<ExecutionRequests<E>> {
self.execution_requests.get(id).cloned()
}
/// Set execution requests to be returned alongside the next generated payload.
pub fn set_next_execution_requests(&mut self, requests: ExecutionRequests<E>) {
self.next_execution_requests = Some(requests);
}
/// Look up a blob and proof by versioned hash across all stored bundles.
pub fn get_blob_and_proof(&self, versioned_hash: &Hash256) -> Option<BlobAndProof<E>> {
self.blobs_bundles
@@ -763,6 +782,11 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
},
};
// Store execution requests for this payload if configured.
if let Some(requests) = self.next_execution_requests.take() {
self.execution_requests.insert(id, requests);
}
let fork_name = execution_payload.fork_name();
if fork_name.deneb_enabled() {
// get random number between 0 and 1 blobs by default

View File

@@ -295,6 +295,10 @@ pub async fn handle_rpc<E: EthSpec>(
})?;
let maybe_blobs = ctx.execution_block_generator.write().get_blobs_bundle(&id);
let maybe_execution_requests = ctx
.execution_block_generator
.read()
.get_execution_requests(&id);
// validate method called correctly according to shanghai fork time
if ctx
@@ -432,8 +436,10 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
// TODO(electra): add EL requests in mock el
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.clone()
.unwrap_or_default()
.into(),
})
.unwrap()
}
@@ -453,7 +459,10 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.clone()
.unwrap_or_default()
.into(),
})
.unwrap()
}
@@ -473,7 +482,9 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.unwrap_or_default()
.into(),
})
.unwrap()
}

View File

@@ -800,6 +800,10 @@ impl<E: EthSpec> MockBuilder<E> {
let head_block_root = head_block_root.unwrap_or(head.canonical_root());
// TODO(gloas): Currently the tests are pre-Gloas and we are not considering
// other payload statuses. This codepath may not be relevant for Gloas.
let head_payload_status = fork_choice::PayloadStatus::Pending;
let head_execution_payload = head
.message()
.body()
@@ -934,7 +938,13 @@ impl<E: EthSpec> MockBuilder<E> {
);
self.el
.insert_proposer(slot, head_block_root, val_index, payload_attributes.clone())
.insert_proposer(
slot,
head_block_root,
head_payload_status,
val_index,
payload_attributes.clone(),
)
.await;
let forkchoice_update_params = ForkchoiceUpdateParameters {
@@ -952,6 +962,7 @@ impl<E: EthSpec> MockBuilder<E> {
finalized_execution_hash,
slot - 1,
head_block_root,
head_payload_status,
)
.await
.map_err(|e| format!("fcu call failed : {:?}", e))?;

View File

@@ -90,6 +90,8 @@ impl<E: EthSpec> MockExecutionLayer<E> {
let timestamp = block_number;
let prev_randao = Hash256::from_low_u64_be(block_number);
let head_block_root = Hash256::repeat_byte(42);
// TODO(gloas): allow statuses other than Pending?
let head_payload_status = fork_choice::PayloadStatus::Pending;
let forkchoice_update_params = ForkchoiceUpdateParameters {
head_root: head_block_root,
head_hash: Some(parent_hash),
@@ -109,7 +111,13 @@ impl<E: EthSpec> MockExecutionLayer<E> {
let slot = Slot::new(0);
let validator_index = 0;
self.el
.insert_proposer(slot, head_block_root, validator_index, payload_attributes)
.insert_proposer(
slot,
head_block_root,
head_payload_status,
validator_index,
payload_attributes,
)
.await;
self.el
@@ -119,6 +127,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
ExecutionBlockHash::zero(),
slot,
head_block_root,
head_payload_status,
)
.await
.unwrap();
@@ -280,6 +289,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
// Use junk values for slot/head-root to ensure there is no payload supplied.
let slot = Slot::new(0);
let head_block_root = Hash256::repeat_byte(13);
// TODO(gloas): reconsider the state_payload_status
self.el
.notify_forkchoice_updated(
block_hash,
@@ -287,6 +297,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
ExecutionBlockHash::zero(),
slot,
head_block_root,
fork_choice::PayloadStatus::Pending,
)
.await
.unwrap();