# Prysm: EIP-7732 (ePBS) fork choice and networking for Gloas vs. spec

Public code only; no network or node was touched. Gloas is not live on mainnet.

## Pins

| Repo | Ref | Commit (newest via api.github.com `/commits/<ref>`) | Commit date |
|---|---|---|---|
| OffchainLabs/prysm | develop | `eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a` (#17674) | 2026-10-27T15:19:43Z |
| ethereum/consensus-specs | master | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` (#5773) | 2026-10-27T16:28:12Z |
| sigp/lighthouse | stable | `a376defa30470923556d91bdeb15a802e297f31f` (v8.3.2) | 2026-10-22T23:27:50Z |

**Citation format**
- Prysm `path#Lx-Ly` means `https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/path#Lx-Ly`.
- Spec `fc.md` means `https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md`.
- Spec `p2p.md` means `.../specs/gloas/p2p-interface.md`, `fork.md` means `.../specs/gloas/fork.md`, and `phase0-p2p.md` means `.../specs/phase0/p2p-interface.md`, all at the same commit.
- Lighthouse `lh:path#L` is at `a376defa…`.

**How the code was read.** All three repos are `git clone --depth 1` checkouts at the pins.
- Parallel read-only passes produced most of the Lighthouse line references.
- I re-read the Prysm and spec lines for C1–C5, C7–C9 and C11 myself, and the key lines for C6.
- Lighthouse lines I re-read myself: `proto_array.rs#L1775-L1799`, `gossip_verified_bid.rs#L304-L316/#L474-L477`, `rpc_methods.rs#L1508-L1515`, `handler.rs#L966-L979`. Every other Lighthouse line is marked "per the read pass" or comes from that pass and was not re-checked.

**Spec version targeted.** Prysm targets **v1.7.0-beta.4**, not master: `WORKSPACE#L285` `consensus_spec_version = "v1.7.0-beta.4"` and `.ethspecify.yml#L1` `version: v1.7.0-beta.4`. See C12.

---

## Candidates (Gloas-only code)

### C1. A bid is marked "seen" before the remaining IGNORE checks run, so a later valid bid from the same builder is IGNOREd

1. **Spec.** `p2p.md#L968-L971`:
   ```
   # [IGNORE] This is the first bid for this slot, parent, and builder
   bid_key = (bid.slot, bid.parent_block_hash, bid.parent_block_root, bid.builder_index)
   if bid_key in seen.execution_payload_bids:
   ```
   The bid is added to `seen` only after every check, including the signature, has passed. `p2p.md#L1081-L1083`:
   ```
   # Mark this bid as seen and update the highest-value bid for this slot/parent
   seen.execution_payload_bids.add(bid_key)
   ```
2. **Prysm.** `beacon-chain/sync/validate_execution_payload_bid.go#L70-L73`:
   ```go
   tupleKey := executionPayloadBidTupleKey(bid)
   if s.hasSeenExecutionPayloadBid(tupleKey) {
       return pubsub.ValidationIgnore, nil
   }
   ```
   `#L120-L123`:
   ```go
   if err := v.VerifySignature(st); err != nil { return pubsub.ValidationReject, err }
   s.setSeenExecutionPayloadBid(bid.Slot(), tupleKey)
   ```
   After the key is set, Prysm still runs IGNORE checks for highest value, builder can cover the bid, builder not exiting, parent hash known, gas limit and head compatibility (`#L124-L160`), and the parent-slot REJECT (`#L161-L168`).
3. **When results differ.** A builder's first correctly signed bid for a (slot, parent hash, parent root) tuple fails one of those later checks for a reason that can clear within the slot: the head has not yet moved, the parent envelope has not arrived, or the builder's balance is short. The spec still accepts that builder's next valid bid for the same tuple. Prysm IGNOREs it for the rest of the slot.
4. **Lighthouse agrees with the spec.** It checks for duplicates at `lh:beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs#L304-L316` and records the bid only after the signature and all other checks pass (`observe_bid`, `#L474-L477`).
5. **Confidence: high.** Disproof attempt: I looked for any code that removes the key when a later check fails and found none. The comment at `#L69` ("a hit here implies a valid-sig bid was already seen") shows this is deliberate, but it is not what the spec says.
6. **Severity:** a valid message is dropped (gossip IGNORE). This is closest to "valid block rejected" at the bid level. It costs bid liveness and proposer revenue; it does not split consensus.

### C2. ExecutionPayloadEnvelopesByRange with `count > MAX_REQUEST_PAYLOADS` returns the *last* 128 envelopes of the range, not the first

1. **Spec.**
   - `p2p.md#L1418-L1420`: "Specifications of request/response methods are equivalent to [BeaconBlocksByRange v2] … with the only difference being the response content type."
   - `phase0-p2p.md#L1555-L1556`: "Clients MUST respond with at least the first block that exists in the range, if they have it, and no more than `MAX_REQUEST_BLOCKS` blocks."
   - `phase0-p2p.md#L1560`: "Clients MAY limit the number of blocks in the response."
2. **Prysm.** `beacon-chain/sync/rpc_execution_payload_envelopes_by_range.go`:
   - `#L231`: `rp.end, err = rp.start.SafeAdd(rp.size - 1)` uses the raw `Count`.
   - `#L253-L256`: `if rp.size > maxRequest { rp.size = maxRequest }` clamps the size, but `rp.end` is not clamped again.
   - The walk then starts at `#L115` from `s.canonicalSuccessorBlock(ctx, rp.end+1)` and goes *backwards*, with `wQuota := params.BeaconConfig().MaxRequestPayloads` (`#L136`).
   - It stops at `if uint64(len(collected)) >= wQuota { break }` (`#L158-L160`), then reverses the list (`#L169`).
3. **When results differ.** A peer requests more than 128 slots, and the canonical chain has more than 128 full payloads in `[start_slot, min(start_slot+count-1, current_slot)]`. Prysm returns the 128 newest envelopes in that window and omits the first envelope in the range. The spec requires that first envelope to be included. A requester that expects a response starting at `start_slot` (as for blocks) sees a gap.
4. **Lighthouse** rejects `count > max_request_payloads` with InvalidData (`lh:beacon_node/lighthouse_network/src/rpc/handler.rs#L966-L979`). That conforms.
5. **Confidence: high.** Disproof attempt: Prysm's own requester never asks for more than 128 (per the read pass), so only other clients are affected.
6. **Severity: cosmetic** (Req/Resp conformance only).

### C3. ExecutionPayloadEnvelopesByRange with `start_slot > current_slot` can return an envelope below `start_slot`

1. **Spec.** The range is `[start_slot, start_slot + count)` (`phase0-p2p.md#L1507`), and "The response MUST contain no more than `count` blocks" (`#L1562`), applied to envelopes through `p2p.md#L1418-L1420`.
2. **Prysm.**
   - `rpc_execution_payload_envelopes_by_range.go#L226-L228`: `if rp.start > current { return rangeParams{start: current, end: current, size: 0}, nil }`.
   - `canonicalSuccessorBlock(current+1)` falls back to the head root (`beacon-chain/db/kv/blocks.go#L840-L846`, "No block found at or above slot — fall back to the head block root.").
   - `#L131-L134` starts from the head's own payload when `FullBeatsEmpty(successorRoot)`.
   - The loop stops only on `blindedEnv.Message.Slot < rp.start` (`#L150-L152`), and `rp.start` is now `current`. `size: 0` is never used as the quota.
3. **When results differ.** A peer asks for a range that starts after the server's current slot, the server's head is at the current slot, and fork choice holds it FULL. Prysm sends the head envelope, whose slot is lower than `start_slot`; the spec sends nothing. Prysm's own client rejects such a response (`beacon-chain/sync/rpc_send_request.go#L1010-L1014`, per the read pass).
4. **Lighthouse agrees with the spec.** It filters every envelope against `[req_start_slot, req_start_slot+count)` before sending (`lh:beacon_node/network/src/network_beacon_processor/rpc_methods.rs#L1508-L1515`).
5. **Confidence: high.** The tip-serving path was added by the merged PR 17533 ("Serve the chain tip's envelope in envelopes by range when fork choice holds the tip full"), which did not re-check the lower bound against the original `start_slot`.
6. **Severity: cosmetic** (Req/Resp only).

### C4. Payload attestations submitted over REST/gRPC are applied to fork choice without the spec's `block.slot == data.slot` check

1. **Spec.** `fc.md#L1153-L1154` (on_payload_attestation_message):
   ```
   if data.slot != state.slot:
       return
   ```
   Here `state = store.block_states[data.beacon_block_root]`, so a message naming a block from another slot records no vote.
2. **Prysm.**
   - `beacon-chain/blockchain/receive_payload_attestation_message.go#L27-L50` performs no slot check. It looks up the PTC with `PtcLookupState(ctx, root, a.Data.Slot)`, then for each PTC seat calls `SetPTCVote(root, idx, a.Data.PayloadPresent, a.Data.BlobDataAvailable)`.
   - `SetPTCVote` (`forkchoice/doubly-linked-tree/gloas.go#L537-L548`) only checks that the node exists.
   - REST `beacon-chain/rpc/eth/beacon/handlers_pool.go#L994-L1046` checks the current slot, PTC membership and signature, then calls `ReceivePayloadAttestationMessage`. It does not check that the block is at `data.slot`.
   - gRPC `SubmitPayloadAttestation` (`beacon-chain/rpc/prysm/v1alpha1/validator/payload_attestation.go#L64-L119`, per the read pass) behaves the same way.
   - The gossip path does check it (`beacon-chain/sync/validate_payload_attestation.go#L85-L92`).
3. **When results differ.** A correctly signed message from a current-slot PTC member names an *older* block root, for example after a skipped slot or from a buggy validator client. The spec ignores it. Prysm sets PTC bits on the older block, which feed `should_extend_payload`, the tiebreaker and `should_build_on_full`.
4. **Lighthouse agrees with the spec.** It has the early return `if block.slot != payload_attestation.data.slot { return Ok(()); }` (`lh:consensus/fork_choice/src/fork_choice.rs#L1426-L1428`).
5. **Confidence: high** on the code path, **medium** that it matters in practice: the submitter must hold a current-slot PTC key, and the votes land on that one node. Disproof attempt: I checked whether `PtcLookupState` binds the block slot. It does not; it only selects a state by dependent root (`receive_payload_attestation_message.go#L52-L82`). The merged PR 17555 ("…skip votes for older blocks") fixed only the in-block path (`process_block.go#L625-L660`).
6. **Severity: fork-choice divergence** (local node only).

### C5. Queued payload attestations are applied after their slot has passed

1. **Spec.** `fc.md#L1167-L1169`:
   ```
   if not is_from_block:
       # Check that the attestation is for the current slot
       assert data.slot == get_current_slot(store)
   ```
2. **Prysm.** `beacon-chain/sync/pending_payload_attestation.go#L68-L107` (`processPendingPayloadAttestation`) replays queued messages when the block arrives. It re-checks only `payloadAttestationCache.Seen(...)` (`#L94`) and `blockSlot != pa.Slot()` (`#L97`), not the current slot.
3. **When results differ.** A PTC message arrives before its block, and the block arrives only after its slot has ended. Prysm applies the queued votes in a later slot; the spec rejects them.
4. **Lighthouse agrees with the spec.** It enforces the current slot for non-block messages inside fork choice (`lh:consensus/fork_choice/src/fork_choice.rs#L1430-L1441`).
5. **Confidence: medium.** I did not trace whether the queue is pruned at the slot boundary; no pruning appears in the function I read.
6. **Severity: fork-choice divergence** (low: it only affects late blocks).

### C6. Init-sync batch path skips `notify_ptc_messages` and can mark a parent FULL from an unverified child bid

1. **Spec.**
   - `fc.md#L1095`: `notify_ptc_messages(store, state, block.body.payload_attestations)` runs for every block in `on_block`.
   - `fc.md#L1059-L1061`: "If this block builds on the parent's full payload, that payload must have been verified by on_execution_payload_envelope … `assert is_payload_verified(store, block.parent_root)`".
   - `fc.md#L243`: the anchor store starts with `payloads={}`.
2. **Prysm.**
   - `handleBlockPayloadAttestations` is called only from the single-block `postBlockProcess` (`beacon-chain/blockchain/process_block.go#L89`). The batch path `onBlockBatch` (`process_block.go#L188-L382`) never calls it.
   - In the batch path, `fillInForkChoiceMissingBlocks` runs at `#L235`, before batch signature verification at `#L330-L341`. It calls `insertFirstPayloadIfNeeded` (`beacon-chain/blockchain/process_block_helpers.go#L444-L447`, `#L457-L469`):
     ```go
     if s.builtOnFullParentInForkchoice(b) && !s.cfg.ForkChoiceStore.HasFullNode(parentRoot) {
         ...
         s.cfg.ForkChoiceStore.MarkFullNode(parentRoot, gasLimit)
     }
     ```
     This marks the parent FULL based only on the child bid's `parent_block_hash`, without an envelope. Per the read pass, nothing rolls this back if the batch later fails.
3. **When results differ.**
   - Batch-imported blocks never get their in-block PTC votes, so their PTC tallies stay empty.
   - A batch whose first block claims a full parent makes Prysm create the parent's FULL node with no verified envelope, even if the batch is later rejected. The spec rejects that block.
4. **Lighthouse agrees with the spec.** It checks `ParentPayloadNotVerified` (`lh:consensus/fork_choice/src/fork_choice.rs#L829-L839`) and applies PTC votes for every imported block (`lh:beacon_node/beacon_chain/src/block_verification.rs#L1782-L1805`).
5. **Confidence: medium.**
   - (a) The missing call is certain: `grep handleBlockPayloadAttestations` shows one non-test caller. Whether it matters is uncertain, because at the end of sync the batch-imported blocks are usually older than the previous slot.
   - (b) The child's state transition binds `parent_execution_requests` to the parent bid (`beacon-chain/core/gloas/parent_payload.go#L49-L56`, per the read pass), which partly compensates. The parent's EL payload is still never checked in this path unless `envelopes[0]` was supplied (`process_block.go#L158-L180`).
6. **Severity: fork-choice divergence.** Case (b) amounts to an invalid parent payload status accepted.

### C7. Restart rebuilds fork choice only up to the justified checkpoint and infers FULL status; stored envelopes are not replayed

1. **Spec.** There is no restart rule; the store is defined as if every block and every envelope had been processed (`on_execution_payload_envelope`, `fc.md#L1107-L1134`). Fork choice after a restart should equal the uninterrupted store.
2. **Prysm.**
   - `beacon-chain/blockchain/setup_forkchoice.go#L33-L39`: `startupHeadRoot()` returns the justified root by default.
   - `#L64-L66`: `if headRoot == fRoot { return nil }` returns before `markFinalizedRootFull`.
   - `resolveChainPayloadStatus` (`#L167-L192`) sets FULL from successor bids (`BlockBuiltOnParentPayload`) and uses `HasExecutionPayloadEnvelope` only for the last block.
   - No call to `InsertPayload` replays envelopes stored in the DB for blocks after the justified root.
3. **When results differ.** After any restart:
   - Blocks after the justified checkpoint are in the DB but not in fork choice. Their envelopes are never applied.
   - When justified equals finalized, the anchor's FULL node is never created.
   - A child that builds on a full parent then fails `ParentPayloadReady` (`beacon-chain/blockchain/chain_info.go#L486-L500`), or fails fork-choice insert with "invalid parent root".
4. **Lighthouse** persists proto-array nodes together with `payload_received` (`lh:consensus/proto_array/src/proto_array_fork_choice.rs#L1116`, per the read pass), so the status survives a restart.
5. **Confidence: high.** This matches the field report in the open PR 17735.
6. **Severity:** valid block rejected (the node stalls) and fork-choice divergence.
7. **Known:** yes, PR 17735, which is open.

### C8. Previous-slot EMPTY/FULL decided by early-counted current-slot votes instead of weight 0 plus the tiebreaker

1. **Spec.** `fc.md#L567-L570`:
   ```
   # [New in Gloas:EIP7732]
   if is_previous_slot_payload_decision(store, node):
       return Gwei(0)
   ```
   `is_previous_slot_payload_decision` is defined at `fc.md#L461-L464`.
2. **Prysm.**
   - `beacon-chain/forkchoice/doubly-linked-tree/gloas.go#L298-L317` (`choosePayloadContent`) compares `fn.weight` with `en.weight` first. It uses `shouldExtendPayload` only on a tie in the previous slot.
   - The only adjustment is `removeProposerBoostFromParent` (`gloas.go#L734-L753`), which removes the boost and nothing else.
   - From `SlotDuration-2s` into each slot, `UpdateHead` runs with attestations processed under `disparity += reorgLateBlockCountAttestations` (2s) (`beacon-chain/blockchain/receive_attestation.go#L27`, `#L94-L104`, `#L163-L166`). `OnAttestation` only requires `VerifyTime(slot+1, disparity)` (`process_attestation.go#L83-L86`).
3. **When results differ.** In the last ~2 s of slot N+1, slot-(N+1) votes are counted toward block N's EMPTY or FULL node, including through a slot-(N+1) child. The spec keeps both at 0 and chooses by `should_extend_payload`. If the vote majority disagrees with the PTC-based tiebreaker, the local head, the FCU and the next proposal's parent status differ. The outcome converges at slot N+2.
4. **Lighthouse agrees with the spec.** It returns `Ok(0)` for non-PENDING nodes of the previous slot (`lh:consensus/proto_array/src/proto_array.rs#L1775-L1799`).
5. **Confidence: medium.** The behaviour is clear, but it is a deliberate look-ahead.
6. **Severity: fork-choice divergence** (transient).
7. **Known:** yes. Issue 17119 is closed (the brief records it as rejected), and the fix PR 17146 was closed unmerged.

### C9. A proposer reorg picks the parent's payload status by weight, not by `get_parent_payload_status(head_block)`

1. **Spec.** `fc.md#L851-L863`: "This function is modified to preserve the payload status of the parent node when a proposer re-org is selected."
   ```
   parent_payload_status = get_parent_payload_status(store, head_block)
   parent_node = ForkChoiceNode(root=parent_root, payload_status=parent_payload_status)
   ```
2. **Prysm.**
   - `GetProposerHead` returns only a root (`beacon-chain/forkchoice/doubly-linked-tree/reorg_late_blocks.go#L107-L171`: `return parent.node.root`).
   - The proposer derives the status in `parentFull` (`beacon-chain/rpc/prysm/v1alpha1/validator/proposer.go#L185-L194`): `if root == parentRoot { return full } … return vs.ForkchoiceFetcher.FullBeatsEmpty(parentRoot)`.
   - `FullBeatsEmpty` is the weight/tiebreak choice made by `choosePayloadContent`.
3. **When results differ.** A late head H (slot N) built on P's EMPTY variant, but at slot N+1 P's FULL variant is heavier, for example because slot-N voters chose P with index 1. The spec reorgs onto P-EMPTY; Prysm builds on P-FULL. The reverse case also differs.
4. **Lighthouse:** not determined; I did not trace how it picks the parent's status after `get_proposer_head`.
5. **Confidence: medium-low.** It is unclear how often P's two variants disagree with H's choice.
6. **Severity: fork-choice divergence.** The local proposal builds on a non-spec parent node.

### C10. `beacon_attestation` / aggregate with `index == 1` is ACCEPTed while the attested payload is optimistic

1. **Spec.** `p2p.md#L455-L458`:
   ```
   # [IGNORE] The attested execution payload is optimistic
   payload_status = block_payload_statuses.get(block_root, PAYLOAD_STATUS_NOT_VALIDATED)
   if payload_status == PAYLOAD_STATUS_NOT_VALIDATED:
       raise GossipIgnore("attested payload is optimistic")
   ```
2. **Prysm.** `beacon-chain/sync/validate_beacon_attestation.go#L373-L399` (`validateGloasCommitteeIndex`):
   - `>= 2` → REJECT
   - same slot with `index != 0` → REJECT
   - `hasBadPayload` → REJECT
   - `!HasFullNode` → IGNORE
   - there is no optimistic check. Full nodes carry an `optimistic` flag (`forkchoice/doubly-linked-tree/gloas.go#L134-L141`, per the read pass).
3. **When results differ.** An index-1 attestation for a payload whose EL verdict is still SYNCING or ACCEPTED. The spec IGNOREs it; Prysm forwards it.
4. **Lighthouse agrees with Prysm.** It checks only `payload_received` (`lh:beacon_node/beacon_chain/src/attestation_verification.rs#L964-L972`, `#L624-L634`).
5. **Confidence: high.**
6. **Severity: cosmetic.** The gossip verdict differs and messages propagate.
7. **Known:** yes. PR 17574, "Ignore Gloas payload-present attestations while the payload is optimistic", is open.

### C11. Execution-payload-bid verdict ordering differences (cosmetic)

1. **Spec.**
   - The highest-value IGNORE (`p2p.md#L973-L977`) runs before the signature REJECT (`#L1077-L1079`).
   - "bid slot > parent slot" is an early REJECT (`p2p.md#L1002-L1004`).
2. **Prysm.**
   - The signature check (`validate_execution_payload_bid.go#L120-L122`) runs before highest-value (`#L124-L127`).
   - The parent-slot REJECT is last (`#L161-L168`). Before it, `#L81-L84` IGNOREs when the next-slot state cache has nothing for the parent. In practice that makes the REJECT unreachable, so the bid gets IGNORE (per the read pass, `core/transition/trailing_slot_state_cache.go#L41-L54`).
3. **When results differ.**
   - A lower-value bid with a bad signature: the spec says IGNORE, Prysm says REJECT, so Prysm penalizes the peer.
   - A bid whose slot is not above its parent's: the spec says REJECT, Prysm says IGNORE.
4. **Lighthouse agrees with the spec** (value check `gossip_verified_bid.rs#L320-L329`, signature last at `#L462-L472`, early `BidNotDescendantOfParent` at `#L337-L344`).
5. **Confidence: medium.**
6. **Severity: cosmetic** (peer scoring only).

### C12. Spec-version lag: builder onboarding at the fork follows v1.7.0-beta.4, not master (consensus-specs PR 5736)

1. **Spec (master).**
   - `fork.md#L69`: "A valid builder registration can use a validator's public key."
   - `fork.md#L86`: `if not is_builder_withdrawal_credential(deposit.withdrawal_credentials):` is checked first. Non-builder deposits stay queued "even if a builder uses the same public key" (`fork.md#L68`).
2. **Prysm.** `beacon-chain/state/state-native/setters_gloas.go#L808-L842` (beta.4 text embedded at `#L743-L794`; called from `core/gloas/upgrade.go#L120-L133`):
   - an existing-validator pubkey keeps the deposit queued (`if _, ok := b.validatorIndexByPubkey(pubkey); ok {… continue}`, `#L810-L814`), and so does a valid pending validator deposit (`#L829-L837`);
   - an existing-builder pubkey tops up that builder *before* the credential check (`#L816-L822`).
3. **When results differ.** At the fork, a builder-credential deposit uses a validator's pubkey, or a non-builder deposit uses the pubkey of a builder just created. Master creates the builder or keeps the deposit queued; Prysm does the reverse, so the post-fork state root differs from a master client.
4. **Lighthouse is also on beta.4** (`lh:consensus/state_processing/src/upgrade/gloas.rs#L258-L305`, per the read pass).
5. **Confidence: high** that the lag exists. **Low** that it triggers, since it needs such deposits pending at the fork.
6. **Severity:** consensus split against master-spec clients. This is spec-version lag, not a bug against beta.4.
7. **Known:** PR 17664 ("Bump consensus spec to v1.7.0-beta.4") is closed. I found no PR bumping past beta.4.

### Minor notes (Gloas-only)

- **Envelope dedupe.** `validate_execution_payload_envelope.go#L77-L80` IGNOREs every envelope for a root that already has a full node. Under the spec, a mismatched builder gets the builder-index REJECT (`p2p.md#L858-L860`), so Prysm under-penalizes the peer. The effect is cosmetic.
- **Lighthouse ordering.** Lighthouse records block PTC votes (`block_verification.rs#L1782-L1805`) before `on_block` (`beacon_chain.rs#L4525`). Prysm records them after insert (`process_block.go#L79-L91`), which is the spec order.

## Affects live code, details withheld

Five further candidates sit in logic shared with the live Fulu fork:
- Prysm, EIP-7732: affects live code, details withheld.
- Prysm, EIP-7732: affects live code, details withheld.
- Prysm, EIP-7732: affects live code, details withheld.
- Prysm, EIP-7732: affects live code, details withheld.
- Prysm, EIP-7732: affects live code, details withheld.

---

## Rules checked and found consistent with the spec

Paths are Prysm `beacon-chain/…` unless stated otherwise.

| Rule (spec) | Prysm location |
|---|---|
| Parent full ⇒ payload verified (`fc.md#L1059-L1061`) | `sync/validate_beacon_block_gloas.go#L51-L59`, `sync/pending_blocks_queue.go#L146-L149`, `forkchoice/doubly-linked-tree/gloas.go#L43-L63`, `store.go#L174-L185` |
| `get_parent_payload_status` hash comparison (`fc.md#L368-L372`) | `gloas.go#L58-L61`, `core/gloas/parent_payload.go#L39-L40` |
| Block PTC votes applied after the block checks (`fc.md#L1095`) | `blockchain/process_block.go#L79-L91`, `#L625-L660` (single-block path only; see C6) |
| on_execution_payload_envelope (`fc.md#L1120-L1133`) | `blockchain/receive_execution_payload_envelope.go#L36-L165`, `core/gloas/payload.go#L70-L196` |
| PTC thresholds `PTC_SIZE // 2`, strict `>` (`fc.md#L101-L102`, `#L341`, `#L362`) | `gloas.go#L283-L284`, `#L294` |
| `should_extend_payload` / tiebreaker (`fc.md#L497-L525`) | `gloas.go#L260-L285`, `#L311-L316` |
| `should_build_on_full` (`fc.md#L475-L485`) | `blockchain/gloas.go#L317-L344`; the FULL-head guard is in the caller, `receive_attestation.go#L184-L190` |
| `should_apply_proposer_boost` (`fc.md#L531-L561`) | `gloas.go#L699-L730` |
| `get_supported_node` (`fc.md#L436-L451`) | `gloas.go#L562-L571` |
| Attestation index 0/1 rules (`fc.md#L938-L944`, `p2p.md#L440-L453`) | `sync/validate_beacon_attestation.go#L373-L399`; aggregates via `validate_aggregate_proof.go#L183` (except C10) |
| Bid with proposer preferences unseen → **IGNORE** (`p2p.md#L1017-L1021`) | `sync/validate_execution_payload_bid.go#L91-L94` |
| Other bid verdicts | `validate_execution_payload_bid.go#L59-L119`, `#L150-L156` |
| Envelope gossip verdicts (`p2p.md#L817-L884`) | `sync/validate_execution_payload_envelope.go#L71-L159` |
| Payload-attestation gossip verdicts (`p2p.md#L891-L949`) | `sync/validate_payload_attestation.go#L51-L122` |
| Proposer-preferences verdicts (`p2p.md#L1103-L1174`) | `sync/validate_signed_proposer_preferences.go#L68-L179` |
| EnvelopesByRange: `count == 0` → InvalidRequest (spec is silent); canonical chain only; head included only if FULL (`p2p.md#L1422-L1431`) | `sync/rpc_execution_payload_envelopes_by_range.go#L115-L169`, `#L218-L220` (except C2 and C3) |
| EnvelopesByRoot: ≤ 128, missing envelopes skipped (`p2p.md#L1472-L1493`) | `sync/rpc_execution_payload_envelopes_by_root.go#L52-L57`, `#L97-L100` |
| Deadlines at the fork boundary: the slot's epoch selects the Gloas BPS (`fc.md#L747-L790`) | `config/params/config.go#L882-L887`, `forkchoice/doubly-linked-tree/node.go#L42`, `store.go#L198-L201` |

**Fork-boundary note.** In the first Gloas slot, the late-block ticker still uses the Fulu offset (`blockchain/process_block.go#L843-L853`). This affects only proposer-prep timing.

## Known-check

**Method.** I searched with `api.github.com/search/issues` using `q=repo:OffchainLabs/prysm <terms>`, which covers issues and PRs, open and closed. I took each item's state from `/pulls/<n>`. Three queries returned nothing because of the API rate limit and are not counted: "ExecutionPayloadEnvelopesByRange", "payload bid seen" and "envelopes range count MaxRequestPayloads".

| Candidate | Status | Queries / evidence |
|---|---|---|
| C1 | not found | "seen bid builder tuple" (0); "execution payload bid gossip" (31, unrelated: 17044, 17217, 17011, 17482, 17556) |
| C2, C3 | not found | "envelopes by range" (18: 16553 merged, 17533 merged, 17655 closed, 17567 and 17770 open, none on these points) |
| C4 | not found | "payload attestation REST" (17100 merged added this path; 17363 merged; 17555 merged fixed the block path only); "payload attestation slot block" |
| C5 | not found | "pending payload attestation current slot" (16909 closed, 17016 closed, 17233 open, all unrelated) |
| C6 | not found | "onBlockBatch payload attestations" (0); "MarkFullNode" (17652, 17093, 16599, all closed, unrelated) |
| C7 | **known**: PR 17735, open | https://github.com/OffchainLabs/prysm/pull/17735 |
| C8 | **known**: issue 17119, closed (rejected per the brief); PR 17146 closed unmerged | "previous slot payload weight" |
| C9 | not found | "proposer head payload status parent" |
| C10 | **known**: PR 17574, open | "index 1 attestation optimistic payload" |
| C11 | not found | "execution payload bid gossip" |
| C12 | PR 17664 (bump to beta.4) closed; no later bump found | "Bump consensus spec"; "onboard builders pending deposits" |
| PTC timing | already indexed: 17464 and 17465, closed | not re-examined |
| LH 9593 | open: "Align Gloas proposer reorg weight logic with fork-choice spec" | https://github.com/sigp/lighthouse/issues/9593 |

## URLs read

- `api.github.com/repos/{OffchainLabs/prysm/commits/develop, ethereum/consensus-specs/commits/master, sigp/lighthouse/commits/stable}`
- Shallow clones of github.com/OffchainLabs/prysm, ethereum/consensus-specs and sigp/lighthouse at the pins. Spec files read: `specs/gloas/{fork-choice,p2p-interface,validator,fork}.md` and `specs/phase0/{p2p-interface,fork-choice}.md`.
- `api.github.com/search/issues` (the queries above), `api.github.com/repos/OffchainLabs/prysm/pulls/{17146,17533,17555,17100,17735,16553}`, `…/issues/17735`, `api.github.com/repos/sigp/lighthouse/issues/9593`

**Not read**
- eips.ethereum.org/EIPS/eip-7732. All rule text here is quoted from consensus-specs.
- The v1.7.0-beta.4 tag. C12 relies on Prysm's embedded spec quote and its pins.
- The body of issue 17119.
- Hive and execution-spec-tests results, which are out of scope for this item.

**Disproof pass.** For each candidate I looked for code that compensates; what I found is under part (5). C7, C8 and C10 are marked known. C12 is version lag, not a bug against the version Prysm declares.
