WIP: Gloas full/empty child fork harness + tests + Option B sketch

Harness/tests (foundation):
- make_gloas_block_with_status: produce a gloas block with explicit parent
  payload status (builds FULL vs EMPTY children); returns its data columns.
- TestRig::build_full_empty_fork: G(full) -> A(full) -> B(FULL child), A -> C(EMPTY).
- SimulateConfig::return_no_envelope_for_block: withhold a block's payload envelope.
- Tests: gloas_build_full_empty_fork_shape (shape), gloas_full_empty_children_
  retain_parent_for_payload (happy path), gloas_empty_child_continues_while_
  parent_payload_withheld (red: C must complete, B+A retained while payload withheld).

Option B sketch (untested, mod.rs) -- to be implemented properly:
- continue_child_lookups on a SingleBlock Imported result (children re-evaluate
  on parent block import, before its payload).
- retain a failed lookup while another lookup awaits it (is_awaited).
This commit is contained in:
dapplion
2026-06-05 00:29:40 +02:00
parent 67e1048878
commit 9afaaf71df
5 changed files with 416 additions and 61 deletions

View File

@@ -509,6 +509,13 @@ impl<T: BeaconChainTypes> BlockLookups<T> {
"Received lookup processing result"
);
let block_root = lookup.block_root();
// Gloas: a block imports into fork choice on block + columns, *before* its payload
// envelope. Children awaiting it must re-evaluate at that point: an EMPTY child can import
// on the parent block alone, while a FULL child re-awaits the parent's payload.
let block_imported = matches!(process_type, BlockProcessType::SingleBlock { .. })
&& matches!(result, BlockProcessingResult::Imported(..));
let lookup_result = match process_type {
BlockProcessType::SingleBlock { .. } => lookup.on_block_processing_result(result, cx),
BlockProcessType::SingleCustodyColumn(_) => {
@@ -519,6 +526,9 @@ impl<T: BeaconChainTypes> BlockLookups<T> {
}
};
self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx);
if block_imported {
self.continue_child_lookups(block_root, cx);
}
}
pub fn on_external_processing_result(
@@ -657,6 +667,22 @@ impl<T: BeaconChainTypes> BlockLookups<T> {
// update metrics because the lookup does not exist.
Err(LookupRequestError::UnknownLookup) => false,
Err(error) => {
// Retain a failed lookup while another lookup awaits it: a FULL Gloas child awaits
// its parent's payload, so the parent's failed payload download must not cascade-
// drop the child. The parent stays until its payload arrives (or it is reaped as
// stuck).
if let Some(block_root) = self.single_block_lookups.get(&id).map(|l| l.block_root())
&& self.is_awaited(block_root)
{
debug!(
id,
source,
?error,
?block_root,
"Retaining failed lookup awaited by a child"
);
return false;
}
debug!(id, source, ?error, "Dropping lookup on request error");
self.drop_lookup_and_children(id, error.into());
self.update_metrics();
@@ -665,6 +691,13 @@ impl<T: BeaconChainTypes> BlockLookups<T> {
}
}
/// Returns true if any lookup is awaiting `block_root` as its parent.
fn is_awaited(&self, block_root: Hash256) -> bool {
self.single_block_lookups
.values()
.any(|lookup| lookup.awaiting_parent() == Some(block_root))
}
/* Helper functions */
/// Drops all the single block requests and returns how many requests were dropped.