[Merge] Optimistic Sync: Stage 1 (#2686)

* Add payload verification status to fork choice

* Pass payload verification status to import_block

* Add valid back-propagation

* Add head safety status latch to API

* Remove ExecutionLayerStatus

* Add execution info to client notifier

* Update notifier logs

* Change use of "hash" to refer to beacon block

* Shutdown on invalid finalized block

* Tidy, add comments

* Fix failing FC tests

* Allow blocks with unsafe head

* Fix forkchoiceUpdate call on startup
This commit is contained in:
Paul Hauner
2021-10-07 22:24:57 +11:00
parent aa1d57aa55
commit 6dde12f311
17 changed files with 395 additions and 156 deletions

View File

@@ -56,6 +56,7 @@ use itertools::process_results;
use itertools::Itertools;
use operation_pool::{OperationPool, PersistedOperationPool};
use parking_lot::{Mutex, RwLock};
use proto_array::ExecutionStatus;
use safe_arith::SafeArith;
use slasher::Slasher;
use slog::{crit, debug, error, info, trace, warn, Logger};
@@ -195,6 +196,7 @@ pub struct HeadInfo {
pub genesis_validators_root: Hash256,
pub proposer_shuffling_decision_root: Hash256,
pub is_merge_complete: bool,
pub execution_payload_block_hash: Option<Hash256>,
}
pub trait BeaconChainTypes: Send + Sync + 'static {
@@ -205,17 +207,23 @@ pub trait BeaconChainTypes: Send + Sync + 'static {
type EthSpec: types::EthSpec;
}
/// Indicates the status of the `ExecutionLayer`.
/// Indicates the EL payload verification status of the head beacon block.
#[derive(Debug, PartialEq)]
pub enum ExecutionLayerStatus {
/// The execution layer is synced and reachable.
Ready,
/// The execution layer either syncing or unreachable.
NotReady,
/// The execution layer is required, but has not been enabled. This is a configuration error.
Missing,
/// The execution layer is not yet required, therefore the status is irrelevant.
NotRequired,
pub enum HeadSafetyStatus {
/// The head block has either been verified by an EL or is does not require EL verification
/// (e.g., it is pre-merge or pre-terminal-block).
///
/// If the block is post-terminal-block, `Some(execution_payload.block_hash)` is included with
/// the variant.
Safe(Option<Hash256>),
/// The head block execution payload has not yet been verified by an EL.
///
/// The `execution_payload.block_hash` of the head block is returned.
Unsafe(Hash256),
/// The head block execution payload was deemed to be invalid by an EL.
///
/// The `execution_payload.block_hash` of the head block is returned.
Invalid(Hash256),
}
pub type BeaconForkChoice<T> = ForkChoice<
@@ -1016,6 +1024,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
genesis_validators_root: head.beacon_state.genesis_validators_root(),
proposer_shuffling_decision_root,
is_merge_complete: is_merge_complete(&head.beacon_state),
execution_payload_block_hash: head
.beacon_block
.message()
.body()
.execution_payload()
.map(|ep| ep.block_hash),
})
})
}
@@ -2308,6 +2322,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let current_slot = self.slot()?;
let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch());
let mut ops = fully_verified_block.confirmation_db_batch;
let payload_verification_status = fully_verified_block.payload_verification_status;
let attestation_observation_timer =
metrics::start_timer(&metrics::BLOCK_PROCESSING_ATTESTATION_OBSERVATION);
@@ -2427,7 +2442,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let _fork_choice_block_timer =
metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_BLOCK_TIMES);
fork_choice
.on_block(current_slot, &block, block_root, &state)
.on_block(
current_slot,
&block,
block_root,
&state,
payload_verification_status,
)
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
}
@@ -3260,6 +3281,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
if new_finalized_checkpoint.epoch != old_finalized_checkpoint.epoch {
// Check to ensure that this finalized block hasn't been marked as invalid.
let finalized_block = self
.fork_choice
.read()
.get_block(&new_finalized_checkpoint.root)
.ok_or(BeaconChainError::FinalizedBlockMissingFromForkChoice(
new_finalized_checkpoint.root,
))?;
if let ExecutionStatus::Invalid(block_hash) = finalized_block.execution_status {
crit!(
self.log,
"Finalized block has an invalid payload";
"msg" => "You must use the `--purge-db` flag to clear the database and restart sync. \
You may be on a hostile network.",
"block_hash" => ?block_hash
);
let mut shutdown_sender = self.shutdown_sender();
shutdown_sender
.try_send(ShutdownReason::Failure(
"Finalized block has an invalid execution payload.",
))
.map_err(BeaconChainError::InvalidFinalizedPayloadShutdownError)?;
}
// Due to race conditions, it's technically possible that the head we load here is
// different to the one earlier in this function.
//
@@ -3420,37 +3465,24 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.map_err(Error::ExecutionForkChoiceUpdateFailed)
}
/// Indicates the status of the execution layer.
pub async fn execution_layer_status(&self) -> Result<ExecutionLayerStatus, BeaconChainError> {
let epoch = self.epoch()?;
if self.spec.merge_fork_epoch.map_or(true, |fork| epoch < fork) {
return Ok(ExecutionLayerStatus::NotRequired);
}
/// Returns the status of the current head block, regarding the validity of the execution
/// payload.
pub fn head_safety_status(&self) -> Result<HeadSafetyStatus, BeaconChainError> {
let head = self.head_info()?;
let head_block = self
.fork_choice
.read()
.get_block(&head.block_root)
.ok_or(BeaconChainError::HeadMissingFromForkChoice(head.block_root))?;
if let Some(execution_layer) = &self.execution_layer {
if execution_layer.is_synced().await {
Ok(ExecutionLayerStatus::Ready)
} else {
Ok(ExecutionLayerStatus::NotReady)
}
} else {
// This branch is slightly more restrictive than what is minimally required.
//
// It is possible for a node without an execution layer (EL) to follow the chain
// *after* the merge fork and *before* the terminal execution block, as long as
// that node is not required to produce blocks.
//
// However, here we say that all nodes *must* have an EL as soon as the merge fork
// happens. We do this because it's very difficult to determine that the terminal
// block has been met if we don't already have an EL. As far as we know, the
// terminal execution block might already exist and we've been rejecting it since
// we don't have an EL to verify it.
//
// I think it is very reasonable to say that the beacon chain expects all BNs to
// be paired with an EL node by the time the merge fork epoch is reached. So, we
// enforce that here.
Ok(ExecutionLayerStatus::Missing)
}
let status = match head_block.execution_status {
ExecutionStatus::Valid(block_hash) => HeadSafetyStatus::Safe(Some(block_hash)),
ExecutionStatus::Invalid(block_hash) => HeadSafetyStatus::Invalid(block_hash),
ExecutionStatus::Unknown(block_hash) => HeadSafetyStatus::Unsafe(block_hash),
ExecutionStatus::Irrelevant(_) => HeadSafetyStatus::Safe(None),
};
Ok(status)
}
/// This function takes a configured weak subjectivity `Checkpoint` and the latest finalized `Checkpoint`.

View File

@@ -51,9 +51,9 @@ use crate::{
metrics, BeaconChain, BeaconChainError, BeaconChainTypes,
};
use execution_layer::ExecutePayloadResponse;
use fork_choice::{ForkChoice, ForkChoiceStore};
use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus};
use parking_lot::RwLockReadGuard;
use proto_array::Block as ProtoBlock;
use proto_array::{Block as ProtoBlock, ExecutionStatus};
use safe_arith::ArithError;
use slog::{debug, error, info, Logger};
use slot_clock::SlotClock;
@@ -232,6 +232,16 @@ pub enum BlockError<T: EthSpec> {
///
/// See `ExecutionPayloadError` for scoring information
ExecutionPayloadError(ExecutionPayloadError),
/// The block references an parent block which has an execution payload which was found to be
/// invalid.
///
/// ## Peer scoring
///
/// TODO(merge): reconsider how we score peers for this.
///
/// The peer sent us an invalid block, but I'm not really sure how to score this in an
/// "optimistic" sync world.
ParentExecutionPayloadInvalid { parent_root: Hash256 },
}
/// Returned when block validation failed due to some issue verifying
@@ -529,6 +539,7 @@ pub struct FullyVerifiedBlock<'a, T: BeaconChainTypes> {
pub state: BeaconState<T::EthSpec>,
pub parent_block: SignedBeaconBlock<T::EthSpec>,
pub confirmation_db_batch: Vec<StoreOp<'a, T::EthSpec>>,
pub payload_verification_status: PayloadVerificationStatus,
}
/// Implemented on types that can be converted into a `FullyVerifiedBlock`.
@@ -1140,52 +1151,42 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
}
// This is the soonest we can run these checks as they must be called AFTER per_slot_processing
let execute_payload_handle = if is_execution_enabled(&state, block.message().body()) {
let execution_layer = chain
.execution_layer
.as_ref()
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
let execution_payload =
block
.message()
.body()
.execution_payload()
.ok_or_else(|| InconsistentFork {
fork_at_slot: eth2::types::ForkName::Merge,
object_fork: block.message().body().fork_name(),
})?;
let (execute_payload_handle, payload_verification_status) =
if is_execution_enabled(&state, block.message().body()) {
let execution_layer = chain
.execution_layer
.as_ref()
.ok_or(ExecutionPayloadError::NoExecutionConnection)?;
let execution_payload =
block
.message()
.body()
.execution_payload()
.ok_or_else(|| InconsistentFork {
fork_at_slot: eth2::types::ForkName::Merge,
object_fork: block.message().body().fork_name(),
})?;
let execute_payload_response = execution_layer
.block_on(|execution_layer| execution_layer.execute_payload(execution_payload));
let execute_payload_response = execution_layer
.block_on(|execution_layer| execution_layer.execute_payload(execution_payload));
match execute_payload_response {
Ok((status, handle)) => match status {
ExecutePayloadResponse::Valid => handle,
ExecutePayloadResponse::Invalid => {
return Err(ExecutionPayloadError::RejectedByExecutionEngine.into());
}
ExecutePayloadResponse::Syncing => {
debug!(
chain.log,
"Optimistically accepting payload";
"msg" => "execution engine is syncing"
);
handle
}
},
Err(e) => {
error!(
chain.log,
"Optimistically accepting payload";
"error" => ?e,
"msg" => "execution engine returned an error"
);
None
match execute_payload_response {
Ok((status, handle)) => match status {
ExecutePayloadResponse::Valid => {
(handle, PayloadVerificationStatus::Verified)
}
ExecutePayloadResponse::Invalid => {
return Err(ExecutionPayloadError::RejectedByExecutionEngine.into());
}
ExecutePayloadResponse::Syncing => {
(handle, PayloadVerificationStatus::NotVerified)
}
},
Err(_) => (None, PayloadVerificationStatus::NotVerified),
}
}
} else {
None
};
} else {
(None, PayloadVerificationStatus::Irrelevant)
};
// If the block is sufficiently recent, notify the validator monitor.
if let Some(slot) = chain.slot_clock.now() {
@@ -1300,6 +1301,7 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
state,
parent_block: parent.beacon_block,
confirmation_db_batch,
payload_verification_status,
})
}
}
@@ -1315,7 +1317,21 @@ fn validate_execution_payload<T: BeaconChainTypes>(
if let Some(execution_payload) = block.body().execution_payload() {
// This logic should match `is_execution_enabled`. We use only the execution block hash of
// the parent here in order to avoid loading the parent state during gossip verification.
let is_merge_complete = parent_block.execution_block_hash != Hash256::zero();
let is_merge_complete = match parent_block.execution_status {
// Optimistically declare that an "unknown" status block has completed the merge.
ExecutionStatus::Valid(_) | ExecutionStatus::Unknown(_) => true,
// It's impossible for an irrelevant block to have completed the merge. It is pre-merge
// by definition.
ExecutionStatus::Irrelevant(_) => false,
// If the parent has an invalid payload then it's impossible to build a valid block upon
// it. Reject the block.
ExecutionStatus::Invalid(_) => {
return Err(BlockError::ParentExecutionPayloadInvalid {
parent_root: parent_block.root,
})
}
};
let is_merge_block =
!is_merge_complete && *execution_payload != <ExecutionPayload<T::EthSpec>>::default();
if !is_merge_block && !is_merge_complete {

View File

@@ -136,6 +136,9 @@ pub enum BeaconChainError {
AltairForkDisabled,
ExecutionLayerMissing,
ExecutionForkChoiceUpdateFailed(execution_layer::Error),
HeadMissingFromForkChoice(Hash256),
FinalizedBlockMissingFromForkChoice(Hash256),
InvalidFinalizedPayloadShutdownError(TrySendError<ShutdownReason>),
}
easy_from_to!(SlotProcessingError, BeaconChainError);

View File

@@ -1,5 +1,5 @@
use crate::{BeaconForkChoiceStore, BeaconSnapshot};
use fork_choice::ForkChoice;
use fork_choice::{ForkChoice, PayloadVerificationStatus};
use itertools::process_results;
use slog::{info, warn, Logger};
use state_processing::state_advance::complete_state_advance;
@@ -164,9 +164,21 @@ pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: It
)
.map_err(|e| format!("Error replaying block: {:?}", e))?;
// Setting this to unverified is the safest solution, since we don't have a way to
// retro-actively determine if they were valid or not.
//
// This scenario is so rare that it seems OK to double-verify some blocks.
let payload_verification_status = PayloadVerificationStatus::NotVerified;
let (block, _) = block.deconstruct();
fork_choice
.on_block(block.slot(), &block, block.canonical_root(), &state)
.on_block(
block.slot(),
&block,
block.canonical_root(),
&state,
payload_verification_status,
)
.map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?;
}

View File

@@ -36,7 +36,7 @@ mod validator_pubkey_cache;
pub use self::beacon_chain::{
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult,
ExecutionLayerStatus, ForkChoiceError, HeadInfo, StateSkipConfig, WhenSlotSkipped,
ForkChoiceError, HeadSafetyStatus, StateSkipConfig, WhenSlotSkipped, HeadInfo
MAXIMUM_GOSSIP_CLOCK_DISPARITY,
};
pub use self::beacon_snapshot::BeaconSnapshot;