# Lighthouse vs. EIP-7732 (ePBS) fork choice and networking, Gloas: spec-conformance review

Code reading only. Nothing was run against any network or node.

## 1. Pins

| Repo / ref | Commit (from api.github.com/repos/…/commits/…) | Commit date |
|---|---|---|
| sigp/lighthouse `stable` (v8.3.2) | `a376defa30470923556d91bdeb15a802e297f31f` | 2026-10-22 |
| sigp/lighthouse `unstable` (8.4.0-rc.0) | `301f3bf2323cd2b3f946ceed026be8075d24e1c3` | 2026-10-27 |
| ethereum/consensus-specs `master` | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` | 2026-10-27 |
| OffchainLabs/prysm `develop` | `eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a` | 2026-10-27 |

URL prefixes. Every citation below is a path plus `#Lx-Ly` at these commits.
- **LH** = `https://github.com/sigp/lighthouse/blob/a376defa30470923556d91bdeb15a802e297f31f/`
- **SP** = `https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/`
- **PR** = `https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/`
- Lighthouse path abbreviations: `BC/` = `beacon_node/beacon_chain/src/`, `PA/` = `consensus/proto_array/src/`, `FC/` = `consensus/fork_choice/src/`, `NBP/` = `beacon_node/network/src/network_beacon_processor/`, `LN/` = `beacon_node/lighthouse_network/src/`.

**Spec version targeted.** Both Lighthouse branches pin `CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-beta.4`, at LH `testing/ef_tests/Makefile#L3-L3` on stable and on unstable. Master differs from beta.4 in `fork.md`: consensus-specs PR 5736, "Separate builder and validator deposits at the Gloas fork", was merged 2026-10-26. That lag is candidate C14.

**stable vs unstable.** I ran `diff -rq` over both trees. No file under `consensus/fork_choice/` or `consensus/proto_array/` differs, and neither do the gossip verifiers (`payload_bid_verification/`, `payload_envelope_verification/`, `payload_attestation_verification/` non-test files, `attestation_verification.rs`, `block_verification.rs`) or `rpc_methods.rs`. Differences that touch this scope:
- `gossip_methods.rs`: stable spawns the column fetch inline. Unstable calls `spawn_fetch_block_columns(..)` instead, which shifts every later line by −19. No gossip verdict mapping changed.
- `rpc/inbound_limiter.rs`: `MAX_REQUEST_DELAY` changes from `Duration::from_secs(15)` to `RESP_TIMEOUT` (10 s), and `rpc/handler.rs` makes `RESP_TIMEOUT` `pub(super)`. This is not Gloas-specific.
- `beacon_chain.rs` and `builder.rs` also differ (builder-client generics). Line numbers I cite from these two files hold at the stable pin only.

All findings below therefore apply to both branches.

## 2. Candidates

Severity vocabulary follows the brief. "Cosmetic" here means a gossip or Req/Resp verdict or peer-scoring difference that changes no state or head.

### C1. on_block order: a block's payload attestations reach fork choice before the fork-choice on_block checks
1. **Spec.** SP `fork-choice.md#L1058-L1061` and `#L1094-L1095`: `if is_parent_node_full(store, block): assert is_payload_verified(store, block.parent_root)`. The finalized-descendant asserts at `#L1067-L1076` also run before `notify_ptc_messages(store, state, block.body.payload_attestations)`. A failed assert aborts the handler, so no PTC vote is recorded.
2. **Client.**
   - LH `BC/block_verification.rs#L1782-L1805` runs right after the state-root check, under a fork-choice write lock: `if let Ok(payload_attestations) = block.message().body().payload_attestations() { … fork_choice.on_payload_attestation(current_slot, indexed_payload_attestation, AttestationFromBlock::True, &ptc.0)`.
   - The checks run later, at import, in LH `FC/fork_choice.rs#L828-L879`: `if builds_on_full && !self.is_payload_received(&block.parent_root()) { return Err(…ParentPayloadNotVerified…) }`, followed by the `FutureSlot`, `FinalizedSlot` and `NotFinalizedDescendant` checks.
   - Nothing undoes the votes if `on_block` then fails.
