Skip to content

Commit 05e20c0

Browse files
committed
[Merge] Block validator duties when EL is not ready (#2672)
* Reject some HTTP endpoints when EL is not ready * Restrict more endpoints * Add watchdog task * Change scheduling * Update to new schedule * Add "syncing" concept * Remove RequireSynced * Add is_merge_complete to head_info * Cache latest_head in Engines * Call consensus_forkchoiceUpdate on startup
1 parent 6f73918 commit 05e20c0

File tree

8 files changed

+403
-55
lines changed

8 files changed

+403
-55
lines changed

Cargo.lock

Lines changed: 65 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ pub struct HeadInfo {
194194
pub genesis_time: u64,
195195
pub genesis_validators_root: Hash256,
196196
pub proposer_shuffling_decision_root: Hash256,
197+
pub is_merge_complete: bool,
197198
}
198199

199200
pub trait BeaconChainTypes: Send + Sync + 'static {
@@ -204,6 +205,19 @@ pub trait BeaconChainTypes: Send + Sync + 'static {
204205
type EthSpec: types::EthSpec;
205206
}
206207

208+
/// Indicates the status of the `ExecutionLayer`.
209+
#[derive(Debug, PartialEq)]
210+
pub enum ExecutionLayerStatus {
211+
/// The execution layer is synced and reachable.
212+
Ready,
213+
/// The execution layer either syncing or unreachable.
214+
NotReady,
215+
/// The execution layer is required, but has not been enabled. This is a configuration error.
216+
Missing,
217+
/// The execution layer is not yet required, therefore the status is irrelevant.
218+
NotRequired,
219+
}
220+
207221
pub type BeaconForkChoice<T> = ForkChoice<
208222
BeaconForkChoiceStore<
209223
<T as BeaconChainTypes>::EthSpec,
@@ -1001,6 +1015,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
10011015
genesis_time: head.beacon_state.genesis_time(),
10021016
genesis_validators_root: head.beacon_state.genesis_validators_root(),
10031017
proposer_shuffling_decision_root,
1018+
is_merge_complete: is_merge_complete(&head.beacon_state),
10041019
})
10051020
})
10061021
}
@@ -3405,6 +3420,39 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
34053420
.map_err(Error::ExecutionForkChoiceUpdateFailed)
34063421
}
34073422

3423+
/// Indicates the status of the execution layer.
3424+
pub async fn execution_layer_status(&self) -> Result<ExecutionLayerStatus, BeaconChainError> {
3425+
let epoch = self.epoch()?;
3426+
if self.spec.merge_fork_epoch.map_or(true, |fork| epoch < fork) {
3427+
return Ok(ExecutionLayerStatus::NotRequired);
3428+
}
3429+
3430+
if let Some(execution_layer) = &self.execution_layer {
3431+
if execution_layer.is_synced().await {
3432+
Ok(ExecutionLayerStatus::Ready)
3433+
} else {
3434+
Ok(ExecutionLayerStatus::NotReady)
3435+
}
3436+
} else {
3437+
// This branch is slightly more restrictive than what is minimally required.
3438+
//
3439+
// It is possible for a node without an execution layer (EL) to follow the chain
3440+
// *after* the merge fork and *before* the terminal execution block, as long as
3441+
// that node is not required to produce blocks.
3442+
//
3443+
// However, here we say that all nodes *must* have an EL as soon as the merge fork
3444+
// happens. We do this because it's very difficult to determine that the terminal
3445+
// block has been met if we don't already have an EL. As far as we know, the
3446+
// terminal execution block might already exist and we've been rejecting it since
3447+
// we don't have an EL to verify it.
3448+
//
3449+
// I think it is very reasonable to say that the beacon chain expects all BNs to
3450+
// be paired with an EL node by the time the merge fork epoch is reached. So, we
3451+
// enforce that here.
3452+
Ok(ExecutionLayerStatus::Missing)
3453+
}
3454+
}
3455+
34083456
/// This function takes a configured weak subjectivity `Checkpoint` and the latest finalized `Checkpoint`.
34093457
/// If the weak subjectivity checkpoint and finalized checkpoint share the same epoch, we compare
34103458
/// roots. If we the weak subjectivity checkpoint is from an older epoch, we iterate back through

beacon_node/beacon_chain/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ mod validator_pubkey_cache;
3636

3737
pub use self::beacon_chain::{
3838
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult,
39-
ForkChoiceError, HeadInfo, StateSkipConfig, WhenSlotSkipped, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
39+
ExecutionLayerStatus, ForkChoiceError, HeadInfo, StateSkipConfig, WhenSlotSkipped,
40+
MAXIMUM_GOSSIP_CLOCK_DISPARITY,
4041
};
4142
pub use self::beacon_snapshot::BeaconSnapshot;
4243
pub use self::chain_config::ChainConfig;

beacon_node/client/src/builder.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,8 +647,55 @@ where
647647

648648
if let Some(beacon_chain) = self.beacon_chain.as_ref() {
649649
let state_advance_context = runtime_context.service_context("state_advance".into());
650-
let log = state_advance_context.log().clone();
651-
spawn_state_advance_timer(state_advance_context.executor, beacon_chain.clone(), log);
650+
let state_advance_log = state_advance_context.log().clone();
651+
spawn_state_advance_timer(
652+
state_advance_context.executor,
653+
beacon_chain.clone(),
654+
state_advance_log,
655+
);
656+
657+
if let Some(execution_layer) = beacon_chain.execution_layer.as_ref() {
658+
let store = beacon_chain.store.clone();
659+
let inner_execution_layer = execution_layer.clone();
660+
661+
let head = beacon_chain
662+
.head_info()
663+
.map_err(|e| format!("Unable to read beacon chain head: {:?}", e))?;
664+
665+
// Issue the head to the execution engine on startup. This ensures it can start
666+
// syncing.
667+
if head.is_merge_complete {
668+
let result = runtime_context
669+
.executor
670+
.runtime()
671+
.upgrade()
672+
.ok_or_else(|| "Cannot update engine head, shutting down".to_string())?
673+
.block_on(async move {
674+
BeaconChain::<
675+
Witness<TSlotClock, TEth1Backend, TEthSpec, THotStore, TColdStore>,
676+
>::update_execution_engine_forkchoice(
677+
inner_execution_layer,
678+
store,
679+
head.finalized_checkpoint.root,
680+
head.block_root,
681+
)
682+
.await
683+
});
684+
685+
// No need to exit early if setting the head fails. It will be set again if/when the
686+
// node comes online.
687+
if let Err(e) = result {
688+
warn!(
689+
log,
690+
"Failed to update head on execution engines";
691+
"error" => ?e
692+
);
693+
}
694+
}
695+
696+
// Spawn a routine that tracks the status of the execution engines.
697+
execution_layer.spawn_watchdog_routine(beacon_chain.slot_clock.clone());
698+
}
652699
}
653700

654701
Ok(Client {

beacon_node/execution_layer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ exit-future = "0.2.0"
2828
tree_hash = { path = "../../consensus/tree_hash"}
2929
tree_hash_derive = { path = "../../consensus/tree_hash_derive"}
3030
parking_lot = "0.11.0"
31+
slot_clock = { path = "../../common/slot_clock" }

0 commit comments

Comments
 (0)