# Lighthouse × EIP-7732 (ePBS, Gloas): spec-conformance review

| | |
|---|---|
| Client | Lighthouse (sigp/lighthouse) |
| EIP | EIP-7732 Enshrined Proposer-Builder Separation (consensus layer: Gloas) |
| Client pin | `a376defa30470923556d91bdeb15a802e297f31f`. This was the head of the default branch `stable` (default branch per the GitHub API), commit "Release v8.3.2 (#10433)", 2026-10-22. LH = `https://github.com/sigp/lighthouse/blob/a376defa30470923556d91bdeb15a802e297f31f/` |
| Spec pin | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2`, head of `ethereum/consensus-specs` `master` on 2026-10-27. CS = `https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/` |
| Spec version | `pyproject.toml` on master says `1.7.0-beta.5`, which is unreleased. The newest release is `v1.7.0-beta.4` (tag commit `aa16bb4c156184e9548a997d53efaf2a228a6304`, published 2026-10-08). Lighthouse's test target is `CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-beta.4` (LH `testing/ef_tests/Makefile` line 3). Between beta.4 and master, `specs/gloas/` changes in only two places: `fork.md` (builder onboarding, see C1) and one comment line in `beacon-chain.md`. |
| EIP status | eips.ethereum.org shows EIP-7732 as "Last Call" (deadline 2026-11-01), requires EIP-7685, `PTC_SIZE = 2**9`. |
| Comparison client | Prysm (OffchainLabs/prysm), default branch `develop`, pinned at `eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a` (2026-10-27). PR = `https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/` |
| Live-code rule | Gloas is not active on mainnet. `GLOAS_FORK_EPOCH: 18446744073709551615` in both CS `configs/mainnet.yaml` line 60 and LH `common/eth2_network_config/built_in_network_configs/mainnet/config.yaml` line 60. The pin's commit message also says "Unschedule Gloas on Hoodi". EIP-7732 code is therefore reported in full, except for one item in code shared with the live Fulu fork (see "Withheld"). |

Method: I read the code from shallow clones of the pinned commits; nothing was built or run. Every line number below refers to the pinned commit. All three repos and the EIP page are public.

**Summary:** three candidates, plus one withheld item.

| # | Candidate | Severity | Confidence | Known? |
|---|---|---|---|---|
| C1 | Builder onboarding at the fork follows beta.4 and differs from master | Consensus split at the fork (state-root divergence) | High that it differs from master; it is a spec-version lag | Not found |
| C2 | `is_head_weak` undercounts equivocating weight | Fork-choice head divergence (a consensus split of the head view; no invalid state) | High | Known: issue #9593, PR #9831 |
| C3 | Payload attestations from a block are applied to fork choice before `on_block` validation | Fork-choice divergence, low impact | Medium | Not found (closest: #10088) |

Everything else I checked in the requested areas matches the spec (see "Rules checked and found correct").

---

## Candidate C1: `onboard_builders_from_pending_deposits` implements beta.4 logic, not current master (beta.5)

**(1) Spec text** (CS `specs/gloas/fork.md`, lines 65-69 and 84-107): https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork.md

> During this pass:
>
> - Only deposits with builder withdrawal credentials can fund builders.
> - Other deposits stay queued, even if a builder uses the same public key.
> - A valid builder registration can use a validator's public key.

```python
    for deposit in state.pending_deposits:
        # Only builder-credential deposits can fund builders at the fork.
        if not is_builder_withdrawal_credential(deposit.withdrawal_credentials):
            pending_deposits.append(deposit)
            continue
        ...
        builder_pubkeys = [builder.pubkey for builder in state.builders]

        if deposit.pubkey not in builder_pubkeys:
            if not is_valid_deposit_signature(
                ...
            ):
                # Preserve validator top-ups when no builder can receive them.
                if deposit.pubkey in validator_pubkeys or is_pending_validator(
                    pending_deposits, deposit.pubkey
                ):
                    pending_deposits.append(deposit)
                continue

            add_builder_to_registry(...)
        else:
            builder_index = BuilderIndex(builder_pubkeys.index(deposit.pubkey))
            state.builders[builder_index].balance += deposit.amount
