Auditing Ethereum Gloas (EIP-7732 ePBS): State Transition Invariants, Builder Pending Payments & PTC Fork-Choice
How EIP-7732 (Enshrined Proposer-Builder Separation) Reshapes Consensus Layer Security In Ethereum's Gloas (EIP-7732) upgrade, execution payload validation is decoupled from beacon block processing. Instead of embeddin
How EIP-7732 (Enshrined Proposer-Builder Separation) Reshapes Consensus Layer Security
In Ethereum's Gloas (EIP-7732) upgrade, execution payload validation is decoupled from beacon block processing. Instead of embedding a full ExecutionPayload inside the BeaconBlockBody, the proposer commits to a SignedExecutionPayloadBid from an in-protocol staked Builder, and the builder subsequently reveals a SignedExecutionPayloadEnvelope attested to by the Payload Timeliness Committee (PTC).
This architectural shift introduces a dual-variant fork-choice tree (EMPTY vs. FULL payload states for the same beacon block root) and an in-protocol builder payment state machine that must remain 100% deterministic across all Consensus Layer clients (prysm, lighthouse, teku, lodestar, nimbus-eth2, grandine).
1. Critical Invariant #1: Builder Pending Payment Quorum & Self-Build (BUILDER_INDEX_SELF_BUILD)
When a proposer self-builds (builder_index == BUILDER_INDEX_SELF_BUILD), the bid value must be 0 and no BuilderPendingPayment should be queued. For external builders (builder_index < len(state.builders)), process_execution_payload_bid records a pending payment in state.builder_pending_payments[SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH].
During process_payload_attestation, if the PTC attests that payload_present == True, weight is accumulated toward the 60% quorum threshold (get_builder_payment_quorum_threshold). A subtle bug class arises if a client:
- Fails to clear
state.builder_pending_paymentsafterprocess_execution_payloadpromotes the payment tostate.builder_pending_withdrawals, causing a double withdrawal at the epoch boundary (process_builder_pending_payments), or - Mutates shared
TransitionCaches(BuilderIndexCache) across competing forks when builder indices are reused.
2. Critical Invariant #2: EMPTY vs. FULL Fork-Choice Tie-Breaking (should_extend_payload)
In Gloas LMD-GHOST fork choice, a node steps into PENDING(block_root) using the whole-block weight (EMPTY + FULL), and then chooses between extending the FULL payload or the EMPTY beacon block:
def get_payload_status_tiebreaker(store, node) -> int:
if not is_previous_slot_payload_decision(store, node.root):
return node.payload_status # FULL (1) > EMPTY (0) for older slots
if node.payload_status == PAYLOAD_STATUS_EMPTY:
return 1
elif node.payload_status == PAYLOAD_STATUS_FULL:
return 2 if should_extend_payload(store, node.root) else 0
If a client only evaluates should_extend_payload when proposer_boost_root is non-zero, it will erroneously prefer FULL over EMPTY at the start of a slot even when the PTC voted that the previous slot's payload or PeerDAS data columns were unavailable.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.