Skip to content
Draft
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ run-ef-tests:
./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests

# Run the tests in the `beacon_chain` crate for all known forks.
# TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead
test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS_BEFORE_GLOAS))
test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS))

test-beacon-chain-%:
env FORK_NAME=$* cargo nextest run --release --features "fork_from_env,slasher/lmdb,$(TEST_FEATURES)" -p beacon_chain
Expand Down
20 changes: 15 additions & 5 deletions beacon_node/beacon_chain/src/beacon_block_streamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,14 +684,16 @@ impl From<Error> for BeaconChainError {
#[cfg(test)]
mod tests {
use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches};
use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec};
use crate::test_utils::{
BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec,
};
use bls::Keypair;
use execution_layer::test_utils::Block;
use fixed_bytes::FixedBytesExtended;
use std::sync::Arc;
use std::sync::LazyLock;
use tokio::sync::mpsc;
use types::{ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot};
use types::{ChainSpec, Epoch, EthSpec, ForkName, Hash256, MinimalEthSpec, Slot};

const VALIDATOR_COUNT: usize = 48;

Expand All @@ -715,16 +717,23 @@ mod tests {
harness
}

// TODO(EIP-7732) Extend this test for gloas
#[tokio::test]
async fn check_all_blocks_from_altair_to_fulu() {
async fn check_all_blocks_from_altair_to_gloas() {
// This test only need to run once to cover all forks (when FORK_NAME == latest)
if let Some(fork_name) = fork_name_from_env()
&& fork_name != ForkName::latest()
{
return;
}

let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize;
let num_epochs = 12;
let num_epochs = 14;
let bellatrix_fork_epoch = 2usize;
let capella_fork_epoch = 4usize;
let deneb_fork_epoch = 6usize;
let electra_fork_epoch = 8usize;
let fulu_fork_epoch = 10usize;
let gloas_fork_epoch = 12usize;
let num_blocks_produced = num_epochs * slots_per_epoch;

let mut spec = test_spec::<MinimalEthSpec>();
Expand All @@ -734,6 +743,7 @@ mod tests {
spec.deneb_fork_epoch = Some(Epoch::new(deneb_fork_epoch as u64));
spec.electra_fork_epoch = Some(Epoch::new(electra_fork_epoch as u64));
spec.fulu_fork_epoch = Some(Epoch::new(fulu_fork_epoch as u64));
spec.gloas_fork_epoch = Some(Epoch::new(gloas_fork_epoch as u64));
let spec = Arc::new(spec);

let harness = get_harness(VALIDATOR_COUNT, spec.clone());
Expand Down
21 changes: 16 additions & 5 deletions beacon_node/beacon_chain/src/block_production/gloas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ pub const BID_VALUE_SELF_BUILD: u64 = 0;
pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0;

type ConsensusBlockValue = u64;
type BlockProductionResult<E> = (BeaconBlock<E, FullPayload<E>>, ConsensusBlockValue);
type BlockProductionResult<E> = (
BeaconBlock<E, FullPayload<E>>,
ConsensusBlockValue,
BeaconState<E>,
Option<ExecutionPayloadEnvelope<E>>,
);

pub type PreparePayloadResult<E> = Result<BlockProposalContentsGloas<E>, BlockProductionError>;
pub type PreparePayloadHandle<E> = JoinHandle<Option<PreparePayloadResult<E>>>;
Expand Down Expand Up @@ -433,7 +438,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
payload_data: Option<ExecutionPayloadData<T::EthSpec>>,
mut state: BeaconState<T::EthSpec>,
verification: ProduceBlockVerification,
) -> Result<(BeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>, u64), BlockProductionError> {
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
let PartialBeaconBlock {
slot,
proposer_index,
Expand Down Expand Up @@ -551,7 +556,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// Construct and cache the ExecutionPayloadEnvelope if we have payload data.
// For local building, we always have payload data.
// For trustless building, the builder will provide the envelope separately.
if let Some(payload_data) = payload_data {
let envelope = if let Some(payload_data) = payload_data {
let beacon_block_root = block.tree_hash_root();
let execution_payload_envelope = ExecutionPayloadEnvelope {
payload: payload_data.payload,
Expand Down Expand Up @@ -581,6 +586,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {

signed_envelope.message.state_root = state.update_tree_hash_cache()?;

let envelope = signed_envelope.message.clone();

// Cache the envelope for later retrieval by the validator for signing and publishing.
let envelope_slot = payload_data.slot;
// TODO(gloas) might be safer to cache by root instead of by slot.
Expand All @@ -594,7 +601,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
slot = %envelope_slot,
"Cached pending execution payload envelope"
);
}

Some(envelope)
} else {
None
};

metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES);

Expand All @@ -605,7 +616,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
"Produced beacon block"
);

Ok((block, consensus_block_value))
Ok((block, consensus_block_value, state, envelope))
}

// TODO(gloas) introduce `ProposerPreferences` so we can build out trustless
Expand Down
33 changes: 31 additions & 2 deletions beacon_node/beacon_chain/src/block_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ use bls::{PublicKey, PublicKeyBytes};
use educe::Educe;
use eth2::types::{BlockGossip, EventKind};
use execution_layer::PayloadStatus;
use fixed_bytes::FixedBytesExtended;
pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus};
use metrics::TryExt;
use parking_lot::RwLockReadGuard;
Expand Down Expand Up @@ -1651,6 +1652,26 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
});
}

