Convert RpcBlock to an enum that indicates availability (#8424)

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Mark Mackey <mark@sigmaprime.io>

Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com>

Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Eitan Seri-Levi
2026-01-27 21:59:32 -08:00
committed by GitHub
parent c4409cdf28
commit f7b5c7ee3f
23 changed files with 1368 additions and 579 deletions

View File

@@ -645,26 +645,36 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
&chain.spec,
)?;
// unzip chain segment and verify kzg in bulk
let (roots, blocks): (Vec<_>, Vec<_>) = chain_segment.into_iter().unzip();
let maybe_available_blocks = chain
.data_availability_checker
.verify_kzg_for_rpc_blocks(blocks)?;
// zip it back up
let mut signature_verified_blocks = roots
.into_iter()
.zip(maybe_available_blocks)
.map(|(block_root, maybe_available_block)| {
let consensus_context = ConsensusContext::new(maybe_available_block.slot())
.set_current_block_root(block_root);
SignatureVerifiedBlock {
block: maybe_available_block,
block_root,
parent: None,
consensus_context,
let mut available_blocks = Vec::with_capacity(chain_segment.len());
let mut signature_verified_blocks = Vec::with_capacity(chain_segment.len());
for (block_root, block) in chain_segment {
let consensus_context =
ConsensusContext::new(block.slot()).set_current_block_root(block_root);
match block {
RpcBlock::FullyAvailable(available_block) => {
available_blocks.push(available_block.clone());
signature_verified_blocks.push(SignatureVerifiedBlock {
block: MaybeAvailableBlock::Available(available_block),
block_root,
parent: None,
consensus_context,
});
}
})
.collect::<Vec<_>>();
RpcBlock::BlockOnly { .. } => {
// RangeSync and BackfillSync already ensure that the chain segment is fully available
// so this shouldn't be possible in practice.
return Err(BlockError::InternalError(
"Chain segment is not fully available".to_string(),
));
}
}
}
chain
.data_availability_checker
.batch_verify_kzg_for_available_blocks(&available_blocks)?;
// verify signatures
let pubkey_cache = get_validator_pubkey_cache(chain)?;
@@ -1297,16 +1307,28 @@ impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for RpcBlock<T::EthSpec>
// Perform an early check to prevent wasting time on irrelevant blocks.
let block_root = check_block_relevancy(self.as_block(), block_root, chain)
.map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?;
let maybe_available = chain
.data_availability_checker
.verify_kzg_for_rpc_block(self.clone())
.map_err(|e| {
BlockSlashInfo::SignatureNotChecked(
self.signed_block_header(),
BlockError::AvailabilityCheck(e),
)
})?;
SignatureVerifiedBlock::check_slashable(maybe_available, block_root, chain)?
let maybe_available_block = match &self {
RpcBlock::FullyAvailable(available_block) => {
chain
.data_availability_checker
.verify_kzg_for_available_block(available_block)
.map_err(|e| {
BlockSlashInfo::SignatureNotChecked(
self.signed_block_header(),
BlockError::AvailabilityCheck(e),
)
})?;
MaybeAvailableBlock::Available(available_block.clone())
}
// No need to perform KZG verification unless we have a fully available block
RpcBlock::BlockOnly { block, block_root } => MaybeAvailableBlock::AvailabilityPending {
block_root: *block_root,
block: block.clone(),
},
};
SignatureVerifiedBlock::check_slashable(maybe_available_block, block_root, chain)?
.into_execution_pending_block_slashable(block_root, chain, notify_execution_layer)
}

View File

