Analysis of whether execution requests being processed at the child's slot (N+1) instead of the parent's slot (N) introduces meaningful differences.
In PR #5094, apply_parent_execution_payload contains:
# Process execution requests from parent's payload. The execution
# requests are processed at state.slot (child's slot), not the parent's slot.In alpha.4, process_execution_payload runs at slot N (parent's slot).
In #5094, process_parent_execution_payload runs at slot N+1 (child's slot).
No — it's not safe at epoch boundaries. By the time apply_parent_execution_payload
runs during process_block at slot N+1, process_slots has already advanced the state
and (if at an epoch boundary) process_epoch has already mutated the state. Setting
state.slot = N would give you epoch E's slot context against epoch E+1's state data
(post-epoch balances, validator activations, exit queue). This mismatch is worse than
the 1-slot offset.
Processing before process_slots is impossible — you need the block to know the
requests, but state_transition runs process_slots before process_block.
process_deposit_request creates PendingDeposit(slot=state.slot).
process_pending_deposits gates processing on:
if deposit.slot > finalized_slot:
break- Alpha.4:
PendingDeposit.slot = N→ processable when epoch of N is finalized - #5094:
PendingDeposit.slot = N+1→ processable when epoch of N+1 is finalized
At an epoch boundary (N = last slot of epoch E, N+1 = first slot of epoch E+1): the deposit waits an extra ~6.4 minutes for epoch E+1 to be finalized. In all other cases, N and N+1 are in the same epoch — no difference.
process_withdrawal_request and process_consolidation_request use
get_current_epoch(state) for eligibility checks (is_active_validator,
SHARD_COMMITTEE_PERIOD, exit queue computation).
At epoch boundaries, a 1-epoch difference could theoretically affect eligibility, but requires a validator to cross an activation/exit/period threshold at that exact epoch — an extremely rare edge case.
For consumers like Lido's oracle that need consistent CL+EL snapshots:
| State | Withdrawals (CL→EL) | Deposits (EL→CL) |
|---|---|---|
block_states[X] (alpha.4) |
Applied ✓ | NOT applied ✗ |
block_states[X] (#5094) |
Applied ✓ | NOT applied ✗ |
block_states[X+1] (both) |
Applied ✓ | Applied ✓ |
The canonical state is identical in both versions. Deposits from slot X only appear
at slot X+1 in the canonical state. In alpha.4, payload_states[X] has deposits at
slot X, but this is a fork choice internal — not exposed via any beacon API, not part
of checkpoint states.
Option 1: Snapshot at slot X+1 paired with EL block from slot X. The CL state at X+1 includes all execution effects from slot X. Works identically in alpha.4 and #5094.
Option 2: Account for in-flight requests.
At slot X, read block.body.parent_execution_requests from block X+1 to know what
deposits are pending. The requests are committed via execution_requests_root in the
bid, so they're deterministic.
Option 3: Pair CL state with the previous slot's EL block. CL state at slot X includes effects from slot X-1's payload (applied during block X's processing). Pair with EL block from slot X-1 for a consistent snapshot with no gap.
The 1-slot offset in execution request processing is an inherent property of deferred
payload processing — it cannot be eliminated without reintroducing dual states
(payload_states). The canonical state behavior is identical between alpha.4 and #5094.
The only concrete new effect is PendingDeposit.slot = N+1 instead of N, which at
epoch boundaries delays deposit processability by ~6.4 minutes. Given deposits are
queue-based with multi-epoch finalization delays, this is acceptable.