# bid-gossip-state: disproof attempt (Nimbus nimbus-3 / Lighthouse lighthouse-8)

Pins read:
- Nimbus `0abc0dd0fc0750a8875c49089d074f7d7197b62c`. This is the `stable` branch, tagged "version v26.10.2", committed 2026-10-23.
- Lighthouse `a376defa30470923556d91bdeb15a802e297f31f`, committed 2026-10-22.
- Prysm `eb681c1d`.
- consensus-specs `8a80ffa8`.

All code came from raw.githubusercontent.com. Commit and issue metadata came from api.github.com. Nothing was run against a network or a node.

## VERDICT: CONFIRMED (narrowed). P2P/gossip only, low severity

At these pins, the builder checks on the `execution_payload_bid` gossip path run on the unadvanced head-block state in both clients:
- Nimbus: `withState(dag.headState)`.
- Lighthouse: `cached_head.snapshot.beacon_state`.

The spec instead runs them on `store.block_states[bid.parent_block_root]` advanced with `process_slots(state, bid.slot)`. The checks affected are: index in range, payload-builder version, `is_active_builder`, `can_builder_cover_bid`, the builder-exit lookup and the signature pubkey.

None of the refutation routes removes either trigger:
- **Case (a), epoch boundary.** This occurs routinely: every bid for slot 0 of an epoch that builds on a block from the previous epoch. Epoch processing changes `finalized_checkpoint` (which drives `is_active_builder`) and `builder_pending_payments` (which drives `can_builder_cover_bid`).
- **Case (b), bid on the head's parent.** The spec's `is_bid_compatible_with_head` explicitly allows this.

The later block state transition (`process_execution_payload_bid`) re-checks activity, coverage and signature on the correct state. So the effect is limited to the gossip verdict:
- A valid bid can be REJECTed. The sending peer is penalised and the bid is not propagated.
- Or a bid can be IGNOREd or forwarded where the spec does the opposite.

There is no consensus split and no invalid block is accepted.

Half of the second part of the candidate is **refuted**: Nimbus *does* have the proposer-lookahead IGNORE at the pin, added by PR #9286. Lighthouse still lacks it. For Lighthouse, case (b) is already a **known open issue (sigp/lighthouse#9520)**.

## Strongest refutation attempt

**Route 5, "the head state is advanced to the wall-clock slot, so case (a) collapses": fails.**
- **Lighthouse.** The pinned file itself shows the snapshot is not advanced. `gossip_verified_bid.rs#L353-L387` reads `let snapshot_state = &cached_head.snapshot.beacon_state;`. It switches to `get_advanced_hot_state(..., bid_slot, ...)` only when `!snapshot_state.fork_name_unchecked().gloas_enabled()`, with the comment "At the Gloas fork boundary the head snapshot is still a pre-Gloas state, so we must use the advanced state instead". That comment only makes sense if the snapshot is the head block's own (unadvanced) post-state. After the fork epoch, the unadvanced snapshot is used.
- **Nimbus.** `blockchain_dag.nim` `updateHead` (L2801 ff.) does `updateState(dag, dag.headState, newHead.bid.atSlot(), ...)`, which puts `dag.headState` at the head block's own slot. The gossip validator reads `dag.headState` directly (`gossip_validation.nim#L1881`), with no `process_slots` to `bid.slot`.