3. **When results differ.** A block passes the state transition but then fails the fork-choice `on_block` checks. Example: finalization advances while the block is in execution/availability verification, so it is no longer a finalized descendant. Its payload attestations, which are valid and already signature-checked, have still raised the parent's PTC timeliness and data-availability votes in Lighthouse. The spec store never records them. That can flip `should_extend_payload`/`should_build_on_full` for the parent from the previous slot.
4. **Prysm.** Agrees with the spec. PR `beacon-chain/blockchain/process_block.go#L79-L91` calls `InsertNode` (where the parent-full check lives, PR `beacon-chain/forkchoice/doubly-linked-tree/gloas.go#L58-L61`, `store.go#L174-L185`) before `handleBlockPayloadAttestations`.
5. **Confidence: low.** Disproof attempt: gossip verification already checks the full-parent condition and finalized descent (LH `block_verification.rs#L989-L999` region), so the window is only a race between verification and import, or a parent-payload state that changes in that window. The same PTC votes usually arrive on gossip anyway. I did not show any reachable path that ends in a different head.
6. **Severity:** fork-choice divergence (narrow).

### C2. is_head_weak leaves out equivocating committee weight (Gloas `should_apply_proposer_boost`)
1. **Spec.** SP `fork-choice.md#L815-L832`: `for index in range(get_committee_count_per_slot(head_state, epoch)): committee = get_beacon_committee(…) head_weight += Gwei(sum(… for index in committee if index in store.equivocating_indices))`. `should_apply_proposer_boost` uses it at `#L544-L546`.
2. **Client.** LH `PA/proto_array.rs#L793-L818`:
   - The comment reads: `// TODO(gloas): the spec adds weight from equivocating validators in the head slot's *committees* … We approximate with equivocating_attestation_score which only tracks equivocating validators whose vote pointed at this block.`
   - The code: `let head_weight = head_node.attestation_score(PayloadStatus::Pending).saturating_add(head_node.equivocating_attestation_score().unwrap_or(0)); head_weight < reorg_threshold`.
   - It is called from `should_apply_proposer_boost` at `#L858-L861`.
3. **When results differ.** Some validators in the parent slot's committees are equivocators who voted for a different branch (or not for the parent). The parent's weight sits just below the reorg threshold without them and above it with them. Lighthouse then calls the parent weak and, if an early equivocating block exists, removes the proposer boost. The spec keeps it, so the head can differ.
4. **Prysm.** Also omits it, so it agrees with Lighthouse rather than the spec. PR `beacon-chain/forkchoice/doubly-linked-tree/gloas.go#L719-L719`: `if p.node.weight*100 >= s.committeeWeight*…ReorgHeadWeightThreshold { return true }`. A grep for "equivocat" in Prysm's forkchoice package found no committee add-back.
5. **Confidence: high**, since the code says so itself. Disproof: none possible, the TODO admits the deviation.
6. **Severity:** fork-choice divergence.
7. **Known:** yes, sigp/lighthouse #9593 (issue, open) and #9831 (PR, open).

### C3. Gossip `beacon_attestation`/`beacon_aggregate_and_proof`, index 1: no IGNORE for an optimistic payload, no REJECT for an invalidated one
1. **Spec.** SP `p2p-interface.md#L455-L462`: `# [IGNORE] The attested execution payload is optimistic … if payload_status == PAYLOAD_STATUS_NOT_VALIDATED: raise GossipIgnore(…)` and `# [REJECT] The attested execution payload is processed and invalid`.
2. **Client.** The only payload check is `payload_received`:
   - Unaggregated, LH `BC/attestation_verification.rs#L964-L972`: `if fork_name.gloas_enabled() && attestation.data.index == 1 && !head_block.payload_received { return Err(Error::UnknownPayloadEnvelope …`
   - Aggregate: `#L624-L634`.
   - The flag is set for both optimistic and valid imports, at LH `PA/proto_array.rs#L905-L906`: `// The envelope arrived: record it so sync stops fetching, whatever the verdict.` / `v29.payload_received = true;`
   - Later invalidation leaves it set. `verify_head_block_is_known` (`attestation_verification.rs#L1244-L1291`) does not look at execution status.
3. **When results differ.**
   - An index-1 attestation names a block whose envelope was imported optimistically (EL SYNCING/ACCEPTED). The spec IGNOREs it; Lighthouse ACCEPTs and forwards it.
   - Same case, but the payload was later invalidated. The spec REJECTs; Lighthouse ACCEPTs.
