Gloas payload cache (#9209)

In Gloas, beacon blocks are imported into fork choice immediately - the payload envelope and data columns arrive
separately. KZG commitments moved from the column sidecar into the execution payload bid, so the existing
`DataAvailabilityChecker` (which assumes block and data are coupled) can't be used for Gloas.


  * Introduced `PendingPayloadCache` to keep track of payload and data columns per block root.
* Added gossip column verification
* Added support for Gloas data column reconstruction
* Payload envelope verification simplified: removed `MaybeAvailableEnvelope`, `ExecutedEnvelope`, `EnvelopeImportData`

Not yet implemented (tracked with TODOs):
- Proper lookup sync for Gloas columns arriving before blocks
- Partial column merging for Gloas
- Moving `load_gloas_payload_bid` disk reads off the async runtime
- Backfill/range sync for Gloas

Based on @eserilev's PR and work in progress. See also #9202 for verification.


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

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

Co-Authored-By: Daniel Knopik <daniel@dknopik.de>

Co-Authored-By: Daniel Knopik <107140945+dknopik@users.noreply.github.com>

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

Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Daniel Knopik
2026-05-13 09:03:34 +02:00
committed by GitHub
parent 9101ddc69d
commit 1a68631180
41 changed files with 2351 additions and 536 deletions

View File

@@ -33,6 +33,7 @@ use crate::data_column_verification::{
GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn,
verify_kzg_for_data_column_list,
};
use crate::kzg_utils::validate_data_columns_with_commitments;
use crate::metrics::{
KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES,
};
@@ -490,8 +491,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
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)
verify_columns_against_block(&self.kzg, available_block.block(), columns)
}
}
}
@@ -504,13 +504,17 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
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() {
match available_block.data() {
AvailableBlockData::NoData => {}
AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs),
AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns),
AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs.iter().cloned()),
AvailableBlockData::DataColumns(columns) => {
// Each block has its own commitments. For Gloas they live in the bid; for
// Fulu they live inline on the column. Verify per block and let the helper
// pick the right path.
verify_columns_against_block(&self.kzg, available_block.block(), columns)?;
}
}
}
@@ -519,11 +523,6 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
.map_err(AvailabilityCheckError::InvalidBlobs)?;
}
if !all_data_columns.is_empty() {
verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg)
.map_err(AvailabilityCheckError::InvalidColumn)?;
}
Ok(())
}
@@ -605,9 +604,21 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
metrics::inc_counter(&KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS);
let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME);
let columns: Vec<_> = verified_data_columns
.into_iter()
.map(|c| c.into_inner())
.collect();
// Fulu columns carry their commitments; reconstruction needs the count to drive the
// per-blob recovery loop.
let kzg_commitments = columns
.first()
.and_then(|c| c.kzg_commitments().ok().cloned())
.ok_or(AvailabilityCheckError::InvalidVariant)?;
let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns(
&self.kzg,
&verified_data_columns,
columns,
&kzg_commitments,
&self.spec,
)
.map_err(|e| {
@@ -676,6 +687,35 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
}
}
/// Verify a batch of data columns belonging to a single block, picking the right commitment
/// source for the block's fork (Fulu: inline on column; Gloas: from the embedded payload bid).
fn verify_columns_against_block<E: EthSpec>(
kzg: &Kzg,
block: &SignedBeaconBlock<E>,
columns: &[Arc<DataColumnSidecar<E>>],
) -> Result<(), AvailabilityCheckError> {
if columns.is_empty() {
return Ok(());
}
if block.fork_name_unchecked().gloas_enabled() {
let commitments = block
.message()
.body()
.signed_execution_payload_bid()
.map(|bid| bid.message.blob_kzg_commitments.clone())
.map_err(|_| {
AvailabilityCheckError::Unexpected(
"Gloas block missing signed_execution_payload_bid".to_string(),
)
})?;
validate_data_columns_with_commitments(kzg, columns.iter(), commitments.as_ref())
.map_err(AvailabilityCheckError::InvalidColumn)
} else {
verify_kzg_for_data_column_list(columns.iter(), kzg)
.map_err(AvailabilityCheckError::InvalidColumn)
}
}
/// Helper struct to group data availability checker metrics.
pub struct DataAvailabilityCheckerMetrics {
pub block_cache_size: usize,
@@ -874,10 +914,13 @@ impl<E: EthSpec> AvailableBlock<E> {
match &block_data {
AvailableBlockData::NoData => {
if columns_required {
return Err(AvailabilityCheckError::MissingCustodyColumns);
} else if blobs_required {
return Err(AvailabilityCheckError::MissingBlobs);
// For Gloas, DA is checked for the PayloadEnvelope, not for the block.
if !block.fork_name_unchecked().gloas_enabled() {
if columns_required {
return Err(AvailabilityCheckError::MissingCustodyColumns);
} else if blobs_required {
return Err(AvailabilityCheckError::MissingBlobs);
}
}
}
AvailableBlockData::Blobs(blobs) => {