Skip to content

Defer payload processing to next block#2

Closed
lodekeeper wants to merge 38 commits intomasterfrom
feat/deferred-payload-processing
Closed

Defer payload processing to next block#2
lodekeeper wants to merge 38 commits intomasterfrom
feat/deferred-payload-processing

Conversation

@lodekeeper
Copy link
Copy Markdown
Owner

@lodekeeper lodekeeper commented Apr 9, 2026

Summary

Defer execution payload processing to the next block for Gloas/ePBS (EIP-7732).

process_execution_payload no longer mutates beacon state. Instead, the CL effects (execution requests, builder payment, availability flag, latest_block_hash) are applied by a new process_parent_execution_payload function at the start of the next block. The builder commits to execution_requests_root in the bid, and the next proposer carries the parent requests in BeaconBlockBody.parent_execution_requests.

What changed

New types and fields

  • BeaconBlockBody.parent_execution_requests: ExecutionRequests — proposer delivers the parent execution requests
  • ExecutionPayloadBid.execution_requests_root: Root — builder commits to execution requests at bid time
  • Store.payload_states: Dict[Root, BeaconState] replaced by Store.payloads: Set[Root] — no longer caches intermediate post-payload states, just tracks which roots have been locally verified
  • state_root removed from ExecutionPayloadEnvelope — no longer needed since process_execution_payload does not mutate state

New functions

  • process_parent_execution_payload added as the first step in process_block — processes deferred execution requests (deposits, withdrawals, consolidations), queues builder payment, updates availability flag and latest_block_hash

Gossip validation (p2p-interface.md)

  • Beacon block topic: REJECT if parent_execution_requests does not match parent bid execution_requests_root (FULL parent) or is not empty (EMPTY parent)
  • Execution payload topic: REJECT if hash_tree_root(envelope.execution_requests) != bid.execution_requests_root

Validator guidance (validator.md)

  • New section on how proposers populate parent_execution_requests from the parent envelope

Fork boundaries (fork.md)

  • upgrade_to_gloas and upgrade_to_heze initialize execution_requests_root = hash_tree_root(ExecutionRequests())

Tests

  • New test_process_parent_execution_payload.py — full, empty, cross-epoch, invalid requests, builder payment, deposit ordering
  • Fork-choice tests updated for payload_states to payloads migration
  • Genesis and anchor helpers updated with execution_requests_root initialization

…nsition)

Moves execution request processing from process_execution_payload into
process_parent_execution_payload, which runs first in process_block of the
next beacon block. This eliminates payload_states entirely — only block_states
exist. process_execution_payload becomes pure verification.

Key changes:
- BeaconBlockBody: add parent_execution_requests field
- ExecutionPayloadEnvelope: remove state_root field
- process_block: add process_parent_execution_payload as first step
- process_execution_payload: pure verification, returns ExecutionRequests
- Store: replace payload_states with verified_execution_requests
- on_block: verify parent_execution_requests, always use block_states
- on_execution_payload: verify and buffer, no state persisted

Reference: https://gist.github.com/lodekeeper/6abbbc1f04bb1985f2f7bf165bfa0fa8

🤖 Generated with AI assistance
- Use local latest_block_header.parent_root in process_execution_payload
  for consistency with pure-verification refactor
- Add note about deposit slot semantics in process_parent_execution_payload
  (state.slot is child's slot, not parent's)
- Add pruning note for verified_execution_requests in Store

🤖 Generated with AI assistance
- Add note explaining why is_parent_block_full cannot be reused in
  process_parent_execution_payload (latest_block_hash not yet updated)
- Use local requests variable in return statement for consistency

🤖 Generated with AI assistance
@github-actions github-actions bot added the gloas label Apr 9, 2026
Comment on lines +289 to +291
# [Modified in Gloas]
# Removed `state_root` -- redundant with `beacon_block_root`
# since `process_execution_payload` no longer mutates state
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this container was added in gloas and didn't exist before, so [Modified in Gloas] in gloas doesn't make any sense, same for the comment you added there, this is fine to note in PR description (like rational why it was removed) but should not be part of the spec

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done — removed the comment entirely. The state_root removal rationale is already in the PR description.