```

This change came from consensus-specs commit `8b374fa81b0918b84fe0063a56b4a482e1fa2c6f`, "Separate builder and validator deposits at the Gloas fork (#5736)" (https://github.com/ethereum/consensus-specs/pull/5736). `git diff v1.7.0-beta.4..8a80ffa -- specs/gloas` shows it is the only normative Gloas change since beta.4.

**(2) Client code** (LH `consensus/state_processing/src/upgrade/gloas.rs`, lines 258-305): https://github.com/sigp/lighthouse/blob/a376defa30470923556d91bdeb15a802e297f31f/consensus/state_processing/src/upgrade/gloas.rs#L258-L305

```rust
    for deposit in &current_pending_deposits {
        // Deposits for existing validators stay in the pending queue.
        if state.get_validator_index(&deposit.pubkey)?.is_some() {
            pending_deposits.push(deposit.clone());
            continue;
        }

        match builder_pubkey_to_index.get(&deposit.pubkey).copied() {
            None => {
                // Deposits without builder withdrawal credentials are for new validators.
                if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) {
                    pending_deposits.push(deposit.clone());
                    ...
                    continue;
                }
                // If there is a valid pending deposit for a new validator with this pubkey,
                // keep this deposit in the pending queue to be applied to that validator later.
                if valid_pending_validator_pubkeys.contains(&deposit.pubkey) {
                    pending_deposits.push(deposit.clone());
                    continue;
                }
                if !is_valid_deposit_signature_cached(builder_onboarding_cache, deposit, spec) {
                    continue;
                }
                builder_pubkey_to_index.insert(deposit.pubkey, builders.len() as u64);
                builders.push(Builder::from_deposit(...)?);
            }
            Some(builder_index) => {
                ...
                builder.balance.safe_add_assign(deposit.amount)?;
            }
        }
    }
```

This matches the beta.4 text, which keeps any deposit whose pubkey is an existing validator, and tops up a builder from any deposit with a builder pubkey regardless of its credentials. The `unstable` branch head at the time of reading (`301f3bf2323cd2b3f946ceed026be8075d24e1c3`) has the same logic.

**(3) When the outputs differ.** The `pending_deposits` queue of the last Fulu state, at the moment `upgrade_to_gloas` runs, contains any of the following:
- (a) A deposit with builder (0xB0) withdrawal credentials and a valid signature whose pubkey already belongs to a validator in the registry.
  - Master registers a new builder and removes the deposit from the queue.
  - Lighthouse keeps it queued, so it later tops up the validator.
- (b) A builder-credential deposit with a valid signature whose pubkey also has an earlier, validly signed new-validator deposit in the queue.
  - Master onboards it as a builder.
  - Lighthouse keeps it queued.
- (c) A deposit without builder credentials (e.g. 0x01/0x02) whose pubkey was onboarded as a builder by an earlier deposit in the same pass.
  - Master keeps it queued ("Other deposits stay queued, even if a builder uses the same public key").
  - Lighthouse adds its amount to the builder's balance and drops it from the queue.

In each case `builders` and `pending_deposits` in the first Gloas state differ, so the state root at the fork boundary differs from that of a client implementing master.

**(4) Other client: Prysm.** PR `beacon-chain/state/state-native/setters_gloas.go`, lines 795-850 (https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/state/state-native/setters_gloas.go#L795-L850). It uses the same beta.4 ordering: `if _, ok := b.validatorIndexByPubkey(pubkey); ok { newPendingDeposits = append(...); continue }` comes first, then `if isExistingBuilder { ... increaseBuilderBalance ...; continue }` before the credential check. Its doc comment (lines ~760-793) quotes the beta.4 spec. So Prysm agrees with Lighthouse and disagrees with master.

**(5) Confidence: high** that Lighthouse's output differs from the current master text; the code and spec are quoted above. **Low** that this is a defect today. Master is the unreleased beta.5, Lighthouse explicitly targets the released beta.4, and Prysm is at the same level. It becomes a real split only if some clients ship beta.5 onboarding and others do not before a network forks to Gloas with such deposits queued. Reaching (a)–(c) needs someone to place such deposits through the deposit contract before the fork, which anyone can do.

**(6) Severity:** consensus split, at the fork-transition state, between beta.4-level and beta.5-level clients.

**Known-check: not found.** Queries run on the GitHub issue search API:
- `repo:sigp/lighthouse onboard builders`: hits #9943, #9954, #9311, #9511; none covers the #5736 semantics.
- `repo:sigp/lighthouse 5736`
- `repo:sigp/lighthouse builder withdrawal credential validator pubkey onboarding`
- `repo:sigp/lighthouse separate builder validator deposits fork`
- `repo:sigp/lighthouse beta.5`
- `repo:OffchainLabs/prysm onboard builders`: #17440, #16796 etc. are about performance only.

Hive and the execution-specs trackers do not cover this; see "Known-check sources" below.

---

## Candidate C2: Gloas `is_head_weak` omits committee-based equivocating weight

**(1) Spec text** (CS `specs/gloas/fork-choice.md`, "Modified `is_head_weak`", lines 807-833): https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#modified-is_head_weak

```python
    # Compute head weight including equivocations
    ...
    head_node = ForkChoiceNode(root=head_root, payload_status=PAYLOAD_STATUS_PENDING)
    head_weight = get_attestation_score(store, head_node, justified_state)
    for index in range(get_committee_count_per_slot(head_state, epoch)):
        committee = get_beacon_committee(head_state, head_block.slot, CommitteeIndex(index))
        head_weight += Gwei(
            sum(
                justified_state.validators[index].effective_balance
                for index in committee
                if index in store.equivocating_indices
            )
        )

    return head_weight < reorg_threshold