4. **Prysm.** Has the REJECT and lacks the optimistic IGNORE, so it agrees with Lighthouse on that half. PR `beacon-chain/sync/validate_beacon_attestation.go#L373-L399`: `if s.hasBadPayload(blockRoot) { return pubsub.ValidationReject … } if !s.cfg.chain.HasFullNode(blockRoot) { return pubsub.ValidationIgnore …`
5. **Confidence: high**, from reading the code. Disproof: I looked for an optimistic or invalid filter in `verify_head_block_is_known`, `verify_attestation_target_root` and the GM error mapping, and found none.
6. **Severity:** cosmetic (gossip verdict). Fork-choice weight from invalid payloads is filtered separately by `get_node_children`, which drops invalid FULL nodes (`proto_array.rs#L1693-L1699`).
7. **Known:** the invalidated half is #10464 (PR, open, "Reject Gloas index 1 attestations to an invalidated payload"). For the optimistic half, nothing found (queries below).

### C4. Gossip `execution_payload`: a block that failed validation gives IGNORE instead of REJECT
1. **Spec.** SP `p2p-interface.md#L840` (inside the envelope validator): `# [REJECT] The envelope's block passes validation`.
2. **Client.** LH `BC/payload_envelope_verification/gossip_verified_envelope.rs#L154-L169`. The comment reads `// 1. Blocks we haven't seen (IGNORE), and // 2. Blocks we've seen that are invalid (REJECT). // Presently these two cases are conflated.`, and the code is `let Some(proto_block) = fork_choice_read_lock.get_block(&beacon_block_root) else { return Err(EnvelopeError::BlockRootUnknown …`. That error maps to IGNORE plus a queued lookup.
3. **When results differ.** An envelope references a block the node has seen and rejected as invalid. The spec REJECTs, which penalises the peer. Lighthouse IGNOREs and queues it.
4. **Prysm.** Agrees with the spec. PR `beacon-chain/sync/validate_execution_payload_envelope.go#L91-L92`: `if err := v.VerifyBlockRootValid(s.hasBadBlock); err != nil { return pubsub.ValidationReject, err }`.
5. **Confidence: high**, self-documented.
6. **Severity:** cosmetic.
7. **Known:** related #8949 (issue, closed, "envelope peer penalties and REJECT/IGNORE mapping"). The TODO is still on stable, so this is effectively known and not fixed.

### C5. Gossip `payload_attestation_message`: a block that failed validation gives IGNORE instead of REJECT
1. **Spec.** SP `p2p-interface.md#L922`: `# [REJECT] The payload attestation's block passes validation`.
2. **Client.** LH `BC/payload_attestation_verification/gossip_verified_payload_attestation.rs#L75-L88`: `// TODO(gloas): These two conditions are conflated. … Presently both cases return IGNORE.`, then `.ok_or(Error::UnknownHeadBlock { beacon_block_root })?`.
3. **When results differ.** A PTC message names a block root the node has seen and found invalid. Spec REJECT; Lighthouse IGNORE.
4. **Prysm.** Agrees with the spec, REJECT on a bad block: PR `beacon-chain/sync/validate_payload_attestation.go#L95-L97`.
5. **Confidence: high.**
6. **Severity:** cosmetic.
7. **Known:** not found as a separate issue. It is a code TODO.

### C6. Gossip `beacon_block`: no REJECT when the parent is not full and the bid does not build on the parent's `latest_block_hash`
1. **Spec.** SP `p2p-interface.md#L662-L666`: `# [REJECT] If the parent is not full, the bid builds on the parent's execution head` / `if not is_parent_node_full(store, block): if bid.parent_block_hash != parent_state.latest_block_hash: raise GossipReject(…)`.
2. **Client.**
   - Gossip verification, LH `BC/block_verification.rs` `new_without_slasher_checks` (`#L906-L1138`), checks bid-parent-root equality (`#L991-L999`) but not `latest_block_hash`.
   - A grep for `latest_block_hash` in `beacon_node/beacon_chain/src` outside tests finds only `test_utils.rs`.
   - The rule is enforced only during the state transition, at LH `consensus/state_processing/src/per_block_processing.rs#L818-L822`: `bid.parent_block_hash() == *latest_block_hash`.