// TODO(gloas): Replace with full process_execution_payload_envelope once available
// in the block import path. This workaround is needed because blocks can arrive before
// envelopes, and subsequent blocks need latest_block_hash set for bid verification.
// https://github.com/sigp/lighthouse/issues/8590
//
// For gloas blocks, simulate the minimal effects of process_execution_payload_envelope
// that are needed for subsequent block processing:
// 1. Set latest_block_hash from the bid so the next block's bid verification passes.
// 2. Cache the block's state_root in latest_block_header so that per_slot_processing's
// cache_state uses it (instead of recomputing from tree hash, which would reflect
// the latest_block_hash change and produce an incorrect state_roots entry).
if let Ok(bid) = block.message().body().signed_execution_payload_bid() {
if let Ok(latest_block_hash) = state.latest_block_hash_mut() {
*latest_block_hash = bid.message.block_hash;
}
if state.latest_block_header().state_root == Hash256::zero() {
state.latest_block_header_mut().state_root = state_root;
}
}

/*
* Apply the block's attestations to fork choice.
*
Expand Down Expand Up @@ -1961,8 +1982,16 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>(
}

let beacon_state_root = if state.slot() == parent_block.slot() {
// Sanity check.
if parent_state_root != parent_block.state_root() {
// TODO(gloas): Revisit this bypass once envelope-processed states are stored
// with correct state roots.
// https://github.com/sigp/lighthouse/issues/8590
//
// Sanity check: skip for gloas because the stored state has latest_block_hash
// set (simulating process_execution_payload_envelope), which changes its tree
// hash vs the block's state_root. The canonical state_root is still correct.
if parent_state_root != parent_block.state_root()
&& !parent_block.fork_name_unchecked().gloas_enabled()
{
return Err(BeaconChainError::DBInconsistent(format!(
"Parent state at slot {} has the wrong state root: {:?} != {:?}",
state.slot(),
Expand Down
18 changes: 18 additions & 0 deletions beacon_node/beacon_chain/src/canonical_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,24 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
self.fork_choice.upgradable_read()
}

/// Replace the beacon state in the cached head snapshot without re-running fork choice.
///
/// This is useful for test scenarios (e.g. gloas envelope processing) where the head block
/// hasn't changed but the state needs updating. The normal `recompute_head_at_slot_internal`
/// exits early when the fork choice view is unchanged, so this method provides a way to
/// update the cached state directly.
pub(crate) fn replace_cached_head_state(&self, new_state: BeaconState<T::EthSpec>) {
let _recompute_head_lock = self.recompute_head_lock.lock();
let mut cached_head = self.cached_head_write_lock();
let old_snapshot = &cached_head.snapshot;
let new_snapshot = Arc::new(BeaconSnapshot {
beacon_block_root: old_snapshot.beacon_block_root,
beacon_block: old_snapshot.beacon_block.clone(),
beacon_state: new_state,
});
cached_head.snapshot = new_snapshot;
}

/// Access a write-lock for fork choice.
#[instrument(skip_all)]
pub fn fork_choice_write_lock(&self) -> RwLockWriteGuard<'_, BeaconForkChoice<T>> {
Expand Down
70 changes: 39 additions & 31 deletions beacon_node/beacon_chain/src/execution_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,39 +63,47 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
notify_execution_layer: NotifyExecutionLayer,
) -> Result<Self, BlockError> {
let payload_verification_status = if is_execution_enabled(state, block.message().body()) {
// Perform the initial stages of payload verification.
//
// We will duplicate these checks again during `per_block_processing`, however these
// checks are cheap and doing them here ensures we have verified them before marking
// the block as optimistically imported. This is particularly relevant in the case
// where we do not send the block to the EL at all.
let block_message = block.message();
partially_verify_execution_payload::<_, FullPayload<_>>(
state,
block.slot(),
block_message.body(),
&chain.spec,
)
.map_err(BlockError::PerBlockProcessingError)?;

match notify_execution_layer {
NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => {
// Create a NewPayloadRequest (no clones required) and check optimistic sync verifications
let new_payload_request: NewPayloadRequest<T::EthSpec> =
block_message.try_into()?;
if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() {
warn!(
block_number = ?block_message.execution_payload().map(|payload| payload.block_number()),
info = "you can silence this warning with --disable-optimistic-finalized-sync",
error = ?e,
"Falling back to slow block hash verification"
);
None
} else {
Some(PayloadVerificationStatus::Optimistic)
// Gloas blocks don't carry an execution payload in the block body (they use
// execution payload bids + envelopes instead), so the pre-merge payload
// verification and EL notification paths don't apply.
if block.message().body().fork_name().gloas_enabled() {
Some(PayloadVerificationStatus::Irrelevant)
} else {
// Perform the initial stages of payload verification.
//
// We will duplicate these checks again during `per_block_processing`, however these
// checks are cheap and doing them here ensures we have verified them before marking
// the block as optimistically imported. This is particularly relevant in the case
// where we do not send the block to the EL at all.
let block_message = block.message();
partially_verify_execution_payload::<_, FullPayload<_>>(
state,
block.slot(),
block_message.body(),
&chain.spec,
)
.map_err(BlockError::PerBlockProcessingError)?;

match notify_execution_layer {
NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => {
// Create a NewPayloadRequest (no clones required) and check optimistic sync verifications
let new_payload_request: NewPayloadRequest<T::EthSpec> =
block_message.try_into()?;
if let Err(e) = new_payload_request.perform_optimistic_sync_verifications()
{
warn!(
block_number = ?block_message.execution_payload().map(|payload| payload.block_number()),
info = "you can silence this warning with --disable-optimistic-finalized-sync",
error = ?e,
"Falling back to slow block hash verification"
);
None
} else {
Some(PayloadVerificationStatus::Optimistic)
}
}
_ => None,
}
_ => None,
}
} else {
Some(PayloadVerificationStatus::Irrelevant)
Expand Down
Loading