```

The same file's `should_apply_proposer_boost` (lines 528-561) calls it as `if not is_head_weak(store, parent_root): return True`. Because `get_weight` uses that result, it affects every head computation, not only the optional re-org.

**(2) Client code** (LH `consensus/proto_array/src/proto_array.rs`, lines 792-820, and its call at lines 859-861): https://github.com/sigp/lighthouse/blob/a376defa30470923556d91bdeb15a802e297f31f/consensus/proto_array/src/proto_array.rs#L792-L820

```rust
    // TODO(gloas): the spec adds weight from equivocating validators in the
    // head slot's *committees*, regardless of who they voted for. We approximate
    // with `equivocating_attestation_score` which only tracks equivocating
    // validators whose vote pointed at this block. ...
        let head_weight = head_node
            .attestation_score(PayloadStatus::Pending)
            .saturating_add(head_node.equivocating_attestation_score().unwrap_or(0));

        head_weight < reorg_threshold
```

The proposer re-org path uses `info.head_node.weight()` with no equivocation term at all (LH `consensus/proto_array/src/proto_array_fork_choice.rs` around lines 844-846). The spec's `get_proposer_head` branch `elif all([head_weak, current_time_ok, proposer_equivocation]): return parent_node` (CS `fork-choice.md` around line 903) has no counterpart: a search of the LH tree for `proposer_equivocation` finds nothing.

**(3) When the outputs differ.** Some validators in the previous slot's beacon committees are in `equivocating_indices` (they have been attester-slashed) and did not last vote for that parent block. The parent's attestation score is then below the re-org threshold without their balance and at or above it with it.
- The spec treats the parent as not weak and applies proposer boost.
- Lighthouse treats it as weak. If the parent's proposer equivocated with a timely block, Lighthouse withholds the boost while the spec applies it.

Heads can then differ for the rest of the slot.

**(4) Other client: Prysm.** PR `beacon-chain/forkchoice/doubly-linked-tree/gloas.go`, lines 700-729 (https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/forkchoice/doubly-linked-tree/gloas.go#L700-L729). It uses `if p.node.weight*100 >= s.committeeWeight*params.BeaconConfig().ReorgHeadWeightThreshold { return true }`, with no committee equivocator weight. It also disagrees with the spec, in a different way: it uses node weight and `>=` on the "not weak" side.

**(5) Confidence: high** that the logic differs; the client's own TODO says so. The practical trigger is narrow: it needs slashed equivocators in the previous slot's committees and a weight right at the boundary.

**(6) Severity:** consensus split of fork-choice head selection (transient head divergence). There is no invalid state, block or fee effect.

**Known-check: known.** https://github.com/sigp/lighthouse/issues/9593 (open, "Align Gloas proposer reorg weight logic with fork-choice spec") and https://github.com/sigp/lighthouse/pull/9831 (open, "Align gloas proposer reorg weight logic w/ spec"). The query used was `repo:sigp/lighthouse is_head_weak equivocating`.

---

## Candidate C3: payload attestations carried in a block are recorded in fork choice before `on_block` accepts the block

**(1) Spec text** (CS `specs/gloas/fork-choice.md`, "Modified `on_block`", lines 1036-1100): https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#modified-on_block

```python
    # If this block builds on the parent's full payload, that payload must
    # have been verified by on_execution_payload_envelope
    if is_parent_node_full(store, block):
        assert is_payload_verified(store, block.parent_root)
    ...
    assert current_slot >= block.slot
    ...
    assert block.slot > finalized_slot
    ...
    assert store.finalized_checkpoint.root == finalized_checkpoint_block
    ...
    state_transition(state, signed_block, validate_result=True)
    ...
    # Notify the store about the payload_attestations in the block
    notify_ptc_messages(store, state, block.body.payload_attestations)