3. **When results differ.** A correctly signed block whose bid's `parent_block_hash` equals neither the parent's bid `block_hash` nor the parent state's `latest_block_hash`. The spec REJECTs it on gossip. Lighthouse ACCEPTs and forwards it, then fails it at import. The block is still never imported, so consensus is unaffected.
4. **Prysm.** Agrees with the spec. PR `beacon-chain/sync/validate_beacon_block_gloas.go#L41-L42`: `if !s.cfg.chain.HasPayloadBlockHash(blk.ParentRoot(), bid.ParentBlockHash()) { return pubsub.ValidationReject, errors.New("bid does not build on the parent's execution head") }`.
5. **Confidence: medium.** Disproof: I looked for an equivalent check that runs before propagation (gossip verification) and found none. I did not trace whether the parent-status lookup in `get_parent_import_status` indirectly rejects this case.
6. **Severity:** cosmetic (an invalid block is propagated, an invalid block is not accepted into the state).
7. **Known:** not found.

### C7. Gossip `execution_payload_bid`: proposer-preferences key derived from the head, not from `bid.parent_block_root`
1. **Spec.** SP `p2p-interface.md#L1017-L1021`: `dependent_root = get_shuffling_dependent_root(store, bid.parent_block_root, proposal_epoch)`, `prefs_key = (bid.slot, dependent_root)`, `if prefs_key not in seen.proposer_preferences: raise GossipIgnore(…)`.
2. **Client.** LH `BC/payload_bid_verification/gossip_verified_bid.rs#L392-L405`: `// Look up the preferences keyed by the dependent root that is canonical from our head's perspective …`, then `head_state.proposer_shuffling_decision_root_at_epoch(proposal_epoch, cached_head.head_block_root(), ctx.spec)?` and `get_preferences(&bid_slot, dependent_root) … else { return Err(PayloadBidError::NoProposerPreferences …`.
3. **When results differ.** The bid's parent is not the head, and the parent's branch has a different dependent root for the proposal epoch. Lighthouse then checks the head branch's preferences instead of the parent's: it IGNOREs bids the spec accepts, and can accept bids the spec IGNOREs. The unseen-preferences verdict itself (IGNORE) matches the spec.
4. **Prysm.** Also IGNOREs on unseen preferences (PR `beacon-chain/sync/validate_execution_payload_bid.go#L85-L94`: `if !ok { return pubsub.ValidationIgnore, nil }`). I did not verify which root Prysm derives the key from.
5. **Confidence: medium.** The choice is deliberate. Disproof: the later head-branch-compatibility IGNORE (spec `#L1040`, LH `#L428-L433`) masks most cases. No concrete case was constructed.
6. **Severity:** cosmetic.
7. **Known:** not found. Related closed PRs: #9036, #9337.

### C8. Gossip `execution_payload_bid`: no proposer-lookahead IGNORE; builder checks run on an un-advanced head state
1. **Spec.**
   - SP `p2p-interface.md#L1013-L1015`: `# [IGNORE] The bid's slot is within the parent's proposer lookahead` / `if proposal_epoch > get_current_epoch(state) + MIN_SEED_LOOKAHEAD`.
   - The builder, balance and signature checks (`#L1048-L1077`) use the parent state after `process_slots(state, bid.slot)`.
2. **Client.**
   - LH `gossip_verified_bid.rs`: a grep for `lookahead` or `min_seed_lookahead` in the file returns nothing.
   - The builder checks at `#L130-L155` and the signature check at `#L462-L472` use `head_state` (sub-review).
3. **When results differ.** Either the parent is more than one epoch behind the bid slot (spec IGNORE; Lighthouse may ACCEPT), or a builder's status or balance changes at an epoch boundary between the head state and `bid.slot`.
4. **Prysm.** Not verified.
5. **Confidence: low.** Likely masked by the preferences key and the head-branch check.
6. **Severity:** cosmetic.
7. **Known:** not found.

