mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-02 04:03:35 +00:00
Merge remote-tracking branch 'origin/unstable' into capella-update
This commit is contained in:
@@ -24,6 +24,12 @@ use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, Prep
|
||||
use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult};
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::historical_blocks::HistoricalBlockError;
|
||||
use crate::light_client_finality_update_verification::{
|
||||
Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate,
|
||||
};
|
||||
use crate::light_client_optimistic_update_verification::{
|
||||
Error as LightClientOptimisticUpdateError, VerifiedLightClientOptimisticUpdate,
|
||||
};
|
||||
use crate::migrate::BackgroundMigrator;
|
||||
use crate::naive_aggregation_pool::{
|
||||
AggregatedAttestationMap, Error as NaiveAggregationError, NaiveAggregationPool,
|
||||
@@ -40,9 +46,8 @@ use crate::observed_operations::{ObservationOutcome, ObservedOperations};
|
||||
use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT};
|
||||
use crate::persisted_fork_choice::PersistedForkChoice;
|
||||
use crate::pre_finalization_cache::PreFinalizationBlockCache;
|
||||
use crate::proposer_prep_service::PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR;
|
||||
use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache};
|
||||
use crate::snapshot_cache::SnapshotCache;
|
||||
use crate::snapshot_cache::{BlockProductionPreState, SnapshotCache};
|
||||
use crate::sync_committee_verification::{
|
||||
Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution,
|
||||
};
|
||||
@@ -52,9 +57,7 @@ use crate::validator_monitor::{
|
||||
HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS,
|
||||
};
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
use crate::BeaconForkChoiceStore;
|
||||
use crate::BeaconSnapshot;
|
||||
use crate::{metrics, BeaconChainError};
|
||||
use crate::{metrics, BeaconChainError, BeaconForkChoiceStore, BeaconSnapshot, CachedHead};
|
||||
use eth2::types::{EventKind, SseBlock, SyncDuty};
|
||||
use execution_layer::{
|
||||
BlockProposalContents, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition,
|
||||
@@ -70,7 +73,7 @@ use itertools::process_results;
|
||||
use itertools::Itertools;
|
||||
use operation_pool::{AttestationRef, OperationPool, PersistedOperationPool};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use proto_array::CountUnrealizedFull;
|
||||
use proto_array::{CountUnrealizedFull, DoNotReOrg, ProposerHeadError};
|
||||
use safe_arith::SafeArith;
|
||||
use slasher::Slasher;
|
||||
use slog::{crit, debug, error, info, trace, warn, Logger};
|
||||
@@ -103,6 +106,7 @@ use store::{
|
||||
use task_executor::{ShutdownReason, TaskExecutor};
|
||||
use tree_hash::TreeHash;
|
||||
use types::beacon_state::CloneConfig;
|
||||
use types::consts::merge::INTERVALS_PER_SLOT;
|
||||
use types::*;
|
||||
|
||||
pub type ForkChoiceError = fork_choice::Error<crate::ForkChoiceStoreError>;
|
||||
@@ -124,6 +128,12 @@ pub const VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1)
|
||||
/// The timeout for the eth1 finalization cache
|
||||
pub const ETH1_FINALIZATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_millis(200);
|
||||
|
||||
/// The latest delay from the start of the slot at which to attempt a 1-slot re-org.
|
||||
fn max_re_org_slot_delay(seconds_per_slot: u64) -> Duration {
|
||||
// Allow at least half of the attestation deadline for the block to propagate.
|
||||
Duration::from_secs(seconds_per_slot) / INTERVALS_PER_SLOT as u32 / 2
|
||||
}
|
||||
|
||||
// These keys are all zero because they get stored in different columns, see `DBColumn` type.
|
||||
pub const BEACON_CHAIN_DB_KEY: Hash256 = Hash256::zero();
|
||||
pub const OP_POOL_DB_KEY: Hash256 = Hash256::zero();
|
||||
@@ -185,6 +195,21 @@ pub enum ProduceBlockVerification {
|
||||
NoVerification,
|
||||
}
|
||||
|
||||
/// Payload attributes for which the `beacon_chain` crate is responsible.
|
||||
pub struct PrePayloadAttributes {
|
||||
pub proposer_index: u64,
|
||||
pub prev_randao: Hash256,
|
||||
}
|
||||
|
||||
/// Define whether a forkchoiceUpdate needs to be checked for an override (`Yes`) or has already
|
||||
/// been checked (`AlreadyApplied`). It is safe to specify `Yes` even if re-orgs are disabled.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OverrideForkchoiceUpdate {
|
||||
#[default]
|
||||
Yes,
|
||||
AlreadyApplied,
|
||||
}
|
||||
|
||||
/// The accepted clock drift for nodes gossiping blocks and attestations. See:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#configuration
|
||||
@@ -344,6 +369,10 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
pub(crate) observed_bls_to_execution_changes:
|
||||
Mutex<ObservedOperations<SignedBlsToExecutionChange, T::EthSpec>>,
|
||||
/// The most recently validated light client finality update received on gossip.
|
||||
pub latest_seen_finality_update: Mutex<Option<LightClientFinalityUpdate<T::EthSpec>>>,
|
||||
/// The most recently validated light client optimistic update received on gossip.
|
||||
pub latest_seen_optimistic_update: Mutex<Option<LightClientOptimisticUpdate<T::EthSpec>>>,
|
||||
/// Provides information from the Ethereum 1 (PoW) chain.
|
||||
pub eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
|
||||
/// Interfaces with the execution client.
|
||||
@@ -1809,6 +1838,40 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some 'LightClientFinalityUpdate' from the network and attempts to verify it
|
||||
pub fn verify_finality_update_for_gossip(
|
||||
self: &Arc<Self>,
|
||||
light_client_finality_update: LightClientFinalityUpdate<T::EthSpec>,
|
||||
seen_timestamp: Duration,
|
||||
) -> Result<VerifiedLightClientFinalityUpdate<T>, LightClientFinalityUpdateError> {
|
||||
VerifiedLightClientFinalityUpdate::verify(
|
||||
light_client_finality_update,
|
||||
self,
|
||||
seen_timestamp,
|
||||
)
|
||||
.map(|v| {
|
||||
metrics::inc_counter(&metrics::FINALITY_UPDATE_PROCESSING_SUCCESSES);
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some 'LightClientOptimisticUpdate' from the network and attempts to verify it
|
||||
pub fn verify_optimistic_update_for_gossip(
|
||||
self: &Arc<Self>,
|
||||
light_client_optimistic_update: LightClientOptimisticUpdate<T::EthSpec>,
|
||||
seen_timestamp: Duration,
|
||||
) -> Result<VerifiedLightClientOptimisticUpdate<T>, LightClientOptimisticUpdateError> {
|
||||
VerifiedLightClientOptimisticUpdate::verify(
|
||||
light_client_optimistic_update,
|
||||
self,
|
||||
seen_timestamp,
|
||||
)
|
||||
.map(|v| {
|
||||
metrics::inc_counter(&metrics::OPTIMISTIC_UPDATE_PROCESSING_SUCCESSES);
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some attestation-type object and attempts to verify it in the context of fork
|
||||
/// choice. If it is valid it is applied to `self.fork_choice`.
|
||||
///
|
||||
@@ -2787,6 +2850,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
if !payload_verification_status.is_optimistic()
|
||||
&& block.slot() + EARLY_ATTESTER_CACHE_HISTORIC_SLOTS >= current_slot
|
||||
{
|
||||
let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_FORK_CHOICE);
|
||||
match fork_choice.get_head(current_slot, &self.spec) {
|
||||
// This block became the head, add it to the early attester cache.
|
||||
Ok(new_head_root) if new_head_root == block_root => {
|
||||
@@ -2820,6 +2884,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
"error" => ?e
|
||||
),
|
||||
}
|
||||
drop(fork_choice_timer);
|
||||
}
|
||||
drop(post_exec_timer);
|
||||
|
||||
@@ -3508,6 +3573,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// signed. If we miss the cache or we're producing a block that conflicts with the head,
|
||||
// fall back to getting the head from `slot - 1`.
|
||||
let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES);
|
||||
|
||||
// Atomically read some values from the head whilst avoiding holding cached head `Arc` any
|
||||
// longer than necessary.
|
||||
let (head_slot, head_block_root) = {
|
||||
@@ -3515,8 +3581,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
(head.head_slot(), head.head_block_root())
|
||||
};
|
||||
let (state, state_root_opt) = if head_slot < slot {
|
||||
// Attempt an aggressive re-org if configured and the conditions are right.
|
||||
if let Some(re_org_state) = self.get_state_for_re_org(slot, head_slot, head_block_root)
|
||||
{
|
||||
info!(
|
||||
self.log,
|
||||
"Proposing block to re-org current head";
|
||||
"slot" => slot,
|
||||
"head_to_reorg" => %head_block_root,
|
||||
);
|
||||
(re_org_state.pre_state, re_org_state.state_root)
|
||||
}
|
||||
// Normal case: proposing a block atop the current head. Use the snapshot cache.
|
||||
if let Some(pre_state) = self
|
||||
else if let Some(pre_state) = self
|
||||
.snapshot_cache
|
||||
.try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT)
|
||||
.and_then(|snapshot_cache| {
|
||||
@@ -3556,6 +3633,400 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
Ok((state, state_root_opt))
|
||||
}
|
||||
|
||||
/// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable.
|
||||
///
|
||||
/// This function will return `None` if proposer re-orgs are disabled.
|
||||
fn get_state_for_re_org(
|
||||
&self,
|
||||
slot: Slot,
|
||||
head_slot: Slot,
|
||||
canonical_head: Hash256,
|
||||
) -> Option<BlockProductionPreState<T::EthSpec>> {
|
||||
let re_org_threshold = self.config.re_org_threshold?;
|
||||
|
||||
if self.spec.proposer_score_boost.is_none() {
|
||||
warn!(
|
||||
self.log,
|
||||
"Ignoring proposer re-org configuration";
|
||||
"reason" => "this network does not have proposer boost enabled"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let slot_delay = self
|
||||
.slot_clock
|
||||
.seconds_from_current_slot_start(self.spec.seconds_per_slot)
|
||||
.or_else(|| {
|
||||
warn!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"error" => "unable to read slot clock"
|
||||
);
|
||||
None
|
||||
})?;
|
||||
|
||||
// Attempt a proposer re-org if:
|
||||
//
|
||||
// 1. It seems we have time to propagate and still receive the proposer boost.
|
||||
// 2. The current head block was seen late.
|
||||
// 3. The `get_proposer_head` conditions from fork choice pass.
|
||||
let proposing_on_time = slot_delay < max_re_org_slot_delay(self.spec.seconds_per_slot);
|
||||
if !proposing_on_time {
|
||||
debug!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"reason" => "not proposing on time",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot);
|
||||
if !head_late {
|
||||
debug!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"reason" => "head not late"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Is the current head weak and appropriate for re-orging?
|
||||
let proposer_head_timer =
|
||||
metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES);
|
||||
let proposer_head = self
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.get_proposer_head(
|
||||
slot,
|
||||
canonical_head,
|
||||
re_org_threshold,
|
||||
self.config.re_org_max_epochs_since_finalization,
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
ProposerHeadError::DoNotReOrg(reason) => {
|
||||
debug!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"reason" => %reason,
|
||||
);
|
||||
}
|
||||
ProposerHeadError::Error(e) => {
|
||||
warn!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"error" => ?e,
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
drop(proposer_head_timer);
|
||||
let re_org_parent_block = proposer_head.parent_node.root;
|
||||
|
||||
// Only attempt a re-org if we hit the snapshot cache.
|
||||
let pre_state = self
|
||||
.snapshot_cache
|
||||
.try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT)
|
||||
.and_then(|snapshot_cache| {
|
||||
snapshot_cache.get_state_for_block_production(re_org_parent_block)
|
||||
})
|
||||
.or_else(|| {
|
||||
debug!(
|
||||
self.log,
|
||||
"Not attempting re-org";
|
||||
"reason" => "missed snapshot cache",
|
||||
"parent_block" => ?re_org_parent_block,
|
||||
);
|
||||
None
|
||||
})?;
|
||||
|
||||
info!(
|
||||
self.log,
|
||||
"Attempting re-org due to weak head";
|
||||
"weak_head" => ?canonical_head,
|
||||
"parent" => ?re_org_parent_block,
|
||||
"head_weight" => proposer_head.head_node.weight,
|
||||
"threshold_weight" => proposer_head.re_org_weight_threshold
|
||||
);
|
||||
|
||||
Some(pre_state)
|
||||
}
|
||||
|
||||
/// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`.
|
||||
///
|
||||
/// The `proposer_head` may be the head block of `cached_head` or its parent. An error will
|
||||
/// be returned for any other value.
|
||||
pub fn get_pre_payload_attributes(
|
||||
&self,
|
||||
proposal_slot: Slot,
|
||||
proposer_head: Hash256,
|
||||
cached_head: &CachedHead<T::EthSpec>,
|
||||
) -> Result<Option<PrePayloadAttributes>, Error> {
|
||||
let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
let head_block_root = cached_head.head_block_root();
|
||||
let parent_block_root = cached_head.parent_block_root();
|
||||
|
||||
// The proposer head must be equal to the canonical head or its parent.
|
||||
if proposer_head != head_block_root && proposer_head != parent_block_root {
|
||||
warn!(
|
||||
self.log,
|
||||
"Unable to compute payload attributes";
|
||||
"block_root" => ?proposer_head,
|
||||
"head_block_root" => ?head_block_root,
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Compute the proposer index.
|
||||
let head_epoch = cached_head.head_slot().epoch(T::EthSpec::slots_per_epoch());
|
||||
let shuffling_decision_root = if head_epoch == proposal_epoch {
|
||||
cached_head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root(proposer_head)?
|
||||
} else {
|
||||
proposer_head
|
||||
};
|
||||
let cached_proposer = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(shuffling_decision_root, proposal_slot);
|
||||
let proposer_index = if let Some(proposer) = cached_proposer {
|
||||
proposer.index as u64
|
||||
} else {
|
||||
if head_epoch + 2 < proposal_epoch {
|
||||
warn!(
|
||||
self.log,
|
||||
"Skipping proposer preparation";
|
||||
"msg" => "this is a non-critical issue that can happen on unhealthy nodes or \
|
||||
networks.",
|
||||
"proposal_epoch" => proposal_epoch,
|
||||
"head_epoch" => head_epoch,
|
||||
);
|
||||
|
||||
// Don't skip the head forward more than two epochs. This avoids burdening an
|
||||
// unhealthy node.
|
||||
//
|
||||
// Although this node might miss out on preparing for a proposal, they should still
|
||||
// be able to propose. This will prioritise beacon chain health over efficient
|
||||
// packing of execution blocks.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (proposers, decision_root, _, fork) =
|
||||
compute_proposer_duties_from_head(proposal_epoch, self)?;
|
||||
|
||||
let proposer_offset = (proposal_slot % T::EthSpec::slots_per_epoch()).as_usize();
|
||||
let proposer = *proposers
|
||||
.get(proposer_offset)
|
||||
.ok_or(BeaconChainError::NoProposerForSlot(proposal_slot))?;
|
||||
|
||||
self.beacon_proposer_cache.lock().insert(
|
||||
proposal_epoch,
|
||||
decision_root,
|
||||
proposers,
|
||||
fork,
|
||||
)?;
|
||||
|
||||
// It's possible that the head changes whilst computing these duties. If so, abandon
|
||||
// this routine since the change of head would have also spawned another instance of
|
||||
// this routine.
|
||||
//
|
||||
// Exit now, after updating the cache.
|
||||
if decision_root != shuffling_decision_root {
|
||||
warn!(
|
||||
self.log,
|
||||
"Head changed during proposer preparation";
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
proposer as u64
|
||||
};
|
||||
|
||||
// Get the `prev_randao` value.
|
||||
let prev_randao = if proposer_head == parent_block_root {
|
||||
cached_head.parent_random()
|
||||
} else {
|
||||
cached_head.head_random()
|
||||
}?;
|
||||
|
||||
Ok(Some(PrePayloadAttributes {
|
||||
proposer_index,
|
||||
prev_randao,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Determine whether a fork choice update to the execution layer should be overridden.
|
||||
///
|
||||
/// This is *only* necessary when proposer re-orgs are enabled, because we have to prevent the
|
||||
/// execution layer from enshrining the block we want to re-org as the head.
|
||||
///
|
||||
/// This function uses heuristics that align quite closely but not exactly with the re-org
|
||||
/// conditions set out in `get_state_for_re_org` and `get_proposer_head`. The differences are
|
||||
/// documented below.
|
||||
fn overridden_forkchoice_update_params(
|
||||
&self,
|
||||
canonical_forkchoice_params: ForkchoiceUpdateParameters,
|
||||
) -> Result<ForkchoiceUpdateParameters, Error> {
|
||||
self.overridden_forkchoice_update_params_or_failure_reason(&canonical_forkchoice_params)
|
||||
.or_else(|e| match e {
|
||||
ProposerHeadError::DoNotReOrg(reason) => {
|
||||
trace!(
|
||||
self.log,
|
||||
"Not suppressing fork choice update";
|
||||
"reason" => %reason,
|
||||
);
|
||||
Ok(canonical_forkchoice_params)
|
||||
}
|
||||
ProposerHeadError::Error(e) => Err(e),
|
||||
})
|
||||
}
|
||||
|
||||
fn overridden_forkchoice_update_params_or_failure_reason(
|
||||
&self,
|
||||
canonical_forkchoice_params: &ForkchoiceUpdateParameters,
|
||||
) -> Result<ForkchoiceUpdateParameters, ProposerHeadError<Error>> {
|
||||
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES);
|
||||
|
||||
// Never override if proposer re-orgs are disabled.
|
||||
let re_org_threshold = self
|
||||
.config
|
||||
.re_org_threshold
|
||||
.ok_or(DoNotReOrg::ReOrgsDisabled)?;
|
||||
|
||||
let head_block_root = canonical_forkchoice_params.head_root;
|
||||
|
||||
// Perform initial checks and load the relevant info from fork choice.
|
||||
let info = self
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.get_preliminary_proposer_head(
|
||||
head_block_root,
|
||||
re_org_threshold,
|
||||
self.config.re_org_max_epochs_since_finalization,
|
||||
)
|
||||
.map_err(|e| e.map_inner_error(Error::ProposerHeadForkChoiceError))?;
|
||||
|
||||
// The slot of our potential re-org block is always 1 greater than the head block because we
|
||||
// only attempt single-slot re-orgs.
|
||||
let head_slot = info.head_node.slot;
|
||||
let re_org_block_slot = head_slot + 1;
|
||||
let fork_choice_slot = info.current_slot;
|
||||
|
||||
// If a re-orging proposal isn't made by the `max_re_org_slot_delay` then we give up
|
||||
// and allow the fork choice update for the canonical head through so that we may attest
|
||||
// correctly.
|
||||
let current_slot_ok = if head_slot == fork_choice_slot {
|
||||
true
|
||||
} else if re_org_block_slot == fork_choice_slot {
|
||||
self.slot_clock
|
||||
.start_of(re_org_block_slot)
|
||||
.and_then(|slot_start| {
|
||||
let now = self.slot_clock.now_duration()?;
|
||||
let slot_delay = now.saturating_sub(slot_start);
|
||||
Some(slot_delay <= max_re_org_slot_delay(self.spec.seconds_per_slot))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !current_slot_ok {
|
||||
return Err(DoNotReOrg::HeadDistance.into());
|
||||
}
|
||||
|
||||
// Only attempt a re-org if we have a proposer registered for the re-org slot.
|
||||
let proposing_at_re_org_slot = {
|
||||
// The proposer shuffling has the same decision root as the next epoch attestation
|
||||
// shuffling. We know our re-org block is not on the epoch boundary, so it has the
|
||||
// same proposer shuffling as the head (but not necessarily the parent which may lie
|
||||
// in the previous epoch).
|
||||
let shuffling_decision_root = info
|
||||
.head_node
|
||||
.next_epoch_shuffling_id
|
||||
.shuffling_decision_block;
|
||||
let proposer_index = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(shuffling_decision_root, re_org_block_slot)
|
||||
.ok_or_else(|| {
|
||||
debug!(
|
||||
self.log,
|
||||
"Fork choice override proposer shuffling miss";
|
||||
"slot" => re_org_block_slot,
|
||||
"decision_root" => ?shuffling_decision_root,
|
||||
);
|
||||
DoNotReOrg::NotProposing
|
||||
})?
|
||||
.index as u64;
|
||||
|
||||
self.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(ProposerHeadError::Error(Error::ExecutionLayerMissing))?
|
||||
.has_proposer_preparation_data_blocking(proposer_index)
|
||||
};
|
||||
if !proposing_at_re_org_slot {
|
||||
return Err(DoNotReOrg::NotProposing.into());
|
||||
}
|
||||
|
||||
// If the current slot is already equal to the proposal slot (or we are in the tail end of
|
||||
// the prior slot), then check the actual weight of the head against the re-org threshold.
|
||||
let head_weak = if fork_choice_slot == re_org_block_slot {
|
||||
info.head_node.weight < info.re_org_weight_threshold
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if !head_weak {
|
||||
return Err(DoNotReOrg::HeadNotWeak {
|
||||
head_weight: info.head_node.weight,
|
||||
re_org_weight_threshold: info.re_org_weight_threshold,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check that the head block arrived late and is vulnerable to a re-org. This check is only
|
||||
// a heuristic compared to the proper weight check in `get_state_for_re_org`, the reason
|
||||
// being that we may have only *just* received the block and not yet processed any
|
||||
// attestations for it. We also can't dequeue attestations for the block during the
|
||||
// current slot, which would be necessary for determining its weight.
|
||||
let head_block_late =
|
||||
self.block_observed_after_attestation_deadline(head_block_root, head_slot);
|
||||
if !head_block_late {
|
||||
return Err(DoNotReOrg::HeadNotLate.into());
|
||||
}
|
||||
|
||||
let parent_head_hash = info.parent_node.execution_status.block_hash();
|
||||
let forkchoice_update_params = ForkchoiceUpdateParameters {
|
||||
head_root: info.parent_node.root,
|
||||
head_hash: parent_head_hash,
|
||||
justified_hash: canonical_forkchoice_params.justified_hash,
|
||||
finalized_hash: canonical_forkchoice_params.finalized_hash,
|
||||
};
|
||||
|
||||
debug!(
|
||||
self.log,
|
||||
"Fork choice update overridden";
|
||||
"canonical_head" => ?head_block_root,
|
||||
"override" => ?info.parent_node.root,
|
||||
"slot" => fork_choice_slot,
|
||||
);
|
||||
|
||||
Ok(forkchoice_update_params)
|
||||
}
|
||||
|
||||
/// Check if the block with `block_root` was observed after the attestation deadline of `slot`.
|
||||
fn block_observed_after_attestation_deadline(&self, block_root: Hash256, slot: Slot) -> bool {
|
||||
let block_delays = self.block_times_cache.read().get_block_delays(
|
||||
block_root,
|
||||
self.slot_clock
|
||||
.start_of(slot)
|
||||
.unwrap_or_else(|| Duration::from_secs(0)),
|
||||
);
|
||||
block_delays.observed.map_or(false, |delay| {
|
||||
delay > self.slot_clock.unagg_attestation_production_delay()
|
||||
})
|
||||
}
|
||||
|
||||
/// Produce a block for some `slot` upon the given `state`.
|
||||
///
|
||||
/// Typically the `self.produce_block()` function should be used, instead of calling this
|
||||
@@ -4188,17 +4659,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// The `PayloadAttributes` are used by the EL to give it a look-ahead for preparing an optimal
|
||||
/// set of transactions for a new `ExecutionPayload`.
|
||||
///
|
||||
/// This function will result in a call to `forkchoiceUpdated` on the EL if:
|
||||
///
|
||||
/// 1. We're in the tail-end of the slot (as defined by PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR)
|
||||
/// 2. The head block is one slot (or less) behind the prepare slot (e.g., we're preparing for
|
||||
/// the next slot and the block at the current slot is already known).
|
||||
/// This function will result in a call to `forkchoiceUpdated` on the EL if we're in the
|
||||
/// tail-end of the slot (as defined by `self.config.prepare_payload_lookahead`).
|
||||
pub async fn prepare_beacon_proposer(
|
||||
self: &Arc<Self>,
|
||||
current_slot: Slot,
|
||||
) -> Result<(), Error> {
|
||||
let prepare_slot = current_slot + 1;
|
||||
let prepare_epoch = prepare_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
// There's no need to run the proposer preparation routine before the bellatrix fork.
|
||||
if self.slot_is_prior_to_bellatrix(prepare_slot) {
|
||||
@@ -4216,127 +4683,68 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Atomically read some values from the canonical head, whilst avoiding holding the cached
|
||||
// head `Arc` any longer than necessary.
|
||||
// Load the cached head and its forkchoice update parameters.
|
||||
//
|
||||
// Use a blocking task since blocking the core executor on the canonical head read lock can
|
||||
// block the core tokio executor.
|
||||
let chain = self.clone();
|
||||
let (head_slot, head_root, head_decision_root, head_random, forkchoice_update_params) =
|
||||
self.spawn_blocking_handle(
|
||||
let maybe_prep_data = self
|
||||
.spawn_blocking_handle(
|
||||
move || {
|
||||
let cached_head = chain.canonical_head.cached_head();
|
||||
let head_block_root = cached_head.head_block_root();
|
||||
let decision_root = cached_head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root(head_block_root)?;
|
||||
Ok::<_, Error>((
|
||||
cached_head.head_slot(),
|
||||
head_block_root,
|
||||
decision_root,
|
||||
cached_head.head_random()?,
|
||||
cached_head.forkchoice_update_parameters(),
|
||||
))
|
||||
|
||||
// Don't bother with proposer prep if the head is more than
|
||||
// `PREPARE_PROPOSER_HISTORIC_EPOCHS` prior to the current slot.
|
||||
//
|
||||
// This prevents the routine from running during sync.
|
||||
let head_slot = cached_head.head_slot();
|
||||
if head_slot + T::EthSpec::slots_per_epoch() * PREPARE_PROPOSER_HISTORIC_EPOCHS
|
||||
< current_slot
|
||||
{
|
||||
debug!(
|
||||
chain.log,
|
||||
"Head too old for proposer prep";
|
||||
"head_slot" => head_slot,
|
||||
"current_slot" => current_slot,
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let canonical_fcu_params = cached_head.forkchoice_update_parameters();
|
||||
let fcu_params =
|
||||
chain.overridden_forkchoice_update_params(canonical_fcu_params)?;
|
||||
let pre_payload_attributes = chain.get_pre_payload_attributes(
|
||||
prepare_slot,
|
||||
fcu_params.head_root,
|
||||
&cached_head,
|
||||
)?;
|
||||
Ok::<_, Error>(Some((fcu_params, pre_payload_attributes)))
|
||||
},
|
||||
"prepare_beacon_proposer_fork_choice_read",
|
||||
"prepare_beacon_proposer_head_read",
|
||||
)
|
||||
.await??;
|
||||
let head_epoch = head_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
// Don't bother with proposer prep if the head is more than
|
||||
// `PREPARE_PROPOSER_HISTORIC_EPOCHS` prior to the current slot.
|
||||
//
|
||||
// This prevents the routine from running during sync.
|
||||
if head_slot + T::EthSpec::slots_per_epoch() * PREPARE_PROPOSER_HISTORIC_EPOCHS
|
||||
< current_slot
|
||||
{
|
||||
debug!(
|
||||
self.log,
|
||||
"Head too old for proposer prep";
|
||||
"head_slot" => head_slot,
|
||||
"current_slot" => current_slot,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure that the shuffling decision root is correct relative to the epoch we wish to
|
||||
// query.
|
||||
let shuffling_decision_root = if head_epoch == prepare_epoch {
|
||||
head_decision_root
|
||||
} else {
|
||||
head_root
|
||||
};
|
||||
|
||||
// Read the proposer from the proposer cache.
|
||||
let cached_proposer = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_slot::<T::EthSpec>(shuffling_decision_root, prepare_slot);
|
||||
let proposer = if let Some(proposer) = cached_proposer {
|
||||
proposer.index
|
||||
} else {
|
||||
if head_epoch + 2 < prepare_epoch {
|
||||
warn!(
|
||||
self.log,
|
||||
"Skipping proposer preparation";
|
||||
"msg" => "this is a non-critical issue that can happen on unhealthy nodes or \
|
||||
networks.",
|
||||
"prepare_epoch" => prepare_epoch,
|
||||
"head_epoch" => head_epoch,
|
||||
);
|
||||
|
||||
// Don't skip the head forward more than two epochs. This avoids burdening an
|
||||
// unhealthy node.
|
||||
//
|
||||
// Although this node might miss out on preparing for a proposal, they should still
|
||||
// be able to propose. This will prioritise beacon chain health over efficient
|
||||
// packing of execution blocks.
|
||||
let (forkchoice_update_params, pre_payload_attributes) =
|
||||
if let Some((fcu, Some(pre_payload))) = maybe_prep_data {
|
||||
(fcu, pre_payload)
|
||||
} else {
|
||||
// Appropriate log messages have already been logged above and in
|
||||
// `get_pre_payload_attributes`.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (proposers, decision_root, _, fork) =
|
||||
compute_proposer_duties_from_head(prepare_epoch, self)?;
|
||||
|
||||
let proposer_index = prepare_slot.as_usize() % (T::EthSpec::slots_per_epoch() as usize);
|
||||
let proposer = *proposers
|
||||
.get(proposer_index)
|
||||
.ok_or(BeaconChainError::NoProposerForSlot(prepare_slot))?;
|
||||
|
||||
self.beacon_proposer_cache.lock().insert(
|
||||
prepare_epoch,
|
||||
decision_root,
|
||||
proposers,
|
||||
fork,
|
||||
)?;
|
||||
|
||||
// It's possible that the head changes whilst computing these duties. If so, abandon
|
||||
// this routine since the change of head would have also spawned another instance of
|
||||
// this routine.
|
||||
//
|
||||
// Exit now, after updating the cache.
|
||||
if decision_root != shuffling_decision_root {
|
||||
warn!(
|
||||
self.log,
|
||||
"Head changed during proposer preparation";
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
proposer
|
||||
};
|
||||
};
|
||||
|
||||
// If the execution layer doesn't have any proposer data for this validator then we assume
|
||||
// it's not connected to this BN and no action is required.
|
||||
let proposer = pre_payload_attributes.proposer_index;
|
||||
if !execution_layer
|
||||
.has_proposer_preparation_data(proposer as u64)
|
||||
.has_proposer_preparation_data(proposer)
|
||||
.await
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "withdrawals")]
|
||||
let withdrawals = match self.spec.fork_name_at_epoch(prepare_epoch) {
|
||||
let withdrawals = match self.spec.fork_name_at_slot::<T::EthSpec>(prepare_slot) {
|
||||
ForkName::Base | ForkName::Altair | ForkName::Merge => None,
|
||||
ForkName::Capella | ForkName::Eip4844 => {
|
||||
// We must use the advanced state because balances can change at epoch boundaries
|
||||
@@ -4360,12 +4768,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.map(|withdrawals_opt| withdrawals_opt.map(|w| w.into()))
|
||||
.map_err(Error::PrepareProposerFailed)?;
|
||||
|
||||
let head_root = forkchoice_update_params.head_root;
|
||||
let payload_attributes = PayloadAttributes::new(
|
||||
self.slot_clock
|
||||
.start_of(prepare_slot)
|
||||
.ok_or(Error::InvalidSlot(prepare_slot))?
|
||||
.as_secs(),
|
||||
head_random,
|
||||
pre_payload_attributes.prev_randao,
|
||||
execution_layer
|
||||
.get_suggested_fee_recipient(proposer as u64)
|
||||
.await,
|
||||
@@ -4379,23 +4788,24 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
self.log,
|
||||
"Preparing beacon proposer";
|
||||
"payload_attributes" => ?payload_attributes,
|
||||
"head_root" => ?head_root,
|
||||
"prepare_slot" => prepare_slot,
|
||||
"validator" => proposer,
|
||||
"parent_root" => ?head_root,
|
||||
);
|
||||
|
||||
let already_known = execution_layer
|
||||
.insert_proposer(prepare_slot, head_root, proposer as u64, payload_attributes)
|
||||
.insert_proposer(prepare_slot, head_root, proposer, payload_attributes)
|
||||
.await;
|
||||
|
||||
// Only push a log to the user if this is the first time we've seen this proposer for this
|
||||
// slot.
|
||||
if !already_known {
|
||||
info!(
|
||||
self.log,
|
||||
"Prepared beacon proposer";
|
||||
"already_known" => already_known,
|
||||
"prepare_slot" => prepare_slot,
|
||||
"validator" => proposer,
|
||||
"parent_root" => ?head_root,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4417,27 +4827,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If either of the following are true, send a fork-choice update message to the
|
||||
// EL:
|
||||
//
|
||||
// 1. We're in the tail-end of the slot (as defined by
|
||||
// PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR)
|
||||
// 2. The head block is one slot (or less) behind the prepare slot (e.g., we're
|
||||
// preparing for the next slot and the block at the current slot is already
|
||||
// known).
|
||||
if till_prepare_slot
|
||||
<= self.slot_clock.slot_duration() / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR
|
||||
|| head_slot + 1 >= prepare_slot
|
||||
{
|
||||
// If we are close enough to the proposal slot, send an fcU, which will have payload
|
||||
// attributes filled in by the execution layer cache we just primed.
|
||||
if till_prepare_slot <= self.config.prepare_payload_lookahead {
|
||||
debug!(
|
||||
self.log,
|
||||
"Pushing update to prepare proposer";
|
||||
"Sending forkchoiceUpdate for proposer prep";
|
||||
"till_prepare_slot" => ?till_prepare_slot,
|
||||
"prepare_slot" => prepare_slot
|
||||
);
|
||||
|
||||
self.update_execution_engine_forkchoice(current_slot, forkchoice_update_params)
|
||||
.await?;
|
||||
self.update_execution_engine_forkchoice(
|
||||
current_slot,
|
||||
forkchoice_update_params,
|
||||
OverrideForkchoiceUpdate::AlreadyApplied,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -4446,7 +4851,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub async fn update_execution_engine_forkchoice(
|
||||
self: &Arc<Self>,
|
||||
current_slot: Slot,
|
||||
params: ForkchoiceUpdateParameters,
|
||||
input_params: ForkchoiceUpdateParameters,
|
||||
override_forkchoice_update: OverrideForkchoiceUpdate,
|
||||
) -> Result<(), Error> {
|
||||
let next_slot = current_slot + 1;
|
||||
|
||||
@@ -4468,6 +4874,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.as_ref()
|
||||
.ok_or(Error::ExecutionLayerMissing)?;
|
||||
|
||||
// Determine whether to override the forkchoiceUpdated message if we want to re-org
|
||||
// the current head at the next slot.
|
||||
let params = if override_forkchoice_update == OverrideForkchoiceUpdate::Yes {
|
||||
let chain = self.clone();
|
||||
self.spawn_blocking_handle(
|
||||
move || chain.overridden_forkchoice_update_params(input_params),
|
||||
"update_execution_engine_forkchoice_override",
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
input_params
|
||||
};
|
||||
|
||||
// Take the global lock for updating the execution engine fork choice.
|
||||
//
|
||||
// Whilst holding this lock we must:
|
||||
|
||||
Reference in New Issue
Block a user