Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0429bca
feat: implement deferred payload processing (payload as pre-state tra…
lodekeeper Apr 9, 2026
a9f8a82
fix: address adversarial review findings
lodekeeper Apr 9, 2026
53a624d
fix: address review findings
lodekeeper Apr 9, 2026
093d37f
fix: address PR review comments on annotation style
lodekeeper Apr 9, 2026
edae793
fix: clean up annotation semantics per cross-fork conventions
lodekeeper Apr 9, 2026
374ac4b
fix: add fork boundary guards for first Gloas block
lodekeeper Apr 9, 2026
8b90efc
fix: guard get_parent_payload_status for pre-Gloas parents
lodekeeper Apr 9, 2026
acfc095
fix: propagate verified_execution_requests to Heze fork-choice Store
lodekeeper Apr 9, 2026
d8a27a4
refactor: use verified_execution_requests for payload status
lodekeeper Apr 9, 2026
6c6d237
chore: apply lint auto-fixes
lodekeeper Apr 9, 2026
5a95ce8
feat: add execution_requests_root to ExecutionPayloadBid
lodekeeper Apr 9, 2026
a488227
refactor: rename verified_execution_payloads to execution_payloads
lodekeeper Apr 9, 2026
81be4ca
revert: remove execution_payloads set and restore get_parent_payload_…
lodekeeper Apr 9, 2026
31dd214
refactor: restore execution_requests_root, use lightweight execution_…
lodekeeper Apr 10, 2026
04c93ad
fix: add set to ignored_dependencies and fix execution_payloads subsc…
lodekeeper Apr 10, 2026
8cbf1f7
refactor: remove redundant fork boundary guard from process_parent_ex…
lodekeeper Apr 10, 2026
ce8674e
refactor: rename execution_payloads to payloads, move to original pos…
lodekeeper Apr 10, 2026
bb00ed6
chore: apply mdformat line-wrap fix to fork_choice README
lodekeeper Apr 10, 2026
03cc5af
fix: address review comments on prose, annotations, and for_ops style
lodekeeper Apr 10, 2026
a99dcfa
fix: change process_execution_payload to return None
lodekeeper Apr 10, 2026
f3a73be
refactor: replace GLOAS_FORK_EPOCH guards with hasattr checks in fork…
lodekeeper Apr 10, 2026
607864c
refactor: move state root caching from process_execution_payload to c…
lodekeeper Apr 10, 2026
0c50274
fix: preserve execution request commitments across forks
lodekeeper Apr 10, 2026
d84874b
docs: document builder payment forfeit for >2-epoch-old parents
lodekeeper Apr 10, 2026
ed184ac
feat: add gossip validation for parent_execution_requests and executi…
lodekeeper Apr 10, 2026
2ece03d
fix: correct envelope field name block_root → beacon_block_root in p2…
lodekeeper Apr 10, 2026
5c329ee
refactor: remove hasattr guards from fork-choice
lodekeeper Apr 10, 2026
6856507
docs: condense state.slot note in process_parent_execution_payload
lodekeeper Apr 10, 2026
e3516b0
fix: add missing mark_payload_available param to apply_next_slots_wit…
lodekeeper Apr 10, 2026
b4013e8
chore: apply mdformat and ruff lint fixes
lodekeeper Apr 10, 2026
8626e6c
docs: align on_block note and comment with spec terminology
lodekeeper Apr 10, 2026
15fa95c
docs: fix on_block comment to say bid commitment, not payload data
lodekeeper Apr 10, 2026
5205752
docs: fix Store note to say verified instead of processed
lodekeeper Apr 10, 2026
b047d20
fix: remove default_factory=set from Store, match equivocating_indice…
lodekeeper Apr 10, 2026
b63faed
fix: move payloads field before defaulted fields in Store dataclass
lodekeeper Apr 10, 2026
9aa090d
fix: revert field move, keep default_factory=set and update helper
lodekeeper Apr 10, 2026
efb3344
fix: return ExecutionRequests from process_execution_payload
lodekeeper Apr 10, 2026
48876d0
fix: don't seed anchor_root in payloads, skip check for finalized par…
lodekeeper Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pysetup/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T
"ProgressiveBitlist",
"ProgressiveList",
"Sequence",
"set",
"Set",
"Tuple",
"uint128",
Expand Down
140 changes: 99 additions & 41 deletions specs/gloas/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
- [New `process_builder_pending_payments`](#new-process_builder_pending_payments)
- [New `process_ptc_window`](#new-process_ptc_window)
- [Block processing](#block-processing)
- [Parent execution payload](#parent-execution-payload)
- [New `process_parent_execution_payload`](#new-process_parent_execution_payload)
- [Withdrawals](#withdrawals)
- [New `get_builder_withdrawals`](#new-get_builder_withdrawals)
- [New `get_builders_sweep_withdrawals`](#new-get_builders_sweep_withdrawals)
Expand Down Expand Up @@ -265,6 +267,8 @@ class ExecutionPayloadBid(Container):
value: Gwei
execution_payment: Gwei
blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
# [New in Gloas:EIP7732]
execution_requests_root: Root
```

#### `SignedExecutionPayloadBid`
Expand All @@ -284,7 +288,6 @@ class ExecutionPayloadEnvelope(Container):
builder_index: BuilderIndex
beacon_block_root: Root
slot: Slot
state_root: Root
```

#### `SignedExecutionPayloadEnvelope`
Expand Down Expand Up @@ -324,6 +327,8 @@ class BeaconBlockBody(Container):
signed_execution_payload_bid: SignedExecutionPayloadBid
# [New in Gloas:EIP7732]
payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]
# [New in Gloas:EIP7732]
parent_execution_requests: ExecutionRequests
```

#### `BeaconState`
Expand Down Expand Up @@ -797,12 +802,15 @@ transitions that trigger an unhandled exception (e.g. a failed `assert` or an
out-of-range list access) are considered invalid. State transitions that cause a
`uint64` overflow or underflow are also considered invalid.

The post-state corresponding to a pre-state `state` and a signed execution
payload envelope `signed_envelope` is defined as
`process_execution_payload(state, signed_envelope, execution_engine)`. State
transitions that trigger an unhandled exception (e.g. a failed `assert` or an
out-of-range list access) are considered invalid. State transitions that cause
an `uint64` overflow or underflow are also considered invalid.
The validity of a signed execution payload envelope `signed_envelope` against a
pre-state `state` is checked by
`process_execution_payload(state, signed_envelope, execution_engine)`. Deferred
effects from the parent payload — execution requests, builder payment, payload
availability, and latest block hash — are applied in the next beacon block via
`process_parent_execution_payload`. Verification failures that trigger an
unhandled exception (e.g. a failed `assert` or an out-of-range list access) are
considered invalid. Verification failures that cause a `uint64` overflow or
underflow are also considered invalid.

### Modified `process_slot`

Expand Down Expand Up @@ -890,6 +898,8 @@ def process_ptc_window(state: BeaconState) -> None:

```python
def process_block(state: BeaconState, block: BeaconBlock) -> None:
# [New in Gloas:EIP7732]
process_parent_execution_payload(state, block)
process_block_header(state, block)
# [Modified in Gloas:EIP7732]
process_withdrawals(state)
Expand All @@ -904,6 +914,79 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_sync_aggregate(state, block.body.sync_aggregate)
```

#### Parent execution payload

##### New `process_parent_execution_payload`

```python
def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None:
"""
Process deferred effects from the parent's execution payload.
Must run first in ``process_block``, before ``process_block_header``, because it reads
``state.latest_block_header.slot`` and ``state.latest_execution_payload_bid``
which are overwritten by ``process_block_header`` and ``process_execution_payload_bid``.
"""
bid = block.body.signed_execution_payload_bid.message
parent_bid = state.latest_execution_payload_bid

# Determine parent payload status from block data
# Note: cannot use is_parent_block_full(state) here because latest_block_hash
# has not been updated yet -- this function is responsible for updating it.
is_parent_full = bid.parent_block_hash == parent_bid.block_hash

if is_parent_full:
parent_slot = state.latest_block_header.slot
parent_epoch = compute_epoch_at_slot(parent_slot)
current_epoch = get_current_epoch(state)
previous_epoch = get_previous_epoch(state)

# Mark the parent payload as available before any later state transition logic observes it.
state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1

# Verify execution requests match the bid commitment
assert (
hash_tree_root(block.body.parent_execution_requests)
== parent_bid.execution_requests_root
)

# Process deferred execution requests from parent's payload
# Note: execution requests observe state.slot (child's slot), not the parent's.
requests = block.body.parent_execution_requests

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

for_ops(requests.deposits, process_deposit_request)
for_ops(requests.withdrawals, process_withdrawal_request)
for_ops(requests.consolidations, process_consolidation_request)

# Queue the builder payment
if parent_epoch == current_epoch:
payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH
payment = state.builder_pending_payments[payment_index]
amount = payment.withdrawal.amount
if amount > 0:
state.builder_pending_withdrawals.append(payment.withdrawal)
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
elif parent_epoch == previous_epoch:
payment_index = parent_slot % SLOTS_PER_EPOCH
payment = state.builder_pending_payments[payment_index]
amount = payment.withdrawal.amount
if amount > 0:
state.builder_pending_withdrawals.append(payment.withdrawal)
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
# Note: if parent is older than previous_epoch, the payment entry
# has already been settled or evicted by process_builder_pending_payments
# at epoch boundaries. No action needed.

# Update latest block hash
state.latest_block_hash = bid.parent_block_hash
else:
# Parent was EMPTY -- no execution requests expected
assert block.body.parent_execution_requests == ExecutionRequests()
```

#### Withdrawals

##### New `get_builder_withdrawals`
Expand Down Expand Up @@ -1545,9 +1628,11 @@ def verify_execution_payload_envelope_signature(

#### New `process_execution_payload`

*Note*: `process_execution_payload` is now an independent check in state
transition. It is called when importing a signed execution payload proposed by
the builder of the current slot.
*Note*: `process_execution_payload` is a verification function called by
fork-choice when importing a signed execution payload. It verifies the payload
against the execution engine without processing execution requests or updating
state. Actual state mutations are deferred to `process_parent_execution_payload`
in the next block.

```python
def process_execution_payload(
Expand All @@ -1559,19 +1644,14 @@ def process_execution_payload(
execution_engine: ExecutionEngine,
# [New in Gloas:EIP7732]
verify: bool = True,
) -> None:
) -> ExecutionRequests:
envelope = signed_envelope.message
payload = envelope.payload

# Verify signature
if verify:
assert verify_execution_payload_envelope_signature(state, signed_envelope)

# Cache latest block header state root
previous_state_root = hash_tree_root(state)
if state.latest_block_header.state_root == Root():
state.latest_block_header.state_root = previous_state_root

# Verify consistency with the beacon block
assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)
assert envelope.slot == state.slot
Expand All @@ -1595,7 +1675,6 @@ def process_execution_payload(
# Verify the execution payload is valid
versioned_hashes = [
kzg_commitment_to_versioned_hash(commitment)
# [Modified in Gloas:EIP7732]
for commitment in committed_bid.blob_kzg_commitments
]
requests = envelope.execution_requests
Expand All @@ -1608,28 +1687,7 @@ def process_execution_payload(
)
)

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

for_ops(requests.deposits, process_deposit_request)
for_ops(requests.withdrawals, process_withdrawal_request)
for_ops(requests.consolidations, process_consolidation_request)

# Queue the builder payment
payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH]
amount = payment.withdrawal.amount
if amount > 0:
state.builder_pending_withdrawals.append(payment.withdrawal)
state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = (
BuilderPendingPayment()
)

# Cache the execution payload hash
state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1
state.latest_block_hash = payload.block_hash

# Verify the state root
if verify:
assert envelope.state_root == hash_tree_root(state)
# Execution request processing, builder payment queueing, availability updates,
# and latest block hash updates are deferred to the next beacon block.
return requests
```
4 changes: 1 addition & 3 deletions specs/gloas/builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,7 @@ After setting these parameters, the builder assembles
`signed_execution_payload_envelope = SignedExecutionPayloadEnvelope(message=envelope, signature=BLSSignature())`,
then verify that the envelope is valid with
`process_execution_payload(state, signed_execution_payload_envelope, execution_engine, verify=False)`.
This function should not trigger an exception.

7. Set `envelope.state_root` to `hash_tree_root(state)`.
This function should not trigger an exception and does not mutate `state`.

After preparing the `envelope` the builder should sign the envelope using:

Expand Down
66 changes: 41 additions & 25 deletions specs/gloas/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,8 @@ def update_latest_messages(

### Modified `Store`

*Note*: `Store` is modified to track the intermediate states of "empty"
consensus blocks, that is, those consensus blocks for which the corresponding
execution payload has not been revealed or has not been included on chain.
*Note*: `Store` is modified to track blocks whose execution payloads have been
verified.

```python
@dataclass
Expand All @@ -146,7 +145,7 @@ class Store(object):
latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict)
unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict)
# [New in Gloas:EIP7732]
payload_states: Dict[Root, BeaconState] = field(default_factory=dict)
payloads: Set[Root] = field(default_factory=set)
# [New in Gloas:EIP7732]
payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict)
# [New in Gloas:EIP7732]
Expand Down Expand Up @@ -181,7 +180,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -
checkpoint_states={justified_checkpoint: copy(anchor_state)},
unrealized_justifications={anchor_root: justified_checkpoint},
# [New in Gloas:EIP7732]
payload_states={anchor_root: copy(anchor_state)},
payloads=set(),
# [New in Gloas:EIP7732]
payload_timeliness_vote={
anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))
Expand Down Expand Up @@ -230,9 +229,9 @@ def is_payload_timely(store: Store, root: Root) -> bool:
# The beacon block root must be known
assert root in store.payload_timeliness_vote

# If the payload is not locally available, the payload
# If the payload is not locally verified, the payload
# is not considered available regardless of the PTC vote
if root not in store.payload_states:
if root not in store.payloads:
return False

return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD
Expand All @@ -249,9 +248,9 @@ def is_payload_data_available(store: Store, root: Root) -> bool:
# The beacon block root must be known
assert root in store.payload_data_availability_vote

# If the payload is not locally available, the blob data
# If the payload is not locally verified, the blob data
# is not considered available regardless of the PTC vote
if root not in store.payload_states:
if root not in store.payloads:
return False

return sum(store.payload_data_availability_vote[root]) > DATA_AVAILABILITY_TIMELY_THRESHOLD
Expand Down Expand Up @@ -488,7 +487,7 @@ def get_node_children(
) -> Sequence[ForkChoiceNode]:
if node.payload_status == PAYLOAD_STATUS_PENDING:
children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)]
if node.root in store.payload_states:
if node.root in store.payloads:
children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL))
return children
else:
Expand Down Expand Up @@ -603,9 +602,9 @@ def validate_on_attestation(store: Store, attestation: Attestation, is_from_bloc
if block_slot == attestation.data.slot:
assert attestation.data.index == 0
# [New in Gloas:EIP7732]
# If attesting for a full node, the payload must be known
# If attesting for a full node, the payload must be verified
if attestation.data.index == 1:
assert attestation.data.beacon_block_root in store.payload_states
assert attestation.data.beacon_block_root in store.payloads

# LMD vote must be consistent with FFG vote target
assert target.root == get_checkpoint_block(
Expand Down Expand Up @@ -726,10 +725,11 @@ def get_payload_attestation_due_ms(epoch: Epoch) -> uint64:

### Modified `on_block`

*Note*: The handler `on_block` is modified to consider the pre `state` of the
given consensus beacon block depending not only on the parent block root, but
also on the parent blockhash. In addition we delay the checking of blob data
availability until the processing of the execution payload.
*Note*: The handler `on_block` is modified to verify the parent's deferred
execution requests against the parent bid's `execution_requests_root`
commitment, and to assert that the parent payload has been verified
(`store.payloads`). In addition we delay the checking of blob data availability
until the processing of the execution payload.

```python
def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
Expand All @@ -744,13 +744,24 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
parent_block = store.blocks[block.parent_root]
bid = block.body.signed_execution_payload_bid.message
parent_bid = parent_block.body.signed_execution_payload_bid.message
# Make a copy of the state to avoid mutability issues
# [Modified in Gloas:EIP7732]
# Verify parent execution requests against the parent bid commitment
if is_parent_node_full(store, block):
assert block.parent_root in store.payload_states
state = copy(store.payload_states[block.parent_root])
# Finalized parents are trusted without local payload verification
assert (
block.parent_root in store.payloads
or compute_epoch_at_slot(parent_block.slot) <= store.finalized_checkpoint.epoch
)
assert (
hash_tree_root(block.body.parent_execution_requests)
== parent_bid.execution_requests_root
)
else:
assert bid.parent_block_hash == parent_bid.parent_block_hash
state = copy(store.block_states[block.parent_root])
assert block.body.parent_execution_requests == ExecutionRequests()

# [Modified in Gloas:EIP7732]
state = copy(store.block_states[block.parent_root])

# Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past.
current_slot = get_current_slot(store)
Expand Down Expand Up @@ -830,14 +841,19 @@ def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEn
# If not, this payload MAY be queued and subsequently considered when blob data becomes available
assert is_data_available(envelope.beacon_block_root)

# Make a copy of the state to avoid mutability issues
# Verify execution payload on a temporary state copy
state = copy(store.block_states[envelope.beacon_block_root])

# Process the execution payload
# Cache latest block header state root (process_execution_payload must not mutate state)
if state.latest_block_header.state_root == Root():
state.latest_block_header.state_root = hash_tree_root(state)
process_execution_payload(state, signed_envelope, EXECUTION_ENGINE)

# Add new state for this payload to the store
store.payload_states[envelope.beacon_block_root] = state
# Verify that the execution requests match the bid commitment
bid = state.latest_execution_payload_bid
assert hash_tree_root(envelope.execution_requests) == bid.execution_requests_root

# Mark this block's execution payload as verified
store.payloads.add(envelope.beacon_block_root)
```

### New `on_payload_attestation_message`
Expand Down
Loading
Loading