### C9. Gossip clock-disparity allowance missing for bid slot and proposer-preferences slot
1. **Spec.** SP `p2p-interface.md#L979` (bid: `is_current_or_next_slot`, which includes `MAXIMUM_GOSSIP_CLOCK_DISPARITY`, `#L344-L358`) and `#L1125` (preferences: `is_past_slot` with disparity).
2. **Client.** LH `gossip_verified_bid.rs#L27-L33`: `if bid_slot == current_slot || bid_slot == current_slot.saturating_add(1u64) { Ok(()) } else { Err(…InvalidBidSlot…) }`. The preferences check is in LH `BC/proposer_preferences_verification/gossip_verified_proposer_preferences.rs#L40-L45` (`proposal_slot <= current_slot`, IGNORE).
3. **When results differ.** A message arrives within 500 ms of a slot boundary. Lighthouse IGNOREs where the spec ACCEPTs.
4. **Prysm.** Not verified.
5. **Confidence: medium.**
6. **Severity:** cosmetic.
7. **Known:** not found.

### C10. ExecutionPayloadEnvelopesByRange rejects any request whose start slot is before Gloas
1. **Spec.** SP `p2p-interface.md#L1418-L1420`: "Specifications of request/response methods are equivalent to [BeaconBlocksByRange v2] … with the only difference being the response content type." BlocksByRange answers with whatever exists in the range ("at least the first block … that exists in the range").
2. **Client.** LH `NBP/rpc_methods.rs#L1440-L1449`: `if !fork_name.gloas_enabled() { return Err((RpcErrorResponse::InvalidRequest, "Requested envelopes for pre-gloas slots")); }`. The check keys on `request_start_slot` only.
3. **When results differ.** A request range straddles `GLOAS_FORK_EPOCH`. The spec answer is the Gloas-part envelopes; Lighthouse returns `InvalidRequest` and nothing, which can stall a peer's sync across the fork.
4. **Prysm.** Agrees with the spec by clamping. PR `beacon-chain/sync/rpc_execution_payload_envelopes_by_range.go#L236-L245`: `// Envelopes only exist from the Gloas fork onward — clamp start if needed.`
5. **Confidence: medium.** Disproof: the spec does not say outright that a range starting before Gloas is valid. Lighthouse's own range sync may never send one.
6. **Severity:** cosmetic (Req/Resp interop).
7. **Known:** not found.

### C11. EnvelopesByRange canonical filter uses FULL vs EMPTY weight, not the child bid's `parent_block_hash`
1. **Spec.** SP `p2p-interface.md#L1422-L1431`: "A payload is part of that chain if the next block in the chain builds on it, i.e. the `parent_block_hash` of that block's execution payload bid equals the `block_hash` of the payload … Clients MUST NOT include envelopes for blocks whose `ForkChoiceNode` in that chain has `payload_status` equal to `PAYLOAD_STATUS_EMPTY`."
2. **Client.**
   - LH `BC/payload_envelope_streamer/beacon_chain_adapter.rs#L66-L72` says: `// Revisit after Lion's execution payload verdict changes merge: by-range responses must // follow the current head's chain, using each child's parent payload status for ancestors. // https://github.com/ethereum/consensus-specs/pull/5608`
   - It calls `get_canonical_payload_status`, LH `PA/proto_array.rs#L1679-L1761`. That function compares the FULL weight with the EMPTY weight, then applies the tiebreaker.
3. **When results differ.** For an ancestor block whose canonical child was built on EMPTY, but whose FULL node now carries more weight, or the reverse. Examples: blocks between justified and finalized, or a stale cached head. Lighthouse serves or omits envelopes contrary to the child-edge rule.
4. **Prysm.** Agrees with the spec. It walks successor bids at PR `beacon-chain/sync/rpc_execution_payload_envelopes_by_range.go#L98-L132`.
5. **Confidence: medium.** Self-acknowledged.
6. **Severity:** cosmetic (Req/Resp content).
7. **Known:** acknowledged in a code comment referencing consensus-specs PR 5608. No Lighthouse issue found.

### C12. Envelopes are not replayed into fork choice after an unclean restart; DB-write failure leaves FULL without an envelope
1. **Spec.** The spec store keeps `store.payloads` (SP `fork-choice.md#L1132-L1133`, `store.payloads[envelope.beacon_block_root] = envelope`). The spec has no persistence model, so this is an implementation-conformance concern.
2. **Client.**
   - `payload_received` exists only inside the persisted proto-array (LH `PA/proto_array.rs#L168-L171`). Fork choice is persisted on epoch transition or reorg (LH `BC/canonical_head.rs#L1564-L1565`, `if is_epoch_transition || reorg_distance.is_some() { self.persist_fork_choice()?;`), on finalization, and on clean shutdown.
   - Envelope import does not persist it (LH `BC/payload_envelope_verification/import.rs#L274-L280` updates memory, then the DB write at `#L305-L312`).
   - On a DB error, `#L312-L324` returns `Err` with `// TODO(gloas) handle db write failure`, and fork choice is not rolled back.
3. **When results differ.** After a crash, envelopes imported since the last persist are on disk but their blocks are EMPTY to fork choice until re-received. Until then, head, `should_build_on_full`, index-1 handling and by-range serving differ from a node that never restarted. A failed DB write instead leaves FULL with no envelope stored.
4. **Prysm.** Not compared.
5. **Confidence: medium** for the mechanism (the reader report was spot-checked). I did not verify a recovery path such as range-sync re-fetch.
6. **Severity:** fork-choice divergence (local, transient).
7. **Known:** related #10414 (PR, open, "Dedup gossip payload envelopes against fork choice after a restart"). The replay gap itself was not found.

### C13. PTC duty requires the head block to be at the duty slot
1. **Spec.** SP `validator.md#L411-L422`: a PTC member does not submit only if no block for the slot was seen.
2. **Client.** LH `BC/beacon_chain.rs#L2229-L2233`: `// Check if we've seen a block for this slot from the canonical head` / `if head.beacon_block.slot() != request_slot { return Err(Error::NoBlockForSlot(request_slot)); }`.
3. **When results differ.** A block for the slot was seen but is not the head (for example, a late block that lost to its parent). The spec's PTC member votes; Lighthouse produces nothing. This changes PTC participation, not anyone's validity.
4. **Prysm.** Not verified.
5. **Confidence: medium.**
6. **Severity:** cosmetic (validator behaviour).
7. **Known:** related #9474 (issue, open, "Emit payload attestation votes for non-canonical envelopes").

### C14. Spec-version lag: the Gloas fork upgrade follows v1.7.0-beta.4 builder onboarding, not master (PR 5736)
1. **Spec.** SP `fork.md#L84-L107` (master) routes by credentials first:
   - `if not is_builder_withdrawal_credential(deposit.withdrawal_credentials): pending_deposits.append(deposit); continue`.
   - A valid builder-credential deposit is applied as a builder even if a validator holds the key. An invalid one is kept only `if deposit.pubkey in validator_pubkeys or is_pending_validator(…)`.
2. **Client.** LH `consensus/state_processing/src/upgrade/gloas.rs#L258-L285`:
   - `if state.get_validator_index(&deposit.pubkey)?.is_some() { pending_deposits.push(deposit.clone()); continue; }`
   - `if valid_pending_validator_pubkeys.contains(&deposit.pubkey) { pending_deposits.push(deposit.clone()); continue; }`
   - `if !is_valid_deposit_signature_cached(…) { continue; }`
   - These lines implement the beta.4 rules. Both branches pin `v1.7.0-beta.4`.
3. **When results differ.** At the fork slot, the pending-deposit queue contains a builder-credential deposit whose pubkey belongs to an existing validator, or to an earlier valid pending validator deposit. Master onboards or tops up a builder; Lighthouse leaves the deposit in the validator queue. The post-fork `builders` list and `pending_deposits` differ, so the state root differs.
4. **Prysm.** Not checked.
5. **Confidence: high** that the code follows beta.4. The difference only matters if devnets or other clients adopt master before the fork.
6. **Severity:** consensus split, if peers target master. Note: this is outside fork-choice/p2p, but the brief asked for it.
7. **Known:** not found in sigp/lighthouse (searched "5736"; the only hit, #5736, is an unrelated issue).

### Live-code items (shared with the live Fulu fork)
- Lighthouse, EIP-7732: affects live code, details withheld.
- Lighthouse, EIP-7732: affects live code, details withheld.

## 3. Rules checked and found consistent with the spec (LH paths at stable pin)

| Rule (SP file#line) | Lighthouse location | Note |
|---|---|---|
| `get_weight` returns 0 for a previous-slot EMPTY/FULL node (`fork-choice.md#L461-L464`, `#L569-L570`) | `PA/proto_array.rs#L1765-L1800` | `Pending \|\| slot+1 != current_slot` gives the normal path; otherwise `Ok(0)` |
| Proposer boost only to ancestors, not to the boosted block's own EMPTY/FULL (`#L582-L587`) | `proto_array.rs#L1784-L1796`, `#L1803-L1834` | Equivalent to `is_ancestor` with PENDING |
| Head key `(weight, root, tiebreaker)` (`#L638-L669`) | `proto_array.rs#L1516-L1518` | Same ordering |
| `get_payload_status_tiebreaker` (`#L515-L526`) | `proto_array.rs#L1940-L1963` | EMPTY gives 1; FULL gives 2 or 0 |
| `should_extend_payload` (`#L497-L510`) | `proto_array.rs#L2004-L2058` | All four `or` branches match |
| `PAYLOAD_TIMELY_THRESHOLD` / `DATA_AVAILABILITY_TIMELY_THRESHOLD` strict `>`, local-availability short-circuit (`#L326-L363`) | `proto_array.rs#L257-L294`, `#L300-L338` | `matching_votes > E::payload_timely_threshold()`. "False" votes = participation − true votes |
| `should_build_on_full` (`#L475-L486`) | `proto_array.rs#L1969-L2002` | Equivalent |
| `should_apply_proposer_boost` equivocation set (`#L531-L561`) | `proto_array.rs#L826-L884` | `node.slot()==parent_slot` equals `block.slot+1==slot` after the earlier return. Only `is_head_weak` differs (C2) |
| `get_node_children` / `get_ancestor` with parent payload status (`#L388-L398`, `#L614-L630`) | `proto_array.rs#L1837-L1938` | Pre-Gloas parent treated as EMPTY. The spec is silent |
| `on_block`: full parent requires a verified payload (`#L1060-L1061`) | `FC/fork_choice.rs#L828-L839` | Same check. Only the ordering of PTC votes differs (C1) |
| `update_proposer_boost_root` timeliness + dependent root (`#L1019-L1031`) | `fork_choice.rs#L881-L902` | Uses parent root for the dependent root; equivalent since block.slot > dependent slot |
| `validate_on_attestation` index ∈ {0,1}, same-slot ⇒ 0, index 1 ⇒ payload verified (`#L937-L944`) | `fork_choice.rs#L1242-L1271` | Match |
| `update_latest_messages`: `payload_present = index == 1` (`#L970`) | `fork_choice.rs#L1355-L1359` | Match |
| `on_payload_attestation_message`: slot must equal block slot; current slot only when not from block; PTC membership (`#L1146-L1179`) | `fork_choice.rs#L1400-L1478` | Match |
| `on_execution_payload_envelope` only after EL verification and data availability (`#L1119-L1133`) | `payload_envelope_verification/import.rs#L172-L184`, `#L274-L280`. INVALID never reaches it (`#L150-L167`) | Match |
| Gossip bid: unseen proposer preferences gives **IGNORE** (`p2p-interface.md#L1017-L1021`) | `payload_bid_verification/gossip_verified_bid.rs#L401-L405`; GM mapping `NBP/gossip_methods.rs#L4277-L4312` | IGNORE, not REJECT. Key derivation: see C7 |
| Gossip attestation index < 2 (REJECT), same-slot index 0 (REJECT), index 1 without envelope (IGNORE + request) (`#L1211-L1213`, `#L441-L453`) | `BC/attestation_verification.rs#L929-L933`, `#L953-L972`, `#L613-L634`, `#L1502-L1508` | Match, except C3 |
| Gossip envelope: finalized slot, slot, builder index, block hash, request limits, signature (`#L846-L877`) | `gossip_verified_envelope.rs#L54-L130`, `#L224-L307` | Match (sub-review) |
| Gossip payload attestation: pre-Gloas REJECT, first-seen, current slot, block at slot, PTC, signature (`#L904-L945`) | `gossip_verified_payload_attestation.rs#L42-L166` | Match, except C5 |
| ByRange max count, ByRoot `MAX_REQUEST_PAYLOADS` (`#L1472`) | `LN/rpc/handler.rs#L966-L979`; `rpc/codec.rs#L563-L569` | Match |
| ByRange `count == 0` | `rpc/handler.rs#L1004` (`if max_responses > 0 {`): the stream is not stored and no chunks are sent | **The spec is silent**, so this is not a difference. Prysm returns InvalidRequest instead (PR `…/rpc_execution_payload_envelopes_by_range.go#L218-L219`) |
| Timing constants: `PAYLOAD_DUE_BPS` 5000, `PAYLOAD_ATTESTATION_DUE_BPS` 7500, `ATTESTATION_DUE_BPS_GLOAS` 2500, fork-aware switch at the first Gloas slot (`validator.md#L43-L48`) | `consensus/types/src/core/chain_spec.rs#L1274-L1276`, `#L984-L990` | Match (sub-review) |
| `parent_execution_requests` empty for a pre-Gloas parent (`validator.md#L253-L254`) | `beacon_chain/src/block_production/mod.rs#L252-L255`, `block_production/gloas.rs#L199-L206` | Match (sub-review) |

## 4. Known-check

Method: GitHub search API, `repo:sigp/lighthouse` (issues and PRs, open and closed), plus direct issue reads. The search API rate-limited one batch, and that batch was re-run.

| Candidate | Status |
|---|---|
| C1 | Not found. Queries: "payload attestation on_block", "payload attestations fork choice block import". Related but different: #10088 (issue, open) and #10101 (PR, open), "Block-carried payload votes are discarded if the envelope arrives late". |
| C2 | **Known**: #9593 (issue, open), #9831 (PR, open). Earlier attempt #9625 (PR, closed). |
| C3 | **Known (invalidated half)**: #10464 (PR, open). Optimistic half not found. Queries: "index 1 attestation optimistic", "payload_received invalid". Related: #10194 (PR, open, "Reject Gloas envelopes for invalidated payloads"). |
| C4 | Related: #8949 (issue, closed, "envelope peer penalties and REJECT/IGNORE mapping"). The TODO is still in code. Queries: "envelope ignore reject gossip", "conflated". |
| C5 | Not found. Queries: "conflated", "passes validation gloas". |
| C6 | Not found. Query: "latest_block_hash gossip" (the only hit, #9186, is unrelated). |
| C7, C8 | Not found. Queries: "proposer preferences bid ignore", "bid proposer lookahead". Related closed: #9036, #9337. |
| C9 | Not found. Query: "gloas clock disparity". That query was rate-limited on the first run and not re-run, so this check is incomplete. |
| C10 | Not found. Queries: "envelopes pre-gloas", "PayloadEnvelopesByRange" (0 hits), "envelopes by range". |
| C11 | Not found as an issue. It is a code comment pointing at consensus-specs PR 5608. |
| C12 | Related: #10414 (PR, open). Query: "persist fork choice envelope". |
| C13 | Related: #9474 (issue, open). |
| C14 | Not found. Query: "5736". |
| Brief's pre-indexed | #10101, #10464, #9593, #9831 as above. #10466 (open PR, "Insert Gloas data column bid only after slot validation") matches no candidate. |

## 5. Limits and not covered

- No hive or execution-spec-tests results were consulted for this item.
- Prysm equivalents for C8, C9, C12, C13 and C14 were not verified.
- For C1, I did not construct a concrete reachable trigger.
- Some gossip, RPC and timing rows come from read-only sub-reviews of the same clones. I re-read their key anchor lines (those quoted in C4, C5, C7, C9–C13).

## 6. URLs read

- api.github.com/repos/{sigp/lighthouse/commits/stable, sigp/lighthouse/commits/unstable, ethereum/consensus-specs/commits/master, OffchainLabs/prysm/commits/develop}
- Shallow clones of github.com/sigp/lighthouse, github.com/ethereum/consensus-specs, github.com/OffchainLabs/prysm
- api.github.com/repos/ethereum/consensus-specs/pulls/5736; api.github.com/search/issues (queries in §4); api.github.com/repos/sigp/lighthouse/issues/{10466,10101,10464,9593,9831,8949,10088,10414}
- eips.ethereum.org/EIPS/eip-7732 was not fetched. Rule text is quoted from consensus-specs master.
