Process envelopes correctly

This commit is contained in:
Pawan Dhananjay
2026-03-26 14:45:49 -07:00
parent 4ca10e95be
commit 2a2647411b
11 changed files with 219 additions and 40 deletions

View File

@@ -2951,12 +2951,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
};
// Import the blocks into the chain.
for signature_verified_block in signature_verified_blocks {
// Import the blocks (and envelopes for Gloas) into the chain.
for (signature_verified_block, maybe_envelope) in signature_verified_blocks {
let block_root = signature_verified_block.block_root();
let block_slot = signature_verified_block.slot();
match self
.process_block(
signature_verified_block.block_root(),
block_root,
signature_verified_block,
notify_execution_layer,
BlockImportSource::RangeSync,
@@ -2969,6 +2970,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
AvailabilityProcessingStatus::Imported(block_root) => {
// The block was imported successfully.
imported_blocks.push((block_root, block_slot));
// Gloas: process the envelope now that the block is in fork choice.
if let Some(envelope) = maybe_envelope
&& let Err(error) = self
.process_range_sync_envelope(
block_root,
envelope,
notify_execution_layer,
)
.await
{
return ChainSegmentResult::Failed {
imported_blocks,
error: BlockError::EnvelopeError(Box::new(error)),
};
}
}
AvailabilityProcessingStatus::MissingComponents(slot, block_root) => {
warn!(
@@ -7219,7 +7236,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
block_data: AvailableBlockData<T::EthSpec>,
) -> Option<StoreOp<'_, T::EthSpec>> {
match block_data {
AvailableBlockData::NoData => None,
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
AvailableBlockData::Blobs(blobs) => {
debug!(
%block_root,

View File

@@ -60,6 +60,7 @@ use crate::execution_payload::{
};
use crate::kzg_utils::blobs_to_data_column_sidecars;
use crate::observed_block_producers::SeenBlock;
use crate::payload_envelope_verification::{AvailableEnvelope, EnvelopeError};
use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS;
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
use crate::{
@@ -328,6 +329,8 @@ pub enum BlockError {
/// It's unclear if this block is valid, but it cannot be fully verified without the parent's
/// execution payload envelope.
ParentEnvelopeUnknown { parent_root: Hash256 },
/// An error occurred while processing the execution payload envelope during range sync.
EnvelopeError(Box<EnvelopeError>),
}
/// Which specific signature(s) are invalid in a SignedBeaconBlock
@@ -591,10 +594,17 @@ pub(crate) fn process_block_slash_info<T: BeaconChainTypes, TErr: BlockBlobError
/// The given `chain_segment` must contain only blocks from the same epoch, otherwise an error
/// will be returned.
#[instrument(skip_all)]
#[allow(clippy::type_complexity)]
pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
mut chain_segment: Vec<(Hash256, RangeSyncBlock<T::EthSpec>)>,
chain: &BeaconChain<T>,
) -> Result<Vec<SignatureVerifiedBlock<T>>, BlockError> {
) -> Result<
Vec<(
SignatureVerifiedBlock<T>,
Option<Box<AvailableEnvelope<T::EthSpec>>>,
)>,
BlockError,
> {
if chain_segment.is_empty() {
return Ok(vec![]);
}
@@ -623,14 +633,30 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
let consensus_context =
ConsensusContext::new(block.slot()).set_current_block_root(block_root);
let available_block = block.into_available_block();
let (available_block, envelope) = match block {
RangeSyncBlock::Base(ab) => (ab, None),
RangeSyncBlock::Gloas { block, envelope } => {
let ab = AvailableBlock::new(
block,
AvailableBlockData::DataInEnvelope,
&chain.data_availability_checker,
chain.spec.clone(),
)
.map_err(BlockError::AvailabilityCheck)?;
(ab, envelope)
}
};
available_blocks.push(available_block.clone());
signature_verified_blocks.push(SignatureVerifiedBlock {
block: MaybeAvailableBlock::Available(available_block),
block_root,
parent: None,
consensus_context,
});
signature_verified_blocks.push((
SignatureVerifiedBlock {
block: MaybeAvailableBlock::Available(available_block),
block_root,
parent: None,
consensus_context,
},
envelope,
));
}
chain
@@ -640,7 +666,7 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
// verify signatures
let pubkey_cache = get_validator_pubkey_cache(chain)?;
let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec);
for svb in &mut signature_verified_blocks {
for (svb, _) in &mut signature_verified_blocks {
signature_verifier
.include_all_signatures(svb.block.as_block(), &mut svb.consensus_context)?;
}
@@ -651,7 +677,7 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
drop(pubkey_cache);
if let Some(signature_verified_block) = signature_verified_blocks.first_mut() {
if let Some((signature_verified_block, _)) = signature_verified_blocks.first_mut() {
signature_verified_block.parent = Some(parent);
}
@@ -1199,7 +1225,7 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify());
match result {
Ok(_) => {
// gloas blocks are always available.
// Gloas blocks are always available — data arrives via the envelope.
let maybe_available = if chain
.spec
.fork_name_at_slot::<T::EthSpec>(block.slot())
@@ -1208,7 +1234,7 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
MaybeAvailableBlock::Available(
AvailableBlock::new(
block,
AvailableBlockData::NoData,
AvailableBlockData::DataInEnvelope,
&chain.data_availability_checker,
chain.spec.clone(),
)

View File

@@ -139,7 +139,9 @@ impl<E: EthSpec> RangeSyncBlock<E> {
pub fn n_blobs(&self) -> usize {
match self {
Self::Base(block) => match block.data() {
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::NoData
| AvailableBlockData::DataInEnvelope
| AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::Blobs(blobs) => blobs.len(),
},
Self::Gloas { .. } => 0,
@@ -149,7 +151,9 @@ impl<E: EthSpec> RangeSyncBlock<E> {
pub fn n_data_columns(&self) -> usize {
match self {
Self::Base(block) => match block.data() {
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::NoData
| AvailableBlockData::DataInEnvelope
| AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::DataColumns(columns) => columns.len(),
},
Self::Gloas { .. } => 0,

View File

@@ -366,7 +366,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
available_block: &AvailableBlock<T::EthSpec>,
) -> Result<(), AvailabilityCheckError> {
match available_block.data() {
AvailableBlockData::NoData => Ok(()),
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => Ok(()),
AvailableBlockData::Blobs(blobs) => verify_kzg_for_blob_list(blobs.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidBlobs),
AvailableBlockData::DataColumns(columns) => {
@@ -388,7 +388,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
for available_block in available_blocks {
match available_block.data().to_owned() {
AvailableBlockData::NoData => {}
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => {}
AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs),
AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns),
}
@@ -655,6 +655,9 @@ pub enum AvailableBlockData<E: EthSpec> {
Blobs(BlobSidecarList<E>),
/// Block is post-PeerDAS and has more than zero blobs
DataColumns(DataColumnSidecarList<E>),
/// Gloas: block data (payload + columns) arrives via the execution payload envelope,
/// not the block itself.
DataInEnvelope,
}
impl<E: EthSpec> AvailableBlockData<E> {
@@ -676,7 +679,7 @@ impl<E: EthSpec> AvailableBlockData<E> {
pub fn blobs(&self) -> Option<BlobSidecarList<E>> {
match self {
AvailableBlockData::NoData => None,
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
AvailableBlockData::Blobs(blobs) => Some(blobs.clone()),
AvailableBlockData::DataColumns(_) => None,
}
@@ -692,7 +695,7 @@ impl<E: EthSpec> AvailableBlockData<E> {
pub fn data_columns(&self) -> Option<DataColumnSidecarList<E>> {
match self {
AvailableBlockData::NoData => None,
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
AvailableBlockData::Blobs(_) => None,
AvailableBlockData::DataColumns(data_columns) => Some(data_columns.clone()),
}
@@ -757,6 +760,8 @@ impl<E: EthSpec> AvailableBlock<E> {
return Err(AvailabilityCheckError::MissingBlobs);
}
}
// Gloas: data availability is handled by the envelope path, not the block.
AvailableBlockData::DataInEnvelope => {}
AvailableBlockData::Blobs(blobs) => {
if !blobs_required {
return Err(AvailabilityCheckError::InvalidAvailableBlockData);
@@ -835,7 +840,7 @@ impl<E: EthSpec> AvailableBlock<E> {
pub fn has_blobs(&self) -> bool {
match self.blob_data {
AvailableBlockData::NoData => false,
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => false,
AvailableBlockData::Blobs(..) => true,
AvailableBlockData::DataColumns(_) => false,
}
@@ -863,6 +868,7 @@ impl<E: EthSpec> AvailableBlock<E> {
AvailableBlockData::DataColumns(data_columns) => {
AvailableBlockData::DataColumns(data_columns.clone())
}
AvailableBlockData::DataInEnvelope => AvailableBlockData::DataInEnvelope,
},
blobs_available_timestamp: self.blobs_available_timestamp,
spec: self.spec.clone(),

View File

@@ -275,7 +275,7 @@ impl<E: EthSpec> PendingComponents<E> {
// Block is available, construct `AvailableExecutedBlock`
let blobs_available_timestamp = match blob_data {
AvailableBlockData::NoData => None,
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => None,
AvailableBlockData::Blobs(_) => self
.verified_blobs
.iter()

View File

@@ -138,7 +138,7 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
};
let (blobs, data_columns) = match block.data() {
AvailableBlockData::NoData => (None, None),
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => (None, None),
AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None),
AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())),
};

View File

@@ -157,7 +157,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
match &block_data {
AvailableBlockData::NoData => (),
AvailableBlockData::NoData | AvailableBlockData::DataInEnvelope => (),
AvailableBlockData::Blobs(_) => new_oldest_blob_slot = Some(block.slot()),
AvailableBlockData::DataColumns(_) => {
new_oldest_data_column_slot = Some(block.slot())

View File

@@ -6,16 +6,25 @@ use fork_choice::PayloadVerificationStatus;
use slot_clock::SlotClock;
use store::StoreOp;
use tracing::{debug, error, info, info_span, instrument, warn};
use types::{BeaconState, BlockImportSource, Hash256, Slot};
use types::{BeaconState, BlockImportSource, EthSpec, Hash256, Slot};
use state_processing::{
VerifySignatures,
envelope_processing::{VerifyStateRoot, process_execution_payload_envelope},
};
use store::DatabaseBlock;
use super::{
AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData,
ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope,
ExecutedEnvelope, MaybeAvailableEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope,
gossip_verified_envelope::verify_envelope_consistency, load_snapshot_from_state_root,
payload_notifier::PayloadNotifier,
};
use crate::{
AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes,
NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics,
payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms,
NotifyExecutionLayer, PayloadVerificationOutcome, block_verification_types::AvailableBlockData,
metrics, payload_envelope_verification::ExecutionPendingEnvelope,
validator_monitor::get_slot_delay_ms,
};
const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64;
@@ -148,6 +157,125 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
}
/// Process an `AvailableEnvelope` from range sync. Unlike the gossip path, the block has
/// already been imported into fork choice so we can skip gossip-specific checks.
///
/// Steps: consistency checks, signature verification, EL verification (newPayload),
/// state processing, await EL result, and import.
#[instrument(skip_all, fields(block_root = ?block_root))]
pub async fn process_range_sync_envelope(
self: &Arc<Self>,
block_root: Hash256,
available_envelope: Box<AvailableEnvelope<T::EthSpec>>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<AvailabilityProcessingStatus, EnvelopeError> {
let signed_envelope = available_envelope.envelope().clone();
let block_slot = signed_envelope.slot();
// Load the block from store (just imported, guaranteed to exist).
let block = match self.store.try_get_full_block(&block_root)? {
Some(DatabaseBlock::Full(block)) => Arc::new(block),
Some(DatabaseBlock::Blinded(_)) | None => {
return Err(EnvelopeError::BlockRootUnknown { block_root });
}
};
// Envelope consistency checks.
let execution_bid = &block
.message()
.body()
.signed_execution_payload_bid()?
.message;
let latest_finalized_slot = self
.canonical_head
.cached_head()
.finalized_checkpoint()
.epoch
.start_slot(T::EthSpec::slots_per_epoch());
verify_envelope_consistency(
&signed_envelope.message,
&block,
execution_bid,
latest_finalized_slot,
)?;
// Load state for signature verification and state processing.
let snapshot =
load_snapshot_from_state_root::<T>(block_root, block.state_root(), &self.store)?;
// Verify the envelope signature.
let is_valid =
signed_envelope.verify_signature_with_state(&snapshot.pre_state, &self.spec)?;
if !is_valid {
return Err(EnvelopeError::BadSignature);
}
// Start EL verification (newPayload) as early as possible.
let payload_notifier = PayloadNotifier::new(
self.clone(),
signed_envelope.clone(),
block.clone(),
notify_execution_layer,
)?;
let payload_verification_future = async move {
let chain = payload_notifier.chain.clone();
if let Some(started_execution) = chain.slot_clock.now_duration() {
chain
.envelope_times_cache
.write()
.set_time_started_execution(block_root, block_slot, started_execution);
}
let payload_verification_status = payload_notifier.notify_new_payload().await?;
Ok(PayloadVerificationOutcome {
payload_verification_status,
})
};
let payload_verification_handle = self
.task_executor
.spawn_handle(
payload_verification_future,
"range_sync_envelope_payload_verification",
)
.ok_or(BeaconChainError::RuntimeShutdown)?;
// Run state processing (signatures already verified above).
let mut state = snapshot.pre_state;
process_execution_payload_envelope(
&mut state,
Some(snapshot.state_root),
&signed_envelope,
VerifySignatures::False,
VerifyStateRoot::True,
&self.spec,
)?;
// Build the ExecutionPendingEnvelope with Available status (columns already bundled).
let execution_pending = ExecutionPendingEnvelope {
signed_envelope: MaybeAvailableEnvelope::Available(*available_envelope),
import_data: EnvelopeImportData {
block_root,
post_state: Box::new(state),
},
payload_verification_handle,
};
// Await EL verification and import.
let executed_envelope = self
.clone()
.into_executed_payload_envelope(execution_pending)
.await?;
match executed_envelope {
ExecutedEnvelope::Available(envelope) => {
self.import_available_execution_payload_envelope(Box::new(envelope))
.await
}
ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError(
"Pending payload envelope not yet implemented".to_owned(),
)),
}
}
/// Accepts a fully-verified payload envelope and awaits on its payload verification handle to
/// get a fully `ExecutedEnvelope`.
///

View File

@@ -75,6 +75,10 @@ impl<E: EthSpec> AvailableEnvelope<E> {
}
}
pub fn envelope(&self) -> &Arc<SignedExecutionPayloadEnvelope<E>> {
&self.envelope
}
pub fn message(&self) -> &ExecutionPayloadEnvelope<E> {
&self.envelope.message
}

View File

@@ -1390,7 +1390,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
return None;
}
// BlobNotRequired is unreachable. Only constructed in `process_gossip_blob`
Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => {
// EnvelopeError is unreachable. Only constructed during range sync envelope processing.
Err(e @ BlockError::InternalError(_))
| Err(e @ BlockError::BlobNotRequired(_))
| Err(e @ BlockError::EnvelopeError(_)) => {
error!(error = %e, "Internal block gossip validation error");
return None;
}

View File

@@ -619,15 +619,6 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
return;
};
// TODO(gloas): Implement Gloas chain segment processing.
// Gloas blocks carry separate envelopes and need a different import path.
if downloaded_blocks
.iter()
.any(|b| matches!(b, RangeSyncBlock::Gloas { .. }))
{
todo!("Gloas chain segment processing");
}
let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64());
let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64());
let sent_blocks = downloaded_blocks.len();