Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d901998
introduce `PayloadStatus` in `ProposerKey`
hopinheimer Apr 5, 2026
b85fa19
adopt tests to the new `ProposerKey` struct
hopinheimer Apr 5, 2026
f0b01d0
resolving divergent `StatePayloadStatus` values
hopinheimer Apr 6, 2026
5007b75
fmt
hopinheimer Apr 6, 2026
7ae5dd4
rounding up some todos
hopinheimer Apr 6, 2026
8cef49f
fmt
hopinheimer Apr 6, 2026
98b9d71
fmt again :|
hopinheimer Apr 8, 2026
cc8f583
Merge remote-tracking branch 'origin/unstable' into re-issue-fcu-on-p…
michaelsproul Apr 21, 2026
63881d0
Use fork_choice::PayloadStatus
michaelsproul Apr 21, 2026
c100092
Revert changes in state advance
michaelsproul Apr 21, 2026
011c453
Add test spec for prepare_payload_on_full_parent
michaelsproul Apr 22, 2026
c95910a
Claude's first attempt at regression test (and fix)
michaelsproul Apr 22, 2026
50d725f
Cleaner (and more correct) implementation
michaelsproul Apr 22, 2026
e6e2990
Simplify
michaelsproul Apr 22, 2026
3ed2fbf
Merge remote-tracking branch 'origin/unstable' into re-issue-fcu-on-p…
michaelsproul Apr 22, 2026
eb24b86
Comment simplifications
michaelsproul Apr 22, 2026
6680929
Simplify a little more
michaelsproul Apr 22, 2026
3be778e
Add test for empty parent payload
michaelsproul Apr 22, 2026
868c578
Merge branch 'unstable' into re-issue-fcu-on-payload
michaelsproul Apr 23, 2026
5d14f08
New tests for epoch advance
michaelsproul Apr 23, 2026
56b311e
Genesis payload attributes test (failing)
michaelsproul Apr 23, 2026
e9e1e44
Fix Gloas genesis in get_expected_withdrawals
michaelsproul Apr 23, 2026
d923804
Fork boundary test
michaelsproul Apr 23, 2026
451d2f2
Clippy
michaelsproul Apr 23, 2026
8b73355
Resolve/clarify remaining TODOs
michaelsproul Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 68 additions & 21 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,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},
Expand Down Expand Up @@ -4706,37 +4706,48 @@ 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))?;
let (state_root, state) = self
.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,
Expand All @@ -4746,9 +4757,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)
Expand Down Expand Up @@ -5960,13 +5995,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);
Expand All @@ -5988,7 +6030,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
Expand Down Expand Up @@ -6035,6 +6077,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.insert_proposer(
prepare_slot,
head_root,
head_payload_status,
proposer,
payload_attributes.clone(),
)
Expand All @@ -6046,6 +6089,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
%prepare_slot,
validator = proposer,
parent_root = ?head_root,
payload_status = ?head_payload_status,
"Prepared beacon proposer"
);
payload_attributes
Expand Down Expand Up @@ -6098,6 +6142,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.update_execution_engine_forkchoice(
current_slot,
forkchoice_update_params,
head_payload_status,
OverrideForkchoiceUpdate::AlreadyApplied,
)
.await?;
Expand All @@ -6110,6 +6155,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
Expand Down Expand Up @@ -6170,6 +6216,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
finalized_hash,
current_slot,
head_block_root,
head_payload_status,
)
.await
.map_err(Error::ExecutionForkChoiceUpdateFailed);
Expand Down
9 changes: 7 additions & 2 deletions beacon_node/beacon_chain/src/canonical_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,8 +814,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.
Expand Down Expand Up @@ -1173,6 +1176,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
Expand All @@ -1195,6 +1199,7 @@ fn spawn_execution_layer_updates<T: BeaconChainTypes>(
.update_execution_engine_forkchoice(
current_slot,
forkchoice_update_params,
head_payload_status,
OverrideForkchoiceUpdate::Yes,
)
.await
Expand Down
30 changes: 30 additions & 0 deletions beacon_node/beacon_chain/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,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,
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading