range sync

This commit is contained in:
Eitan Seri-Levi
2026-04-28 15:39:40 +02:00
parent d8790f6677
commit 82dde267b5
6 changed files with 205 additions and 46 deletions

View File

@@ -3150,7 +3150,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
};
// Import the blocks into the chain.
for signature_verified_block in signature_verified_blocks {
for (signature_verified_block, _envelope) in signature_verified_blocks {
let block_slot = signature_verified_block.slot();
match self
.process_block(

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::{
@@ -320,6 +321,20 @@ pub enum BlockError {
bid_parent_root: Hash256,
block_parent_root: Hash256,
},
/// The child block is known but its parent execution payload envelope has not been received yet.
///
/// ## Peer scoring
///
/// 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>),
PayloadEnvelopeError {
e: Box<EnvelopeError>,
penalize_peer: bool,
},
}
/// Which specific signature(s) are invalid in a SignedBeaconBlock
@@ -486,6 +501,36 @@ impl From<ArithError> for BlockError {
}
}
impl From<EnvelopeError> for BlockError {
fn from(e: EnvelopeError) -> Self {
let penalize_peer = match &e {
// REJECT per spec: peer sent invalid envelope data
EnvelopeError::BadSignature
| EnvelopeError::BuilderIndexMismatch { .. }
| EnvelopeError::BlockHashMismatch { .. }
| EnvelopeError::SlotMismatch { .. }
| EnvelopeError::IncorrectBlockProposer { .. } => true,
// IGNORE per spec: not the peer's fault
EnvelopeError::BlockRootUnknown { .. }
| EnvelopeError::PriorToFinalization { .. }
| EnvelopeError::UnknownValidator { .. } => false,
// Internal errors: not the peer's fault
EnvelopeError::BeaconChainError(_)
| EnvelopeError::BeaconStateError(_)
| EnvelopeError::BlockProcessingError(_)
| EnvelopeError::EnvelopeProcessingError(_)
| EnvelopeError::ExecutionPayloadError(_)
| EnvelopeError::BlockError(_)
| EnvelopeError::InternalError(_)
| EnvelopeError::OptimisticSyncNotSupported { .. } => false,
};
BlockError::PayloadEnvelopeError {
e: Box::new(e),
penalize_peer,
}
}
}
/// Stores information about verifying a payload against an execution engine.
#[derive(Debug, PartialEq, Clone, Encode, Decode)]
pub struct PayloadVerificationOutcome {
@@ -583,10 +628,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![]);
}
@@ -615,14 +667,29 @@ 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::NoData,
&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
@@ -632,7 +699,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)?;
}
@@ -643,7 +710,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);
}
@@ -1191,7 +1258,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())
@@ -1946,6 +2013,21 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>(
BlockError::from(BeaconChainError::MissingBeaconBlock(block.parent_root()))
})?;
// For post-Gloas parent blocks, the execution payload arrives via the envelope.
// If the parent's execution payload envelope hasn't arrived yet,
// return an unknown parent error so the block gets sent to the
// reprocess queue.
if chain
.spec
.fork_name_at_slot::<T::EthSpec>(parent_block.slot())
.gloas_enabled()
{
let _envelope = chain
.store
.get_payload_envelope(&root)?
.ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?;
}
// Load the parent block's state from the database, returning an error if it is not found.
// It is an error because if we know the parent block we should also know the parent state.
// Retrieve any state that is advanced through to at most `block.slot()`: this is

View File