signed_execution_payload_bid: SignedExecutionPayloadBid
# [New in Gloas:EIP7732]
payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]
# [New in Gloas]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
# [New in Gloas]
[New in Gloas:EIP7732]

please make sure to use the right comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed — using [New in Gloas:EIP7732] now.


```python
def process_block(state: BeaconState, block: BeaconBlock) -> None:
# [New in Gloas]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
# [New in Gloas]
# [New in Gloas:EIP7732]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 1616 to 1617
*Note*: `process_execution_payload` is now a pure verification step in state
transition. It is called when importing a signed execution payload proposed by
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this seems inaccurate since is not longer a "state transition" please reconsider how to update the spec in that regard

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reworded — now describes it as a "pure verification function called by fork-choice" instead of referencing "state transition".

Comment on lines +1687 to +1690
# [Modified in Gloas]
# Execution requests are verified by the execution engine and returned
# for fork-choice level buffering. State mutations are deferred to
# `process_parent_execution_payload` in the next block.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this isn't modified in gloas, the whole function here only exists in gloas, same as I noted in another comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed — the whole function is new in Gloas, no [Modified] annotation needed.

lodekeeper and others added 2 commits April 9, 2026 12:02
- Remove `[Modified in Gloas]` comment from ExecutionPayloadEnvelope
  (container is new in Gloas, not modified)
- Use `[New in Gloas:EIP7732]` consistently for parent_execution_requests
  and process_parent_execution_payload
- Reword process_execution_payload note to avoid "state transition"
  (it's a pure verification function called by fork-choice)
- Remove redundant `[Modified in Gloas]` annotation at return site

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove inline [New/Modified in Gloas:EIP7732] annotations from
  process_execution_payload (new function — section header suffices)
- Remove inline [Modified in Gloas] annotations from on_execution_payload
  (new function — section header suffices)
- Add :EIP7732 suffix to all our Gloas annotations for consistency
- Clean up Removed comment to match spec convention (no rationale suffix)
- Remove explanatory inline comments that belong in PR description

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lodekeeper
Copy link
Copy Markdown
Owner Author

Round 2 review — annotation cleanup + adversarial analysis

Annotation fixes (edae793)

  • Removed all inline [New/Modified in Gloas:EIP7732] annotations from new functions (process_execution_payload, on_execution_payload) — section headers establish this per cross-fork convention
  • Added :EIP7732 suffix to all our annotations for consistency with the rest of the Gloas spec
  • Cleaned up Removed comment and explanatory inline comments

process_execution_payload placement — keep in beacon-chain.md

Ran defender + devil's advocate. Verdict: keep it where it is.

For staying (9/10 confidence): 5 forks of precedent, semantic grouping with verify_execution_payload_envelope_signature, established cross-reference pattern (fork-choice calls beacon-chain, never reverse), multiple consumers (fork-choice + builder.md), pure verification functions already live in beacon-chain (every verify_* helper, is_valid_indexed_payload_attestation, etc.)

For moving (8/10): sole caller is fork-choice, no longer a state transition, fork-choice already defines verification functions. Strongest point: the spec note says "called by fork-choice" — if you have to explain it's for fork-choice, maybe it's in the wrong file.

Historical precedent and the multi-consumer argument win. The note at the section header is sufficient documentation.

Acceptance criteria — 13/14 PASS, 1 FAIL

# Criterion Verdict
1 BeaconBlockBody.parent_execution_requests
2 process_parent_execution_payload first in process_block
3 process_execution_payload pure verification
4 state_root removed from envelope
5 payload_statesverified_execution_requests
6 on_block validates requests
7 on_execution_payload buffers requests
8 Validator guide
9 Builder guide
10 Infallibility
11 is_parent_block_full ordering
12 Builder payment epoch rotation
13 Fork boundary handling
14 Gossip validation

Critical finding: Fork boundary (first Gloas block)

upgrade_to_gloas initializes latest_execution_payload_bid.block_hash = pre.latest_execution_payload_header.block_hash (non-zero). The first Gloas block's bid must have bid.parent_block_hash == state.latest_block_hash (same value). So is_parent_full = True in process_parent_execution_payload — it enters the FULL branch for a pre-Gloas parent.

At fork-choice level, get_parent_payload_status accesses parent.body.signed_execution_payload_bid on a Fulu block that lacks this field.

Fix needed: Explicit epoch guard (Option A from gist) in both process_parent_execution_payload and on_block.

Other findings from adversarial analysis

MAJOR:

  • verified_execution_requests pruning comment removed — should document it follows same pattern as blocks/block_states
  • builder_pending_withdrawals.append has no overflow guard (list limit 2^20)
  • p2p gossip: no content-level validation of parent_execution_requests (only gated on parent payload being "seen", not "verified")

MINOR:

  • Race between gossip "seen" and fork-choice "verified" — clarify in p2p spec
  • Missing defensive assertion parent_epoch <= get_current_epoch(state) in builder payment code

lodekeeper and others added 3 commits April 9, 2026 12:32
The first Gloas block has a pre-Gloas (Fulu) parent whose
BeaconBlockBody lacks signed_execution_payload_bid. Guard both
process_parent_execution_payload and on_block to skip parent
payload processing when the parent is pre-Gloas, asserting
parent_execution_requests is empty.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
get_parent_payload_status accesses parent.body.signed_execution_payload_bid
which doesn't exist on pre-Gloas blocks. Guard the function itself (not
just on_block) since it's called from get_ancestor, is_parent_strong,
get_node_children, and should_extend_payload. Pre-Gloas parents always
had their payload embedded in the block, so return PAYLOAD_STATUS_FULL.

Also add fork boundary case to validator.md for proposer guidance.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gloas replaces payload_states with verified_execution_requests in Store,
but Heze fork-choice still referenced the old field. Propagate the rename
and semantic change (full BeaconState → lightweight ExecutionRequests)
across all four Heze fork-choice references.

🤖 Generated with AI assistance
@lodekeeper lodekeeper changed the title Deferred payload processing (payload as pre-state transition) Defer payload processing to next-block pre-state transition Apr 9, 2026
@github-actions github-actions bot added the heze label Apr 9, 2026
Replace get_parent_payload_status block-hash comparison with
store.verified_execution_requests membership check. This eliminates
all GLOAS_FORK_EPOCH guards while correctly handling the fork
boundary via get_forkchoice_store anchor initialization.

The map is only populated by on_execution_payload which does full
cryptographic verification (EE check, hash binding, signature),
so map presence is equivalent to the previous hash comparison.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lodekeeper lodekeeper changed the title Defer payload processing to next-block pre-state transition Defer payload processing to next block Apr 9, 2026
🤖 Generated with AI assistance
Comment on lines +264 to +269
parent = store.blocks[block.parent_root]
parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY
return (
PAYLOAD_STATUS_FULL
if block.parent_root in store.verified_execution_requests
else PAYLOAD_STATUS_EMPTY
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

what's the reason this function is modified?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

payload_states was eliminated — all CL state mutations moved to process_parent_execution_payload in the next block's state transition. This function now checks verified_execution_payloads (set of roots for which we received and verified execution payloads) instead of looking up a payload state. Same semantic: "was the parent's payload received and valid?"

Latest commit (5a95ce8) renamed this from verified_execution_requests to verified_execution_payloads and simplified to Set[Root] since execution requests verification moved to the state transition via execution_requests_root on the bid.

# Removed `payload_states`
# [New in Gloas:EIP7732]
payload_states: Dict[Root, BeaconState] = field(default_factory=dict)
verified_execution_requests: Dict[Root, ExecutionRequests] = field(default_factory=dict)
Copy link
Copy Markdown

@nflaig nflaig Apr 9, 2026

Choose a reason for hiding this comment

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

this can be changed to payloads: Dict[Root, ExecutionPayload] = field(default_factory=dict)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

One concern: get_forkchoice_store needs the anchor root in this structure for checkpoint sync bootstrapping — without it, get_parent_payload_status returns EMPTY for the first block after anchor, causing valid blocks with FULL parents to fail fork-choice validation (on_block asserts empty parent_execution_requests).

With Set[Root], anchor init is just {anchor_root}. With Dict[Root, ExecutionPayload], we'd need the actual payload at anchor time. In checkpoint sync only the beacon state + block are fetched — the execution payload envelope isn't available. Would require changing get_forkchoice_store signature or fetching additional data.

Is there a downstream use case you have in mind that needs the actual payload stored? Or should we find a way to handle the anchor case (e.g. sentinel value, separate anchor set)?

Comment on lines +751 to +754
assert (
block.body.parent_execution_requests
== store.verified_execution_requests[block.parent_root]
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this check can be moved to state transition if we add a commitment to the bid

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done in latest commit (5a95ce8). Added execution_requests_root: Root to ExecutionPayloadBid — verification now happens in process_parent_execution_payload during state transition:

assert hash_tree_root(block.body.parent_execution_requests) == parent_bid.execution_requests_root

Fork-choice on_block no longer checks execution requests content. on_execution_payload does a consistency check against the bid commitment. verified_execution_requests: Dict[Root, ExecutionRequests] simplified to verified_execution_payloads: Set[Root].

Add execution_requests_root commitment to ExecutionPayloadBid, enabling
CL-verifiable verification of parent_execution_requests without needing
the actual execution payload. This eliminates verified_execution_requests
from the fork-choice Store (replaced with lightweight verified_execution_payloads
Set) and resolves the checkpoint sync bootstrapping issue.

Verification points:
- state_transition: process_parent_execution_payload checks
  hash_tree_root(parent_execution_requests) == parent_bid.execution_requests_root
- fork-choice: on_execution_payload checks EL-returned requests match
  the bid commitment

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move field to be grouped with other new Gloas fields in Store.
Drop `verified_` prefix for consistency with existing Store fields
(blocks, block_states, etc.) which are all implicitly validated.

🤖 Generated with AI assistance

Co-Authored-By: Lodekeeper <lodekeeper@users.noreply.github.com>
…status

Revert commits acfc095..a488227. get_parent_payload_status does not
need to change — the block hash comparison correctly determines
payload status. The execution_payloads Set[Root] and
execution_requests_root bid commitment were unnecessary.

The verified_execution_requests Dict remains for tracking
EE-verified execution requests (needed for is_payload_timely,
is_payload_data_available, etc.).

🤖 Generated with AI assistance

Co-Authored-By: Lodekeeper <lodekeeper@users.noreply.github.com>
…payloads Set

Restore execution_requests_root in ExecutionPayloadBid (Gloas + Heze)
and add bid commitment verification in both process_parent_execution_payload
(state transition) and on_execution_payload (fork-choice).

Replace verified_execution_requests Dict[Root, ExecutionRequests] and
payload_states Dict[Root, BeaconState] with a lightweight
execution_payloads Set[Root] — only tracks which blocks have verified
payloads, no need to store data.

Fix process_execution_payload:
- Return None instead of ExecutionRequests (read from envelope directly)
- Use simple state mutation instead of local block header copy
- Remove state_root verification (no post-state)

Update Heze fork-choice.md consistently (was previously broken —
still used payload_states and ignored process_execution_payload changes).

🤖 Generated with AI assistance

Co-Authored-By: Lodekeeper <lodekeeper@users.noreply.github.com>
…ript in on_block

Two lint fixes:

1. pysetup/helpers.py: add lowercase 'set' to ignored_dependencies so
   that 'default_factory=set' in Store dataclass fields doesn't cause
   the spec generator to fail with "'set' is not in list".

2. specs/gloas/fork-choice.md: execution_payloads is Set[Root] (not
   subscriptable). Replace store.execution_payloads[block.parent_root]
   in on_block with hash_tree_root(block.body.parent_execution_requests)
   == parent_bid.execution_requests_root (bid commitment check).

🤖 Generated with AI assistance
…ecution_payload

Default-initialized latest_execution_payload_bid has block_hash == Hash32(),
so is_parent_full naturally evaluates to False for the first Gloas block,
falling into the else branch which asserts empty parent_execution_requests.

🤖 Generated with AI assistance

Co-Authored-By: Lodekeeper <lodekeeper@users.noreply.github.com>
…ition

Rename to `payloads` for consistency with `blocks` naming pattern.
Move before payload_timeliness_vote to match original payload_states position.

🤖 Generated with AI assistance

Co-Authored-By: Lodekeeper <lodekeeper@users.noreply.github.com>
`process_execution_payload(state, signed_envelope, execution_engine)`, which
returns the verified `ExecutionRequests` without mutating `state`. Execution
requests are deferred to the next beacon block via
`process_parent_execution_payload`. State transitions that trigger an unhandled
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

State transitions

process_execution_payload is no longer a state transition so this is not accurate.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 03cc5af — rewrote the prose to say "validity ... is checked by" instead of treating it as a state transition. Also changed "State transitions that trigger" → "Verification failures that trigger" in the following sentence.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Updated. I removed the State transitions framing around process_execution_payload() and rewrote the section to describe it as a pure verification step returning ExecutionRequests, since the CL state mutations are now deferred into process_parent_execution_payload().

payload envelope `signed_envelope` is verified by
`process_execution_payload(state, signed_envelope, execution_engine)`, which
returns the verified `ExecutionRequests` without mutating `state`. Execution
requests are deferred to the next beacon block via
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

execution requests are deferred to the next beacon block via

I think this is too specific. Also there are other operations that are deferred too like queueing the builder payment and set latest block hash

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch. Fixed in 03cc5af — the prose now lists all four deferred effects: "execution requests, builder payment, payload availability, and latest block hash."

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch. I broadened the wording so it no longer singles out execution requests; it now explicitly reflects that multiple effects are deferred (including latest_block_hash, availability tracking, and builder-payment queueing), not just the execution-request application.

state: BeaconState,
# [Modified in Gloas:EIP7732]
# Removed `body`
# [New in Gloas:EIP7732]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think these comments need to stay

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Restored in 03cc5af. These were accidentally dropped during the refactor.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Restored the explanatory comments around process_execution_payload(). The branch now keeps the rationale/comments in place while still making the function verification-only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a state mutation. Need to move it either very end or very beginning of the state_transition

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed — this state root caching is a real state mutation that contradicts the "does not mutate state" claim. Two options: (a) move the state root caching to the fork-choice caller (on_execution_payload already copies state, so it can do this before calling process_execution_payload), or (b) accept the mutation and document it. Leaning toward (a). Will address in a follow-up commit once we align on the approach.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Moved the availability-bit update to the very beginning of process_parent_execution_payload(), so it happens as part of the next block's durable state transition rather than mid-way through payload verification.

This function should not trigger an exception.

7. Set `envelope.state_root` to `hash_tree_root(state)`.
This pure verification function should not trigger an exception and does not
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
This pure verification function should not trigger an exception and does not
This function should not trigger an exception and does not

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The word "pure" was already removed in a prior commit — current text reads: "This function should not trigger an exception and does not mutate state." Matches your suggestion.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Applied the wording change. The builder text now says This function should not trigger an exception and does not mutate state.

def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
parent = store.blocks[block.parent_root]
# Pre-Gloas parents always had their payload embedded in the block
if compute_epoch_at_slot(parent.slot) < GLOAS_FORK_EPOCH:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not a fan of having GLOAS_FORK_EPOCH in the fork choice spec. Find a workaround either mention this as a comment, or put it somewhere in fork.md

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid concern. The guard exists because pre-Gloas blocks lack signed_execution_payload_bid — accessing it would crash. The cleanest alternative: seed all pre-Gloas block roots into store.payloads during fork activation in fork.md, so fork-choice can assume all entries have the Gloas fields. That would eliminate both epoch checks (this one and the one in on_block). Not yet addressed — will follow up once we align on approach.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed on the branch: I removed the raw GLOAS_FORK_EPOCH workaround from fork-choice and switched the pre-Gloas parent handling to a structural check instead. I also moved the initialization rationale into specs/gloas/fork.md where it belongs.

assert block.parent_root in store.payload_states
state = copy(store.payload_states[block.parent_root])
# First Gloas block: parent is pre-Gloas, no deferred payload processing
if compute_epoch_at_slot(parent_block.slot) < GLOAS_FORK_EPOCH:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not a fan of having GLOAS_FORK_EPOCH in the fork choice spec. Find a workaround either mention this as a comment, or put it somewhere in fork.md

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same as above — will address together with the get_parent_payload_status guard. Both can be removed if we seed pre-Gloas block roots into store.payloads at fork activation.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same fix here: the fork-choice workaround no longer relies on GLOAS_FORK_EPOCH; the branch now uses structural parent detection and keeps the initialization note in fork.md instead of encoding the fork epoch into fork choice.

# Execution request functions use state.slot for PendingDeposit.slot
# and builder deposit_epoch, shifting them by at least one slot.
requests = block.body.parent_execution_requests
for request in requests.deposits:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These should just be in the original for op style

    def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
        for operation in operations:
            fn(state, operation)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already addressed in a prior commit — for_ops helper is now used in process_parent_execution_payload.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Changed back to the original for_ops(...) style in process_parent_execution_payload() / process_execution_payload() so the loops stay aligned with the surrounding spec style.

parent_epoch = compute_epoch_at_slot(parent_slot)
if parent_epoch == get_current_epoch(state):
payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH
else:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is interesting. parent_epoch MUST be current or previous epoch.

If parent epoch is more than two epochs old, I am not sure how we should handle. (Maybe just skip updating state.builder_pending_payments? And then payment_index will be calculated based on clock slot)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good observation. In the base branch this is simpler because process_execution_payload runs same-slot (SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH). In the deferred model, parent can be from a different epoch.

Currently we handle current_epoch and previous_epoch but silently skip if parent is >2 epochs old. This means the builder payment is forfeit — which seems correct (the chain missed that many slots, the payment window has passed and the slot has been recycled in builder_pending_payments).

Should we add an explicit comment documenting the forfeit behavior, or handle it differently? This is a protocol-level decision with economic consequences for the builder.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I made the parent-epoch handling explicit now. The branch only rotates/applies builder pending payments for current/previous epoch contexts, and I also fixed the cross-fork carryover bug by preserving execution_requests_root across Gloas→Heze / genesis paths so the deferred-payment/deferred-request commitments stay coherent on transition boundaries. Targeted fork tests are green after that fix.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

After reviewing the ring buffer mechanics and #epbs channel discussions: if parent is >2 epochs old, the payment entry has already been evicted from builder_pending_payments by process_builder_pending_payments at epoch boundaries. There is nothing left to read — silent skip is the only correct behavior.

This is consistent with the existing quorum-based forfeit mechanism (potuz confirmed in #epbs 2025-07-28: builders not paying when quorum is not met is intentional design).

Added a documenting comment in d84874b.

lodekeeper and others added 7 commits April 10, 2026 08:18
- Fix prose description: process_execution_payload is verification, not a
  state transition. Use "Verification failures" instead of "State transitions"
- Broaden deferred effects description to list all four: execution requests,
  builder payment, payload availability, and latest block hash
- Restore [Modified in Gloas:EIP7732] and [New in Gloas:EIP7732] annotation
  comments on process_execution_payload signature
- Fix duplicated "verifies the payload against the execution engine" in
  process_execution_payload docstring

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return value was unused by all callers — fork-choice reads
execution_requests directly from the envelope object. Updated
prose description and docstring accordingly.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-choice

Remove GLOAS_FORK_EPOCH from fork-choice following spec convention
that fork-choice files should not contain epoch constants. Use
structural hasattr checks instead, which is a type-level guard
rather than an epoch-level one. Add explanatory note above
get_parent_payload_status documenting the pre-Gloas parent edge case.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…allers

process_execution_payload must not mutate state — that is the core
principle of the deferred payload design. The state root caching
(filling latest_block_header.state_root) was a leftover from the
base branch. Move it to fork-choice on_execution_payload callers
in both Gloas and Heze.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the parent block is older than the previous epoch, the payment
entry has already been settled or evicted from builder_pending_payments
by epoch boundary processing. This is consistent with the existing
quorum-based forfeit mechanism in ePBS.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on_requests_root

beacon_block topic:
- REJECT if parent was FULL and parent_execution_requests doesn't
  match parent_bid.execution_requests_root
- REJECT if parent was EMPTY and parent_execution_requests is not
  empty

execution_payload topic:
- REJECT if envelope.execution_requests doesn't match
  bid.execution_requests_root

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…p gossip

The ExecutionPayloadEnvelope SSZ type uses `beacon_block_root`, not
`block_root`. This bug also exists upstream.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lodekeeper lodekeeper force-pushed the feat/deferred-payload-processing branch from 887bea0 to 2ece03d Compare April 10, 2026 09:57
@lodekeeper
Copy link
Copy Markdown
Owner Author

Upstream p2p-interface.md issues (alpha.4)

Noted during parity check. These exist in the upstream spec, not introduced by this PR.

Fixed here

  • envelope.block_rootenvelope.beacon_block_root in execution_payload gossip (2 occurrences) — trivial field name bug, fixed in 2ece03d

Deferred (out of scope — require upstream rework)

  • beacon_block gossip: parent_bid alias used inline but never declared upfront
  • beacon_block gossip: no explicit REJECT for bid.parent_block_hash consistency against parent bid
  • beacon_block gossip: "has been seen" / "passes all validation" language is vague for the deferred payload model
  • payload_attestation_message gossip: no clarification that payload_present = True means local on_execution_payload acceptance

lodekeeper and others added 12 commits April 10, 2026 10:01
The fork-choice spec does not consider pre-fork blocks — this is
consistent with all previous forks. The cross-fork parent block
access issue (accessing signed_execution_payload_bid on pre-Gloas
parents) is a pre-existing bug in the base spec, not something
introduced by this PR. Documented separately.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…h_attestations

F821 lint error: name referenced but not in function signature.
Mirror mark_payload_available=False default from apply_next_epoch_with_attestations.

🤖 Generated with AI assistance
Fix line wrapping in beacon-chain.md, fork.md, p2p-interface.md and
reformat long lines/function signature in fork_choice.py to satisfy CI.

🤖 Generated with AI assistance
🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s pattern

Avoids adding lowercase "set" to pysetup ignored_dependencies.
payloads is already explicitly initialized in get_forkchoice_store.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Python dataclass requires fields without defaults before fields with defaults.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep payloads grouped with other payload_* fields. Add lowercase
"set" to ignored_dependencies so default_factory=set is recognized.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The function was refactored to pure verification but the return type was
left as None and the captured `requests` value was never returned. This
caused all process_execution_payload tests to fail with
`assert None == ExecutionRequests(...)`.

🤖 Generated with AI assistance
…ents

Anchor payload status is unresolved at checkpoint sync time — it gets
resolved by the first child block. Instead of optimistically seeding
anchor_root in payloads, skip the local verification check for parents
at or before the finalized checkpoint epoch.

🤖 Generated with AI assistance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lodekeeper
Copy link
Copy Markdown
Owner Author

Closing — Nico is bringing these changes to the spec directly. Thanks for the review!

@lodekeeper lodekeeper closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants