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 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_payments after process_execution_payload promotes the payment to state.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.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to