@@ -2,10 +2,11 @@ use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityC
pub use crate::data_availability_checker::{
AvailableBlock, AvailableBlockData, MaybeAvailableBlock,
};
use crate::payload_envelope_verification::AvailableEnvelope;
use crate::{BeaconChainTypes, PayloadVerificationOutcome};
use educe::Educe;
use state_processing::ConsensusContext;
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use types::data::BlobIdentifier;
use types::{
@@ -45,38 +46,60 @@ impl<E: EthSpec> LookupBlock<E> {
/// This includes any and all blobs/columns required, including zero if
/// none are required. This can happen if the block is pre-deneb or if
/// it's simply past the DA boundary.
#[derive(Clone, Educe)]
#[educe(Hash(bound(E: EthSpec)))]
pub struct RangeSyncBlock<E: EthSpec> {
block: AvailableBlock<E>,
pub enum RangeSyncBlock<E: EthSpec> {
Base(AvailableBlock<E>),
Gloas {
block: Arc<SignedBeaconBlock<E>>,
envelope: Option<Box<AvailableEnvelope<E>>>,
},
}
impl<E: EthSpec> Hash for RangeSyncBlock<E> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.block_root().hash(state);
}
}
impl<E: EthSpec> Debug for RangeSyncBlock<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "RpcBlock({:?})", self.block_root())
write!(f, "RangeSyncBlock({:?})", self.block_root())
}
}
impl<E: EthSpec> RangeSyncBlock<E> {
pub fn block_root(&self) -> Hash256 {
self.block.block_root()
match self {
RangeSyncBlock::Base(block) => block.block_root(),
RangeSyncBlock::Gloas { block, .. } => block.canonical_root(),
}
}
pub fn as_block(&self) -> &SignedBeaconBlock<E> {
self.block.block()
match self {
RangeSyncBlock::Base(block) => block.block(),
RangeSyncBlock::Gloas { block, .. } => block,
}
}
pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.block_cloned()
match self {
RangeSyncBlock::Base(block) => block.block_cloned(),
RangeSyncBlock::Gloas { block, .. } => block.clone(),
}
}
pub fn block_data(&self) -> &AvailableBlockData<E> {
self.block.data()
match self {
RangeSyncBlock::Base(block) => block.data(),
RangeSyncBlock::Gloas { .. } => {
unreachable!("block_data called on Gloas variant — use envelope data instead")
}
}
}
}
impl<E: EthSpec> RangeSyncBlock<E> {
/// Constructs an `RangeSyncBlock` from a block and availability data.
/// Constructs a `RangeSyncBlock` from a block and availability data.
///
/// # Errors
///
@@ -94,32 +117,53 @@ impl<E: EthSpec> RangeSyncBlock<E> {
T: BeaconChainTypes<EthSpec = E>,
{
let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?;
Ok(Self {
block: available_block,
})
Ok(Self::Base(available_block))
}
pub fn new_gloas(
block: Arc<SignedBeaconBlock<E>>,
envelope: Option<Box<AvailableEnvelope<E>>>,
) -> Self {
Self::Gloas { block, envelope }
}
#[allow(clippy::type_complexity)]
pub fn deconstruct(self) -> (Hash256, Arc<SignedBeaconBlock<E>>, AvailableBlockData<E>) {
self.block.deconstruct()
match self {
RangeSyncBlock::Base(block) => block.deconstruct(),
RangeSyncBlock::Gloas { .. } => {
unreachable!("deconstruct called on Gloas variant")
}
}
}
pub fn n_blobs(&self) -> usize {
match self.block_data() {
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::Blobs(blobs) => blobs.len(),
match self {
RangeSyncBlock::Base(block) => match block.data() {
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::Blobs(blobs) => blobs.len(),
},
RangeSyncBlock::Gloas { .. } => 0,
}
}
pub fn n_data_columns(&self) -> usize {
match self.block_data() {
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::DataColumns(columns) => columns.len(),
match self {
RangeSyncBlock::Base(block) => match block.data() {
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::DataColumns(columns) => columns.len(),
},
RangeSyncBlock::Gloas { .. } => 0,
}
}
pub fn into_available_block(self) -> AvailableBlock<E> {
self.block
match self {
RangeSyncBlock::Base(block) => block,
RangeSyncBlock::Gloas { .. } => {
unreachable!("into_available_block called on Gloas variant")
}
}
}
}
@@ -387,31 +431,31 @@ impl<E: EthSpec> AsBlock<E> for AvailableBlock<E> {
impl<E: EthSpec> AsBlock<E> for RangeSyncBlock<E> {
fn slot(&self) -> Slot {
self.as_block().slot()
RangeSyncBlock::as_block(self).slot()
}
fn epoch(&self) -> Epoch {
self.as_block().epoch()
RangeSyncBlock::as_block(self).epoch()
}
fn parent_root(&self) -> Hash256 {
self.as_block().parent_root()
RangeSyncBlock::as_block(self).parent_root()
}
fn state_root(&self) -> Hash256 {
self.as_block().state_root()
RangeSyncBlock::as_block(self).state_root()
}
fn signed_block_header(&self) -> SignedBeaconBlockHeader {
self.as_block().signed_block_header()
RangeSyncBlock::as_block(self).signed_block_header()
}
fn message(&self) -> BeaconBlockRef<'_, E> {
self.as_block().message()
RangeSyncBlock::as_block(self).message()
}
fn as_block(&self) -> &SignedBeaconBlock<E> {
self.block.as_block()
RangeSyncBlock::as_block(self)
}
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
self.block.block_cloned()
RangeSyncBlock::block_cloned(self)
}
fn canonical_root(&self) -> Hash256 {
self.block.block_root()
self.block_root()
}
}

View File

@@ -21,7 +21,7 @@ use tracing::{debug, error, instrument};
use types::data::{BlobIdentifier, FixedBlobSidecarList, PartialDataColumn};
use types::{
BlobSidecar, BlobSidecarList, BlockImportSource, ChainSpec, DataColumnSidecar,
DataColumnSidecarList, Epoch, EthSpec, Hash256, PartialDataColumnSidecarError,
DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarError,
PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, new_non_zero_usize,
};
@@ -540,6 +540,11 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
self.da_check_required_for_epoch(epoch) && self.spec.is_peer_das_enabled_for_epoch(epoch)
}
/// Determines if execution payload envelopes are required for an epoch (Gloas and later).
pub fn envelopes_required_for_epoch(&self, epoch: Epoch) -> bool {
self.spec.fork_name_at_epoch(epoch) >= ForkName::Gloas
}
/// See `Self::blobs_required_for_epoch`
fn blobs_required_for_block(&self, block: &SignedBeaconBlock<T::EthSpec>) -> bool {
block.num_expected_blobs() > 0 && self.blobs_required_for_epoch(block.epoch())