Skip to content

Fix valid Gloas bids rejected with InvalidGasLimit after an empty parent - #9905

Open
jimmygchen wants to merge 5 commits into
sigp:unstablefrom
jimmygchen:codex/fix-gloas-parent-gas-limit
Open

Fix valid Gloas bids rejected with InvalidGasLimit after an empty parent#9905
jimmygchen wants to merge 5 commits into
sigp:unstablefrom
jimmygchen:codex/fix-gloas-parent-gas-limit

Conversation

@jimmygchen

Copy link
Copy Markdown
Member

Issue Addressed

After an empty parent, a valid payload bid can be rejected with InvalidGasLimit. The check currently uses the head state’s latest bid, which can refer to an unreceived payload, instead of the execution payload referenced by bid.parent_block_hash.

The spec validates against the known parent execution payload’s gas limit. This bug causes valid bids to be ignored and not propagated.

Proposed Changes

  • Find the known execution payload referenced by bid.parent_block_hash in the parent branch.
  • Use that payload’s committed gas limit for validation.
  • Ignore the bid when its parent execution payload is unknown.
  • Add a regression test for an unreceived head payload followed by a bid on the last known execution payload.

@jimmygchen
jimmygchen requested a review from jxs as a code owner August 21, 2026 14:18
@jimmygchen jimmygchen added bug Something isn't working gloas ready-for-review The code is ready for review labels Aug 21, 2026

@eserilev eserilev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one small testing nit and maybe opportunity to add a TODO

other than that lgtm

Comment on lines +510 to +523
}

#[test]
fn gas_limit_uses_known_execution_parent_after_unreceived_payload() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice if we could test GossipVerifiedBid::new directly here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. valid_bid now calls GossipVerifiedPayloadBid::new directly with a non-zero EL genesis hash and no stored genesis envelope. The finalized-history regression also goes through the constructor.

};

/// Find the beacon block carrying the known execution payload referenced by a bid.
pub(crate) fn find_execution_payload_block_root<T: BeaconChainTypes>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noting that since is_bid_compatible_with_head runs first this while loop is bounded. if we didnt call that check before, we could end up looping all the way back to finalization which in a non finalized network is still probably not a big deal. but nice to keep this bounded i think

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the ancestry lookup after the signature check and cache the gas limit by execution block hash, so we don't repeat the walk across slots.

};

if let Some(block_hash) = block.execution_payload_block_hash {
if fork_choice_read.is_payload_received(&root) && block_hash == parent_block_hash {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now we are (correctly assuming) that payload received means payload is valid. but with optimistic sync (when we eventually implement it) this might no longer be the case. might be worth adding a TODO here so that we dont forget

feel like theres probably other places in the codebase that could be affected once we impl optimistic sync

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note here.

@mergify

mergify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@mergify

mergify Bot commented Aug 24, 2026

Copy link
Copy Markdown

Some required checks have failed. Could you please take a look @jimmygchen? 🙏

@mergify mergify Bot added waiting-on-author The reviewer has suggested changes and awaits thier implementation. and removed ready-for-review The code is ready for review labels Aug 24, 2026
Use the EL genesis hash carried by the genesis bid when no payload envelope exists. Cache resolved parent gas limits per slot so invalid follow-up bids do not repeat historical ancestry scans.
@jimmygchen jimmygchen added ready-for-review The code is ready for review and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Aug 24, 2026

@eserilev eserilev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few changes and I think we should be good. most are just comment nits

could we also add one more test that checks the case where the payload isnt in fork choice or the store? would want to ensure we dont iterate to genesis in that case

Comment on lines +98 to +102
fn find_parent_execution_payload_gas_limit_in_store<T: BeaconChainTypes>(
store: &BeaconStore<T>,
search_start_beacon_block_root: Hash256,
parent_execution_block_hash: ExecutionBlockHash,
) -> Result<u64, PayloadBidError> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function could potentially iterate all the way back to genesis? am i wrong about that?

is_bid_compatible_with_head check can still pass even if its payload hasnt been imported yet.

Maybe we should only enter this loop when parent_block_hash == head_state.latest_block_hash() so that we have guarantees that the payload was in fact imported

Comment on lines +51 to +52
if fork_choice_read.is_payload_received(&block_root)
&& block_hash == parent_execution_block_hash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we could also add an elif block_hash == parent_execution_block_hash && !is_payload_received and return a payload unknown error if true

Comment on lines +106 to +108
PayloadBidError::InternalError(format!(
"failed to load beacon block while finding execution payload: {e:?}"
))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this should be an internal error, I think it should be a ParentExecutionPayloadUnknown. If we ever added bid reprocessing we could send the bid to the reprocess queue in this case and trigger a lookup?

Comment on lines +26 to +31
pub(super) enum ParentExecutionPayloadLocation {
/// The beacon block identifying the parent execution payload is available in fork choice.
BeaconBlock(Hash256),
/// Continue the canonical ancestry search in the database from this beacon block root.
SearchStoreFrom(Hash256),
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this enum is a bit confusing to me

What if we renamed to something like

enum ForkChoiceLookup {
   Found(Hash256),
   NotFound { continue_from: Hash256 }
}

or something where fork choice found and not found is mentioned explicitly?

SearchStoreFrom(Hash256),
}

/// Locate the beacon block that identifies the bid's parent execution payload in fork choice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe add additional info on what happens if its not found in fork choice?

Comment on lines +71 to +74
/// Return the execution block hash and gas limit represented by a beacon block.
///
/// Gloas genesis represents the EL genesis block with `parent_block_hash` because it has no payload
/// envelope of its own.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
/// Return the execution block hash and gas limit represented by a beacon block.
///
/// Gloas genesis represents the EL genesis block with `parent_block_hash` because it has no payload
/// envelope of its own.
/// Return the block hash and gas limit this beacon block committed to.
///
/// Pre-Gloas blocks embed the payload. Gloas blocks commit to a payload via their bid.
/// Gloas genesis is a special case since the block commits to an empty payload. The EL genesis
/// hash comes from the bid's `parent_block_hash`

Comment on lines +96 to +97
/// Continue searching canonical ancestry in the database after fork choice reaches its finalized
/// boundary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Continue searching canonical ancestry in the database after fork choice reaches its finalized
/// boundary.
/// Iterate parent roots in the store looking for the block that committed to the parent execution payload.
/// Used when the payload we are looking for has already been pruned by fork choice.

(this comment is only true if we add the parent_block_hash == head_state.latest_block_hash() check I mentioned below)

Comment on lines +113 to +114
// Non-genesis Gloas blocks only carry an execution payload after its envelope arrives.
// Compare the hash first so unrelated ancestors do not require another database read.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
// Non-genesis Gloas blocks only carry an execution payload after its envelope arrives.
// Compare the hash first so unrelated ancestors do not require another database read.
// For gloas blocks post-genesis, we must check that the payload has been received

Comment on lines +135 to +136
/// Load the gas limit for `parent_execution_block_hash` from the beacon block previously identified
/// as representing that execution payload.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Load the gas limit for `parent_execution_block_hash` from the beacon block previously identified
/// as representing that execution payload.
/// Return the gas limit committed by the beacon block at `parent_execution_payload_beacon_block_root`
/// for the payload `parent_execution_block_hash`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working gloas ready-for-review The code is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants