Reject importing Gloas block until parent's payload is imported (#9382)

Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Lion - dapplion
2026-06-04 17:53:05 +02:00
committed by GitHub
parent 91456fb218
commit d98de9f8dd
6 changed files with 116 additions and 44 deletions

View File

@@ -70,7 +70,7 @@ use bls::{PublicKey, PublicKeyBytes};
use educe::Educe; use educe::Educe;
use eth2::types::{BlockGossip, EventKind}; use eth2::types::{BlockGossip, EventKind};
use execution_layer::PayloadStatus; use execution_layer::PayloadStatus;
pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus}; pub use fork_choice::{AttestationFromBlock, ParentImportStatus, PayloadVerificationStatus};
use metrics::TryExt; use metrics::TryExt;
use parking_lot::RwLockReadGuard; use parking_lot::RwLockReadGuard;
use proto_array::Block as ProtoBlock; use proto_array::Block as ProtoBlock;
@@ -870,7 +870,7 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch());
let (parent_block, block) = let (parent_block, block) =
verify_parent_block_is_known::<T>(&fork_choice_read_lock, block)?; verify_parent_block_and_envelope_are_known::<T>(&fork_choice_read_lock, block)?;
// [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root.
if let Ok(bid) = block.message().body().signed_execution_payload_bid() if let Ok(bid) = block.message().body().signed_execution_payload_bid()
@@ -882,13 +882,6 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
}); });
} }
// TODO(gloas) The following validation can only be completed once fork choice has been implemented:
// The block's parent execution payload (defined by bid.parent_block_hash) has been seen
// (via gossip or non-gossip sources) (a client MAY queue blocks for processing
// once the parent payload is retrieved). If execution_payload verification of block's execution
// payload parent by an execution node is complete, verify the block's execution payload
// parent (defined by bid.parent_block_hash) passes all validation.
drop(fork_choice_read_lock); drop(fork_choice_read_lock);
// Track the number of skip slots between the block and its parent. // Track the number of skip slots between the block and its parent.
@@ -1381,32 +1374,23 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
.observe_proposal(block_root, block.message()) .observe_proposal(block_root, block.message())
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?;
if let Some(parent) = chain match chain
.canonical_head .canonical_head
.fork_choice_read_lock() .fork_choice_read_lock()
.get_block(&block.parent_root()) .get_parent_import_status(block.as_block())
{ {
// Reject any block where the parent has an invalid payload. It's impossible for a valid ParentImportStatus::Imported(parent) => {
// block to descend from an invalid parent. if parent.execution_status.is_invalid() {
if parent.execution_status.is_invalid() { return Err(BlockError::ParentExecutionPayloadInvalid {
return Err(BlockError::ParentExecutionPayloadInvalid { parent_root: block.parent_root(),
});
}
}
ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => {
return Err(BlockError::ParentUnknown {
parent_root: block.parent_root(), parent_root: block.parent_root(),
}); });
} }
} else {
// Reject any block if its parent is not known to fork choice.
//
// A block that is not in fork choice is either:
//
// - Not yet imported: we should reject this block because we should only import a child
// after its parent has been fully imported.
// - Pre-finalized: if the parent block is _prior_ to finalization, we should ignore it
// because it will revert finalization. Note that the finalized block is stored in fork
// choice, so we will not reject any child of the finalized block (this is relevant during
// genesis).
return Err(BlockError::ParentUnknown {
parent_root: block.parent_root(),
});
} }
/* /*
@@ -1862,19 +1846,20 @@ pub fn get_block_header_root(block_header: &SignedBeaconBlockHeader) -> Hash256
block_root block_root
} }
/// Verify the parent of `block` is known, returning some information about the parent block from /// Verify the parent block — and, for a post-Gloas FULL child, the parent payload — are known to
/// fork choice. /// fork choice; both missing cases return `ParentUnknown`.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn verify_parent_block_is_known<T: BeaconChainTypes>( fn verify_parent_block_and_envelope_are_known<T: BeaconChainTypes>(
fork_choice_read_lock: &RwLockReadGuard<BeaconForkChoice<T>>, fork_choice_read_lock: &RwLockReadGuard<BeaconForkChoice<T>>,
block: Arc<SignedBeaconBlock<T::EthSpec>>, block: Arc<SignedBeaconBlock<T::EthSpec>>,
) -> Result<(ProtoBlock, Arc<SignedBeaconBlock<T::EthSpec>>), BlockError> { ) -> Result<(ProtoBlock, Arc<SignedBeaconBlock<T::EthSpec>>), BlockError> {
if let Some(proto_block) = fork_choice_read_lock.get_block(&block.parent_root()) { match fork_choice_read_lock.get_parent_import_status(&block) {
Ok((proto_block, block)) ParentImportStatus::Imported(parent) => Ok((parent, block)),
} else { ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => {
Err(BlockError::ParentUnknown { Err(BlockError::ParentUnknown {
parent_root: block.parent_root(), parent_root: block.parent_root(),
}) })
}
} }
} }
@@ -1901,7 +1886,7 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>(
if !chain if !chain
.canonical_head .canonical_head
.fork_choice_read_lock() .fork_choice_read_lock()
.contains_block(&block.parent_root()) .is_parent_imported(block.as_block())
{ {
return Err(BlockError::ParentUnknown { return Err(BlockError::ParentUnknown {
parent_root: block.parent_root(), parent_root: block.parent_root(),

View File

@@ -85,7 +85,7 @@ pub use beacon_fork_choice_store::{
}; };
pub use block_verification::{ pub use block_verification::{
BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock,
IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, ParentImportStatus,
PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars,
get_block_root, signature_verify_chain_segment, get_block_root, signature_verify_chain_segment,
}; };

View File

@@ -9,7 +9,7 @@ use beacon_chain::{
custody_context::NodeCustodyType, custody_context::NodeCustodyType,
test_utils::{ test_utils::{
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
MakeAttestationOptions, test_spec, MakeAttestationOptions, fork_name_from_env, test_spec,
}, },
}; };
use beacon_chain::{ use beacon_chain::{
@@ -359,6 +359,10 @@ fn update_data_column_signed_header<E: EthSpec>(
#[tokio::test] #[tokio::test]
async fn chain_segment_full_segment() { async fn chain_segment_full_segment() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
store_envelopes_for_chain_segment(&chain_segment, &harness); store_envelopes_for_chain_segment(&chain_segment, &harness);
@@ -399,6 +403,10 @@ async fn chain_segment_full_segment() {
#[tokio::test] #[tokio::test]
async fn chain_segment_varying_chunk_size() { async fn chain_segment_varying_chunk_size() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode);
let blocks: Vec<RangeSyncBlock<E>> = let blocks: Vec<RangeSyncBlock<E>> =
@@ -679,6 +687,10 @@ async fn get_invalid_sigs_harness(
} }
#[tokio::test] #[tokio::test]
async fn invalid_signature_gossip_block() { async fn invalid_signature_gossip_block() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
// Ensure the block will be rejected if imported on its own (without gossip checking). // Ensure the block will be rejected if imported on its own (without gossip checking).
@@ -735,6 +747,10 @@ async fn invalid_signature_gossip_block() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_block_proposal() { async fn invalid_signature_block_proposal() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let (chain_segment, chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
let harness = get_invalid_sigs_harness(&chain_segment).await; let harness = get_invalid_sigs_harness(&chain_segment).await;
@@ -774,6 +790,10 @@ async fn invalid_signature_block_proposal() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_randao_reveal() { async fn invalid_signature_randao_reveal() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
let harness = get_invalid_sigs_harness(&chain_segment).await; let harness = get_invalid_sigs_harness(&chain_segment).await;
@@ -802,6 +822,10 @@ async fn invalid_signature_randao_reveal() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_proposer_slashing() { async fn invalid_signature_proposer_slashing() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
let harness = get_invalid_sigs_harness(&chain_segment).await; let harness = get_invalid_sigs_harness(&chain_segment).await;
@@ -844,6 +868,10 @@ async fn invalid_signature_proposer_slashing() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_attester_slashing() { async fn invalid_signature_attester_slashing() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
let harness = get_invalid_sigs_harness(&chain_segment).await; let harness = get_invalid_sigs_harness(&chain_segment).await;
@@ -965,6 +993,10 @@ async fn invalid_signature_attester_slashing() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_attestation() { async fn invalid_signature_attestation() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await;
let mut checked_attestation = false; let mut checked_attestation = false;
@@ -1090,6 +1122,10 @@ async fn invalid_signature_deposit() {
#[tokio::test] #[tokio::test]
async fn invalid_signature_exit() { async fn invalid_signature_exit() {
// TODO(gloas): re-enable for Gloas once range sync imports payload envelopes.
if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await;
for &block_index in BLOCK_INDICES { for &block_index in BLOCK_INDICES {
let harness = get_invalid_sigs_harness(&chain_segment).await; let harness = get_invalid_sigs_harness(&chain_segment).await;

View File

@@ -3148,6 +3148,14 @@ async fn weak_subjectivity_sync_test(
.store .store
.put_payload_envelope(&wss_block_root, &envelope) .put_payload_envelope(&wss_block_root, &envelope)
.unwrap(); .unwrap();
// `from_anchor` doesn't mark the anchor's payload received, so do it here; otherwise the
// first forward block (a FULL child of the anchor) would be rejected with `ParentUnknown`.
beacon_chain
.canonical_head
.fork_choice_write_lock()
.on_valid_payload_envelope_received(wss_block_root)
.unwrap();
} }
// Apply blocks forward to reach head. // Apply blocks forward to reach head.

View File

@@ -207,6 +207,18 @@ pub enum InvalidPayloadAttestation {
}, },
} }
/// The import status of a block's parent, as seen by fork choice.
#[allow(clippy::large_enum_variant)]
pub enum ParentImportStatus {
/// The parent block is imported and the child's bid commits to a parent payload known to fork
/// choice.
Imported(ProtoBlock),
/// The parent block is not known to fork choice.
UnknownBlock,
/// The parent block is known, but the child's bid commits to a payload not known to fork choice.
UnknownPayload,
}
impl<T> From<String> for Error<T> { impl<T> From<String> for Error<T> {
fn from(e: String) -> Self { fn from(e: String) -> Self {
Error::ProtoArrayStringError(e) Error::ProtoArrayStringError(e)
@@ -1537,6 +1549,37 @@ where
&& self.is_finalized_checkpoint_or_descendant(*block_root) && self.is_finalized_checkpoint_or_descendant(*block_root)
} }
/// Returns `true` if the block's parent is imported (and, for a post-Gloas FULL child, its
/// parent's payload is imported too). See [`Self::get_parent_import_status`].
pub fn is_parent_imported(&self, block: &SignedBeaconBlock<E>) -> bool {
matches!(
self.get_parent_import_status(block),
ParentImportStatus::Imported(_)
)
}
/// Returns the import status of the parent of `block`.
///
/// A post-Gloas FULL child also requires the parent's payload (committed to by the child's bid)
/// to have been received by fork choice.
pub fn get_parent_import_status(&self, block: &SignedBeaconBlock<E>) -> ParentImportStatus {
if let Some(parent_block) = self.get_block(&block.parent_root()) {
let Some(parent_block_hash) = parent_block.execution_payload_block_hash else {
// Pre-Gloas parent: payload is embedded in the block, so treat as imported.
return ParentImportStatus::Imported(parent_block);
};
if block.is_parent_block_full(parent_block_hash)
&& !self.is_payload_received(&block.parent_root())
{
ParentImportStatus::UnknownPayload
} else {
ParentImportStatus::Imported(parent_block)
}
} else {
ParentImportStatus::UnknownBlock
}
}
/// Called by the proposer to decide whether to build on the full or empty parent. /// Called by the proposer to decide whether to build on the full or empty parent.
pub fn should_build_on_full( pub fn should_build_on_full(
&self, &self,

View File

@@ -4,9 +4,9 @@ mod metrics;
pub use crate::fork_choice::{ pub use crate::fork_choice::{
AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters,
InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus, InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, ParentImportStatus,
PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, PayloadVerificationStatus, PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29,
ResetPayloadStatuses, QueuedAttestation, ResetPayloadStatuses,
}; };
pub use fork_choice_store::ForkChoiceStore; pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{ pub use proto_array::{