@@ -1,204 +1,151 @@
use crate::data_availability_checker::AvailabilityCheckError;
pub use crate::data_availability_checker::{AvailableBlock, MaybeAvailableBlock};
use crate::data_column_verification::{CustodyDataColumn, CustodyDataColumnList};
use crate::{PayloadVerificationOutcome, get_block_root};
use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityChecker};
pub use crate::data_availability_checker::{
AvailableBlock, AvailableBlockData, MaybeAvailableBlock,
};
use crate::{BeaconChainTypes, PayloadVerificationOutcome};
use educe::Educe;
use ssz_types::VariableList;
use state_processing::ConsensusContext;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use types::data::BlobIdentifier;
use types::{
BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256,
BeaconBlockRef, BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256,
SignedBeaconBlock, SignedBeaconBlockHeader, Slot,
};
/// A block that has been received over RPC. It has 2 internal variants:
///
/// 1. `BlockAndBlobs`: A fully available post deneb block with all the blobs available. This variant
/// is only constructed after making consistency checks between blocks and blobs.
/// Hence, it is fully self contained w.r.t verification. i.e. this block has all the required
/// data to get verified and imported into fork choice.
/// 1. `FullyAvailable`: A fully available block. This can either be a pre-deneb block, a
/// post-Deneb block with blobs, a post-Fulu block with the columns the node is required to custody,
/// or a post-Deneb block that doesn't require blobs/columns. Hence, it is fully self contained w.r.t
/// verification. i.e. this block has all the required data to get verified and imported into fork choice.
///
/// 2. `Block`: This can be a fully available pre-deneb block **or** a post-deneb block that may or may
/// not require blobs to be considered fully available.
///
/// Note: We make a distinction over blocks received over gossip because
/// in a post-deneb world, the blobs corresponding to a given block that are received
/// over rpc do not contain the proposer signature for dos resistance.
/// 2. `BlockOnly`: This is a post-deneb block that requires blobs to be considered fully available.
#[derive(Clone, Educe)]
#[educe(Hash(bound(E: EthSpec)))]
pub struct RpcBlock<E: EthSpec> {
block_root: Hash256,
block: RpcBlockInner<E>,
pub enum RpcBlock<E: EthSpec> {
FullyAvailable(AvailableBlock<E>),
BlockOnly {
block: Arc<SignedBeaconBlock<E>>,
block_root: Hash256,
},
}
impl<E: EthSpec> Debug for RpcBlock<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "RpcBlock({:?})", self.block_root)
write!(f, "RpcBlock({:?})", self.block_root())
}
}
impl<E: EthSpec> RpcBlock<E> {
pub fn block_root(&self) -> Hash256 {
self.block_root
match self {
RpcBlock::FullyAvailable(available_block) => available_block.block_root(),
RpcBlock::BlockOnly { block_root, .. } => *block_root,
}
}
pub fn as_block(&self) -> &SignedBeaconBlock<E> {
match &self.block {
RpcBlockInner::Block(block) => block,
RpcBlockInner::BlockAndBlobs(block, _) => block,
RpcBlockInner::BlockAndCustodyColumns(block, _) => block,
match self {
RpcBlock::FullyAvailable(available_block) => available_block.block(),
RpcBlock::BlockOnly { block, .. } => block,
}
}
pub fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
match &self.block {
RpcBlockInner::Block(block) => block.clone(),
RpcBlockInner::BlockAndBlobs(block, _) => block.clone(),
RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(),
match self {
RpcBlock::FullyAvailable(available_block) => available_block.block_cloned(),
RpcBlock::BlockOnly { block, .. } => block.clone(),
}
}
pub fn blobs(&self) -> Option<&BlobSidecarList<E>> {
match &self.block {
RpcBlockInner::Block(_) => None,
RpcBlockInner::BlockAndBlobs(_, blobs) => Some(blobs),
RpcBlockInner::BlockAndCustodyColumns(_, _) => None,
pub fn block_data(&self) -> Option<&AvailableBlockData<E>> {
match self {
RpcBlock::FullyAvailable(available_block) => Some(available_block.data()),
RpcBlock::BlockOnly { .. } => None,
}
}
pub fn custody_columns(&self) -> Option<&CustodyDataColumnList<E>> {
match &self.block {
RpcBlockInner::Block(_) => None,
RpcBlockInner::BlockAndBlobs(_, _) => None,
RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => Some(data_columns),
}
}
}
/// Note: This variant is intentionally private because we want to safely construct the
/// internal variants after applying consistency checks to ensure that the block and blobs
/// are consistent with respect to each other.
#[derive(Debug, Clone, Educe)]
#[educe(Hash(bound(E: EthSpec)))]
enum RpcBlockInner<E: EthSpec> {
/// Single block lookup response. This should potentially hit the data availability cache.
Block(Arc<SignedBeaconBlock<E>>),
/// This variant is used with parent lookups and by-range responses. It should have all blobs
/// ordered, all block roots matching, and the correct number of blobs for this block.
BlockAndBlobs(Arc<SignedBeaconBlock<E>>, BlobSidecarList<E>),
/// This variant is used with parent lookups and by-range responses. It should have all
/// requested data columns, all block roots matching for this block.
BlockAndCustodyColumns(Arc<SignedBeaconBlock<E>>, CustodyDataColumnList<E>),
}
impl<E: EthSpec> RpcBlock<E> {
/// Constructs a `Block` variant.
pub fn new_without_blobs(
block_root: Option<Hash256>,
/// Constructs an `RpcBlock` from a block and optional availability data.
///
/// This function creates an RpcBlock which can be in one of two states:
/// - `FullyAvailable`: When `block_data` is provided, the block contains all required
/// data for verification.
/// - `BlockOnly`: When `block_data` is `None`, the block may still need additional
/// data to be considered fully available (used during block lookups or when blobs
/// will arrive separately).
///
/// # Validation
///
/// When `block_data` is provided, this function validates that:
/// - Block data is not provided when not required.
/// - Required blobs are present and match the expected count.
/// - Required custody columns are included based on the nodes custody requirements.
///
/// # Errors
///
/// Returns `AvailabilityCheckError` if:
/// - `InvalidAvailableBlockData`: Block data is provided but not required.
/// - `MissingBlobs`: Block requires blobs but they are missing or incomplete.
/// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete.
pub fn new<T>(
block: Arc<SignedBeaconBlock<E>>,
) -> Self {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
Self {
block_root,
block: RpcBlockInner::Block(block),
block_data: Option<AvailableBlockData<E>>,
da_checker: &DataAvailabilityChecker<T>,
spec: Arc<ChainSpec>,
) -> Result<Self, AvailabilityCheckError>
where
T: BeaconChainTypes<EthSpec = E>,
{
match block_data {
Some(block_data) => Ok(RpcBlock::FullyAvailable(AvailableBlock::new(
block, block_data, da_checker, spec,
)?)),
None => Ok(RpcBlock::BlockOnly {
block_root: block.canonical_root(),
block,
}),
}
}
/// Constructs a new `BlockAndBlobs` variant after making consistency
/// checks between the provided blocks and blobs. This struct makes no
/// guarantees about whether blobs should be present, only that they are
/// consistent with the block. An empty list passed in for `blobs` is
/// viewed the same as `None` passed in.
pub fn new(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<E>>,
blobs: Option<BlobSidecarList<E>>,
) -> Result<Self, AvailabilityCheckError> {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
// Treat empty blob lists as if they are missing.
let blobs = blobs.filter(|b| !b.is_empty());
if let (Some(blobs), Ok(block_commitments)) = (
blobs.as_ref(),
block.message().body().blob_kzg_commitments(),
) {
if blobs.len() != block_commitments.len() {
return Err(AvailabilityCheckError::MissingBlobs);
}
for (blob, &block_commitment) in blobs.iter().zip(block_commitments.iter()) {
let blob_commitment = blob.kzg_commitment;
if blob_commitment != block_commitment {
return Err(AvailabilityCheckError::KzgCommitmentMismatch {
block_commitment,
blob_commitment,
});
}
}
}
let inner = match blobs {
Some(blobs) => RpcBlockInner::BlockAndBlobs(block, blobs),
None => RpcBlockInner::Block(block),
};
Ok(Self {
block_root,
block: inner,
})
}
pub fn new_with_custody_columns(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<E>>,
custody_columns: Vec<CustodyDataColumn<E>>,
) -> Result<Self, AvailabilityCheckError> {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
if block.num_expected_blobs() > 0 && custody_columns.is_empty() {
// The number of required custody columns is out of scope here.
return Err(AvailabilityCheckError::MissingCustodyColumns);
}
// Treat empty data column lists as if they are missing.
let inner = if !custody_columns.is_empty() {
RpcBlockInner::BlockAndCustodyColumns(block, VariableList::new(custody_columns)?)
} else {
RpcBlockInner::Block(block)
};
Ok(Self {
block_root,
block: inner,
})
}
#[allow(clippy::type_complexity)]
pub fn deconstruct(
self,
) -> (
Hash256,
Arc<SignedBeaconBlock<E>>,
Option<BlobSidecarList<E>>,
Option<CustodyDataColumnList<E>>,
Option<AvailableBlockData<E>>,
) {
let block_root = self.block_root();
match self.block {
RpcBlockInner::Block(block) => (block_root, block, None, None),
RpcBlockInner::BlockAndBlobs(block, blobs) => (block_root, block, Some(blobs), None),
RpcBlockInner::BlockAndCustodyColumns(block, data_columns) => {
(block_root, block, None, Some(data_columns))
match self {
RpcBlock::FullyAvailable(available_block) => {
let (block_root, block, block_data) = available_block.deconstruct();
(block_root, block, Some(block_data))
}
RpcBlock::BlockOnly { block, block_root } => (block_root, block, None),
}
}
pub fn n_blobs(&self) -> usize {
match &self.block {
RpcBlockInner::Block(_) | RpcBlockInner::BlockAndCustodyColumns(_, _) => 0,
RpcBlockInner::BlockAndBlobs(_, blobs) => blobs.len(),
if let Some(block_data) = self.block_data() {
match block_data {
AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0,
AvailableBlockData::Blobs(blobs) => blobs.len(),
}
} else {
0
}
}
pub fn n_data_columns(&self) -> usize {
match &self.block {
RpcBlockInner::Block(_) | RpcBlockInner::BlockAndBlobs(_, _) => 0,
RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => data_columns.len(),
if let Some(block_data) = self.block_data() {
match block_data {
AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0,
AvailableBlockData::DataColumns(columns) => columns.len(),
}
} else {
0
}
}
}
@@ -500,17 +447,21 @@ impl<E: EthSpec> AsBlock<E> for RpcBlock<E> {
self.as_block().message()
}
fn as_block(&self) -> &SignedBeaconBlock<E> {
match &self.block {
RpcBlockInner::Block(block) => block,
RpcBlockInner::BlockAndBlobs(block, _) => block,
RpcBlockInner::BlockAndCustodyColumns(block, _) => block,
match self {
Self::BlockOnly {
block,
block_root: _,
} => block,
Self::FullyAvailable(available_block) => available_block.block(),
}
}
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
match &self.block {
RpcBlockInner::Block(block) => block.clone(),
RpcBlockInner::BlockAndBlobs(block, _) => block.clone(),
RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(),
match self {
RpcBlock::FullyAvailable(available_block) => available_block.block_cloned(),
RpcBlock::BlockOnly {
block,
block_root: _,
} => block.clone(),
}
}
fn canonical_root(&self) -> Hash256 {

View File

@@ -1,17 +1,17 @@
use crate::blob_verification::{
GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list,
};
use crate::block_verification_types::{
AvailabilityPendingExecutedBlock, AvailableExecutedBlock, RpcBlock,
};
use crate::block_verification_types::{AvailabilityPendingExecutedBlock, AvailableExecutedBlock};
use crate::data_availability_checker::overflow_lru_cache::{
DataAvailabilityCheckerInner, ReconstructColumnsDecision,
};
use crate::{
BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics,
};
use educe::Educe;
use kzg::Kzg;
use slot_clock::SlotClock;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Debug;
use std::num::NonZeroUsize;
@@ -31,8 +31,8 @@ mod state_lru_cache;
use crate::data_availability_checker::error::Error;
use crate::data_column_verification::{
CustodyDataColumn, GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn,
KzgVerifiedDataColumn, verify_kzg_for_data_column_list,
GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn,
verify_kzg_for_data_column_list,
};
use crate::metrics::{
KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES,
@@ -366,151 +366,51 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
.remove_pre_execution_block(block_root);
}
/// Verifies kzg commitments for an RpcBlock, returns a `MaybeAvailableBlock` that may
/// include the fully available block.
///
/// WARNING: This function assumes all required blobs are already present, it does NOT
/// check if there are any missing blobs.
pub fn verify_kzg_for_rpc_block(
/// Verifies kzg commitments for an `AvailableBlock`.
pub fn verify_kzg_for_available_block(
&self,
block: RpcBlock<T::EthSpec>,
) -> Result<MaybeAvailableBlock<T::EthSpec>, AvailabilityCheckError> {
let (block_root, block, blobs, data_columns) = block.deconstruct();
if self.blobs_required_for_block(&block) {
return if let Some(blob_list) = blobs {
verify_kzg_for_blob_list(blob_list.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidBlobs)?;
Ok(MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::Blobs(blob_list),
blobs_available_timestamp: None,
spec: self.spec.clone(),
}))
} else {
Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block })
};
available_block: &AvailableBlock<T::EthSpec>,
) -> Result<(), AvailabilityCheckError> {
match available_block.data() {
AvailableBlockData::NoData => Ok(()),
AvailableBlockData::Blobs(blobs) => verify_kzg_for_blob_list(blobs.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidBlobs),
AvailableBlockData::DataColumns(columns) => {
verify_kzg_for_data_column_list(columns.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidColumn)
}
}
if self.data_columns_required_for_block(&block) {
return if let Some(data_column_list) = data_columns.as_ref() {
verify_kzg_for_data_column_list(
data_column_list
.iter()
.map(|custody_column| custody_column.as_data_column()),
&self.kzg,
)
.map_err(AvailabilityCheckError::InvalidColumn)?;
Ok(MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::DataColumns(
data_column_list
.into_iter()
.map(|d| d.clone_arc())
.collect(),
),
blobs_available_timestamp: None,
spec: self.spec.clone(),
}))
} else {
Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block })
};
}
Ok(MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::NoData,
blobs_available_timestamp: None,
spec: self.spec.clone(),
}))
}
/// Checks if a vector of blocks are available. Returns a vector of `MaybeAvailableBlock`
/// This is more efficient than calling `verify_kzg_for_rpc_block` in a loop as it does
/// all kzg verification at once
///
/// WARNING: This function assumes all required blobs are already present, it does NOT
/// check if there are any missing blobs.
/// Performs batch kzg verification for a vector of `AvailableBlocks`. This is more efficient than
/// calling `verify_kzg_for_available_block` in a loop.
#[instrument(skip_all)]
pub fn verify_kzg_for_rpc_blocks(
pub fn batch_verify_kzg_for_available_blocks(
&self,
blocks: Vec<RpcBlock<T::EthSpec>>,
) -> Result<Vec<MaybeAvailableBlock<T::EthSpec>>, AvailabilityCheckError> {
let mut results = Vec::with_capacity(blocks.len());
let all_blobs = blocks
.iter()
.filter(|block| self.blobs_required_for_block(block.as_block()))
// this clone is cheap as it's cloning an Arc
.filter_map(|block| block.blobs().cloned())
.flatten()
.collect::<Vec<_>>();
available_blocks: &[AvailableBlock<T::EthSpec>],
) -> Result<(), AvailabilityCheckError> {
let mut all_blobs = Vec::new();
let mut all_data_columns = Vec::new();
for available_block in available_blocks {
match available_block.data().to_owned() {
AvailableBlockData::NoData => {}
AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs),
AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns),
}
}
// verify kzg for all blobs at once
if !all_blobs.is_empty() {
verify_kzg_for_blob_list(all_blobs.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidBlobs)?;
}
let all_data_columns = blocks
.iter()
.filter(|block| self.data_columns_required_for_block(block.as_block()))
// this clone is cheap as it's cloning an Arc
.filter_map(|block| block.custody_columns().cloned())
.flatten()
.map(CustodyDataColumn::into_inner)
.collect::<Vec<_>>();
// verify kzg for all data columns at once
if !all_data_columns.is_empty() {
// Attributes fault to the specific peer that sent an invalid column
verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidColumn)?;
}
for block in blocks {
let (block_root, block, blobs, data_columns) = block.deconstruct();
let maybe_available_block = if self.blobs_required_for_block(&block) {
if let Some(blobs) = blobs {
MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::Blobs(blobs),
blobs_available_timestamp: None,
spec: self.spec.clone(),
})
} else {
MaybeAvailableBlock::AvailabilityPending { block_root, block }
}
} else if self.data_columns_required_for_block(&block) {
if let Some(data_columns) = data_columns {
MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::DataColumns(
data_columns.into_iter().map(|d| d.into_inner()).collect(),
),
blobs_available_timestamp: None,
spec: self.spec.clone(),
})
} else {
MaybeAvailableBlock::AvailabilityPending { block_root, block }
}
} else {
MaybeAvailableBlock::Available(AvailableBlock {
block_root,
block,
blob_data: AvailableBlockData::NoData,
blobs_available_timestamp: None,
spec: self.spec.clone(),
})
};
results.push(maybe_available_block);
}
Ok(results)
Ok(())
}
/// Determines the blob requirements for a block. If the block is pre-deneb, no blobs are required.
@@ -749,7 +649,8 @@ async fn availability_cache_maintenance_service<T: BeaconChainTypes>(
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
// TODO(#8633) move this to `block_verification_types.rs`
pub enum AvailableBlockData<E: EthSpec> {
/// Block is pre-Deneb or has zero blobs
NoData,
@@ -759,31 +660,161 @@ pub enum AvailableBlockData<E: EthSpec> {
DataColumns(DataColumnSidecarList<E>),
}
impl<E: EthSpec> AvailableBlockData<E> {
pub fn new_with_blobs(blobs: BlobSidecarList<E>) -> Self {
if blobs.is_empty() {
Self::NoData
} else {
Self::Blobs(blobs)
}
}
pub fn new_with_data_columns(columns: DataColumnSidecarList<E>) -> Self {
if columns.is_empty() {
Self::NoData
} else {
Self::DataColumns(columns)
}
}
pub fn blobs(&self) -> Option<BlobSidecarList<E>> {
match self {
AvailableBlockData::NoData => None,
AvailableBlockData::Blobs(blobs) => Some(blobs.clone()),
AvailableBlockData::DataColumns(_) => None,
}
}
pub fn blobs_len(&self) -> usize {
if let Some(blobs) = self.blobs() {
blobs.len()
} else {
0
}
}
pub fn data_columns(&self) -> Option<DataColumnSidecarList<E>> {
match self {
AvailableBlockData::NoData => None,
AvailableBlockData::Blobs(_) => None,
AvailableBlockData::DataColumns(data_columns) => Some(data_columns.clone()),
}
}
pub fn data_columns_len(&self) -> usize {
if let Some(data_columns) = self.data_columns() {
data_columns.len()
} else {
0
}
}
}
/// A fully available block that is ready to be imported into fork choice.
#[derive(Debug)]
#[derive(Debug, Clone, Educe)]
#[educe(Hash(bound(E: EthSpec)))]
pub struct AvailableBlock<E: EthSpec> {
block_root: Hash256,
block: Arc<SignedBeaconBlock<E>>,
#[educe(Hash(ignore))]
blob_data: AvailableBlockData<E>,
#[educe(Hash(ignore))]
/// Timestamp at which this block first became available (UNIX timestamp, time since 1970).
blobs_available_timestamp: Option<Duration>,
#[educe(Hash(ignore))]
pub spec: Arc<ChainSpec>,
}
impl<E: EthSpec> AvailableBlock<E> {
pub fn __new_for_testing(
block_root: Hash256,
block: Arc<SignedBeaconBlock<E>>,
data: AvailableBlockData<E>,
/// Constructs an `AvailableBlock` from a block and blob data.
///
/// This function validates that:
/// - Block data is not provided when not required (pre-Deneb or past DA boundary)
/// - Required blobs are present and match the expected count
/// - Required custody columns are complete based on the node's custody requirements
/// - KZG commitments in blobs match those in the block
///
/// Returns `AvailabilityCheckError` if:
/// - `InvalidAvailableBlockData`: Block data is provided but not required
/// - `MissingBlobs`: Block requires blobs but they are missing or incomplete
/// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete
/// - `KzgCommitmentMismatch`: Blob KZG commitment doesn't match block commitment
pub fn new<T>(
block: Arc<SignedBeaconBlock<T::EthSpec>>,
block_data: AvailableBlockData<T::EthSpec>,
da_checker: &DataAvailabilityChecker<T>,
spec: Arc<ChainSpec>,
) -> Self {
Self {
block_root,
block,
blob_data: data,
blobs_available_timestamp: None,
spec,
) -> Result<Self, AvailabilityCheckError>
where
T: BeaconChainTypes<EthSpec = E>,
{
// Ensure block availability
let blobs_required = da_checker.blobs_required_for_block(&block);
let columns_required = da_checker.data_columns_required_for_block(&block);
match &block_data {
AvailableBlockData::NoData => {
if columns_required {
return Err(AvailabilityCheckError::MissingCustodyColumns);
} else if blobs_required {
return Err(AvailabilityCheckError::MissingBlobs);
}
}
AvailableBlockData::Blobs(blobs) => {
if !blobs_required {
return Err(AvailabilityCheckError::InvalidAvailableBlockData);
}
let Ok(block_kzg_commitments) = block.message().body().blob_kzg_commitments()
else {
return Err(AvailabilityCheckError::Unexpected(
"Expected blobs but could not fetch KZG commitments from the block"
.to_owned(),
));
};
if blobs.len() != block_kzg_commitments.len() {
return Err(AvailabilityCheckError::MissingBlobs);
}
for (blob, &block_kzg_commitment) in blobs.iter().zip(block_kzg_commitments.iter())
{
if blob.kzg_commitment != block_kzg_commitment {
return Err(AvailabilityCheckError::KzgCommitmentMismatch {
blob_commitment: blob.kzg_commitment,
block_commitment: block_kzg_commitment,
});
}
}
}
AvailableBlockData::DataColumns(data_columns) => {
if !columns_required {
return Err(AvailabilityCheckError::InvalidAvailableBlockData);
}
let mut column_indices = da_checker
.custody_context
.sampling_columns_for_epoch(block.epoch(), &spec)
.iter()
.collect::<HashSet<_>>();
for data_column in data_columns {
column_indices.remove(data_column.index());
}
if !column_indices.is_empty() {
return Err(AvailabilityCheckError::MissingCustodyColumns);
}
}
}
Ok(Self {
block_root: block.canonical_root(),
block,
blob_data: block_data,
blobs_available_timestamp: None,
spec: spec.clone(),
})
}
pub fn block(&self) -> &SignedBeaconBlock<E> {
@@ -801,6 +832,10 @@ impl<E: EthSpec> AvailableBlock<E> {
&self.blob_data
}
pub fn block_root(&self) -> Hash256 {
self.block_root
}
pub fn has_blobs(&self) -> bool {
match self.blob_data {
AvailableBlockData::NoData => false,
@@ -864,7 +899,9 @@ impl<E: EthSpec> MaybeAvailableBlock<E> {
mod test {
use super::*;
use crate::CustodyContext;
use crate::block_verification_types::RpcBlock;
use crate::custody_context::NodeCustodyType;
use crate::data_column_verification::CustodyDataColumn;
use crate::test_utils::{
EphemeralHarnessType, NumBlobs, generate_data_column_indices_rand_order,
generate_rand_block_and_data_columns, get_kzg,
@@ -926,8 +963,16 @@ mod test {
&spec,
);
let block_root = Hash256::random();
let custody_columns = custody_context.custody_columns_for_epoch(None, &spec);
let requested_columns = &custody_columns[..10];
// Get 10 columns using the "latest" CGC (head) that block lookup would use.
// The CGC change becomes effective after CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS,
// which is typically epoch 2+ for MinimalEthSpec.
let future_epoch = Epoch::new(10); // Far enough in the future to have the CGC change effective
let requested_columns = custody_context.sampling_columns_for_epoch(future_epoch, &spec);
assert_eq!(
requested_columns.len(),
10,
"future epoch should have 10 sampling columns"
);
da_checker
.put_rpc_custody_columns(
block_root,
@@ -1005,8 +1050,16 @@ mod test {
&spec,
);
let block_root = Hash256::random();
let custody_columns = custody_context.custody_columns_for_epoch(None, &spec);
let requested_columns = &custody_columns[..10];
// Get 10 columns using the "latest" CGC that gossip subscriptions would use.
// The CGC change becomes effective after CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS,
// which is typically epoch 2+ for MinimalEthSpec.
let future_epoch = Epoch::new(10); // Far enough in the future to have the CGC change effective
let requested_columns = custody_context.sampling_columns_for_epoch(future_epoch, &spec);
assert_eq!(
requested_columns.len(),
10,
"future epoch should have 10 sampling columns"
);
let gossip_columns = data_columns
.into_iter()
.filter(|d| requested_columns.contains(d.index()))
@@ -1059,9 +1112,6 @@ mod test {
let custody_columns = if index == 0 {
// 128 valid data columns in the first block
data_columns
.into_iter()
.map(CustodyDataColumn::from_asserted_custody)
.collect::<Vec<_>>()
} else {
// invalid data columns in the second block
data_columns
@@ -1079,17 +1129,30 @@ mod test {
.clone(),
});
CustodyDataColumn::from_asserted_custody(Arc::new(invalid_sidecar))
.as_data_column()
.clone()
})
.collect::<Vec<_>>()
};
RpcBlock::new_with_custody_columns(None, Arc::new(block), custody_columns)
let block_data = AvailableBlockData::new_with_data_columns(custody_columns);
let da_checker = Arc::new(new_da_checker(spec.clone()));
RpcBlock::new(Arc::new(block), Some(block_data), &da_checker, spec.clone())
.expect("should create RPC block with custody columns")
})
.collect::<Vec<_>>();
let available_blocks = blocks_with_columns
.iter()
.filter_map(|block| match block {
RpcBlock::FullyAvailable(available_block) => Some(available_block.clone()),
RpcBlock::BlockOnly { .. } => None,
})
.collect::<Vec<_>>();
// WHEN verifying all blocks together (totalling 256 data columns)
let verification_result = da_checker.verify_kzg_for_rpc_blocks(blocks_with_columns);
let verification_result =
da_checker.batch_verify_kzg_for_available_blocks(&available_blocks);
// THEN batch block verification should fail due to 128 invalid columns in the second block
verification_result.expect_err("should have failed to verify blocks");
@@ -1132,7 +1195,7 @@ mod test {
// Add 64 columns to the da checker (enough to be able to reconstruct)
// Order by all_column_indices_ordered, then take first 64
let custody_columns = custody_context.custody_columns_for_epoch(None, &spec);
let custody_columns = custody_context.sampling_columns_for_epoch(epoch, &spec);
let custody_columns = custody_columns
.iter()
.filter_map(|&col_idx| data_columns.iter().find(|d| *d.index() == col_idx).cloned())

View File

@@ -22,6 +22,7 @@ pub enum Error {
BlockReplayError(state_processing::BlockReplayError),
RebuildingStateCaches(BeaconStateError),
SlotClockError,
InvalidAvailableBlockData,
}
#[derive(PartialEq, Eq)]
@@ -44,7 +45,8 @@ impl Error {
| Error::ParentStateMissing(_)
| Error::BlockReplayError(_)
| Error::RebuildingStateCaches(_)
| Error::SlotClockError => ErrorCategory::Internal,
| Error::SlotClockError
| Error::InvalidAvailableBlockData => ErrorCategory::Internal,
Error::InvalidBlobs { .. }
| Error::InvalidColumn { .. }
| Error::ReconstructColumnsError { .. }

View File

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

View File

@@ -78,7 +78,7 @@ pub use block_verification::{
BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock,
IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature,
PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars,
get_block_root,
get_block_root, signature_verify_chain_segment,
};
pub use block_verification_types::AvailabilityPendingExecutedBlock;
pub use block_verification_types::ExecutedBlock;

View File

@@ -1,12 +1,12 @@
use crate::blob_verification::GossipVerifiedBlob;
use crate::block_verification_types::{AsBlock, RpcBlock};
use crate::block_verification_types::{AsBlock, AvailableBlockData, RpcBlock};
use crate::custody_context::NodeCustodyType;
use crate::data_column_verification::CustodyDataColumn;
use crate::data_availability_checker::DataAvailabilityChecker;
use crate::graffiti_calculator::GraffitiSettings;
use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas};
use crate::observed_operations::ObservationOutcome;
pub use crate::persisted_beacon_chain::PersistedBeaconChain;
use crate::{BeaconBlockResponseWrapper, get_block_root};
use crate::{BeaconBlockResponseWrapper, CustodyContext, get_block_root};
use crate::{
BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler,
StateSkipConfig,
@@ -212,6 +212,34 @@ pub fn test_spec<E: EthSpec>() -> ChainSpec {
spec.target_aggregators_per_committee = DEFAULT_TARGET_AGGREGATORS;
spec
}
pub fn test_da_checker<E: EthSpec>(
spec: Arc<ChainSpec>,
node_custody_type: NodeCustodyType,
) -> DataAvailabilityChecker<EphemeralHarnessType<E>> {
let slot_clock = TestingSlotClock::new(
Slot::new(0),
Duration::from_secs(0),
Duration::from_secs(spec.seconds_per_slot),
);
let kzg = get_kzg(&spec);
let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap());
let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
let custody_context = Arc::new(CustodyContext::new(
node_custody_type,
ordered_custody_column_indices,
&spec,
));
let complete_blob_backfill = false;
DataAvailabilityChecker::new(
complete_blob_backfill,
slot_clock,
kzg,
store,
custody_context,
spec,
)
.expect("should initialise data availability checker")
}
pub struct Builder<T: BeaconChainTypes> {
eth_spec_instance: T::EthSpec,
@@ -2380,8 +2408,16 @@ where
) -> Result<SignedBeaconBlockHash, BlockError> {
self.set_current_slot(slot);
let (block, blob_items) = block_contents;
// Determine if block is available: it's available if it doesn't require blobs,
// or if it requires blobs and we have them
let has_blob_commitments = block
.message()
.body()
.blob_kzg_commitments()
.is_ok_and(|c| !c.is_empty());
let is_available = !has_blob_commitments || blob_items.is_some();
let rpc_block = self.build_rpc_block_from_blobs(block_root, block, blob_items)?;
let rpc_block = self.build_rpc_block_from_blobs(block, blob_items, is_available)?;
let block_hash: SignedBeaconBlockHash = self
.chain
.process_block(
@@ -2405,7 +2441,15 @@ where
let (block, blob_items) = block_contents;
let block_root = block.canonical_root();
let rpc_block = self.build_rpc_block_from_blobs(block_root, block, blob_items)?;
// Determine if block is available: it's available if it doesn't require blobs,
// or if it requires blobs and we have them
let has_blob_commitments = block
.message()
.body()
.blob_kzg_commitments()
.is_ok_and(|c| !c.is_empty());
let is_available = !has_blob_commitments || blob_items.is_some();
let rpc_block = self.build_rpc_block_from_blobs(block, blob_items, is_available)?;
let block_hash: SignedBeaconBlockHash = self
.chain
.process_block(
@@ -2436,7 +2480,13 @@ where
.blob_kzg_commitments()
.is_ok_and(|c| !c.is_empty());
if !has_blobs {
return RpcBlock::new_without_blobs(Some(block_root), block);
return RpcBlock::new(
block,
Some(AvailableBlockData::NoData),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)
.unwrap();
}
// Blobs are stored as data columns from Fulu (PeerDAS)
@@ -2447,23 +2497,39 @@ where
.get_data_columns(&block_root, fork_name)
.unwrap()
.unwrap();
let custody_columns = columns
.into_iter()
.map(CustodyDataColumn::from_asserted_custody)
.collect::<Vec<_>>();
RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns).unwrap()
let custody_columns = columns.into_iter().collect::<Vec<_>>();
let block_data = AvailableBlockData::new_with_data_columns(custody_columns);
RpcBlock::new(
block,
Some(block_data),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)
.unwrap()
} else {
let blobs = self.chain.get_blobs(&block_root).unwrap().blobs();
RpcBlock::new(Some(block_root), block, blobs).unwrap()
let block_data = if let Some(blobs) = blobs {
AvailableBlockData::new_with_blobs(blobs)
} else {
AvailableBlockData::NoData
};
RpcBlock::new(
block,
Some(block_data),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)
.unwrap()
}
}
/// Builds an `RpcBlock` from a `SignedBeaconBlock` and `BlobsList`.
pub fn build_rpc_block_from_blobs(
&self,
block_root: Hash256,
block: Arc<SignedBeaconBlock<E, FullPayload<E>>>,
blob_items: Option<(KzgProofs<E>, BlobsList<E>)>,
is_available: bool,
) -> Result<RpcBlock<E>, BlockError> {
Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) {
let epoch = block.slot().epoch(E::slots_per_epoch());
@@ -2476,11 +2542,37 @@ where
let columns = generate_data_column_sidecars_from_block(&block, &self.spec)
.into_iter()
.filter(|d| sampling_columns.contains(d.index()))
.map(CustodyDataColumn::from_asserted_custody)
.collect::<Vec<_>>();
RpcBlock::new_with_custody_columns(Some(block_root), block, columns)?
if is_available {
let block_data = AvailableBlockData::new_with_data_columns(columns);
RpcBlock::new(
block,
Some(block_data),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
} else {
RpcBlock::new(
block,
None,
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
}
} else if is_available {
RpcBlock::new(
block,
Some(AvailableBlockData::NoData),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
} else {
RpcBlock::new_without_blobs(Some(block_root), block)
RpcBlock::new(
block,
None,
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
}
} else {
let blobs = blob_items
@@ -2489,7 +2581,27 @@ where
})
.transpose()
.unwrap();
RpcBlock::new(Some(block_root), block, blobs)?
if is_available {
let block_data = if let Some(blobs) = blobs {
AvailableBlockData::new_with_blobs(blobs)
} else {
AvailableBlockData::NoData
};
RpcBlock::new(
block,
Some(block_data),
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
} else {
RpcBlock::new(
block,
None,
&self.chain.data_availability_checker,
self.chain.spec.clone(),
)?
}
})
}