```

In the spec, PTC votes from a block reach the store only after every `on_block` assertion has passed.

**(2) Client code** (LH `beacon_node/beacon_chain/src/block_verification.rs`, lines 1755 and 1782-1805): https://github.com/sigp/lighthouse/blob/a376defa30470923556d91bdeb15a802e297f31f/beacon_node/beacon_chain/src/block_verification.rs#L1782-L1805

```rust
        let mut fork_choice = chain.canonical_head.fork_choice_write_lock();
        ...
        // Register each payload attestation in the block with fork choice.
        if let Ok(payload_attestations) = block.message().body().payload_attestations() {
            for (i, payload_attestation) in payload_attestations.iter().enumerate() {
                ...
                if let Err(e) = fork_choice.on_payload_attestation(
                    current_slot,
                    indexed_payload_attestation,
                    AttestationFromBlock::True,
                    &ptc.0,
                ) && !matches!(e, ForkChoiceError::InvalidPayloadAttestation(_))
```

`fork_choice.on_block(...)` runs later, in LH `beacon_node/beacon_chain/src/beacon_chain.rs` around lines 4515-4535. The full-parent check it performs (LH `consensus/fork_choice/src/fork_choice.rs` lines 829-839, `InvalidBlock::ParentPayloadNotVerified`) has no earlier equivalent in `beacon_node/beacon_chain/src`: a search for `ParentPayloadNotVerified` finds it only in `fork_choice.rs`.

**(3) When the outputs differ.** A block whose bid builds on the parent's full payload (`bid.parent_block_hash` equals the parent bid's `block_hash`) arrives before this node has verified the parent's envelope. It passes the state transition, then fails `on_block` with `ParentPayloadNotVerified`; the finalized-descendant and future-slot assertions are other ways for it to fail there. Its included payload attestations for the parent have already been written to the canonical fork-choice store's PTC vote arrays. The spec store would not hold them.
- This changes `payload_timeliness` / `payload_data_availability` for the parent, and with it `should_extend_payload` and the EMPTY/FULL tiebreak, compared with a spec node.
- Lighthouse also records them with `AttestationFromBlock::True`, which bypasses the `data.slot == current_slot` check that gossip votes need. The spec likewise skips that check for block-carried votes, so the only difference is the ordering relative to block acceptance.

**(4) Other client: Prysm.** PR `beacon-chain/blockchain/process_block.go`, lines 79-91 (https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/blockchain/process_block.go#L79-L91). Prysm calls `s.cfg.ForkChoiceStore.InsertNode(...)` first and returns on error, then calls `s.handleBlockPayloadAttestations(...)` (defined at lines 624-640). This agrees with the spec ordering.

**(5) Confidence: medium.** The ordering difference is clear in the code. I did not trace whether some earlier gossip/RPC gate always rejects such blocks before this point. The votes are BLS-valid PTC votes that could also arrive over gossip, which limits the impact.

**(6) Severity:** fork-choice divergence, low impact (payload-status tiebreak only). This is not a state or encoding mismatch. Of the brief's categories, the closest is "consensus split", and only of the transient head view.

**Known-check: not found.** Queries run:
- `repo:sigp/lighthouse payload attestation fork choice on_block`
- `repo:sigp/lighthouse payload attestations fork choice before import`
- `repo:sigp/lighthouse payload attestations block import fork choice invalid block`

The closest hit is https://github.com/sigp/lighthouse/issues/10088 ("Block-carried payload votes are discarded if the envelope arrives late", open). It concerns regular index-1 attestations being dropped, not PTC votes being kept for a block that is later rejected, so it is related but not the same behaviour.

---

## Withheld

- Lighthouse, EIP-7732: affects live code, details withheld.

---

## Candidates considered and dismissed (attempted disproofs)

- **Pubkey cache not refreshed before `apply_parent_execution_payload`.** In the Gloas path, withdrawal and consolidation requests look validators up via `state.pubkey_cache()` (LH `consensus/state_processing/src/per_block_processing/process_operations.rs` line 805 and others). Unlike the pre-Gloas path (line 76, `state.update_pubkey_cache()?`), nothing in the Gloas path refreshes the cache first. It was dismissed, at low residual confidence, because:
  - states loaded from the store run `build_all_caches` (LH `beacon_node/store/src/hot_cold_store.rs` lines 1171, 1832, 2054, 2369);
  - `BeaconState::build_caches` calls `update_pubkey_cache` (LH `consensus/types/src/state/beacon_state.rs` lines 2756-2763);
  - new validators are inserted into an in-sync cache (`beacon_state.rs` lines 2466-2470).

  I found no production path that processes a block with a stale cache. The EF harness does not refresh it for the parent-payload case (`testing/ef_tests/src/cases/operations.rs` around lines 597-604), but that is test-only.
- **`upgrade_to_gloas` step order.** Lighthouse onboards builders before `initialize_ptc_window` (LH `upgrade/gloas.rs` lines 180-181); the spec does the reverse (CS `fork.md` lines 228-232). Onboarding touches only `builders` and `pending_deposits`, and `compute_ptc` reads neither, so the result is the same.
- **Missing `assert state.slot > GENESIS_SLOT` in the bid processing.** At slot 0, `state.get_block_root(state.slot().safe_sub(1)?)` (LH `per_block_processing.rs` line 837) errors anyway. The block is rejected either way; only the error kind differs.
- **Bid and envelope signature domain computed from `bid.slot` / `spec.fork_at_epoch`** rather than `state.fork` (LH `per_block_processing/signature_sets.rs` lines ~458-468). The slot-equality checks make the domains identical for any block that passes the spec.
- **`settle_builder_payment` clears before appending; the spec appends then clears** (LH `per_block_processing.rs` lines 704-726). These are two separate lists and the withdrawal is cloned first, so the result is the same.
- **`process_slot` clears the availability bit after epoch processing** (LH `per_slot_processing.rs` lines ~51-69). No epoch-processing function reads `execution_payload_availability`.
- **SSZ decode-time caps on ProgressiveLists.** For example, LH `block/beacon_block_body.rs` line 148 and `execution/execution_payload.rs` line 29. The spec enforces the same caps later with `assert len(...) <= ...` (CS `beacon-chain.md` lines 2242-2247, 1792-1795). The accept/reject result is the same; only the error stage differs.

---

## Rules checked and found correctly implemented

All LH paths are at commit `a376defa30470923556d91bdeb15a802e297f31f`. Spec line numbers refer to CS `specs/gloas/beacon-chain.md` (BC) or `fork-choice.md` (FC) at `8a80ffa8`.

**Bid processing (BC 2144-2217)** — LH `consensus/state_processing/src/per_block_processing.rs`

| Rule | LH lines |
|---|---|
| Self-build (`BUILDER_INDEX_SELF_BUILD`): `amount == 0` and the signature is the point at infinity | 740-748 |
| Builder must be active | 753-756 |
| `builder.version == PAYLOAD_BUILDER_VERSION` | 759-766 |
| `can_builder_cover_bid` | 769-777 |
| Bid signature (bulk-verified at lines 146-162 when that path is used) | 779-793 |
| Blob commitments `<=` the per-epoch maximum | 797-805 |
| `bid.slot == state.slot` | 808-815 |
| `bid.parent_block_hash == state.latest_block_hash` | 818-826 |
| `bid.block_hash != bid.parent_block_hash` | 829-835 |
| `parent_block_root == get_block_root(slot-1)` | 837-845 |
| `prev_randao == get_randao_mix(current_epoch)` | 847-855 |
| Pending payment recorded only when `amount > 0`, at index `SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH`, with weight 0 and the proposer index of `state.slot` | 858-882 |
| Bid cached in state | 885-896 |
| Block-processing order: parent payload, then header, then withdrawals, then bid (BC 1751-1773) | 138, 168, 196, 199 |

**Builder balance boundaries (BC 1051-1066, 1165-1194)** — LH `consensus/types/src/state/beacon_state.rs`
- `can_builder_cover_bid` (lines 3765-3784) computes `min = MIN_DEPOSIT_AMOUNT + pending` with checked addition, returns false if `balance < min`, and otherwise tests `balance - min >= bid`. The boundaries match the spec:
  - a zero bid with `balance == min` is accepted;
  - a bid equal to `balance - min` is accepted;
  - any balance below `MIN_DEPOSIT_AMOUNT` is rejected.
- `get_pending_balance_to_withdraw_for_builder` (lines 3247-3269) sums both `builder_pending_withdrawals` and `builder_pending_payments` for the builder.
- `is_active_builder` (lines ~3786-3795): `deposit_epoch < finalized_checkpoint.epoch` and `withdrawable_epoch == FAR_FUTURE_EPOCH`.

**Withdrawals (BC 1856-2094)** — LH `consensus/state_processing/src/per_block_processing/withdrawals.rs`

| Rule | LH lines |
|---|---|
| Order: builder pending withdrawals, then partial withdrawals, then builder sweep, then validator sweep | 20-75 |
| `get_builder_withdrawals` (limit `MAX_WITHDRAWALS_PER_PAYLOAD - 1`, builder-flagged index, `fee_recipient`) | 77-117 |
| `get_builders_sweep_withdrawals` (limit `min(len, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)`, `withdrawable_epoch <= epoch && balance > 0`, wrap-around) | 186-241 |
| `get_builder_balance_after_withdrawals` | 331-346 |
| `apply_withdrawals` | 442-466 |
| `update_payload_expected_withdrawals` | 372-378 |
| `update_builder_pending_withdrawals` | 380-388 |
| `update_next_withdrawal_builder_index` | 400-413 |
| `process_withdrawals` returns early when the parent was empty (`latest_block_hash != latest_execution_payload_bid.block_hash`) | 512-554 |

**Builder registry operations** — LH `per_block_processing/process_operations.rs` unless noted
- `process_builder_deposit_request` (BC 2322-2362): lines 925-972.
- `process_builder_exit_request` (BC 2365-2385): lines 986-1015.
- `initiate_builder_exit` (BC 1523-1534): lines 601-620. It has an extra early return that can never trigger, because the caller requires `is_active_builder`.
- `settle_builder_payment` (BC 1535-1545): `per_block_processing.rs` lines 704-726.
- `get_index_for_new_builder` and `add_builder_to_registry` (BC 2287-2321): `beacon_state.rs` lines 2480-2525.
- `is_valid_builder_deposit_signature` (BC 2268-2286): `consensus/types/src/builder/builder_deposit_request.rs` lines 30-46.

**Parent payload / envelope (deferred execution, BC 1774-1855, 2104-2122)**
- `process_parent_execution_payload`: an empty parent requires empty requests; a full parent checks the requests root and then applies (`per_block_processing.rs` lines 562-595).
- `apply_parent_execution_payload` (`per_block_processing.rs`):
  - request-count limits (lines 616, 666-698);
  - payment settled for the current/previous epoch, or appended directly when older (lines 620-640);
  - request order: deposits, withdrawals, consolidations, builder deposits, builder exits (lines 643-647);
  - availability bit and `latest_block_hash` updated (lines 650-659).
- Envelope checks (slot, `beacon_block_root`, builder index vs bid, `block_hash` and parent hash vs the committed bid): `consensus/state_processing/src/envelope_processing.rs` lines 105-229. The engine `new_payload` call there is a TODO at line 231; it is performed elsewhere in the import path, which I did not trace.
- Envelope signature: self-build uses the proposer key, otherwise the builder key, under `DOMAIN_BEACON_BUILDER` (`consensus/types/src/execution/signed_execution_payload_envelope.rs` lines 93-120).

**Payload attestations and the PTC**

| Rule | LH location |
|---|---|
| `process_payload_attestation` (BC 2485+): `beacon_block_root == latest_block_header.parent_root`, `data.slot + 1 == state.slot` | `per_block_processing/verify_payload_attestation.rs` lines 18-35 |
| `get_indexed_payload_attestation`: PTC members whose bit is set, sorted | `common/get_payload_attesting_indices.rs` lines 10-39 |
| `get_indexed_payload_attestation` cache | `consensus_context.rs` lines 197-212 |
| `is_valid_indexed_payload_attestation`: non-empty, non-strictly sorted (duplicates allowed, as in the spec), `DOMAIN_PTC_ATTESTER` | `per_block_processing/is_valid_indexed_payload_attestation.rs` lines 6-20 |
| PTC size: mainnet 512, minimal 16 (matches CS `presets/mainnet/gloas.yaml` line 6 and `presets/minimal/gloas.yaml` line 6, and EIP `PTC_SIZE = 2**9`) | `consensus/types/src/core/eth_spec.rs` lines 553, 623 |
| PTC window length 96 / 24 = `(MIN_SEED_LOOKAHEAD + 2) * SLOTS_PER_EPOCH` | `eth_spec.rs` lines 554, 624 |
| `get_ptc` (BC 1389) | `beacon_state.rs` lines 3591-3623 |
| `compute_ptc` (BC 1263) | `beacon_state.rs` lines 3639-3677 |
| `compute_balance_weighted_selection` (BC 1195): 16-bit randomness, `eb * 65535 >= MAX_EFFECTIVE_BALANCE_ELECTRA * r` | `beacon_state.rs` lines 3704-3763 |
| `process_ptc_window` (BC 1734) | `per_epoch_processing/single_pass.rs` lines 539-580 |
| `initialize_ptc_window` at the fork: one zeroed epoch plus `1 + MIN_SEED_LOOKAHEAD` computed epochs | `upgrade/gloas.rs` lines 191-218 |
| `process_builder_pending_payments` (BC 1716) with quorum `(total // SLOTS_PER_EPOCH) * 6 // 10` (BC 1430), placed after pending consolidations | `single_pass.rs` lines 583-632, ordering at lines 460-476 |
| Fork-choice thresholds `PAYLOAD_TIMELY_THRESHOLD` = `DATA_AVAILABILITY_TIMELY_THRESHOLD` = `PTC_SIZE/2`, compared strictly `>` (FC 337-362) | `eth_spec.rs` lines 470-477; `consensus/proto_array/src/proto_array.rs` lines 257-338 |

**Attestations (BC 1074-1090, 1327-1388, 2388-2482)**
- `is_attestation_same_slot`: `beacon_state.rs` lines 2633-2646.
- `get_attestation_participation_flag_indices` (payload-matching head flag): `common/get_attestation_participation.rs` lines 40-92.
- `data.index < 2`: `verify_attestation.rs` lines 82-85.
- Builder-payment weight accumulation for same-slot attestations: `process_operations.rs` lines 366-448.

**Fork transition (`upgrade_to_gloas`, CS `fork.md` lines 137-235)** — LH `upgrade/gloas.rs` lines 67-184
- Every field default matches: `latest_block_hash = pre.latest_execution_payload_header.block_hash` (line 167); `builders` empty; `next_withdrawal_builder_index = 0`; `execution_payload_availability` all ones (lines 161-164); `builder_pending_payments` default (line 165); `builder_pending_withdrawals` and `payload_expected_withdrawals` empty.
- `latest_execution_payload_bid` is built field by field as in the spec, including `builder_index = BUILDER_INDEX_SELF_BUILD`, `slot = pre.latest_block_header.slot`, and `execution_requests_root = hash_tree_root(ExecutionRequests())` (lines 123-136).
- The fork version and epoch are set correctly (lines 84-88).
- The only exception is the onboarding logic in C1.

**SSZ containers and constants (BC 653-995)**
- Field order and types match for all of these: `Builder`, `BuilderPendingPayment`, `BuilderPendingWithdrawal`, `BuilderDepositRequest`, `BuilderExitRequest`, `PayloadAttestationData`, `PayloadAttestation`, `PayloadAttestationMessage`, `IndexedPayloadAttestation`, `ExecutionPayloadBid`, `SignedExecutionPayloadBid`, `ExecutionPayloadEnvelope`, `SignedExecutionPayloadEnvelope`, Gloas `BeaconBlockBody`, `BeaconState` (all 46 fields), `ExecutionPayload`, `ExecutionRequests`.
- Locations: `consensus/types/src/builder/*.rs`, `attestation/payload_attestation*.rs`, `attestation/indexed_payload_attestation.rs`, `execution/execution_payload_bid.rs` lines 53-116, `execution/execution_payload_envelope.rs` lines 20-32, `block/beacon_block_body.rs` lines 101-250, `state/beacon_state.rs` lines 441-811.
- Constants (`consensus/types/src/core/consts.rs` lines 29-43): `BUILDER_INDEX_SELF_BUILD = u64::MAX`, `BUILDER_INDEX_FLAG = 1 << 40`, `PAYLOAD_BUILDER_VERSION = 0`.
- Domains 0x0B-0x0E (`core/chain_spec.rs` lines 1320-1323); builder withdrawal prefix `0xB0` (line 1258).

**Fork choice: missing or late payload (empty-slot semantics) (FC)** — LH `consensus/proto_array/src/proto_array.rs` unless noted

| Rule | LH location |
|---|---|
| `on_block` asserts `is_payload_verified` when building on a full parent (FC 1060-1061) | `consensus/fork_choice/src/fork_choice.rs` lines 829-839 |
| `get_parent_payload_status` / `is_parent_node_full` (FC 368-379) | lines 205-207, 672-684 |
| `get_node_children`: EMPTY always; FULL only when the payload is verified (FC 614-628) | lines 1886-1938 |
| `get_weight`: zero for previous-slot EMPTY/FULL nodes (FC 567-589) | lines 1765-1834 |
| `get_payload_status_tiebreaker` and `should_extend_payload` (FC 497-525) | lines 1940-1963, 2004-2058 |
| `get_head` sort key `(weight, root, tiebreaker)` (FC 644-670) | lines 1472-1520 |
| `on_payload_attestation_message` vote recording (FC 1136-1187) | `fork_choice.rs` lines 1400-1478; `proto_array_fork_choice.rs` lines 715-750 |
| `validate_on_attestation` Gloas rules (index in {0,1}; same-slot index 0; index 1 needs a verified payload) (FC 938-944) | `fork_choice.rs` lines 1242-1271 |

---

## Known-check sources and their coverage

- **Lighthouse issues and PRs (open and closed).** Searched with the GitHub search API using the queries listed under each candidate. Some extra queries hit the unauthenticated rate limit (`repo:sigp/lighthouse proposer_equivocation reorg gloas`); I re-ran them as `repo:sigp/lighthouse proposer equivocation reorg`, which returned #9593. The query `repo:sigp/lighthouse update_pubkey_cache gloas` returned 0 results.
- **Hive** (https://hive.ethpandaops.io, `discovery.json` and `glamsterdam-quick/listing.jsonl`). The Glamsterdam suites (`eels/consume-rlp`, `consume-engine`, `consume-enginex`, `consume-wirex`) run only execution clients (erigon, besu, reth, nethermind, geth, nimbus-el, ethrex). No Lighthouse or other consensus-client results exist there, so hive cannot confirm or refute these candidates.
- **execution-specs "Test Release Tracker: glamsterdam-devnet" issues.** Found with `repo:ethereum/execution-specs Test Release Tracker glamsterdam-devnet`: e.g. #3518 (`glamsterdam-devnet@v8.1.4`), #3479, #3452, #3311, #3034, #3217. These track execution-layer test fixtures. The #3518 body does not mention Lighthouse or the consensus layer, so it gives no coverage of CL Gloas behaviour.

## Not covered / limits

- I did not trace the full data-availability gate before `on_execution_payload_envelope` in Lighthouse.
- I did not trace whether gossip-level checks reject C3-type blocks before import.
- I did not check the inherited Electra validator-sweep helpers beyond their call order.
- I did not read the fork-choice `get_proposer_head` re-org path beyond what is listed under C2.
- The beta.5 status of C1 depends on whether consensus-specs releases #5736 unchanged.

## URLs read

- https://eips.ethereum.org/EIPS/eip-7732
- https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/beacon-chain.md
- https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork.md
- https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md
- https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/configs/mainnet.yaml
- https://github.com/ethereum/consensus-specs/tree/v1.7.0-beta.4 (diffed against master for `specs/gloas`)
- https://github.com/ethereum/consensus-specs/releases
- https://github.com/ethereum/consensus-specs/pull/5736 (via commit `8b374fa81b0918b84fe0063a56b4a482e1fa2c6f`)
- https://github.com/sigp/lighthouse/tree/a376defa30470923556d91bdeb15a802e297f31f (files cited above)
- https://github.com/sigp/lighthouse/blob/301f3bf2323cd2b3f946ceed026be8075d24e1c3/consensus/state_processing/src/upgrade/gloas.rs (`unstable` head)
- https://github.com/OffchainLabs/prysm/tree/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a (files cited above)
- https://github.com/sigp/lighthouse/issues/9593
- https://github.com/sigp/lighthouse/pull/9831
- https://github.com/sigp/lighthouse/issues/10088
- https://github.com/sigp/lighthouse/issues/9943
- https://github.com/sigp/lighthouse/pull/9954
- https://github.com/sigp/lighthouse/pull/9817
- https://github.com/OffchainLabs/prysm/pull/17440
- https://github.com/OffchainLabs/prysm/pull/16796
- https://hive.ethpandaops.io/
- https://hive.ethpandaops.io/discovery.json
- https://hive.ethpandaops.io/glamsterdam-quick/listing.jsonl
- https://github.com/ethereum/execution-specs/issues/3518
- https://github.com/ethereum/execution-specs/issues/3479
- https://github.com/ethereum/execution-specs/issues/3452
- https://github.com/ethereum/execution-specs/issues/3311
- https://github.com/ethereum/execution-specs/issues/3217
- https://api.github.com/search/issues (queries as listed)
