Add max_blobs_per_block check to data column gossip validation (#8198)

Addresses this spec change
https://github.com/ethereum/consensus-specs/pull/4650

Add `max_blobs_per_block` to gossip data column check so we reject large columns before processing. (we currently do this check during processing)


  


Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Jimmy Chen
2025-10-15 12:52:35 +11:00
committed by GitHub
parent 60df5f4ab6
commit 5886a48d96
3 changed files with 107 additions and 11 deletions

View File

@@ -161,6 +161,15 @@ pub enum GossipDataColumnError {
/// ///
/// The column sidecar is invalid and the peer is faulty /// The column sidecar is invalid and the peer is faulty
InconsistentProofsLength { cells_len: usize, proofs_len: usize }, InconsistentProofsLength { cells_len: usize, proofs_len: usize },
/// The number of KZG commitments exceeds the maximum number of blobs allowed for the fork. The
/// sidecar is invalid.
///
/// ## Peer scoring
/// The column sidecar is invalid and the peer is faulty
MaxBlobsPerBlockExceeded {
max_blobs_per_block: usize,
commitments_len: usize,
},
} }
impl From<BeaconChainError> for GossipDataColumnError { impl From<BeaconChainError> for GossipDataColumnError {
@@ -220,7 +229,7 @@ impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedDataColumn<T, O>
column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>, column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>,
chain: &BeaconChain<T>, chain: &BeaconChain<T>,
) -> Result<Self, GossipDataColumnError> { ) -> Result<Self, GossipDataColumnError> {
verify_data_column_sidecar(&column_sidecar)?; verify_data_column_sidecar(&column_sidecar, &chain.spec)?;
// Check if the data column is already in the DA checker cache. This happens when data columns // Check if the data column is already in the DA checker cache. This happens when data columns
// are made available through the `engine_getBlobs` method. If it exists in the cache, we know // are made available through the `engine_getBlobs` method. If it exists in the cache, we know
@@ -475,7 +484,7 @@ pub fn validate_data_column_sidecar_for_gossip<T: BeaconChainTypes, O: Observati
chain: &BeaconChain<T>, chain: &BeaconChain<T>,
) -> Result<GossipVerifiedDataColumn<T, O>, GossipDataColumnError> { ) -> Result<GossipVerifiedDataColumn<T, O>, GossipDataColumnError> {
let column_slot = data_column.slot(); let column_slot = data_column.slot();
verify_data_column_sidecar(&data_column)?; verify_data_column_sidecar(&data_column, &chain.spec)?;
verify_index_matches_subnet(&data_column, subnet, &chain.spec)?; verify_index_matches_subnet(&data_column, subnet, &chain.spec)?;
verify_sidecar_not_from_future_slot(chain, column_slot)?; verify_sidecar_not_from_future_slot(chain, column_slot)?;
verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?; verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?;
@@ -529,6 +538,7 @@ pub fn validate_data_column_sidecar_for_gossip<T: BeaconChainTypes, O: Observati
/// Verify if the data column sidecar is valid. /// Verify if the data column sidecar is valid.
fn verify_data_column_sidecar<E: EthSpec>( fn verify_data_column_sidecar<E: EthSpec>(
data_column: &DataColumnSidecar<E>, data_column: &DataColumnSidecar<E>,
spec: &ChainSpec,
) -> Result<(), GossipDataColumnError> { ) -> Result<(), GossipDataColumnError> {
if data_column.index >= E::number_of_columns() as u64 { if data_column.index >= E::number_of_columns() as u64 {
return Err(GossipDataColumnError::InvalidColumnIndex(data_column.index)); return Err(GossipDataColumnError::InvalidColumnIndex(data_column.index));
@@ -540,6 +550,14 @@ fn verify_data_column_sidecar<E: EthSpec>(
let cells_len = data_column.column.len(); let cells_len = data_column.column.len();
let commitments_len = data_column.kzg_commitments.len(); let commitments_len = data_column.kzg_commitments.len();
let proofs_len = data_column.kzg_proofs.len(); let proofs_len = data_column.kzg_proofs.len();
let max_blobs_per_block = spec.max_blobs_per_block(data_column.epoch()) as usize;
if commitments_len > max_blobs_per_block {
return Err(GossipDataColumnError::MaxBlobsPerBlockExceeded {
max_blobs_per_block,
commitments_len,
});
}
if cells_len != commitments_len { if cells_len != commitments_len {
return Err(GossipDataColumnError::InconsistentCommitmentsLength { return Err(GossipDataColumnError::InconsistentCommitmentsLength {
@@ -782,16 +800,22 @@ pub fn observe_gossip_data_column<T: BeaconChainTypes>(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::data_column_verification::{ use crate::data_column_verification::{
GossipDataColumnError, validate_data_column_sidecar_for_gossip, GossipDataColumnError, GossipVerifiedDataColumn, validate_data_column_sidecar_for_gossip,
}; };
use crate::observed_data_sidecars::Observe; use crate::observed_data_sidecars::Observe;
use crate::test_utils::BeaconChainHarness; use crate::test_utils::{
BeaconChainHarness, EphemeralHarnessType, generate_data_column_sidecars_from_block,
};
use eth2::types::BlobsBundle;
use execution_layer::test_utils::generate_blobs;
use std::sync::Arc;
use types::{DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkName, MainnetEthSpec}; use types::{DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkName, MainnetEthSpec};
type E = MainnetEthSpec; type E = MainnetEthSpec;
#[tokio::test] #[tokio::test]
async fn empty_data_column_sidecars_fails_validation() { async fn test_validate_data_column_sidecar_for_gossip() {
// Setting up harness is slow, we initialise once and use it for all gossip validation tests.
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
let harness = BeaconChainHarness::builder(E::default()) let harness = BeaconChainHarness::builder(E::default())
.spec(spec.into()) .spec(spec.into())
@@ -801,6 +825,44 @@ mod test {
.build(); .build();
harness.advance_slot(); harness.advance_slot();
let verify_fn = |column_sidecar: DataColumnSidecar<E>| {
let col_index = column_sidecar.index;
validate_data_column_sidecar_for_gossip::<_, Observe>(
column_sidecar.into(),
DataColumnSubnetId::from_column_index(col_index, &harness.spec),
&harness.chain,
)
};
empty_data_column_sidecars_fails_validation(&harness, &verify_fn).await;
data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await;
}
#[tokio::test]
async fn test_new_for_block_publishing() {
// Setting up harness is slow, we initialise once and use it for all gossip validation tests.
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
let harness = BeaconChainHarness::builder(E::default())
.spec(spec.into())
.deterministic_keypairs(64)
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
harness.advance_slot();
let verify_fn = |column_sidecar: DataColumnSidecar<E>| {
GossipVerifiedDataColumn::<_>::new_for_block_publishing(
column_sidecar.into(),
&harness.chain,
)
};
empty_data_column_sidecars_fails_validation(&harness, &verify_fn).await;
data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await;
}
async fn empty_data_column_sidecars_fails_validation<D>(
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
verify_fn: &impl Fn(DataColumnSidecar<E>) -> Result<D, GossipDataColumnError>,
) {
let slot = harness.get_current_slot(); let slot = harness.get_current_slot();
let state = harness.get_current_state(); let state = harness.get_current_state();
let ((block, _blobs_opt), _state) = harness let ((block, _blobs_opt), _state) = harness
@@ -823,14 +885,47 @@ mod test {
.unwrap(), .unwrap(),
}; };
let result = validate_data_column_sidecar_for_gossip::<_, Observe>( let result = verify_fn(column_sidecar);
column_sidecar.into(),
DataColumnSubnetId::from_column_index(index, &harness.spec),
&harness.chain,
);
assert!(matches!( assert!(matches!(
result.err(), result.err(),
Some(GossipDataColumnError::UnexpectedDataColumn) Some(GossipDataColumnError::UnexpectedDataColumn)
)); ));
} }
async fn data_column_sidecar_commitments_exceed_max_blobs_per_block<D>(
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
verify_fn: &impl Fn(DataColumnSidecar<E>) -> Result<D, GossipDataColumnError>,
) {
let slot = harness.get_current_slot();
let epoch = slot.epoch(E::slots_per_epoch());
let state = harness.get_current_state();
let max_blobs_per_block = harness.spec.max_blobs_per_block(epoch) as usize;
let fork = harness.spec.fork_name_at_epoch(epoch);
// Generate data column sidecar with blob count exceeding max_blobs_per_block.
let blob_count = max_blobs_per_block + 1;
let BlobsBundle::<E> {
commitments: preloaded_commitments_single,
proofs: _,
blobs: _,
} = generate_blobs(1, fork).unwrap().0;
let ((block, _blobs_opt), _state) = harness
.make_block_with_modifier(state, slot, |block| {
*block.body_mut().blob_kzg_commitments_mut().unwrap() =
vec![preloaded_commitments_single[0]; blob_count].into();
})
.await;
let column_sidecar = generate_data_column_sidecars_from_block(&block, &harness.spec)
.into_iter()
.next()
.unwrap();
let result = verify_fn(Arc::try_unwrap(column_sidecar).unwrap());
assert!(matches!(
result.err(),
Some(GossipDataColumnError::MaxBlobsPerBlockExceeded { .. })
));
}
} }

View File

@@ -3380,7 +3380,7 @@ pub fn generate_rand_block_and_data_columns<E: EthSpec>(
} }
/// Generate data column sidecars from pre-computed cells and proofs. /// Generate data column sidecars from pre-computed cells and proofs.
fn generate_data_column_sidecars_from_block<E: EthSpec>( pub fn generate_data_column_sidecars_from_block<E: EthSpec>(
block: &SignedBeaconBlock<E>, block: &SignedBeaconBlock<E>,
spec: &ChainSpec, spec: &ChainSpec,
) -> DataColumnSidecarList<E> { ) -> DataColumnSidecarList<E> {

View File

@@ -708,6 +708,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
| GossipDataColumnError::InvalidKzgProof { .. } | GossipDataColumnError::InvalidKzgProof { .. }
| GossipDataColumnError::UnexpectedDataColumn | GossipDataColumnError::UnexpectedDataColumn
| GossipDataColumnError::InvalidColumnIndex(_) | GossipDataColumnError::InvalidColumnIndex(_)
| GossipDataColumnError::MaxBlobsPerBlockExceeded { .. }
| GossipDataColumnError::InconsistentCommitmentsLength { .. } | GossipDataColumnError::InconsistentCommitmentsLength { .. }
| GossipDataColumnError::InconsistentProofsLength { .. } | GossipDataColumnError::InconsistentProofsLength { .. }
| GossipDataColumnError::NotFinalizedDescendant { .. } => { | GossipDataColumnError::NotFinalizedDescendant { .. } => {