**Route 5b, "`is_bid_compatible_with_head` restricts the parent so case (b) cannot differ": fails.** The spec's function (p2p-interface.md#L401-L424) returns True for `bid.parent_block_root == head_block.parent_root and bid.parent_block_hash == head_bid.parent_block_hash`. That means a bid on the head's parent passes the filter. Both clients implement this compatibility check (Nimbus `#L1877`, Lighthouse `#L428`). The Lighthouse pin even handles this case for RANDAO only (`#L436-L446`: "Head compatibility also permits a bid on the head's parent", added by PR #10061). The builder checks after it still use `head_state`.

When the parent is the head's parent, the head state already contains the head block's effects:
- its own bid's `builder_pending_payments` entry, which lowers coverage for that builder;
- any builder voluntary exit in the head block, which sets `withdrawable_epoch` and makes a spec-active builder look inactive (REJECT);
- `process_withdrawals`.

**Route 1, "another check gives the spec's result": fails.** No other check on either path recomputes the builder fields on the parent state. Lighthouse's `verify_bid_state_conditions` (#L125-L158) is reused at block production against the production state, per its doc comment. That reuse protects the proposer's own block, but it does not change the gossip verdict.

**Route 2, "the condition cannot occur on a valid chain": fails.** Here is the concrete case (a) for `is_active_builder` (beacon-chain.md#L1054-L1064: `builder.deposit_epoch < state.finalized_checkpoint.epoch`):
1. A builder registered in epoch D gets `deposit_epoch = compute_epoch_at_slot(slot)` (beacon-chain.md#L2316).
2. On a healthy chain, the epoch transition into D+3 finalizes D+1 (`process_justification_and_finalization` is first in `process_epoch`, #L1633-L1654).
3. A bid for the first slot of D+3 that builds on the last block of D+2 is therefore valid under the spec (state advanced: finalized = D+1 > D).
4. The head-state reader sees finalized = D (not > D) and returns REJECT "builder is not active".

The window is one slot, once per newly registered builder, unless finality stalls. For `can_builder_cover_bid`, `process_builder_pending_payments` (#L1719-L1731) drops below-quorum payments from the previous epoch at the boundary. This lowers `get_pending_balance_to_withdraw_for_builder` (#L1168-L1178), so the spec can accept a bid that the head-state reader IGNOREs ("cannot cover").

**Route 3, "the spec was misread": fails.** p2p-interface.md#L1011 `state = store.block_states[bid.parent_block_root]`. #L1048-L1049 `state = state.copy(); process_slots(state, bid.slot)`. Then #L1051-L1079 run index, version, `is_active_builder`, `can_builder_cover_bid`, builder-exit and signature on that state.

**Route 4, "a later commit fixes it": fails as of 2026-10-27.**
- Nimbus. The newest commits touching `gossip_validation.nim`:
  - On `stable`: 64ac3fe926 (2026-10-17) and d8e424e44b (#9286, lookahead).
  - On `unstable`: 29a55f0911 (2026-10-24, "Cache PTC in EpochRef", #9341). Its diff changes payload-attestation validation, not the bid builder checks.
- Lighthouse. The newest commits on `unstable` touching `gossip_verified_bid.rs` predate the pin (latest 95206a0248, 2026-10-07). PR #10061 ("Fix gossip bid rejection at epoch boundaries and on the head's parent", merged 2026-09-23) changed only the `prev_randao` source and the `EpochOutOfBounds` handling, not the builder checks.

## Corrected record

- **Spec**: consensus-specs `8a80ffa8`.
  - specs/gloas/p2p-interface.md#L1011: "`state = store.block_states[bid.parent_block_root]`".
  - #L1048-L1049: "`state = state.copy()` / `process_slots(state, bid.slot)`".
  - #L1051-L1079: builder index, version, active, cover, exit and signature checks on that state.
  - #L1013-L1015: "[IGNORE] The bid's slot is within the parent's proposer lookahead".
  - specs/gloas/beacon-chain.md#L1054-L1064 `is_active_builder` (`builder.deposit_epoch < state.finalized_checkpoint.epoch`).
  - #L1184-L1192 `can_builder_cover_bid`.
  - #L1719-L1731 `process_builder_pending_payments`.
  - Block-level re-check: #L2175-L2181 (`assert is_active_builder`, `can_builder_cover_bid`, `verify_execution_payload_bid_signature`).
- **Nimbus** `0abc0dd0fc0750a8875c49089d074f7d7197b62c`, beacon_chain/gossip_processing/gossip_validation.nim:
  - #L1881-L1894: `withState(dag.headState)`, index, active and version checks.
  - #L1916-L1920: cover check on the head state.
  - #L1962-L1967: exit check using the head-state builder.
  - #L1988-L1996: signature with `builder[].pubkey` from the head state.
  - The lookahead IGNORE **is present** at #L1871-L1874.
- **Lighthouse** `a376defa`, beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs:
  - #L353-L387: head_state is the unadvanced snapshot except at the fork boundary.
  - #L125-L158: `verify_bid_state_conditions` on head_state.
  - #L449-L472: called with `head_state`, exit check, and the signature set using `get_builder_pubkey_from_state(head_state, ..)`.
  - **No proposer-lookahead IGNORE**: no "lookahead"/`MIN_SEED_LOOKAHEAD` in the file. Lighthouse instead derives `dependent_root` from `head_state.proposer_shuffling_decision_root_at_epoch` at #L394. What Lighthouse returns for a bid two or more epochs past its parent was not determined. It most likely ends as an IGNORE or an internal error through missing preferences. This needs the parent (= head) to be more than about one epoch old, i.e. a liveness gap. Its impact is low.
- **Condition**:
  - (a) A bid for slot `k*SLOTS_PER_EPOCH` whose parent block is in epoch k-1, when epoch processing advances finality past a builder's `deposit_epoch` (spec accepts; Nimbus/Lighthouse REJECT). Or when it drops below-quorum pending payments, so the spec accepts where Nimbus/Lighthouse IGNORE "cannot cover".
  - (b) A bid with `parent_block_root == head.parent_root` when the head block contains a builder voluntary exit (REJECT "not active") or adds a pending payment for that builder (IGNORE "cannot cover"), while the spec accepts.
  - (c) Theoretical, not verified: builder indices are reused (`get_index_for_new_builder`, beacon-chain.md#L2290). The index check and signature pubkey can then differ between the head state and the parent state.
- **Other clients**: Prysm `eb681c1d` `beacon-chain/sync/validate_execution_payload_bid.go` (#L81, #L95-L97) is reported by the candidate as following the spec. I did not re-read it in depth for this item.
- **Confidence**:
  - Divergence exists in code: high.
  - Case (a) via finality is reachable once per new builder: medium-high.
  - Practical impact: low, since builders would normally bid one slot later and the block-level checks are correct.
- **Severity**: gossip verdict only. Closest category is "valid block rejected", applied to a valid *bid* message rejected or ignored at P2P, with peer down-scoring of honest builders' relays. It is **not** a consensus split or "invalid block accepted": `process_execution_payload_bid` re-validates on the correct state (beacon-chain.md#L2175-L2181). Glamsterdam/Gloas is not active on mainnet, so the live-code withholding rule does not apply.

## Known-check

- **sigp/lighthouse#9520** (issue, OPEN), "Non-canonical payload bid handling": "We always just validate against the head block + state". It cites `gossip_verified_bid.rs#L130-L135` at an earlier commit. This **covers case (b) for Lighthouse** and loosely case (a). So the brief's "not found" note is incorrect for Lighthouse.
- **sigp/lighthouse#10061** (PR, merged 2026-09-23), "Fix gossip bid rejection at epoch boundaries and on the head's parent": fixed the RANDAO/epoch-bounds part only. The builder checks remain on head_state.
- **sigp/lighthouse#9924** (PR, OPEN), "Gloas gossip handlers": spec gossip test harness. **#10263** (PR, OPEN): IL-bits check on bids. Neither addresses this.
- **status-im/nimbus-eth2#9286** (merged 2026-10-17), "Implement proposer lookahead check in ExecutionPayloadBid gossip": present in the pin.
- Nimbus search (`ExecutionPayloadBid state`, `bid headState`) found no issue or PR about the head-state vs parent-state builder checks. Hits were #8042, #8340, #8941, #9334, which are unrelated. So it is **not known** for Nimbus.
- Public hive or execution-spec-tests results for this gossip rule: not checked. Spec gossip vectors for payload bids are what LH #9924 consumes; I did not find a published result for this case.

## URLs read

- https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/p2p-interface.md
- https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md
- https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/gossip_processing/gossip_validation.nim
- https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/consensus_object_pools/blockchain_dag.nim
- https://raw.githubusercontent.com/sigp/lighthouse/a376defa/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs
- https://raw.githubusercontent.com/OffchainLabs/prysm/eb681c1d/beacon-chain/sync/validate_execution_payload_bid.go (fetched; not analysed in depth)
- https://api.github.com/repos/status-im/nimbus-eth2/commits?sha=stable&path=beacon_chain/gossip_processing/gossip_validation.nim (and `sha=unstable`)
- https://api.github.com/repos/status-im/nimbus-eth2/commits/0abc0dd0fc0750a8875c49089d074f7d7197b62c
- https://api.github.com/repos/status-im/nimbus-eth2/commits/29a55f0911
- https://api.github.com/repos/sigp/lighthouse/commits?sha=unstable&path=beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs
- https://api.github.com/repos/sigp/lighthouse/commits/a376defa
- https://api.github.com/repos/sigp/lighthouse/commits/4b1f3c204a
- https://api.github.com/repos/sigp/lighthouse/pulls/10061
- https://api.github.com/repos/sigp/lighthouse/issues/9924
- https://api.github.com/repos/sigp/lighthouse/issues/9520
- https://api.github.com/repos/sigp/lighthouse/issues/10263
- https://api.github.com/search/issues (queries: `repo:status-im/nimbus-eth2 ExecutionPayloadBid state`, `repo:status-im/nimbus-eth2 bid headState`, `repo:sigp/lighthouse bid head_state`, `repo:sigp/lighthouse payload bid parent state gossip`)
