# VERDICT: CONFIRMED

At Grandine `develop` commit `52cd677117b5d93bf477fe4843148070b0303444`, a second *distinct*, otherwise-valid `payload_attestation_message` from the same PTC validator in the same slot is accepted, propagated, and replaces that validator's stored timeliness/data-availability bits. This conflicts with the pinned Gloas gossip rule: “This is the first valid payload attestation from this validator index”; the rule keys `seen.payload_attestation_validators` by `(data.slot, validator_index)`, ignores an existing key, and adds the key after validation ([p2p-interface.md#L908-L911, L947-L948 at `8a80ffa8`](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/p2p-interface.md#L908-L948)). The relevant Gloas feature is EIP-7732. This is not live-mainnet code.

## Corrected candidate record

- **Client / EIP:** Grandine / EIP-7732 (Gloas payload-attestation gossip).
- **Spec:** The quote above is the required first-valid rule. The fork-choice handler deliberately assigns the vote fields (`payload_timeliness_vote[ptc_index] = data.payload_present` and data availability analogously), so overwriting is normal *after* the gossip admission gate; it is not a replacement for that gate ([fork-choice.md#L1136-L1188](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md#L1136-L1188)). The threshold is strict `> 256`, with mainnet PTC size 512 ([fork-choice.md#L97-L102](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md#L97-L102), [mainnet preset](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/presets/mainnet/gloas.yaml#L1-L11)).
- **Pinned Grandine evidence:** `fork_choice_store/src/store.rs#L4259-L4381` validates the message but leaves the first-valid check commented out at `#L4364-L4376` and returns `Accept` at `#L4378-L4381` ([raw source](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_store/src/store.rs#L4259-L4381)). The mutator maps that result to `P2pMessage::Accept` for a gossip ID and returns it for application ([`mutator.rs#L2719-L2775`](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/mutator.rs#L2719-L2775)). `apply_payload_attestation_batch` then sets the PTC presence bit and unconditionally assigns `payload_present` and `blob_data_available` at `#L4802-L4830` ([raw source](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_store/src/store.rs#L4787-L4836)).
- **Trigger:** During one Gloas slot, a PTC member signs two valid messages with the same `(slot, validator_index, beacon_block_root)` but conflicting data, for example first `payload_present=false`, then `payload_present=true` (and an otherwise valid signature for each). The referenced block must be known, valid, at that slot, and the validator must be in the PTC. Grandine accepts the first; it also accepts and forwards the second, then changes that PTC position to true. With a locally verified payload and 256 other true votes, this flips Grandine from 256 (not timely) to 257 (timely), while a conforming receiver that saw false first retains false. That can alter the previous-slot FULL/EMPTY fork-choice decision near the threshold.
- **Other implementation:** Nimbus at `0abc0dd0fc0750a8875c49089d074f7d7197b62c` checks `payloadAttestationPool.isSeen` and returns `errIgnore("... duplicate message from validator")` ([gossip validation #L2005-L2024](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/gossip_processing/gossip_validation.nim#L2005-L2024)). Its pool implements `isSeen` as membership of `validator_index` in the slot's `seen` set ([pool #L34-L63](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/consensus_object_pools/payload_attestation_pool.nim#L34-L63)). Thus Nimbus and other implementations following the pinned gossip rule retain the first valid message; Grandine retains the last one it processes. The spec handler itself overwrites its per-position vote, but only a message which passed its `Seen` gossip gate reaches that point.
- **Confidence / severity:** **High / fork-choice divergence.** This does not by itself make a beacon block invalid or accept an invalid state transition. It is a local fork-choice and gossip-propagation divergence requiring a malicious or faulty PTC validator plus threshold-adjacent votes.

## Refutation attempts

**Strongest attempt — transport-level deduplication.** This does not refute the candidate. Grandine's P2P handler hands every decoded `PayloadAttestationMessage` to `on_gossip_payload_attestation` ([`p2p/src/network.rs#L2750-L2763`](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/p2p/src/network.rs#L2750-L2763)); the controller creates a normal unverified gossip item ([`controller.rs#L798-L812`](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/controller.rs#L798-L812)), and its task only calls the store validator ([`tasks.rs#L729-L758`](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/tasks.rs#L729-L758)). I found no logical `(slot, validator)` set in this path. The pinned `eth2_libp2p` submodule is `35d69804fac416956998ef3e783b2338bcee75e7`; its gossipsub message ID is SHA-256 of a prefix plus the message data (and topic in Gloas), then truncated ([`config.rs#L445-L495`](https://raw.githubusercontent.com/grandinetech/eth2_libp2p/35d69804fac416956998ef3e783b2338bcee75e7/src/config.rs#L445-L495)). Conflicting signed messages have different payload bytes, hence different gossipsub IDs; byte-level duplicate caching cannot supply the spec's logical deduplication.

**Could the condition be impossible on a valid chain?** No invariant found prevents a PTC member from signing two individually valid data objects. The pinned state-transition checks each included aggregate's parent root, previous slot, and signature ([beacon-chain.md#L2483-L2499](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md#L2483-L2499)); it does not make a wire equivocation impossible. The still-open consensus-specs issue [#5333, “payload equivocations and PTC timeliness”](https://github.com/ethereum/consensus-specs/issues/5333) specifically says payload equivocation is not penalized and relies on the gossip rule to keep it from propagating. This supports, rather than refutes, the importance of the missing gate.

**Was the spec misread?** No. The apparent counterargument is real but inapplicable: `on_payload_attestation_message` overwrites vote values, as quoted above. The networking specification separately and explicitly requires that only the first *valid gossip message* for `(slot, validator_index)` be accepted. Grandine's own TODO says it needs consensus on whether an override vote is allowed, immediately above commented code labelled with that IGNORE rule; the pinned spec resolves the question for gossip in favour of first-valid.

**Later fix / merged PR?** None found at the stated pin. `develop` resolves exactly to `52cd677117b5d93bf477fe4843148070b0303444` ([branch API](https://api.github.com/repos/grandinetech/grandine/branches/develop)); the compare to that pin is identical ([compare API](https://api.github.com/repos/grandinetech/grandine/compare/52cd677117b5d93bf477fe4843148070b0303444...develop)). The requested file-history query ([commits API](https://api.github.com/repos/grandinetech/grandine/commits?path=fork_choice_store%2Fsrc%2Fstore.rs&sha=develop&per_page=100)) did not identify a later logical-dedup fix. Grandine [PR #705](https://github.com/grandinetech/grandine/pull/705) is **closed, not merged**; its description says “skip first valid payload attestation message check” and its published patch comments out this exact guard. It confirms the choice was noticed, not that it was fixed.

## Known-check and test-result check

Grandine issues/PR searches for `payload attestation`, `PTC`, `override`, `equivocation`, and the exact first-valid phrase found no merged or open Grandine fix for this behavior. Relevant records are: Grandine **#705 closed/unmerged** (explicitly skips the check); consensus-specs **#5333 open** (related PTC/payload equivocation discussion); and consensus-specs **#5209 closed/unmerged** (proposed rejection of conflicting aggregate votes in blocks, a different layer). The requester-named Grandine **#918 open** concerns unbounded progressive containers, and **#989 open PR** concerns Gloas fork-boundary clock deadlines, so neither is this candidate ([#918](https://github.com/grandinetech/grandine/issues/918), [#989](https://github.com/grandinetech/grandine/pull/989)).

I also checked the publicly visible Grandine test registration: it includes the consensus-spec-tests `on_payload_attestation_message` fork-choice vectors ([`spec_tests.rs#L194-L212`](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/spec_tests.rs#L194-L212)), but that is not evidence that this logical duplicate-gossip case is covered. I found no public Hive/Glamsterdam-devnet result documenting this exact first-valid override in a GitHub search for `payload attestation`; absence of a located result is not proof that no run exists. No network, node, or devnet interaction was performed.

## URLs read

- Pinned Gloas networking and fork-choice specifications: [p2p-interface](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/p2p-interface.md), [fork-choice](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md), [beacon-chain](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md), [mainnet preset](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/presets/mainnet/gloas.yaml).
- Pinned Grandine source: [store](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_store/src/store.rs), [controller](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/controller.rs), [tasks](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/fork_choice_control/src/tasks.rs), [P2P handler](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/p2p/src/network.rs), [submodule mapping](https://raw.githubusercontent.com/grandinetech/grandine/52cd677117b5d93bf477fe4843148070b0303444/.gitmodules), and [pinned message-ID function](https://raw.githubusercontent.com/grandinetech/eth2_libp2p/35d69804fac416956998ef3e783b2338bcee75e7/src/config.rs).
- Comparator and public-record checks: [Nimbus validator](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/gossip_processing/gossip_validation.nim), [Nimbus pool](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/consensus_object_pools/payload_attestation_pool.nim), [Grandine #705 API record](https://api.github.com/repos/grandinetech/grandine/pulls/705), [consensus-specs #5333 API record](https://api.github.com/repos/ethereum/consensus-specs/issues/5333), [consensus-specs #5209 API record](https://api.github.com/repos/ethereum/consensus-specs/pulls/5209), and [public Hive search](https://api.github.com/search/issues?q=repo%3Aethereum%2Fhive+%22payload+attestation%22).
