Merge remote-tracking branch 'origin/unstable' into tree-states

This commit is contained in:
Michael Sproul
2022-03-28 09:24:09 +11:00
187 changed files with 5903 additions and 2368 deletions

View File

@@ -15,6 +15,7 @@ tree-states = ["store/milhouse"]
[dev-dependencies]
maplit = "1.0.2"
environment = { path = "../../lighthouse/environment" }
serde_json = "1.0.58"
[dependencies]
merkle_proof = { path = "../../consensus/merkle_proof" }

View File

@@ -961,7 +961,6 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> {
}
/// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain.
/// You can use this `shuffling_id` to read from the shuffling cache.
///
/// The block root may not be known for two reasons:
///

View File

@@ -4,6 +4,7 @@ use crate::attestation_verification::{
VerifiedUnaggregatedAttestation,
};
use crate::attester_cache::{AttesterCache, AttesterCacheKey};
use crate::beacon_proposer_cache::compute_proposer_duties_from_head;
use crate::beacon_proposer_cache::BeaconProposerCache;
use crate::block_times_cache::BlockTimesCache;
use crate::block_verification::{
@@ -35,6 +36,7 @@ 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::sync_committee_verification::{
Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution,
@@ -51,8 +53,8 @@ use crate::{metrics, BeaconChainError};
use eth2::types::{
EventKind, SseBlock, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead, SyncDuty,
};
use execution_layer::{ExecutionLayer, PayloadStatus};
use fork_choice::{AttestationFromBlock, ForkChoice};
use execution_layer::{ExecutionLayer, PayloadAttributes, PayloadStatus};
use fork_choice::{AttestationFromBlock, ForkChoice, InvalidationOperation};
use futures::channel::mpsc::Sender;
use itertools::process_results;
use itertools::Itertools;
@@ -108,6 +110,11 @@ pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::zero();
/// Defines how old a block can be before it's no longer a candidate for the early attester cache.
const EARLY_ATTESTER_CACHE_HISTORIC_SLOTS: u64 = 4;
/// Defines a distance between the head block slot and the current slot.
///
/// If the head block is older than this value, don't bother preparing beacon proposers.
const PREPARE_PROPOSER_HISTORIC_EPOCHS: u64 = 4;
/// Reported to the user when the justified block has an invalid execution payload.
pub const INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON: &str =
"Justified block has an invalid execution payload.";
@@ -202,6 +209,7 @@ pub struct HeadInfo {
pub proposer_shuffling_decision_root: Hash256,
pub is_merge_transition_complete: bool,
pub execution_payload_block_hash: Option<ExecutionBlockHash>,
pub random: Hash256,
}
pub trait BeaconChainTypes: Send + Sync + 'static {
@@ -1110,6 +1118,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.beacon_state
.proposer_shuffling_decision_root(head.beacon_block_root)?;
// The `random` value is used whilst producing an `ExecutionPayload` atop the head.
let current_epoch = head.beacon_state.current_epoch();
let random = *head.beacon_state.get_randao_mix(current_epoch)?;
Ok(HeadInfo {
slot: head.beacon_block.slot(),
block_root: head.beacon_block_root,
@@ -1128,6 +1140,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.execution_payload()
.ok()
.map(|ep| ep.block_hash),
random,
})
})
}
@@ -3135,49 +3148,29 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// This method must be called whenever an execution engine indicates that a payload is
/// invalid.
///
/// If the `latest_root` is known to fork-choice it will be invalidated. If it is not known, an
/// error will be returned.
/// Fork choice will be run after the invalidation. The client may be shut down if the `op`
/// results in the justified checkpoint being invalidated.
///
/// If `latest_valid_hash` is `None` or references a block unknown to fork choice, no other
/// blocks will be invalidated. If `latest_valid_hash` is a block known to fork choice, all
/// blocks between the `latest_root` and the `latest_valid_hash` will be invalidated (which may
/// cause further, second-order invalidations).
///
/// ## Notes
///
/// Use these rules to set `latest_root`:
///
/// - When `forkchoiceUpdated` indicates an invalid block, set `latest_root` to be the
/// block root that was the head of the chain when `forkchoiceUpdated` was called.
/// - When `executePayload` returns an invalid block *during* block import, set
/// `latest_root` to be the parent of the beacon block containing the invalid
/// payload (because the block containing the payload is not present in fork choice).
/// - When `executePayload` returns an invalid block *after* block import, set
/// `latest_root` to be root of the beacon block containing the invalid payload.
/// See the documentation of `InvalidationOperation` for information about defining `op`.
pub fn process_invalid_execution_payload(
&self,
latest_root: Hash256,
latest_valid_hash: Option<ExecutionBlockHash>,
op: &InvalidationOperation,
) -> Result<(), Error> {
debug!(
self.log,
"Invalid execution payload in block";
"latest_valid_hash" => ?latest_valid_hash,
"latest_root" => ?latest_root,
"latest_valid_ancestor" => ?op.latest_valid_ancestor(),
"block_root" => ?op.block_root(),
);
// Update fork choice.
if let Err(e) = self
.fork_choice
.write()
.on_invalid_execution_payload(latest_root, latest_valid_hash)
{
if let Err(e) = self.fork_choice.write().on_invalid_execution_payload(op) {
crit!(
self.log,
"Failed to process invalid payload";
"error" => ?e,
"latest_valid_hash" => ?latest_valid_hash,
"latest_root" => ?latest_root,
"latest_valid_ancestor" => ?op.latest_valid_ancestor(),
"block_root" => ?op.block_root(),
);
}
@@ -3395,13 +3388,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current);
// Used later for the execution engine.
let new_head_execution_block_hash_opt = new_head
.beacon_block
.message()
.body()
.execution_payload()
.ok()
.map(|ep| ep.block_hash);
let is_merge_transition_complete = is_merge_transition_complete(&new_head.beacon_state);
drop(lag_timer);
@@ -3597,24 +3583,260 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
// If this is a post-merge block, update the execution layer.
if let Some(new_head_execution_block_hash) = new_head_execution_block_hash_opt {
if is_merge_transition_complete {
let finalized_execution_block_hash = finalized_block
.execution_status
.block_hash()
.unwrap_or_else(ExecutionBlockHash::zero);
if let Err(e) = self.update_execution_engine_forkchoice_blocking(
finalized_execution_block_hash,
beacon_block_root,
new_head_execution_block_hash,
) {
crit!(
self.log,
"Failed to update execution head";
"error" => ?e
);
}
if is_merge_transition_complete {
let current_slot = self.slot()?;
if let Err(e) = self.update_execution_engine_forkchoice_blocking(current_slot) {
crit!(
self.log,
"Failed to update execution head";
"error" => ?e
);
}
// Performing this call immediately after
// `update_execution_engine_forkchoice_blocking` might result in two calls to fork
// choice updated, one *without* payload attributes and then a second *with*
// payload attributes.
//
// This seems OK. It's not a significant waste of EL<>CL bandwidth or resources, as
// far as I know.
if let Err(e) = self.prepare_beacon_proposer_blocking() {
crit!(
self.log,
"Failed to prepare proposers after fork choice";
"error" => ?e
);
}
}
Ok(())
}
pub fn prepare_beacon_proposer_blocking(&self) -> Result<(), Error> {
let execution_layer = self
.execution_layer
.as_ref()
.ok_or(Error::ExecutionLayerMissing)?;
execution_layer
.block_on_generic(|_| self.prepare_beacon_proposer_async())
.map_err(Error::PrepareProposerBlockingFailed)?
}
/// Determines the beacon proposer for the next slot. If that proposer is registered in the
/// `execution_layer`, provide the `execution_layer` with the necessary information to produce
/// `PayloadAttributes` for future calls to fork choice.
///
/// 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).
pub async fn prepare_beacon_proposer_async(&self) -> Result<(), Error> {
let execution_layer = self
.execution_layer
.clone()
.ok_or(Error::ExecutionLayerMissing)?;
// Nothing to do if there are no proposers registered with the EL, exit early to avoid
// wasting cycles.
if !execution_layer.has_any_proposer_preparation_data().await {
return Ok(());
}
let head = self.head_info()?;
let current_slot = self.slot()?;
// 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(());
}
// We only start to push preparation data for some chain *after* the transition block
// has been imported.
//
// There is no payload preparation for the transition block (i.e., the first block with
// execution enabled in some chain).
if head.execution_payload_block_hash.is_none() {
return Ok(());
};
let head_epoch = head.slot.epoch(T::EthSpec::slots_per_epoch());
let prepare_slot = current_slot + 1;
let prepare_epoch = prepare_slot.epoch(T::EthSpec::slots_per_epoch());
// 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.proposer_shuffling_decision_root
} else {
head.block_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.
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.
if !execution_layer
.has_proposer_preparation_data(proposer as u64)
.await
{
return Ok(());
}
let payload_attributes = PayloadAttributes {
timestamp: self
.slot_clock
.start_of(prepare_slot)
.ok_or(Error::InvalidSlot(prepare_slot))?
.as_secs(),
prev_randao: head.random,
suggested_fee_recipient: execution_layer
.get_suggested_fee_recipient(proposer as u64)
.await,
};
debug!(
self.log,
"Preparing beacon proposer";
"payload_attributes" => ?payload_attributes,
"head_root" => ?head.block_root,
"prepare_slot" => prepare_slot,
"validator" => proposer,
);
let already_known = execution_layer
.insert_proposer(
prepare_slot,
head.block_root,
proposer as u64,
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,
);
}
let till_prepare_slot =
if let Some(duration) = self.slot_clock.duration_to_slot(prepare_slot) {
duration
} else {
// `SlotClock::duration_to_slot` will return `None` when we are past the start
// of `prepare_slot`. Don't bother sending a `forkchoiceUpdated` in that case,
// it's too late.
//
// This scenario might occur on an overloaded/under-resourced node.
warn!(
self.log,
"Delayed proposer preparation";
"prepare_slot" => prepare_slot,
"validator" => proposer,
);
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
{
debug!(
self.log,
"Pushing update to prepare proposer";
"till_prepare_slot" => ?till_prepare_slot,
"prepare_slot" => prepare_slot
);
// Use the blocking method here so that we don't form a queue of these functions when
// routinely calling them.
self.update_execution_engine_forkchoice_async(current_slot)
.await?;
}
Ok(())
@@ -3622,9 +3844,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
pub fn update_execution_engine_forkchoice_blocking(
&self,
finalized_execution_block_hash: ExecutionBlockHash,
head_block_root: Hash256,
head_execution_block_hash: ExecutionBlockHash,
current_slot: Slot,
) -> Result<(), Error> {
let execution_layer = self
.execution_layer
@@ -3632,34 +3852,76 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or(Error::ExecutionLayerMissing)?;
execution_layer
.block_on_generic(|_| {
self.update_execution_engine_forkchoice_async(
finalized_execution_block_hash,
head_block_root,
head_execution_block_hash,
)
})
.block_on_generic(|_| self.update_execution_engine_forkchoice_async(current_slot))
.map_err(Error::ForkchoiceUpdate)?
}
pub async fn update_execution_engine_forkchoice_async(
&self,
finalized_execution_block_hash: ExecutionBlockHash,
head_block_root: Hash256,
head_execution_block_hash: ExecutionBlockHash,
current_slot: Slot,
) -> Result<(), Error> {
let execution_layer = self
.execution_layer
.as_ref()
.ok_or(Error::ExecutionLayerMissing)?;
// Take the global lock for updating the execution engine fork choice.
//
// Whilst holding this lock we must:
//
// 1. Read the canonical head.
// 2. Issue a forkchoiceUpdated call to the execution engine.
//
// This will allow us to ensure that we provide the execution layer with an *ordered* view
// of the head. I.e., we will never communicate a past head after communicating a later
// one.
//
// There is a "deadlock warning" in this function. The downside of this nice ordering is the
// potential for deadlock. I would advise against any other use of
// `execution_engine_forkchoice_lock` apart from the one here.
let forkchoice_lock = execution_layer.execution_engine_forkchoice_lock().await;
// Deadlock warning:
//
// We are taking the `self.fork_choice` lock whilst holding the `forkchoice_lock`. This
// is intentional, since it allows us to ensure a consistent ordering of messages to the
// execution layer.
let (head_block_root, head_hash, finalized_hash) =
if let Some(params) = self.fork_choice.read().get_forkchoice_update_parameters() {
if let Some(head_hash) = params.head_hash {
(
params.head_root,
head_hash,
params
.finalized_hash
.unwrap_or_else(ExecutionBlockHash::zero),
)
} else {
// The head block does not have an execution block hash, there is no need to
// send an update to the EL.
return Ok(());
}
} else {
warn!(
self.log,
"Missing forkchoice params";
"msg" => "please report this non-critical bug"
);
return Ok(());
};
let forkchoice_updated_response = self
.execution_layer
.as_ref()
.ok_or(Error::ExecutionLayerMissing)?
.notify_forkchoice_updated(
head_execution_block_hash,
finalized_execution_block_hash,
None,
)
.notify_forkchoice_updated(head_hash, finalized_hash, current_slot, head_block_root)
.await
.map_err(Error::ExecutionForkChoiceUpdateFailed);
// The head has been read and the execution layer has been updated. It is now valid to send
// another fork choice update.
drop(forkchoice_lock);
match forkchoice_updated_response {
Ok(status) => match &status {
PayloadStatus::Valid | PayloadStatus::Syncing => Ok(()),
@@ -3687,8 +3949,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// The execution engine has stated that all blocks between the
// `head_execution_block_hash` and `latest_valid_hash` are invalid.
self.process_invalid_execution_payload(
head_block_root,
Some(*latest_valid_hash),
&InvalidationOperation::InvalidateMany {
head_block_root,
always_invalidate_head: true,
latest_valid_ancestor: *latest_valid_hash,
},
)?;
Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status })
@@ -3705,7 +3970,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
//
// Using a `None` latest valid ancestor will result in only the head block
// being invalidated (no ancestors).
self.process_invalid_execution_payload(head_block_root, None)?;
self.process_invalid_execution_payload(
&InvalidationOperation::InvalidateOne {
block_root: head_block_root,
},
)?;
Err(BeaconChainError::ExecutionForkChoiceUpdateInvalid { status })
}

View File

@@ -8,9 +8,14 @@
//! very simple to reason about, but it might store values that are useless due to finalization. The
//! values it stores are very small, so this should not be an issue.
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use lru::LruCache;
use smallvec::SmallVec;
use types::{BeaconStateError, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned};
use state_processing::state_advance::partial_state_advance;
use std::cmp::Ordering;
use types::{
BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned,
};
/// The number of sets of proposer indices that should be cached.
const CACHE_SIZE: usize = 16;
@@ -125,3 +130,56 @@ impl BeaconProposerCache {
Ok(())
}
}
/// Compute the proposer duties using the head state without cache.
pub fn compute_proposer_duties_from_head<T: BeaconChainTypes>(
current_epoch: Epoch,
chain: &BeaconChain<T>,
) -> Result<(Vec<usize>, Hash256, Fork), BeaconChainError> {
// Take a copy of the head of the chain.
let head = chain.head()?;
let mut state = head.beacon_state;
let head_state_root = head.beacon_block.state_root();
// Advance the state into the requested epoch.
ensure_state_is_in_epoch(&mut state, head_state_root, current_epoch, &chain.spec)?;
let indices = state
.get_beacon_proposer_indices(&chain.spec)
.map_err(BeaconChainError::from)?;
let dependent_root = state
// The only block which decides its own shuffling is the genesis block.
.proposer_shuffling_decision_root(chain.genesis_block_root)
.map_err(BeaconChainError::from)?;
Ok((indices, dependent_root, state.fork()))
}
/// If required, advance `state` to `target_epoch`.
///
/// ## Details
///
/// - Returns an error if `state.current_epoch() > target_epoch`.
/// - No-op if `state.current_epoch() == target_epoch`.
/// - It must be the case that `state.canonical_root() == state_root`, but this function will not
/// check that.
pub fn ensure_state_is_in_epoch<E: EthSpec>(
state: &mut BeaconState<E>,
state_root: Hash256,
target_epoch: Epoch,
spec: &ChainSpec,
) -> Result<(), BeaconChainError> {
match state.current_epoch().cmp(&target_epoch) {
// Protects against an inconsistent slot clock.
Ordering::Greater => Err(BeaconStateError::SlotOutOfBounds.into()),
// The state needs to be advanced.
Ordering::Less => {
let target_slot = target_epoch.start_slot(E::slots_per_epoch());
partial_state_advance(state, Some(state_root), target_slot, spec)
.map_err(BeaconChainError::from)
}
// The state is suitable, nothing to do.
Ordering::Equal => Ok(()),
}
}

View File

@@ -56,7 +56,7 @@ use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus};
use parking_lot::RwLockReadGuard;
use proto_array::Block as ProtoBlock;
use safe_arith::ArithError;
use slog::{debug, error, Logger};
use slog::{debug, error, info, Logger};
use slot_clock::SlotClock;
use ssz::Encode;
use state_processing::per_block_processing::is_merge_transition_block;
@@ -78,6 +78,30 @@ use types::{
SignedBeaconBlock, SignedBeaconBlockHeader, Slot,
};
const POS_PANDA_BANNER: &str = r#"
,,, ,,, ,,, ,,,
;" ^; ;' ", ;" ^; ;' ",
; s$$$$$$$s ; ; s$$$$$$$s ;
, ss$$$$$$$$$$s ,' ooooooooo. .oooooo. .oooooo..o , ss$$$$$$$$$$s ,'
;s$$$$$$$$$$$$$$$ `888 `Y88. d8P' `Y8b d8P' `Y8 ;s$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$ 888 .d88'888 888Y88bo. $$$$$$$$$$$$$$$$$$
$$$$P""Y$$$Y""W$$$$$ 888ooo88P' 888 888 `"Y8888o. $$$$P""Y$$$Y""W$$$$$
$$$$ p"LFG"q $$$$$ 888 888 888 `"Y88b $$$$ p"LFG"q $$$$$
$$$$ .$$$$$. $$$$ 888 `88b d88'oo .d8P $$$$ .$$$$$. $$$$
$$DcaU$$$$$$$$$$ o888o `Y8bood8P' 8""88888P' $$DcaU$$$$$$$$$$
"Y$$$"*"$$$Y" "Y$$$"*"$$$Y"
"$b.$$" "$b.$$"
.o. . o8o . .o8
.888. .o8 `"' .o8 "888
.8"888. .ooooo. .o888oooooo oooo ooo .oooo. .o888oo .ooooo. .oooo888
.8' `888. d88' `"Y8 888 `888 `88. .8' `P )88b 888 d88' `88bd88' `888
.88ooo8888. 888 888 888 `88..8' .oP"888 888 888ooo888888 888
.8' `888. 888 .o8 888 . 888 `888' d8( 888 888 .888 .o888 888
o88o o8888o`Y8bod8P' "888"o888o `8' `Y888""8o "888"`Y8bod8P'`Y8bod88P"
"#;
/// Maximum block slot number. Block with slots bigger than this constant will NOT be processed.
const MAXIMUM_BLOCK_SLOT_NUMBER: u64 = 4_294_967_296; // 2^32
@@ -309,6 +333,8 @@ pub enum ExecutionPayloadError {
///
/// The peer is not necessarily invalid.
PoWParentMissing(ExecutionBlockHash),
/// The execution node is syncing but we fail the conditions for optimistic sync
UnverifiedNonOptimisticCandidate,
}
impl From<execution_layer::Error> for ExecutionPayloadError {
@@ -1114,9 +1140,13 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
// early.
// - Doing the check here means we can keep our fork-choice implementation "pure". I.e., no
// calls to remote servers.
if is_merge_transition_block(&state, block.message().body()) {
validate_merge_block(chain, block.message())?
}
let valid_merge_transition_block =
if is_merge_transition_block(&state, block.message().body()) {
validate_merge_block(chain, block.message())?;
true
} else {
false
};
// The specification declares that this should be run *inside* `per_block_processing`,
// however we run it here to keep `per_block_processing` pure (i.e., no calls to external
@@ -1126,6 +1156,29 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
// `randao` may change.
let payload_verification_status = notify_new_payload(chain, &state, block.message())?;
// If the payload did not validate or invalidate the block, check to see if this block is
// valid for optimistic import.
if payload_verification_status == PayloadVerificationStatus::NotVerified {
let current_slot = chain
.slot_clock
.now()
.ok_or(BeaconChainError::UnableToReadSlot)?;
if !chain
.fork_choice
.read()
.is_optimistic_candidate_block(
current_slot,
block.slot(),
&block.parent_root(),
&chain.spec,
)
.map_err(BeaconChainError::from)?
{
return Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into());
}
}
// If the block is sufficiently recent, notify the validator monitor.
if let Some(slot) = chain.slot_clock.now() {
let epoch = slot.epoch(T::EthSpec::slots_per_epoch());
@@ -1236,6 +1289,14 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
});
}
if valid_merge_transition_block {
info!(chain.log, "{}", POS_PANDA_BANNER);
info!(chain.log, "Proof of Stake Activated"; "slot" => block.slot());
info!(chain.log, ""; "Terminal POW Block Hash" => ?block.message().execution_payload()?.parent_hash.into_root());
info!(chain.log, ""; "Merge Transition Block Root" => ?block.message().tree_hash_root());
info!(chain.log, ""; "Merge Transition Execution Hash" => ?block.message().execution_payload()?.block_hash.into_root());
}
Ok(Self {
block,
block_root,

View File

@@ -138,6 +138,7 @@ pub enum BeaconChainError {
AltairForkDisabled,
ExecutionLayerMissing,
ExecutionForkChoiceUpdateFailed(execution_layer::Error),
PrepareProposerBlockingFailed(execution_layer::Error),
ExecutionForkChoiceUpdateInvalid {
status: PayloadStatus,
},
@@ -160,6 +161,7 @@ pub enum BeaconChainError {
head_state: Checkpoint,
fork_choice: Hash256,
},
InvalidSlot(Slot),
}
easy_from_to!(SlotProcessingError, BeaconChainError);

View File

@@ -12,7 +12,7 @@ use crate::{
ExecutionPayloadError,
};
use execution_layer::PayloadStatus;
use fork_choice::PayloadVerificationStatus;
use fork_choice::{InvalidationOperation, PayloadVerificationStatus};
use proto_array::{Block as ProtoBlock, ExecutionStatus};
use slog::debug;
use slot_clock::SlotClock;
@@ -68,7 +68,13 @@ pub fn notify_new_payload<T: BeaconChainTypes>(
// This block has not yet been applied to fork choice, so the latest block that was
// imported to fork choice was the parent.
let latest_root = block.parent_root();
chain.process_invalid_execution_payload(latest_root, Some(latest_valid_hash))?;
chain.process_invalid_execution_payload(
&InvalidationOperation::InvalidateMany {
head_block_root: latest_root,
always_invalidate_head: false,
latest_valid_ancestor: latest_valid_hash,
},
)?;
Err(ExecutionPayloadError::RejectedByExecutionEngine { status }.into())
}
@@ -141,13 +147,33 @@ pub fn validate_merge_block<T: BeaconChainTypes>(
}
.into()),
None => {
debug!(
chain.log,
"Optimistically accepting terminal block";
"block_hash" => ?execution_payload.parent_hash,
"msg" => "the terminal block/parent was unavailable"
);
Ok(())
let current_slot = chain
.slot_clock
.now()
.ok_or(BeaconChainError::UnableToReadSlot)?;
// Ensure the block is a candidate for optimistic import.
if chain
.fork_choice
.read()
.is_optimistic_candidate_block(
current_slot,
block.slot(),
&block.parent_root(),
&chain.spec,
)
.map_err(BeaconChainError::from)?
{
debug!(
chain.log,
"Optimistically accepting terminal block";
"block_hash" => ?execution_payload.parent_hash,
"msg" => "the terminal block/parent was unavailable"
);
Ok(())
} else {
Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into())
}
}
}
}

View File

@@ -3,7 +3,7 @@ pub mod attestation_verification;
mod attester_cache;
mod beacon_chain;
mod beacon_fork_choice_store;
mod beacon_proposer_cache;
pub mod beacon_proposer_cache;
mod beacon_snapshot;
pub mod block_reward;
mod block_times_cache;
@@ -28,6 +28,7 @@ pub mod observed_operations;
mod persisted_beacon_chain;
mod persisted_fork_choice;
mod pre_finalization_cache;
pub mod proposer_prep_service;
pub mod schema_change;
mod shuffling_cache;
pub mod state_advance_timer;

View File

@@ -0,0 +1,74 @@
use crate::{BeaconChain, BeaconChainTypes};
use slog::{debug, error};
use slot_clock::SlotClock;
use std::sync::Arc;
use task_executor::TaskExecutor;
use tokio::time::sleep;
/// At 12s slot times, the means that the payload preparation routine will run 4s before the start
/// of each slot (`12 / 3 = 4`).
pub const PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR: u32 = 3;
/// Spawns a routine which ensures the EL is provided advance notice of any block producers.
///
/// This routine will run once per slot, at `slot_duration / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR`
/// before the start of each slot.
///
/// The service will not be started if there is no `execution_layer` on the `chain`.
pub fn start_proposer_prep_service<T: BeaconChainTypes>(
executor: TaskExecutor,
chain: Arc<BeaconChain<T>>,
) {
// Avoid spawning the service if there's no EL, it'll just error anyway.
if chain.execution_layer.is_some() {
executor.clone().spawn(
async move { proposer_prep_service(executor, chain).await },
"proposer_prep_service",
);
}
}
/// Loop indefinitely, calling `BeaconChain::prepare_beacon_proposer_async` at an interval.
async fn proposer_prep_service<T: BeaconChainTypes>(
executor: TaskExecutor,
chain: Arc<BeaconChain<T>>,
) {
let slot_duration = chain.slot_clock.slot_duration();
loop {
match chain.slot_clock.duration_to_next_slot() {
Some(duration) => {
let additional_delay = slot_duration
- chain.slot_clock.slot_duration() / PAYLOAD_PREPARATION_LOOKAHEAD_FACTOR;
sleep(duration + additional_delay).await;
debug!(
chain.log,
"Proposer prepare routine firing";
);
let inner_chain = chain.clone();
executor.spawn(
async move {
if let Err(e) = inner_chain.prepare_beacon_proposer_async().await {
error!(
inner_chain.log,
"Proposer prepare routine failed";
"error" => ?e
);
}
},
"proposer_prep_update",
);
continue;
}
None => {
error!(chain.log, "Failed to read slot clock");
// If we can't read the slot clock, just wait another slot.
sleep(slot_duration).await;
continue;
}
};
}
}

View File

@@ -337,14 +337,20 @@ where
let el_runtime = ExecutionLayerRuntime::default();
let urls = urls
let urls: Vec<SensitiveUrl> = urls
.iter()
.map(|s| SensitiveUrl::parse(*s))
.collect::<Result<_, _>>()
.unwrap();
let execution_layer = ExecutionLayer::from_urls(
urls,
Some(Address::repeat_byte(42)),
let config = execution_layer::Config {
execution_endpoints: urls,
secret_files: vec![],
suggested_fee_recipient: Some(Address::repeat_byte(42)),
..Default::default()
};
let execution_layer = ExecutionLayer::from_config(
config,
el_runtime.task_executor.clone(),
el_runtime.log.clone(),
)

View File

@@ -3,7 +3,7 @@ use crate::{BeaconChainTypes, BeaconStore};
use ssz::{Decode, DecodeError, Encode};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::{File, OpenOptions};
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::Path;
use store::{DBColumn, Error as StoreError, StoreItem};
@@ -257,7 +257,7 @@ impl From<Error> for BeaconChainError {
impl ValidatorPubkeyCacheFile {
/// Opens an existing file for reading and writing.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
OpenOptions::new()
File::options()
.read(true)
.write(true)
.create(false)
@@ -455,7 +455,7 @@ mod test {
let cache = ValidatorPubkeyCache::<T>::load_from_file(&path).expect("should open cache");
drop(cache);
let mut file = OpenOptions::new()
let mut file = File::options()
.write(true)
.append(true)
.open(&path)

View File

@@ -5,7 +5,11 @@ use beacon_chain::{
BeaconChainError, BlockError, ExecutionPayloadError, HeadInfo, StateSkipConfig,
WhenSlotSkipped, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON,
};
use execution_layer::{
json_structures::JsonPayloadAttributesV1, ExecutionLayer, PayloadAttributes,
};
use proto_array::ExecutionStatus;
use slot_clock::SlotClock;
use task_executor::ShutdownReason;
use types::*;
@@ -54,6 +58,10 @@ impl InvalidPayloadRig {
self
}
fn execution_layer(&self) -> ExecutionLayer {
self.harness.chain.execution_layer.clone().unwrap()
}
fn block_hash(&self, block_root: Hash256) -> ExecutionBlockHash {
self.harness
.chain
@@ -85,6 +93,19 @@ impl InvalidPayloadRig {
self.harness.chain.head_info().unwrap()
}
fn previous_payload_attributes(&self) -> PayloadAttributes {
let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap();
let json = mock_execution_layer
.server
.take_previous_request()
.expect("no previous request");
let params = json.get("params").expect("no params");
let payload_param_json = params.get(1).expect("no payload param");
let attributes: JsonPayloadAttributesV1 =
serde_json::from_value(payload_param_json.clone()).unwrap();
attributes.into()
}
fn move_to_terminal_block(&self) {
let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap();
mock_execution_layer
@@ -231,8 +252,10 @@ fn valid_invalid_syncing() {
/// `latest_valid_hash`.
#[test]
fn invalid_payload_invalidates_parent() {
let mut rig = InvalidPayloadRig::new();
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid); // Import a valid transition block.
rig.move_to_first_justification(Payload::Syncing);
let roots = vec![
rig.import_block(Payload::Syncing),
@@ -258,6 +281,7 @@ fn invalid_payload_invalidates_parent() {
fn justified_checkpoint_becomes_invalid() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid); // Import a valid transition block.
rig.move_to_first_justification(Payload::Syncing);
let justified_checkpoint = rig.head_info().current_justified_checkpoint;
@@ -305,7 +329,9 @@ fn pre_finalized_latest_valid_hash() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
let blocks = rig.build_blocks(num_blocks, Payload::Syncing);
let mut blocks = vec![];
blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block.
blocks.extend(rig.build_blocks(num_blocks - 1, Payload::Syncing));
assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch);
@@ -330,7 +356,11 @@ fn pre_finalized_latest_valid_hash() {
for i in E::slots_per_epoch() * finalized_epoch..num_blocks {
let slot = Slot::new(i);
let root = rig.block_root_at_slot(slot).unwrap();
assert!(rig.execution_status(root).is_not_verified());
if slot == 1 {
assert!(rig.execution_status(root).is_valid());
} else {
assert!(rig.execution_status(root).is_not_verified());
}
}
}
@@ -344,7 +374,10 @@ fn latest_valid_hash_will_validate() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
let blocks = rig.build_blocks(4, Payload::Syncing);
let mut blocks = vec![];
blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block.
blocks.extend(rig.build_blocks(4, Payload::Syncing));
let latest_valid_root = rig
.block_root_at_slot(Slot::new(LATEST_VALID_SLOT))
@@ -357,7 +390,7 @@ fn latest_valid_hash_will_validate() {
assert_eq!(rig.head_info().slot, LATEST_VALID_SLOT);
for slot in 0..=4 {
for slot in 0..=5 {
let slot = Slot::new(slot);
let root = if slot > 0 {
// If not the genesis slot, check the blocks we just produced.
@@ -386,7 +419,9 @@ fn latest_valid_hash_is_junk() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
let blocks = rig.build_blocks(num_blocks, Payload::Syncing);
let mut blocks = vec![];
blocks.push(rig.import_block(Payload::Valid)); // Import a valid transition block.
blocks.extend(rig.build_blocks(num_blocks, Payload::Syncing));
assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch);
@@ -408,7 +443,11 @@ fn latest_valid_hash_is_junk() {
for i in E::slots_per_epoch() * finalized_epoch..num_blocks {
let slot = Slot::new(i);
let root = rig.block_root_at_slot(slot).unwrap();
assert!(rig.execution_status(root).is_not_verified());
if slot == 1 {
assert!(rig.execution_status(root).is_valid());
} else {
assert!(rig.execution_status(root).is_not_verified());
}
}
}
@@ -421,6 +460,7 @@ fn invalidates_all_descendants() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid); // Import a valid transition block.
let blocks = rig.build_blocks(num_blocks, Payload::Syncing);
assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch);
@@ -493,6 +533,7 @@ fn switches_heads() {
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid); // Import a valid transition block.
let blocks = rig.build_blocks(num_blocks, Payload::Syncing);
assert_eq!(rig.head_info().finalized_checkpoint.epoch, finalized_epoch);
@@ -571,8 +612,9 @@ fn invalid_during_processing() {
#[test]
fn invalid_after_optimistic_sync() {
let mut rig = InvalidPayloadRig::new();
let mut rig = InvalidPayloadRig::new().enable_attestations();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid); // Import a valid transition block.
let mut roots = vec![
rig.import_block(Payload::Syncing),
@@ -599,3 +641,55 @@ fn invalid_after_optimistic_sync() {
let head = rig.harness.chain.head_info().unwrap();
assert_eq!(head.block_root, roots[1]);
}
#[test]
fn payload_preparation() {
let mut rig = InvalidPayloadRig::new();
rig.move_to_terminal_block();
rig.import_block(Payload::Valid);
let el = rig.execution_layer();
let head = rig.harness.chain.head().unwrap();
let current_slot = rig.harness.chain.slot().unwrap();
assert_eq!(head.beacon_state.slot(), 1);
assert_eq!(current_slot, 1);
let next_slot = current_slot + 1;
let proposer = head
.beacon_state
.get_beacon_proposer_index(next_slot, &rig.harness.chain.spec)
.unwrap();
let fee_recipient = Address::repeat_byte(99);
// Provide preparation data to the EL for `proposer`.
el.update_proposer_preparation_blocking(
Epoch::new(1),
&[ProposerPreparationData {
validator_index: proposer as u64,
fee_recipient,
}],
)
.unwrap();
rig.harness
.chain
.prepare_beacon_proposer_blocking()
.unwrap();
let payload_attributes = PayloadAttributes {
timestamp: rig
.harness
.chain
.slot_clock
.start_of(next_slot)
.unwrap()
.as_secs(),
prev_randao: *head
.beacon_state
.get_randao_mix(head.beacon_state.current_epoch())
.unwrap(),
suggested_fee_recipient: fee_recipient,
};
assert_eq!(rig.previous_payload_attributes(), payload_attributes);
}