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

@@ -2851,11 +2851,42 @@ where
.await
.expect("newPayload should succeed");
// Store the envelope.
// Store the envelope and the data columns derived from the block.
//
// Production stores columns inside `import_available_execution_payload_envelope` after
// the cache is satisfied. The harness sidesteps that flow but must still persist columns
// or the `DataColumnMissing` invariant fires for any block with `num_expected_blobs > 0`.
let block = self
.chain
.store
.get_blinded_block(&block_root)
.expect("should read block from store")
.expect("block should exist in store");
let mut ops = vec![];
let block_with_full_payload = self
.chain
.store
.make_full_block(&block_root, block.clone())
.expect("should reconstruct full block");
let columns =
generate_data_column_sidecars_from_block(&block_with_full_payload, &self.spec);
if !columns.is_empty()
&& let Some(store_op) = self.chain.get_blobs_or_columns_store_op(
block_root,
block.slot(),
AvailableBlockData::DataColumns(columns),
)
{
ops.push(store_op);
}
ops.push(store::StoreOp::PutPayloadEnvelope(
block_root,
std::sync::Arc::new(signed_envelope),
));
self.chain
.store
.put_payload_envelope(&block_root, &signed_envelope)
.expect("should store envelope");
.do_atomically_with_block_and_blobs_cache(ops)
.expect("should persist envelope and columns");
// Update fork choice so it knows the payload was received.
self.chain
@@ -2876,11 +2907,10 @@ where
block: Arc<SignedBeaconBlock<E>>,
) -> RangeSyncBlock<E> {
let block_root = block_root.unwrap_or_else(|| get_block_root(&block));
let has_blobs = block
.message()
.body()
.blob_kzg_commitments()
.is_ok_and(|c| !c.is_empty());
// For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the
// body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already
// handles both shapes.
let has_blobs = block.num_expected_blobs() > 0;
if !has_blobs {
return RangeSyncBlock::new(
block,
@@ -3782,7 +3812,26 @@ pub fn generate_rand_block_and_blobs<E: EthSpec>(
SignedBeaconBlock::Fulu(SignedBeaconBlockFulu {
ref mut message, ..
}) => add_blob_transactions!(message, FullPayloadFulu<E>, num_blobs, u, fork_name),
// TODO(EIP-7732) Add `SignedBeaconBlock::Gloas` variant
SignedBeaconBlock::Gloas(SignedBeaconBlockGloas {
ref mut message, ..
}) => {
// For Gloas, commitments are in the bid, not directly in the body.
// BlobSidecars cannot be created for Gloas because there's no merkle proof
// from the block body to the commitments. Return early with empty blob_sidecars.
let num_blobs = match num_blobs {
NumBlobs::Random => u.int_in_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS)?,
NumBlobs::Number(n) => n,
NumBlobs::None => 0,
};
let (bundle, _transactions) =
execution_layer::test_utils::generate_blobs::<E>(num_blobs, fork_name).unwrap();
message
.body
.signed_execution_payload_bid
.message
.blob_kzg_commitments = bundle.commitments.clone();
return Ok((block, blob_sidecars));
}
_ => return Ok((block, blob_sidecars)),
};