resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-04-30 01:51:26 +02:00
544 changed files with 48684 additions and 18351 deletions

View File

@@ -41,13 +41,13 @@ use crate::{
metrics,
validator_monitor::get_slot_delay_ms,
};
use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead};
use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseLateHead};
use fork_choice::{
ExecutionStatus, ForkChoiceStore, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock,
ResetPayloadStatuses,
ExecutionStatus, ForkChoiceStore, ForkChoiceView, ForkchoiceUpdateParameters, PayloadStatus,
ProtoBlock, ResetPayloadStatuses,
};
use itertools::process_results;
use lighthouse_tracing::SPAN_RECOMPUTE_HEAD;
use logging::crit;
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
use slot_clock::SlotClock;
@@ -58,7 +58,6 @@ use store::{
Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreConfig, iter::StateRootsIterator,
};
use task_executor::{JoinHandle, ShutdownReason};
use tracing::info_span;
use tracing::{debug, error, info, instrument, warn};
use types::*;
@@ -108,6 +107,8 @@ pub struct CachedHead<E: EthSpec> {
/// This value may be distinct to the `self.snapshot.beacon_state.finalized_checkpoint`.
/// This value should be used over the beacon state value in practically all circumstances.
finalized_checkpoint: Checkpoint,
/// The payload status of the head block, as determined by fork choice.
head_payload_status: proto_array::PayloadStatus,
/// The `execution_payload.block_hash` of the block at the head of the chain. Set to `None`
/// before Bellatrix.
head_hash: Option<ExecutionBlockHash>,
@@ -237,6 +238,10 @@ impl<E: EthSpec> CachedHead<E> {
finalized_hash: self.finalized_hash,
}
}
pub fn head_payload_status(&self) -> proto_array::PayloadStatus {
self.head_payload_status
}
}
/// Represents the "canonical head" of the beacon chain.
@@ -267,6 +272,7 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
pub fn new(
fork_choice: BeaconForkChoice<T>,
snapshot: Arc<BeaconSnapshot<T::EthSpec>>,
head_payload_status: proto_array::PayloadStatus,
) -> Self {
let fork_choice_view = fork_choice.cached_fork_choice_view();
let forkchoice_update_params = fork_choice.get_forkchoice_update_parameters();
@@ -274,6 +280,7 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
snapshot,
justified_checkpoint: fork_choice_view.justified_checkpoint,
finalized_checkpoint: fork_choice_view.finalized_checkpoint,
head_payload_status,
head_hash: forkchoice_update_params.head_hash,
justified_hash: forkchoice_update_params.justified_hash,
finalized_hash: forkchoice_update_params.finalized_hash,
@@ -301,21 +308,34 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
store: &BeaconStore<T>,
spec: &ChainSpec,
) -> Result<(), Error> {
let fork_choice =
let mut fork_choice =
<BeaconChain<T>>::load_fork_choice(store.clone(), reset_payload_statuses, spec)?
.ok_or(Error::MissingPersistedForkChoice)?;
let current_slot_for_head = fork_choice.fc_store().get_current_slot();
let (_, head_payload_status) = fork_choice.get_head(current_slot_for_head, spec)?;
let fork_choice_view = fork_choice.cached_fork_choice_view();
let beacon_block_root = fork_choice_view.head_block_root;
let beacon_block = store
.get_full_block(&beacon_block_root)?
.ok_or(Error::MissingBeaconBlock(beacon_block_root))?;
let current_slot = fork_choice.fc_store().get_current_slot();
let (_, beacon_state) = store
.get_advanced_hot_state(beacon_block_root, current_slot, beacon_block.state_root())?
.ok_or(Error::MissingBeaconState(beacon_block.state_root()))?;
// Load the execution envelope from the store if the head has a Full payload.
let execution_envelope = if head_payload_status == PayloadStatus::Full {
store
.get_payload_envelope(&beacon_block_root)?
.map(Arc::new)
} else {
None
};
let snapshot = BeaconSnapshot {
beacon_block_root,
execution_envelope,
beacon_block: Arc::new(beacon_block),
beacon_state,
};
@@ -325,6 +345,7 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
snapshot: Arc::new(snapshot),
justified_checkpoint: fork_choice_view.justified_checkpoint,
finalized_checkpoint: fork_choice_view.finalized_checkpoint,
head_payload_status,
head_hash: forkchoice_update_params.head_hash,
justified_hash: forkchoice_update_params.justified_hash,
finalized_hash: forkchoice_update_params.finalized_hash,
@@ -367,6 +388,26 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
Ok((head, execution_status))
}
/// Returns `true` if the payload for this block is canonical (Full) according to fork choice.
pub fn block_has_canonical_payload(
&self,
root: &Hash256,
spec: &ChainSpec,
) -> Result<bool, Error> {
let cached_head = self.cached_head();
let head_root = cached_head.head_block_root();
let head_payload_status = cached_head.head_payload_status();
if *root == head_root {
return Ok(head_payload_status == PayloadStatus::Full);
}
self.fork_choice_read_lock()
.get_canonical_payload_status(root, spec)
.map(|status| status == PayloadStatus::Full)
.map_err(Error::ForkChoiceError)
}
/// Returns a clone of `self.cached_head`.
///
/// Takes a read-lock on `self.cached_head` for a short time (just long enough to clone it).
@@ -517,22 +558,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// such a case it's critical that the `BeaconChain` keeps importing blocks so that the
/// situation can be rectified. We avoid returning an error here so that calling functions
/// can't abort block import because an error is returned here.
#[instrument(name = "lh_recompute_head_at_slot", skip(self), level = "info", fields(slot = %current_slot))]
pub async fn recompute_head_at_slot(self: &Arc<Self>, current_slot: Slot) {
let span = info_span!(
SPAN_RECOMPUTE_HEAD,
slot = %current_slot
);
metrics::inc_counter(&metrics::FORK_CHOICE_REQUESTS);
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_TIMES);
let chain = self.clone();
match self
.spawn_blocking_handle(
move || {
let _guard = span.enter();
chain.recompute_head_at_slot_internal(current_slot)
},
move || chain.recompute_head_at_slot_internal(current_slot),
"recompute_head_internal",
)
.await
@@ -598,11 +632,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
justified_checkpoint: old_cached_head.justified_checkpoint(),
finalized_checkpoint: old_cached_head.finalized_checkpoint(),
};
let old_payload_status = old_cached_head.head_payload_status();
let mut fork_choice_write_lock = self.canonical_head.fork_choice_write_lock();
// Recompute the current head via the fork choice algorithm.
fork_choice_write_lock.get_head(current_slot, &self.spec)?;
let (_, new_payload_status) = fork_choice_write_lock.get_head(current_slot, &self.spec)?;
// Downgrade the fork choice write-lock to a read lock, without allowing access to any
// other writers.
@@ -647,9 +682,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
});
}
// Exit early if the head or justified/finalized checkpoints have not changed, there's
// nothing to do.
if new_view == old_view {
// Exit early if the head, checkpoints, and payload status have not changed.
if new_view == old_view && new_payload_status == old_payload_status {
debug!(
head = ?new_view.head_block_root,
"No change in canonical head"
@@ -669,26 +703,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
drop(fork_choice_read_lock);
// If the head has changed, update `self.canonical_head`.
let new_cached_head = if new_view.head_block_root != old_view.head_block_root {
let new_cached_head = if new_view.head_block_root != old_view.head_block_root
|| new_payload_status != old_payload_status
{
metrics::inc_counter(&metrics::FORK_CHOICE_CHANGED_HEAD);
// TODO(gloas): could optimise this to reuse state and rest of snapshot if just the
// payload status has changed.
let mut new_snapshot = {
let beacon_block = self
.store
.get_full_block(&new_view.head_block_root)?
.ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?;
// Load the execution envelope from the store if the head has a Full payload.
let state_root = beacon_block.state_root();
let execution_envelope = if new_payload_status == PayloadStatus::Full {
let envelope = self
.store
.get_payload_envelope(&new_view.head_block_root)?
.map(Arc::new)
.ok_or(Error::MissingExecutionPayloadEnvelope(
new_view.head_block_root,
))?;
Some(envelope)
} else {
None
};
let (_, beacon_state) = self
.store
.get_advanced_hot_state(
new_view.head_block_root,
current_slot,
beacon_block.state_root(),
)?
.ok_or(Error::MissingBeaconState(beacon_block.state_root()))?;
.get_advanced_hot_state(new_view.head_block_root, current_slot, state_root)?
.ok_or(Error::MissingBeaconState(state_root))?;
BeaconSnapshot {
beacon_block: Arc::new(beacon_block),
execution_envelope,
beacon_block_root: new_view.head_block_root,
beacon_state,
}
@@ -702,6 +752,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
snapshot: Arc::new(new_snapshot),
justified_checkpoint: new_view.justified_checkpoint,
finalized_checkpoint: new_view.finalized_checkpoint,
head_payload_status: new_payload_status,
head_hash: new_forkchoice_update_parameters.head_hash,
justified_hash: new_forkchoice_update_parameters.justified_hash,
finalized_hash: new_forkchoice_update_parameters.finalized_hash,
@@ -729,6 +780,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
snapshot: old_cached_head.snapshot.clone(),
justified_checkpoint: new_view.justified_checkpoint,
finalized_checkpoint: new_view.finalized_checkpoint,
head_payload_status: new_payload_status,
head_hash: new_forkchoice_update_parameters.head_hash,
justified_hash: new_forkchoice_update_parameters.justified_hash,
finalized_hash: new_forkchoice_update_parameters.finalized_hash,
@@ -749,7 +801,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let new_snapshot = &new_cached_head.snapshot;
let old_snapshot = &old_cached_head.snapshot;
// If the head changed, perform some updates.
// Only run on head *block* changes - payload status changes only need the
// `cached_head` update above, not re-org detection or event emission.
if new_snapshot.beacon_block_root != old_snapshot.beacon_block_root
&& let Err(e) =
self.after_new_head(&old_cached_head, &new_cached_head, new_head_proto_block)
@@ -779,8 +832,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.
@@ -829,15 +885,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.slot()
.epoch(T::EthSpec::slots_per_epoch());
// These fields are used for server-sent events.
let state_root = new_snapshot.beacon_state_root();
// This field is used for server-sent events.
let head_slot = new_snapshot.beacon_state.slot();
let dependent_root = new_snapshot
.beacon_state
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Next);
let prev_dependent_root = new_snapshot
.beacon_state
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current);
match BlockShufflingIds::try_from_head(
new_snapshot.beacon_block_root,
@@ -868,6 +917,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.as_utf8_lossy(),
&self.slot_clock,
self.event_handler.as_ref(),
&self.spec,
);
if is_epoch_transition || reorg_distance.is_some() {
@@ -875,33 +925,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.op_pool.prune_attestations(self.epoch()?);
}
// Register server-sent-events for a new head.
if let Some(event_handler) = self
.event_handler
.as_ref()
.filter(|handler| handler.has_head_subscribers())
{
match (dependent_root, prev_dependent_root) {
(Ok(current_duty_dependent_root), Ok(previous_duty_dependent_root)) => {
event_handler.register(EventKind::Head(SseHead {
slot: head_slot,
block: new_snapshot.beacon_block_root,
state: state_root,
current_duty_dependent_root,
previous_duty_dependent_root,
epoch_transition: is_epoch_transition,
execution_optimistic: new_head_is_optimistic,
}));
}
(Err(e), _) | (_, Err(e)) => {
warn!(
error = ?e,
"Unable to find dependent roots, cannot register head event"
);
}
}
}
// Register a server-sent-event for a reorg (if necessary).
if let Some(depth) = reorg_distance
&& let Some(event_handler) = self
@@ -956,15 +979,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.start_slot(T::EthSpec::slots_per_epoch()),
);
self.observed_slashable.write().prune(
self.observed_column_sidecars.write().prune(
new_view
.finalized_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch()),
);
self.attester_cache
.prune_below(new_view.finalized_checkpoint.epoch);
self.observed_slashable.write().prune(
new_view
.finalized_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch()),
);
if let Some(event_handler) = self.event_handler.as_ref()
&& event_handler.has_finalized_subscribers()
@@ -983,26 +1010,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// The store migration task and op pool pruning require the *state at the first slot of the
// finalized epoch*, rather than the state of the latest finalized block. These two values
// will only differ when the first slot of the finalized epoch is a skip slot.
//
// Use the `StateRootsIterator` directly rather than `BeaconChain::state_root_at_slot`
// to ensure we use the same state that we just set as the head.
let new_finalized_slot = new_view
.finalized_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch());
let new_finalized_state_root = process_results(
StateRootsIterator::new(&self.store, &new_snapshot.beacon_state),
|mut iter| {
iter.find_map(|(state_root, slot)| {
if slot == new_finalized_slot {
Some(state_root)
} else {
None
}
})
},
)?
.ok_or(Error::MissingFinalizedStateRoot(new_finalized_slot))?;
let new_finalized_state_root = if new_finalized_slot == finalized_proto_block.slot {
// Fast-path for the common case where the finalized state is not at a skipped slot.
finalized_proto_block.state_root
} else {
// Use the `StateRootsIterator` directly rather than `BeaconChain::state_root_at_slot`
// to ensure we use the same state that we just set as the head.
process_results(
StateRootsIterator::new(&self.store, &new_snapshot.beacon_state),
|mut iter| {
iter.find_map(|(state_root, slot)| {
if slot == new_finalized_slot {
Some(state_root)
} else {
None
}
})
},
)?
.ok_or(Error::MissingFinalizedStateRoot(new_finalized_slot))?
};
let update_cache = true;
let new_finalized_state = self
@@ -1163,6 +1194,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
@@ -1185,6 +1217,7 @@ fn spawn_execution_layer_updates<T: BeaconChainTypes>(
.update_execution_engine_forkchoice(
current_slot,
forkchoice_update_params,
head_payload_status,
OverrideForkchoiceUpdate::Yes,
)
.await
@@ -1334,6 +1367,7 @@ fn observe_head_block_delays<E: EthSpec, S: SlotClock>(
head_block_graffiti: String,
slot_clock: &S,
event_handler: Option<&ServerSentEventHandler<E>>,
spec: &ChainSpec,
) {
let Some(block_time_set_as_head) = slot_clock.now_duration() else {
// Practically unreachable: the slot clock's time should not be before the UNIX epoch.
@@ -1392,8 +1426,8 @@ fn observe_head_block_delays<E: EthSpec, S: SlotClock>(
.as_millis() as i64,
);
// The time from the start of the slot when all blobs have been observed. Technically this
// is the time we last saw a blob related to this block/slot.
// The time from the start of the slot when all blobs/data columns have been observed. Technically this
// is the time we last saw a blob/data column related to this block/slot.
metrics::set_gauge(
&metrics::BEACON_BLOB_DELAY_ALL_OBSERVED_SLOT_START,
block_delays
@@ -1463,7 +1497,7 @@ fn observe_head_block_delays<E: EthSpec, S: SlotClock>(
// Determine whether the block has been set as head too late for proper attestation
// production.
let late_head = attestable_delay >= slot_clock.unagg_attestation_production_delay();
let late_head = attestable_delay >= spec.get_unaggregated_attestation_due();
// If the block was enshrined as head too late for attestations to be created for it,
// log a debug warning and increment a metric.