| Client / repository | Branch / ref | Commit | Spec commit |
|---|---|---|---|
| Nimbus: status-im/nimbus-eth2 | stable | [`0abc0dd0fc0750a8875c49089d074f7d7197b62c`](https://api.github.com/repos/status-im/nimbus-eth2/commits/stable) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Nimbus: status-im/nimbus-eth2 | unstable | [`5babd698516450d8b8e60c2af610a5853baaa9a9`](https://api.github.com/repos/status-im/nimbus-eth2/commits/unstable) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Lodestar: ChainSafe/lodestar | unstable | [`e4b4f615e68c1a4ea7c57632e6ed8d824624573f`](https://api.github.com/repos/ChainSafe/lodestar/commits/unstable) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Grandine: grandinetech/grandine | develop | [`52cd677117b5d93bf477fe4843148070b0303444`](https://api.github.com/repos/grandinetech/grandine/commits/develop) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Prysm (comparison): OffchainLabs/prysm | develop | [`eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a`](https://api.github.com/repos/OffchainLabs/prysm/commits/develop) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Teku (comparison): Consensys/teku | master | [`2c35574b3373cbe8ad121a768d98f066aec05b73`](https://api.github.com/repos/Consensys/teku/commits/master) | `8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2` |
| Specification: ethereum/consensus-specs | master | [`8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2`](https://api.github.com/repos/ethereum/consensus-specs/commits/master) | same |
| Specification release | v1.7.0-beta.4 | `aa16bb4c156184e9548a997d53efaf2a228a6304` | tagged baseline |

# Merge 02: Gloas fork choice and networking

This report merges the supplied Nimbus, Lodestar and Grandine reviews dated 2026-10-27; no additional investigation or findings were added. Source item identifiers: `item-nimbus`, `item-lodestar`, `item-grandine`. All 19 candidates are copied below, including uncertainty and disproof attempts. Original C1 etc. labels map in order to client-prefixed labels. In copied prose, local C-references refer to that client’s numbered candidate. Nimbus-7 and grandine-7 were not supplied in six-part form; missing trigger/comparison/disproof or confidence fields are not invented. All inputs describe Gloas-only candidates and report no live-code candidate. No client/node experiments were performed.

## Specification baseline and citation keys

Abbreviations: `N@` = `https://github.com/status-im/nimbus-eth2/blob/0abc0dd0fc0750a8875c49089d074f7d7197b62c/`, `NU@` = the same at `5babd698…`, `S@` = `https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/`, `P@` = `https://github.com/OffchainLabs/prysm/blob/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/`.

All three inputs target v1.7.0-beta.4. Nimbus declares it at `N@beacon_chain/spec/datatypes/base.nim#L83`; Lodestar at [spec-tests-version.json#L1-L12][target]; Grandine at `G:scripts/download_spec_tests.sh#L4`. The inputs compared beta.4 with pinned master: Gloas fork-choice, p2p-interface and validator text is identical, while onboarding changed in spec PR 5736. Nimbus left inherited phase0 deadline rounding (PR 5756) unchecked. Nimbus unstable fixes branch-specific PTC lookup in PR 9341 (`NU@beacon_chain/fork_choice/fork_choice_epbs.nim#L228-L256`, `NU@beacon_chain/gossip_processing/gossip_validation.nim#L2055`); bid builder checks still use head state (`#L1881`).

Short names used below:
- `G:` = `https://github.com/grandinetech/grandine/blob/52cd677117b5d93bf477fe4843148070b0303444/`
- `S:` = `https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/`
- `N:` = `https://github.com/status-im/nimbus-eth2/blob/0abc0dd0fc0750a8875c49089d074f7d7197b62c/`

## Candidates (copied from source items)


## Nimbus

### nimbus-1: In-block payload attestations update only the set bits, not every PTC seat the attester holds

1. **Spec.** `S@specs/gloas/fork-choice.md#L265-L289` (`notify_ptc_messages`) builds `get_indexed_payload_attestation(...)` and then, `for idx in indexed_payload_attestation.attesting_indices: on_payload_attestation_message(store, PayloadAttestationMessage(validator_index=idx, ...), is_from_block=True)`. Then `#L1136-L1188`: "Get all positions of the attester in the PTC … `for ptc_index, validator_index in enumerate(ptc): if validator_index == ptc_message.validator_index: ptc_indices.append(ptc_index)` … `for ptc_index in ptc_indices: payload_timeliness_vote[ptc_index] = data.payload_present`". `S@specs/gloas/beacon-chain.md#L1266-L1269`: "Get the payload timeliness committee, **with possible duplicates**". `#L1419-L1424`: the attesting indices are the set bits only, sorted and not de-duplicated.
2. **Nimbus.** `N@beacon_chain/fork_choice/fork_choice.nim#L519-L528`:
   ```nim
   for pa in blck.body.payload_attestations:
     if pa.data.slot + 1 < slot:
       continue
     let tally = self.backend.mgetPtcTally(
       pa.data.beacon_block_root, pa.data.slot)
     for i in 0 ..< pa.aggregation_bits.len:
       if pa.aggregation_bits[i]:
         tally.voted[i] = true
         tally.present[i] = pa.data.payload_present
         tally.available[i] = pa.data.blob_data_available
   ```
   Only the bit positions that are set get written. The wire path (`fork_choice_epbs.nim#L237-L244`) does loop over every seat. Unstable has the same in-block code.
3. **When results differ.** Some validator holds two or more seats in the PTC for slot N. A block at slot N+1 includes a payload attestation for the slot-N block that sets only some of that validator's seats. A proposer can build this on purpose: the aggregate signature over the de-duplicated set stays valid. The spec counts every seat of that validator, Nimbus counts only the set bits. If the node did not also receive that validator's message on the wire, `payload_timeliness` / `payload_data_availability` counts differ, which changes `should_extend_payload` → `get_payload_status_tiebreaker` (FULL vs EMPTY for the previous-slot block) and `should_build_on_full` near the 256-vote threshold. A second, harmless difference: Nimbus does not check `data.slot == slot of beacon_block_root`, but tallies only matter for previous-slot blocks.
4. **Prysm agrees with the spec.** `P@beacon-chain/blockchain/process_block.go#L625-L659` collects the voters, then writes every seat: "The spec writes a voter's vote to every PTC seat it holds, not only the seats whose bit is set."
5. **Confidence: medium.** Disproof tried: I looked for a de-duplication step before fork choice and found none in `process_block`. The divergence needs a validator with duplicate seats and an aggregate covering only some of them. Honest aggregators probably set all seats (I did not check `payload_attestation_pool.nim` aggregation in detail). It only flips the result near the threshold.
6. **Severity:** fork-choice divergence (FULL/EMPTY choice for the previous-slot block, plus what the proposer builds on).

### nimbus-2: (stable only) The PTC used for gossip REJECT and for fork-choice votes comes from the head state, not the attested block's state

1. **Spec.** `S@specs/gloas/fork-choice.md#L1139-L1164`: `state = store.block_states[data.beacon_block_root]` … `ptc = get_ptc(state, data.slot)` … `assert len(ptc_indices) > 0`. The p2p `payload_attestation_message` rule (`S@specs/gloas/p2p-interface.md#L886-L950`) also checks membership in `get_ptc(state, data.slot)` for the attested block's state.
2. **Nimbus stable.**
   - `N@beacon_chain/gossip_processing/gossip_validation.nim#L2042-L2052`: `withState(dag.headState): … for idx in get_ptc(dag.cfg, forkyState.data, data.slot): … if not present: return dag.checkedReject("PayloadAttestationMessage: validator not in ptc")`.
   - `N@beacon_chain/fork_choice/fork_choice_epbs.nim#L234-L248`: `withState(dag.headState): … for ptc_index, vidx in enumerate(get_ptc(dag.cfg, forkyState.data, slot)):`.
3. **When results differ.** The attested block sits on a branch whose PTC for that slot differs from the head branch. This happens during non-finality or deep forks, where the branches split before the epoch boundary that fixed `ptc_window`. A valid message is then REJECTed and its vote dropped, or a non-member's vote is counted.
4. **Prysm agrees with the spec.** It looks up a state for the attested block's branch: `P@beacon-chain/blockchain/receive_payload_attestation_message.go#L52-L67` (`PtcLookupState` compares dependent roots), used at `P@beacon-chain/sync/validate_payload_attestation.go#L98-L113`.
5. **Confidence: high** that stable differs. **Known:** fixed on unstable by PR 9341 (merged 2026-10-24, base `unstable`), not yet on stable. That PR's description gives the same reason ("may have different PTC shufflings … branching non-finality").
6. **Severity:** a valid message rejected on gossip (peer-scoring) plus fork-choice divergence in PTC tallies, only on forked branches.

### nimbus-3: Execution-payload-bid builder checks run on the head state instead of the parent state advanced to `bid.slot`

1. **Spec.** `S@specs/gloas/p2p-interface.md#L951-L1085`: `state = store.block_states[bid.parent_block_root]` … `state = state.copy(); process_slots(state, bid.slot)` … "[REJECT] The builder index is valid", "[REJECT] The builder is a payload builder", "[REJECT] The builder is active", "[IGNORE] The builder can cover the bid", and the bid signature is checked against `state`. `is_active_builder` (`S@specs/gloas/beacon-chain.md#L1054-L1064`) depends on `builder.deposit_epoch < state.finalized_checkpoint.epoch`.
2. **Nimbus.** `N@beacon_chain/gossip_processing/gossip_validation.nim#L1881-L1907`:
   ```nim
   withState(dag.headState):
     when consensusFork >= ConsensusFork.Gloas:
       # [REJECT] The builder index is valid
       if bid.builder_index >= forkyState.data.builders.lenu64:
         return dag.checkedReject(...)
       # [REJECT] The builder is active
       if not is_active_builder(forkyState.data, bid.builder_index):
         return dag.checkedReject("ExecutionPayloadBid: builder is not active")
   ```
   `can_builder_cover_bid(forkyState.data, …)` and the builder pubkey come from the same head state. Unstable has the same code (`NU@…#L1881`).
3. **When results differ.**
   - (a) The bid is for the first slot of a new epoch and the head state has not been advanced across the boundary. The spec's `process_slots` runs epoch processing; if that moves finality (`finalized_checkpoint.epoch`), a newly registered builder becomes active. The spec accepts the bid and Nimbus REJECTs it. The reverse holds for builder exits and pending payments that epoch processing settles, which changes `can_builder_cover_bid`.
   - (b) The bid builds on the head's parent, which `is_bid_compatible_with_head` allows. The spec uses the parent's state; Nimbus uses the head state, which already includes the head block's effects on builders and balances.
4. **Prysm agrees with the spec.** `P@beacon-chain/sync/validate_execution_payload_bid.go#L81` calls `st := transition.NextSlotStateReadOnly(parentBlockRoot[:], bid.Slot())` and then `VerifyBuilderActive(st)` at `#L95-L97`.
5. **Confidence: medium.** Disproof tried: I checked whether Nimbus's head state is advanced to the wall slot before gossip validation and found no such step in this function. Whether the epoch-boundary case happens depends on when a builder's deposit epoch passes finality. Not found in Nimbus issues: queries "builder active bid", "headState bid".
6. **Severity:** wrong gossip verdict. A valid bid is rejected and the peer penalised, or an unusable bid is accepted. This is not a block-validity split: block processing uses the real state.

### nimbus-4: Bid gossip checks run in a different order, so some duplicate or low-value bids get REJECT where the spec says IGNORE

1. **Spec.** `S@specs/gloas/p2p-interface.md#L968-L978`. The first two checks are "[IGNORE] This is the first bid for this slot, parent, and builder" and "[IGNORE] This is the highest value bid seen …". Only after them come "[REJECT] The bid's execution payment is zero", "[REJECT] … block hash … equal to its parent block hash", "[REJECT] … blob KZG commitment count".
2. **Nimbus.** `N@beacon_chain/gossip_processing/gossip_validation.nim#L1845-L1858` runs the REJECT checks (`execution_payment != 0`, `block_hash == parent_block_hash`, too many commitments) first. The first-seen and highest-bid IGNOREs come later, at `#L1904-L1915`.
3. **When results differ.** A bid repeats an already-seen (slot, parent hash, parent root, builder) key, or is not above the best value, *and* has a nonzero `execution_payment`, `block_hash == parent_block_hash`, or too many commitments. The spec returns IGNORE and Nimbus returns REJECT.
4. **Prysm is partly like Nimbus.** It checks `block_hash != parent_block_hash` (REJECT) before the first-seen IGNORE: `P@beacon-chain/sync/validate_execution_payload_bid.go#L59-L75`.
5. **Confidence: high** that the order differs. The observable effect is limited to the verdict and peer scoring.
6. **Severity:** cosmetic (gossip verdict / peer score only).

### nimbus-5: In stable `is_head_weak`, the head weight is a proto-array node weight that can include proposer boost and leaves out the FULL variant

1. **Spec.** `S@specs/gloas/fork-choice.md#L807-L833`: `head_node = ForkChoiceNode(root=head_root, payload_status=PAYLOAD_STATUS_PENDING); head_weight = get_attestation_score(store, head_node, justified_state)`, plus the effective balance of committee members in `store.equivocating_indices`. `get_attestation_score` counts no proposer boost and covers both payload variants.
2. **Nimbus.** `N@beacon_chain/fork_choice/fork_choice_epbs.nim#L138-L158`:
   ```nim
   let proto_node = self.backend.proto_array.node(head_root).valueOr:
     return true
   var head_weight = proto_node.weight.Gwei
   if head_root == self.backend.proto_array.previousProposerBoostRoot:
     head_weight -= self.backend.proto_array.previousProposerBoostScore
   ```
   `proto_array.node(root)` returns the `indices` (EMPTY/pending) node, not the FULL node (`N@beacon_chain/fork_choice/proto_array.nim#L80-L81,L119-L120`). Its `weight` includes the subtree, and that subtree includes boost carried up from a boosted child (`proto_array.nim#L282-L319`). The weights are also from the previous `find_head`, because `get_head` calls `should_apply_proposer_boost` before `find_head` (`fork_choice.nim#L645-L656`).
3. **When results differ.** In `should_apply_proposer_boost`, Nimbus calls `is_head_weak` on the parent of the boosted block (`fork_choice_epbs.nim#L179-L180`). Two cases:
   - The boosted block builds on the parent's EMPTY variant and was boosted in the previous `find_head`. The parent's node weight then includes the boost score (`PROPOSER_SCORE_BOOST`, 40% of a committee, above the 20% `REORG_HEAD_WEIGHT_THRESHOLD`), so Nimbus treats the parent as strong and applies the boost. The spec may find the parent weak and, if the parent's proposer equivocated in a timely way, withhold the boost.
   - Weight on the parent's FULL variant is left out, so Nimbus can call the parent weak when the spec does not.

   Either way, the head can differ during the slot.
4. **Prysm.** Not checked in the time available.
5. **Confidence: low to medium.** Disproof tried: the boost subtraction only covers `head_root == previousProposerBoostRoot`, which is the boosted block itself, never its parent. The case needs a weak parent plus a timely proposer equivocation, which is rare. I did not build a test.
6. **Severity:** fork-choice divergence (proposer-boost application in equivocation scenarios).

### nimbus-6: A `beacon_attestation` with index 1 is accepted when the envelope is only quarantined (not verified)

1. **Spec.** `S@specs/gloas/p2p-interface.md#L426-L463` (`verify_attestation_payload_status`): "[IGNORE] The corresponding execution payload envelope has been seen and verified … `if not is_payload_verified(store, block_root): raise GossipIgnore`". `is_payload_verified` means `root in store.payloads`, which is set only by `on_execution_payload_envelope`.
2. **Nimbus.** `N@beacon_chain/gossip_processing/gossip_validation.nim#L1012-L1016`:
   ```nim
   if not pool.dag.db.containsExecutionPayloadEnvelope(block_root) and
       not envelopeQuarantine[].hasOrphan(block_root):
     return errIgnore(
       "SingleAttestation: execution payload envelope has not been seen")
   ```
   The aggregate path has the same check (`#L1216-L1220`).
3. **When results differ.** The envelope for the attested block sits in Nimbus's envelope quarantine as an orphan (received but not yet verified or imported). The spec IGNOREs an index-1 attestation; Nimbus goes on and can ACCEPT and forward it. Fork-choice effect is small: `validate_on_attestation` runs again when the vote is applied (`fork_choice.nim#L400-L405`).
4. **Prysm.** Not checked in the time available.
5. **Confidence: low.** I did not trace whether `hasOrphan` entries are verified envelopes waiting on columns, or unverified ones.
6. **Severity:** cosmetic (gossip verdict / propagation).

### nimbus-7: Spec-version lag
Nimbus declares `SPEC_VERSION = "1.7.0-beta.4"` (`N@beacon_chain/spec/datatypes/base.nim#L83`, same on unstable). Master (beta.5-dev) adds the Gloas `fork.md` builder-onboarding change (PR 5736), which I did not review against Nimbus's fork upgrade because it is outside this item's files. For the three files in scope, beta.4 and master are identical, so the lag does not affect fork choice or p2p. Severity if PR 5736 is not implemented: state-root split at the fork. Confidence: low; not verified.

**Not a candidate: `get_proposer_head` / Lighthouse 9593.** Neither branch implements the proposer re-org path: grep for `is_head_late`, `is_parent_strong`, `get_proposer_head`, `proposer_equivocation` finds nothing under `beacon_chain/`. That changes only local proposal behaviour, not validity. `is_head_weak` is used only by `should_apply_proposer_boost`.


## Lodestar

### lodestar-1 — Spec-version lag: mixed-key deposits at the Gloas upgrade

1. **Spec:** `if not is_builder_withdrawal_credential(deposit.withdrawal_credentials):` then `pending_deposits.append(deposit)`. Master routes by credentials first and permits a builder registration using a validator key. [S:fork.md#L60-L119][sfork].
2. **Lodestar:** `if (isValidatorKnown(state, validatorIndex)) {` precedes builder processing; `state.builders.get(builderIndex).balance += deposit.amount;` precedes the credential check. [L:upgradeStateToGloas.ts#L208-L245][lonboard].
3. **Condition:** At upgrade, a valid builder deposit using an existing validator key, or one with an earlier valid queued validator deposit, remains queued in Lodestar but registers a builder in master. Conversely, a new builder registration followed by a validator-credential deposit for that key sends the second deposit to the builder in Lodestar, while master queues it. Registry, balances, pending deposits and state root differ.
4. **Teku: agrees with Lodestar's older rule**, testing existing validators, then existing builders, before credentials. [T:GloasStateUpgrade.java#L222-L260][tonboard].
5. **High confidence. Disproof:** Directly compared tag/master; Lodestar correctly reflects its declared beta.4 baseline. This is **spec-version lag**, not a claim that beta.4 vectors should fail. Master deliberately changes routing.
6. **Severity: consensus split** between old/master upgrade rules on these states. **Known:** [Lodestar 10428](https://github.com/ChainSafe/lodestar/pull/10428), **open**; [spec 5736](https://github.com/ethereum/consensus-specs/pull/5736), **merged/closed**. Queries: `gloas`, `epbs`, `7732`, `onboard` within ChainSafe/lodestar, both states. No candidate-specific public Hive/EEST verdict obtained.

### lodestar-2 — In-block PTC votes do not expand duplicate-validator positions

1. **Spec:** `for idx in indexed_payload_attestation.attesting_indices:` dispatches each signer to the message handler; `ptc_indices.append(ptc_index)` collects every occurrence of that signer. [S:fork-choice.md#L268-L288][snotify], [#L1156-L1187][spositions]. Indexed extraction selects validators by bits; signature validation does not require selecting all occurrences. [S:beacon-chain.md#L1091-L1115][sindexed], [#L1407-L1428][sbits].
2. **Lodestar:** `ptcIndices.push(i);` forwards raw set-bit positions from the block. [L:importBlock.ts#L280-L298][lblockptc]. The recipient updates only `for (const ptcIndex of ptcIndices) {`. [L:protoArray.ts#L758-L789][lnotify].
3. **Condition:** A validator occupies multiple PTC positions. A valid next-block aggregate selects one occurrence but omits another, with the signature for the selected occurrence(s). The observer missed the direct gossip vote. The spec updates all occurrences; Lodestar updates selected ones only. A head witness has otherwise matching positive timeliness and DA counts of 256 in Lodestar, 257 after expansion, a verified parent payload, and a timely boosted child choosing EMPTY. Previous-slot FULL/EMPTY weights are zero: spec quorum prefers FULL; Lodestar's missing quorum allows the boosted EMPTY choice. Stored votes and head differ. Opposite votes can likewise overwrite different positions.
4. **Teku: agrees with spec**, converting block aggregates into messages and calculating every committee position. [T:ForkChoice.java#L1268-L1307][tptc]. Its [regression test](https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTest.java#L718-L750) expands one selected occurrence to three. Read, not run.
5. **High confidence. Disproof:** Block validation checks parent root, previous slot and indexed signature, not all-occurrences bits. [L:processPayloadAttestation.ts#L7-L26][lprocessptc]. Normal gossip already expands all positions, masking this when received directly; it does not repair an observer importing only the block. The witness uses strict >256, not >=256.
6. **Severity: fork-choice divergence. Not found for block-import expansion.** Both-state queries: `gloas`, `epbs`, `7732`, `"PTC" "duplicate"`, `"notifyPtcMessages"`. Related [9369](https://github.com/ChainSafe/lodestar/pull/9369), **merged/closed**, fixes gossip/pool duplicates; its [changed files](https://api.github.com/repos/ChainSafe/lodestar/pulls/9369/files) exclude block import. No matching Hive/EEST verdict obtained.

### lodestar-3 — Index-1 gossip checks existence, not optimism/invalidation

1. **Spec:** `raise GossipIgnore("attested payload is optimistic")`; `raise GossipReject("attested payload is invalid")`. [S:p2p-interface.md#L426-L463][spayloadstatus]. The helper is called for [aggregates, #L789-L808][saggregatecall] and [singles, #L1265-L1281][ssinglecall].
2. **Lodestar:** Both validators only test `!chain.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot))`. [L:attestation.ts#L294-L335][lattstatus], [L:aggregateAndProof.ts#L76-L113][laggstatus]. The predicate is `return this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL) !== undefined;`. [L:protoArray.ts#L2071-L2079][lhaspayload].
3. **Condition:** An otherwise valid new index-1 vote references an earlier-slot Gloas block with an imported FULL variant still SYNCING/ACCEPTED. Lodestar can ACCEPT/forward; spec IGNOREs. If FULL was later invalidated but remains indexed, Lodestar can ACCEPT instead of REJECT. Immediate-invalid import with **no** FULL is not this witness: the existence guard ignores it.
4. **Teku: agrees with spec**, rejecting tracked invalid roots, deferring missing payloads and checking optimism on FULL. [T:GossipValidationHelper.java#L244-L298][tgossipstatus].
5. **High confidence for gossip verdict. Disproof:** Preprocessing waits only for absent FULL, not existing optimistic FULL. [L:processor/index.ts#L568-L586][lpreprocess]. Envelope import permits SYNCING/ACCEPTED and inserts that status. [L:importExecutionPayload.ts#L250-L277][loptimisticimport], [#L323-L332][limportfc]. Normal signatures/targets/committee checks must still pass; no head-divergence experiment was run.
6. **Severity: cosmetic** under the required taxonomy: specifically gossip-verdict/propagation divergence, not formatting. **Known:** [10438](https://github.com/ChainSafe/lodestar/pull/10438), **open**, covers invalid FULL and explicitly leaves optimistic IGNORE undone; [9637](https://github.com/ChainSafe/lodestar/pull/9637), **open**, earlier invalid-payload proposal. Located through both-state `gloas`/`epbs`/`7732` searches and rule-name matches in those PRs. No matching Hive/EEST verdict obtained.

### lodestar-4 — Zero-count EnvelopesByRange returns InvalidRequest

1. **Spec:** “The response MUST contain no more than `count` blocks.” [S:phase0/p2p-interface.md#L1553-L1570][srangebase]. Gloas inherits the range semantics with envelope content; count is Uint64 with no positive minimum stated. [S:gloas/p2p-interface.md#L1396-L1431][srange].
2. **Lodestar:** `throw new ResponseError(RespStatus.INVALID_REQUEST, "count < 1");`. [L:executionPayloadEnvelopesByRange.ts#L111-L134][lrangezero].
3. **Condition:** A well-encoded zero-count request with an in-retention Gloas start slot describes an empty range; Lodestar responds with an error instead of zero chunks.
4. **Teku: agrees with empty-range interpretation.** Validates overflow/maximum only, then completes an empty range. [T:ExecutionPayloadEnvelopesByRangeMessageHandler.java#L82-L174][trangevalid].
5. **Low confidence. Disproof:** No positive minimum found in inherited text, but the response bound alone does not explicitly prohibit InvalidRequest for zero. A clarification permitting that rejection would disprove this candidate.
6. **Severity: cosmetic. Not found.** Both-state queries: `gloas`, `epbs`, `7732`, `"EnvelopesByRange"`, `"envelopes" "zero count"` (last: zero hits). No matching Hive/EEST verdict obtained.

### lodestar-5 — Weak-head equivocation term can substitute head-state balances

1. **Spec:** `justified_state.validators[index].effective_balance`: use head-state committees but justified-state balances for equivocators. [S:fork-choice.md#L810-L832][sweak].
2. **Lodestar:** `if (!balance) {` falls back to `balance = state.effectiveBalanceIncrements[validatorIndex];` inside the **Gloas-only** branch. [L:forkChoice.ts#L1717-L1754][lweak]. The fallback uses a slashed/inactive-zeroed cache ([balance.ts#L51-L82][lbalcache]), losing raw balance for this extra term.
3. **Condition:** An equivocating head-committee member is already slashed in the justified state but remains assigned duties before exit. Its actual justified effective balance differs from its later head balance. The fallback adds the latter; near the reorg threshold, weakness, proposer reorg or boost suppression can differ. Zeroed cache alone, with equal actual balances, does not suffice.
4. **Teku: agrees with spec**, reading `justifiedState.getValidators().get(validatorIndex).getEffectiveBalance().longValue();` while traversing head committees. [T:ForkChoiceUtilGloas.java#L463-L488][tweak], used in [#L506-L536][tweakuse].
5. **Medium confidence. Disproof:** Cache zeroing includes already-slashed validators, not just missing indices. Equal head/justified balances reproduce no discrepancy. A complete reachable threshold-crossing chain fixture was not built; this remains conditional. The shared utility is not alleged faulty: the Gloas-only consumer substitutes the wrong state's balance. General committee-equivocator weight is implemented.
6. **Severity: fork-choice divergence. Not found for balance-source edge.** Both-state queries: `"is_head_weak"`, `"is_head_weak" "balance"`, `"isHeadWeak" "balance"`, `gloas`, `epbs`, `7732`. Related [9232](https://github.com/ChainSafe/lodestar/issues/9232), **open**, [9339](https://github.com/ChainSafe/lodestar/pull/9339), **closed/unmerged**, and [9654](https://github.com/ChainSafe/lodestar/pull/9654), **merged/closed**, concern implementing equivocation weight, not this balance source. [Lighthouse 9593](https://github.com/sigp/lighthouse/issues/9593), **open**, concerns a different total-weight approximation. No matching Hive/EEST verdict obtained.


## Grandine

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

1. **Spec (master)**: `S:fork.md#L65-L70` and `#L84-L106`.
   - "Only deposits with builder withdrawal credentials can fund builders. … A valid builder registration can use a validator's public key."
   - Code:
     ```python
     # Only builder-credential deposits can fund builders at the fork.
     if not is_builder_withdrawal_credential(deposit.withdrawal_credentials):
         pending_deposits.append(deposit); continue
     ...
     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(...)
     ```
   - beta.4 instead had `# Deposits for existing validators stay in the pending queue` / `if deposit.pubkey in validator_pubkeys:` first. It also had an `is_pending_validator` check that kept the deposit queued before any builder was created.
2. **Grandine**: `G:helper_functions/src/fork.rs#L984-L1059`.
   - L992-L997: `// Deposits for existing validators are never onboarded and never have their signature checked.` … `let is_for_validator = deposits.iter().map(|deposit| state.validators.pubkeys().contains(&deposit.pubkey))`
   - L1056-L1059: `if is_for_validator[index] { pending_deposits.push(*deposit); continue; }`
   - L1029-L1033 keep a `pending_validator_pubkeys` set "that the spec's `is_pending_validator` would find". That is the beta.4 ordering.
   - Called from `upgrade_to_gloas` at `G:helper_functions/src/fork.rs#L942-L943`.
3. **When the results differ**: at the Fulu→Gloas upgrade, `state.pending_deposits` holds a validly signed deposit with builder (0x03) withdrawal credentials whose pubkey either:
   - (a) already belongs to a validator, or
   - (b) belongs to a validly signed pending deposit for a new validator that sits earlier in the queue.

   Master turns it into a builder (new builder registry entry, deposit removed from the queue). Grandine leaves it in the queue, where it is later applied as a validator top-up or validator creation. The post-upgrade state root differs.
4. **Nimbus**: `N:beacon_chain/spec/beaconstate.nim#L2308-L2385` cites `v1.7.0-alpha.12` and has `# Deposits for existing validators stay in the pending queue` / `if findValidatorIndex(state.validators…, deposit.pubkey).isSome: pending_deposits.add(deposit); continue`. It agrees with **Grandine**, i.e. it also lags master.
5. **Confidence: high** that the code follows beta.4 and not master.
   - Disproof attempted: I checked whether master's change is also present elsewhere in Grandine (none found in `fork.rs`). I also checked whether a later loop branch re-onboards validator pubkeys; it does not, because `continue` happens before the builder path.
   - Whether this becomes a live split depends on which spec release the Glamsterdam devnets and mainnet adopt. Today it is a lag against master, not against the tagged release.
6. **Severity**: consensus split (state root at the fork block) if clients disagree on the version. Otherwise cosmetic (version lag).

### grandine-2. `is_head_weak` counts slashed equivocators as 0, while the spec adds their `effective_balance`

1. **Spec**: `S:fork-choice.md#L810-L833`.
   ```python
   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))
   ```
   There is no `slashed` or active filter on the added equivocator weight.
2. **Grandine**:
   - `G:fork_choice_store/src/store.rs#L1501-L1524` adds `self.justified_active_balances.get(index).copied().unwrap_or_default()` for each equivocating committee member.
   - `justified_active_balances` is built by `active_balances` at `G:fork_choice_store/src/store.rs#L6328-L6344`: `if predicates::is_active_validator(validator, epoch) && !validator.slashed { effective_balance } else { 0 }`.
   - `is_head_weak` feeds `should_apply_proposer_boost` (`#L1630-L1667`), which gates proposer boost in `update_head_segment_id` (`#L6210-L6216`).
3. **When the results differ**:
   - Setup: an attester slashing has put validator V into `equivocating_indices`, V is already marked `slashed` in the justified checkpoint state, and V is in a committee of the boosted block's parent's slot.
   - Effect: Grandine's head weight for that parent is lower than the spec's by V's effective balance.
   - If the parent's weight lands between the two values relative to `REORG_HEAD_WEIGHT_THRESHOLD`, Grandine treats the parent as weak and the spec does not. If additionally an early (PTC-timely) equivocating sibling of the parent exists, Grandine withholds proposer boost and the spec applies it, so the heads can differ.
   - The same happens for a committee member that is not yet active in the justified state: the spec still uses `effective_balance`, Grandine uses 0.
4. **Nimbus**: `N:beacon_chain/fork_choice/fork_choice_epbs.nim#L131-L158` adds `balances[val].effective_balance` from `self.checkpoints.justified.balances` for validators whose vote slot is `FAR_FUTURE_SLOT`. I did not verify whether Nimbus's justified `balances` zeroes slashed validators, so this is **undetermined**.
5. **Confidence: medium.**
   - The code difference is clear.
   - Disproof attempted: I checked whether Grandine adds the equivocator weight from another source. It does not; the only source is `justified_active_balances`.
   - The trigger needs a slashing included on chain before the current justified checkpoint while the slashed validator is still in committees. Slashed validators stay in committees until their exit epoch, so this is reachable.
   - The impact needs a threshold edge plus an early proposer equivocation, which is narrow.
6. **Severity**: fork-choice divergence (proposer-boost application). Narrow trigger.

### grandine-3. `payload_attestation_message` gossip: no "first valid message from this validator" IGNORE, and later messages overwrite PTC votes

1. **Spec**: `S:p2p-interface.md#L908-L911`.
   ```python
   # [IGNORE] This is the first valid payload attestation from this validator index
   payload_attestation_key = (data.slot, validator_index)
   if payload_attestation_key in seen.payload_attestation_validators:
       raise GossipIgnore("already seen payload attestation from this validator")
   ```
   Plus `#L947-L948` (`seen.payload_attestation_validators.add(...)`).
2. **Grandine**:
   - `G:fork_choice_store/src/store.rs#L4364-L4376` has the check commented out: `// TODO: need rough consensus from implementors whether override vote is allowed, or preventing it as the commented code.` … `// [IGNORE] The payload_attestation_message is the first valid message received from the validator …` / `// if let Some(votes) = self.payload_vote.get(&block_root) … { return Ok(PayloadAttestationAction::Ignore(...)); }`
   - It then returns `Accept` (`#L4378-L4381`).
   - `apply_payload_attestation_batch` (`#L4787-L4836`) does `timeliness_votes.set(pos, data.payload_present)` and `availability_votes.set(pos, data.blob_data_available)`, so the newest message wins.
   - A repo-wide search for a seen-set of payload attesters (`seen_payload_attest|observe.*payload_attest|PayloadAttestationValidators`) found nothing.
3. **When the results differ**:
   - Setup: a PTC member publishes two different signed `PayloadAttestationMessage`s for the same slot and block, for example first `payload_present=false` and then `true`. Gossipsub message-id dedup does not catch them because the contents differ.
   - Spec node: IGNOREs the second message, so it is not forwarded and its PTC vote stays at the first value.
   - Grandine: ACCEPTs and forwards the second message, and overwrites the vote.
   - Consequences: `payload_timeliness` and `payload_data_availability` (`#L1188-L1243`) feed `should_extend_payload`, the previous-slot EMPTY/FULL tiebreak and `should_build_on_full`. So the head's payload status and the proposer's choice to build on FULL or EMPTY can differ between Grandine and spec-following nodes.
   - A coordinated PTC minority flipping votes near the threshold of 256 can split views.
4. **Nimbus**: `N:beacon_chain/gossip_processing/gossip_validation.nim#L2005-L2035` returns `errIgnore("PayloadAttestationMessage: duplicate message from validator")` at about L2024. It agrees with the **spec**.
5. **Confidence: high** for the gossip-verdict difference, since it is an explicit TODO with the code commented out. **Medium** for an actual head divergence, which needs equivocating PTC members.
   - Disproof attempted: I looked for dedup in the p2p crate and the fork_choice_control tasks and found none. Gossipsub only dedups byte-identical messages.
6. **Severity**:
   - Gossip verdict: ACCEPT vs IGNORE.
   - Fork-choice divergence in the payload-status choice (FULL/EMPTY) for the previous-slot block.

### grandine-4. Block-included attestations with `index == 1` for a payload that is not verified are delayed and applied later; the spec rejects them for fork choice

1. **Spec**: `S:fork-choice.md#L942-L944`.
   ```python
   # If attesting for a full node, the payload must be known
   if attestation.data.index == 1:
       assert is_payload_verified(store, attestation.data.beacon_block_root)
   ```
   This runs in `validate_on_attestation(..., is_from_block=True)`. A failed assert means `on_attestation` makes no store update. For gossip only, `S:p2p-interface.md#L448-L453` allows "MAY queue".
2. **Grandine**: `G:fork_choice_store/src/store.rs#L3206-L3218`.
   ```rust
   if index == 1 && !self.is_payload_verified(beacon_block_root) {
       if is_from_block {
           // TODO(gloas): the block should be delayed in this place
           // bail!(Error::AttestationDataInvalidPayloadStatus { ... });
       }
       return Ok(PartialAttestationAction::DelayUntilPayload(beacon_block_root));
   }
   ```
3. **When the results differ**:
   - Setup: an imported block carries an attestation with `index = 1` for block R, and R's envelope has not passed `on_execution_payload_envelope` locally. For example, the envelope arrives late or is missing.
   - Spec: that attestation never updates `latest_messages`.
   - Grandine: queues it and applies it once the envelope arrives. The vote then counts toward R's FULL node, and possibly replaces a later spec-kept latest message, giving different FULL/EMPTY weights.
4. **Nimbus**: `N:beacon_chain/fork_choice/fork_choice.nim#L498-L510` per the subagent report. Block attestations skip the `validate_on_attestation` index checks entirely. That matches neither the spec nor Grandine.
5. **Confidence: medium.** The TODO shows the authors know.
   - Disproof attempted: I checked whether block import itself is delayed until the payload. It is only when the block builds on a FULL parent (`#L2188-L2196`), not for attestations to other blocks.
   - Divergence only arises while the payload is missing locally, so it is often transient.
6. **Severity**: fork-choice divergence (weights). Usually transient.

### grandine-5. Block payload attestations reach the fork-choice store after the block's head computation (asynchronous task)

1. **Spec**: `S:fork-choice.md#L1086-L1098`.
   - Order: `store.blocks[block_root] = block` → `payload_timeliness_vote[...] = [None]*PTC_SIZE` → `notify_ptc_messages(store, state, block.body.payload_attestations)` → `record_block_timeliness` → `update_proposer_boost_root`.
   - The votes are therefore in the store atomically with the block, before any subsequent `get_head`.
2. **Grandine**:
   - `G:fork_choice_store/src/store.rs#L4548-L4685` (`apply_block`) records timeliness, sets boost, inserts the block and calls `update_head_segment_id()` with no PTC votes from the block.
   - The block's `payload_attestations` are validated in a separately spawned `BlockPayloadAttestationsTask` (`G:fork_choice_control/src/mutator.rs#L3630`, `#L5261-L5284`; `G:fork_choice_control/src/tasks.rs#L430-L463`). They are applied later through `MutatorMessage::BlockPayloadAttestations` → `apply_payload_attestation_batch`.
3. **When the results differ**: between import of block B, whose PTC aggregate shows the parent's payload timely or late, and completion of the task:
   - Grandine's head and payload-status view (`should_extend_payload`, `should_build_on_full`, the EMPTY/FULL tiebreak for B's parent) is computed without those votes.
   - A proposer or attester acting in that window (for example, a block at the slot start) can choose differently than the spec would.
   - After the task completes, the views converge.
4. **Nimbus**: per the subagent report, `N:beacon_chain/fork_choice/fork_choice.nim#L519-L529` records the block's PTC tallies before `record_block_timeliness` (L535) and before proto-array insertion (L560-L571). It agrees with the **spec**. The brief notes that Lighthouse also records them early.
5. **Confidence: low to medium.** The ordering difference is certain; the observable impact needs timing. Disproof attempted: I checked whether `apply_block` itself reads `body.payload_attestations`. It does not.
6. **Severity**: transient fork-choice divergence.

### grandine-6. Envelopes are not replayed into fork choice after restart — **known**

1. **Spec**: `is_payload_verified` is `root in store.payloads`, and `on_block` asserts it for FULL parents (`S:fork-choice.md#L314-L320`, `#L1060-L1061`). The spec does not model restarts.
2. **Grandine**:
   - `G:fork_choice_control/src/mutator.rs#L448-L500` (`process_unfinalized_blocks`) replays persisted blocks only. No stored envelope is fed to `apply_execution_payload_envelope` (`G:fork_choice_store/src/store.rs#L4887-L4922`).
   - Blocks on FULL parents hit `DelayUntilPayload` (`mutator.rs#L1088-L1116`) and request the envelope from the network (`P2pMessage::PayloadEnvelopeNeeded`), even though it is on disk (`storage.rs#L689-L770`).
3. **When the results differ**: after a restart, until envelopes are refetched, unfinalized FULL-parent blocks are not imported and FULL nodes are missing. The head can be an EMPTY variant or an older block.
4. **Nimbus**: not checked.
5. **Confidence: high** (matches the open issue).
6. **Severity**: liveness, plus a fork-choice divergence after restart.

**Known**: https://github.com/grandinetech/grandine/issues/886 ("Load payload envelopes from storage during restart", open). Fix PR https://github.com/grandinetech/grandine/pull/887 ("Replay persisted execution payload envelopes at restart", open, not merged).

### grandine-7. Payload/attestation deadlines at the fork boundary and the fixed 16-tick split — **known** (not re-derived)

`G:fork_choice_store/src/store.rs#L6413-L6423` (`is_before_due_bps_deadline`) and `G:clock/src/lib.rs#L358-L364` (`ticks_per_slot`: 16 ticks post-Gloas, 12 pre-Gloas). With mainnet 2500 and 7500 BPS and 16 ticks per slot, `(tick+1)*10000 <= due_bps*16` equals the spec's `time_into_slot_ms < threshold` at tick resolution (checked arithmetically).

Known:
- https://github.com/grandinetech/grandine/pull/989 ("Fix clock deadlines at the Gloas fork boundary", open)
- https://github.com/grandinetech/grandine/issues/912 ("Validator tick schedule is a fixed 16-way slot split, *_DUE_BPS config values are not used", open). With non-default BPS configs, the deadlines would not track the config.

---

## Cross-client comparison

Cells summarize supplied reviews only. Nimbus means stable. “Differs” includes uncertain candidates; “not checked” includes unclassifiable evidence. Prysm/Teku are comparison-only; no Lighthouse review was supplied. Checked-rule sources follow the table.

| Rule / source | Nimbus | Lodestar | Grandine | Prysm | Teku |
|---|---|---|---|---|---|
| Fork-upgrade credential-first onboarding | differs (see nimbus-7) | differs (see lodestar-1) | differs (see grandine-1) | not checked | not checked |
| Block PTC duplicate-seat expansion | differs (see nimbus-1) | differs (see lodestar-2) | agrees with spec | agrees with spec | agrees with spec |
| PTC membership from attested branch (Nimbus stable) | differs (see nimbus-2) | agrees with spec | agrees with spec | agrees with spec | not checked |
| Bid builder checks from parent state advanced to bid slot | differs (see nimbus-3) | not checked | not checked | agrees with spec | not checked |
| Bid IGNORE/REJECT ordering | differs (see nimbus-4) | not checked | not checked | not checked | not checked |
| Weak-head base score: no boost, include FULL | differs (see nimbus-5) | not checked | not checked | not checked | not checked |
| Weak-head equivocator raw justified balance | not checked | differs (see lodestar-5) | differs (see grandine-2) | not checked | agrees with spec |
| Index-1 gossip: verified, nonoptimistic, noninvalid payload | differs (see nimbus-6) | differs (see lodestar-3) | agrees with spec | not checked | agrees with spec |
| PTC gossip first valid message only | agrees with spec | agrees with spec | differs (see grandine-3) | not checked | not checked |
| Block index-1 attestation requires verified payload | not checked | not checked | differs (see grandine-4) | not checked | not checked |
| Block PTC votes applied before head computation | agrees with spec | agrees with spec | differs (see grandine-5) | not checked | not checked |
| Restart persisted-envelope replay | not checked | not checked | differs (see grandine-6) | not checked | not checked |
| Fork-boundary clock and configurable BPS deadlines | not checked | agrees with spec | differs (see grandine-7) | not checked | not checked |
| ByRange zero count | not checked | differs (see lodestar-4) | agrees with spec | not checked | agrees with spec |
| PTC timely / DA strict threshold and absent-vote exclusion | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| should_build_on_full | agrees with spec | not checked | agrees with spec | not checked | not checked |
| should_extend_payload incl. boost clauses | agrees with spec | not checked | agrees with spec | not checked | not checked |
| Previous-slot EMPTY/FULL zero weight | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| Payload status tiebreak, previous and older slots | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| Boost only to proper ancestors / parent payload variant | agrees with spec | not checked | agrees with spec | not checked | not checked |
| Boost equivocation scan uses PTC deadline | agrees with spec | not checked | agrees with spec | not checked | not checked |
| Weak-head equivocator set included (balance source separately above) | agrees with spec | agrees with spec | agrees with spec | not checked | agrees with spec |
| Fork-choice indices 0/1, same-slot 0 | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| PTC message block known, matching/current slot, all seats | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| Unknown proposer preferences IGNORE | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| Bid fee recipient / randao / lookahead / parent-slot verdicts | agrees with spec | not checked | not checked | not checked | not checked |
| Bid compatibility with head | agrees with spec | not checked | agrees with spec | not checked | not checked |
| PTC gossip current-slot/dedup/unknown-root/block-slot order | agrees with spec | agrees with spec | differs (see grandine-3) | not checked | not checked |
| Envelope import adds FULL | agrees with spec | agrees with spec | not checked | not checked | not checked |
| Block parent/future/finalized/ancestry validation | not checked | agrees with spec | not checked | not checked | not checked |
| Envelope state/bid linkage, header checks, DA before insertion | not checked | agrees with spec | not checked | not checked | not checked |
| Proposer equivocation reorg branch | not checked | agrees with spec | not checked | not checked | not checked |
| Gossip same-slot index 1 and index >1 REJECT | agrees with spec | agrees with spec | agrees with spec | not checked | not checked |
| Envelope gossip verdicts and bounds | not checked | agrees with spec | agrees with spec | not checked | not checked |
| ByRange canonical FULL only | not checked | agrees with spec | agrees with spec | not checked | not checked |
| ByRoot missing skipped | not checked | agrees with spec | agrees with spec | not checked | not checked |
| Block timeliness two thresholds and current slot | not checked | not checked | agrees with spec | not checked | not checked |
| Boost-root same-dependent-root check | not checked | not checked | agrees with spec | not checked | not checked |
| FULL parent verified before import | not checked | not checked | agrees with spec | not checked | not checked |
| Latest-message payload_present = index 1 | not checked | not checked | not checked | not checked | not checked |
| Bid parent hash known/delivered | not checked | not checked | agrees with spec | not checked | not checked |
| Bid fee-recipient mismatch IGNORE | not checked | not checked | agrees with spec | not checked | not checked |
| Optional proposer reorg implementation | agrees with spec | not checked | agrees with spec | not checked | not checked |


Matrix qualifications: Nimbus onboarding is supported by grandine-1; nimbus-7 itself left it unverified. Teku follows older onboarding and Prysm partly shares bid ordering, but neither has a numbered candidate. Grandine also alleges Nimbus ByRange serves EMPTY envelopes (`N:beacon_chain/sync/sync_protocol.nim#L431-L485`), rejects zero count, and skips block-attestation checks (`N:beacon_chain/fork_choice/fork_choice.nim#L498-L510`); these subagent observations are not promoted to new candidates. Grandine-6 calls restart replay a known liveness issue; Lodestar found no independent spec violation, since fresh stores have empty payloads ([sanchor][sanchor]). Grandine-7’s default tick arithmetic agreed; its concerns are boundary/custom settings.

Checked-rule evidence from the supplied tables (all at pins above):
- Nimbus: `N@beacon_chain/fork_choice/fork_choice_epbs.nim#L47-L128,L147-L199,L217-L282`; `N@beacon_chain/fork_choice/proto_array.nim#L270-L293,L590-L614`; `N@beacon_chain/gossip_processing/gossip_validation.nim#L995-L1004,L1864-L1874,L1922-L1946,L2016-L2036`.
- Lodestar: `https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/` + `packages/fork-choice/src/protoArray/protoArray.ts#L869-L912,L1290-L1323,L1597-L1613`; `packages/fork-choice/src/forkChoice/forkChoice.ts#L519-L552,L765-L825,L2142-L2189`; `packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts#L25-L110`; `packages/beacon-node/src/chain/blocks/verifyPayloadsDataAvailability.ts#L21-L51`; `packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts#L35-L154`; `packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts#L39-L136`; `packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts#L70-L97`; `packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts#L27-L89`; `packages/config/src/forkConfig/index.ts#L255-L270`.
- Grandine: `G:fork_choice_store/src/store.rs#L1007-L1243,L1630-L1757,L2188-L2196,L2465-L2504,L3188-L3226,L4004-L4381,L4561-L4598,L5996-L6165`; `G:fork_choice_control/src/queries.rs#L735-L784,L1814-L1846`; `G:p2p/src/network.rs#L1631-L1641`. Optional proposer reorgs: `S:validator.md#L195`; both Nimbus and Grandine reported no implementation. Grandine explicitly did not audit latest-message assignment.

## Known-check summary and limits (copied from items)

### Nimbus

## 4. Known-check (GitHub search API, `repo:status-im/nimbus-eth2`, open and closed issues and PRs)

| Candidate | Status | Queries |
|---|---|---|
| C1 | not found | "payload attestation", "ptc", "duplicate ptc", "aggregation_bits payload", "is_from_block", "7732", "gloas", "epbs" |
| C2 | **known, fixed on unstable only**: https://github.com/status-im/nimbus-eth2/pull/9341 (closed, merged 2026-10-24 into `unstable`) | "ptc" |
| C3 | not found | "builder active bid", "headState bid", "gloas" |
| C4 | not found | "gloas", "epbs"; related ordering PRs 9119 (envelope seen rule) and 8860 (gossip rules for alpha.13), both closed, not this issue |
| C5 | not found | "is_head_weak" (only PR 8559, closed, the original handlers) |
| C6 | not found | "orphan envelope attestation" (0 hits), "gloas" |
| C7 | n/a | — |

Already-indexed items seen: 9344 (open PR, envelope replay on restart), 9203 (open issue, EL-unvalidated head), 9011 (open issue, PTC waits for EL), 9008 (closed), 8631 (not returned by these queries; listed as closed in the brief).


### Lodestar

## Known-check, public results, and validation limits

GitHub API searches had no open/closed restriction. All query terms above are prefixed `repo:ChainSafe/lodestar`; API form: `https://api.github.com/search/issues?q=<query>&per_page=100`. Broad searches returned 769 `gloas`, 105 `epbs`, 68 `7732` hits; only their first 100 results were retrieved. Specific rule queries supplemented them. **“Not found” is bounded by those results, not proof of novelty.**

| Pre-indexed item | State / disposition |
|---|---|
| [10484](https://github.com/ChainSafe/lodestar/pull/10484) | Open; envelope API execution_optimistic derives from parent/PENDING rather than own FULL. Known API issue, distinct from C3. |
| [10481](https://github.com/ChainSafe/lodestar/issues/10481) | Open; serial DA waits delay other imports. Queuing unavailable payloads is spec-permitted; no extra validity mismatch established here. Fix 10485 appeared open in search. |
| [10468](https://github.com/ChainSafe/lodestar/pull/10468) | Merged/closed, 2026-10-26; known fork-epoch PTC duties-root fix. |
| [10005](https://github.com/ChainSafe/lodestar/pull/10005) | Merged/closed, 2026-09-04; known orphan-envelope historical-sync fix. Remaining [10008](https://github.com/ChainSafe/lodestar/issues/10008) is open. Its devnet-9 observations are author-reported, not reproduced here or evidence testing C2–C5. |

[Hive](https://hive.ethpandaops.io/) yielded no readable result rows through the web tool; direct retrieval returned **403 Forbidden**. [Alternate host](https://hive-results.ethdevops.io/) was unavailable. Searches `site:hive.ethpandaops.io Glamsterdam Lodestar`, `site:github.com/ethereum/execution-spec-tests Glamsterdam Lodestar`, and API `repo:ChainSafe/lodestar gloas hive` (zero hits) supplied no candidate-specific verdict at these pins. The [EEST changelog](https://github.com/ethereum/execution-spec-tests/blob/main/docs/CHANGELOG.md) and [execution-specs releases](https://github.com/ethereum/execution-specs/releases) describe EL fixtures, not Lodestar CL fork-choice/gossip verdicts. No candidate is claimed new, reproduced or disproved from absent Hive/EEST results.

**Local checks:** re-read/compared pinned raw Lodestar sources; compared requested specs to beta.4; traced import callers, DA/signature/EL gates, PTC bit expansion and threshold counterexample; checked SSZ bounds to discard a false lead; verified report anchors and byte limit. No executable client/spec tests were run.


### Grandine

## 4. Known-check (grandinetech/grandine issues and PRs, open and closed)

The GitHub search API (`api.github.com/search/issues?q=repo:grandinetech/grandine …`) was used. The rate limit was hit after 11 queries. Queries `reorg`, `is:pr payload attestation seen`, `builder onboarding`, `v1.7.0-beta.4` and `attestations in block payload` were **not run**.

| Query | Hits (relevant) |
|---|---|
| `7732` | #282 Implement EIP-7732 (open) |
| `gloas` (118) | #989 Fix clock deadlines at the Gloas fork boundary (open PR); #912 fixed 16-way tick split (open); #886 Load payload envelopes from storage during restart (open) |
| `epbs` | #502 Add Forkchoice changes for ePBS (closed PR); #579 ProposerPreferences gossip (open PR); #795 circuit-breaker (closed) |
| `is_head_weak`, `equivocating`, `slashed equivocating`, `payload_attestation duplicate` | 0 hits |
| `payload attestation` (42) | #673 "Delay attestations with payload_present=1 until envelope received" (open PR, related to C4 but about the gossip path); no duplicate/first-message issue |
| `proposer preferences` | #972, #859 (closed PRs); nothing on the IGNORE verdict |
| `envelope restart` | #886 (open), #887 (open PR) |
| `proposer boost` | #840 proposer boost dependent_root (closed); #561 (closed) |

Per candidate:
- **C1**: not found. Queries: `7732`, `gloas`, `epbs`; `builder onboarding` was not run because of the rate limit.
- **C2**: not found (`is_head_weak`, `equivocating`, `slashed equivocating`).
- **C3**: not found (`payload attestation`, `payload_attestation duplicate`). The in-code TODO shows the team is aware.
- **C4**: partially known. The in-code TODO at `store.rs#L3208` and PR #673 (open) address delaying gossip attestations. No issue covers the from-block discard rule.
- **C5**: not found (`payload attestation`, `gloas`).
- **C6**: **known**, #886 (open issue) and #887 (open PR).
- **C7**: **known**, #989 (open PR) and #912 (open issue).

Previously indexed and not re-opened here: 989 (open), 904, 840, 832, 742 (closed), 864 (merged). Of these, only #840 and #989 showed up in my searches. #904, #832, #742 and #864 were not re-read.

---


Nimbus did not check Hive or execution-spec-tests results. Grandine did not fetch those dashboards, calling them out of scope. Lodestar attempted public-result checks but obtained no candidate-specific verdict; its access failures and search bounds are preserved above. Thus none of these inputs establishes novelty or reproduction from public test results. No missing result is treated as a pass.

[target]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/spec-tests-version.json#L1-L12
[sfork]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork.md#L60-L119
[lonboard]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/state-transition/src/slot/upgradeStateToGloas.ts#L208-L245
[tonboard]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forktransition/GloasStateUpgrade.java#L222-L260
[snotify]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#L268-L288
[spositions]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#L1156-L1187
[sindexed]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/beacon-chain.md#L1091-L1115
[sbits]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/beacon-chain.md#L1407-L1428
[lblockptc]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/chain/blocks/importBlock.ts#L280-L298
[lnotify]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/fork-choice/src/protoArray/protoArray.ts#L758-L789
[tptc]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java#L1268-L1307
[lprocessptc]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/state-transition/src/block/processPayloadAttestation.ts#L7-L26
[spayloadstatus]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/p2p-interface.md#L426-L463
[saggregatecall]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/p2p-interface.md#L789-L808
[ssinglecall]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/p2p-interface.md#L1265-L1281
[lattstatus]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/chain/validation/attestation.ts#L294-L335
[laggstatus]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/chain/validation/aggregateAndProof.ts#L76-L113
[lhaspayload]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/fork-choice/src/protoArray/protoArray.ts#L2071-L2079
[tgossipstatus]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java#L244-L298
[lpreprocess]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/network/processor/index.ts#L568-L586
[loptimisticimport]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts#L250-L277
[limportfc]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts#L323-L332
[srangebase]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/phase0/p2p-interface.md#L1553-L1570
[srange]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/p2p-interface.md#L1396-L1431
[lrangezero]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts#L111-L134
[trangevalid]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/ExecutionPayloadEnvelopesByRangeMessageHandler.java#L82-L174
[sweak]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#L810-L832
[lweak]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/fork-choice/src/forkChoice/forkChoice.ts#L1717-L1754
[tweak]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java#L463-L488
[tweakuse]: https://github.com/Consensys/teku/blob/2c35574b3373cbe8ad121a768d98f066aec05b73/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java#L506-L536
[sanchor]: https://github.com/ethereum/consensus-specs/blob/8a80ffa82bd49aac0dd6db17f9e8150aa257fbc2/specs/gloas/fork-choice.md#L228-L255
[lbalcache]: https://github.com/ChainSafe/lodestar/blob/e4b4f615e68c1a4ea7c57632e6ed8d824624573f/packages/state-transition/src/util/balance.ts#L51-L82
