# PTC duplicate-seat block import review

## VERDICT CONFIRMED

At the pinned sources, block-import vote recording is a real fork-choice divergence. The Gloas spec’s `notify_ptc_messages` first derives `attesting_indices` from set aggregation bits, then dispatches once per validator; `on_payload_attestation_message` explicitly finds **all** PTC positions matching that validator and writes each vote. Nimbus instead writes only each set bit’s position in the block bitfield. Lodestar likewise extracts only set positions and passes those seat indices directly into fork choice. If a validator occurs more than once in the slot PTC and the included aggregate sets only one of those positions, those implementations count fewer seats than the spec; near the strict `> 256` PTC threshold this can change payload timeliness/data availability and thus fork choice.

## Strongest attempted refutation

The most promising refutation was that validity or aggregation rules require every duplicate seat bit to be set together. The pinned spec does not impose that constraint: `get_indexed_payload_attestation` maps each set bit independently to its seat’s validator index, sorts that list, and `is_valid_indexed_payload_attestation` checks only nonempty sorted indices and a valid aggregate signature over the listed validator public keys. No rule requires equal bits for equal validator indices. The spec explicitly allows duplicate PTC selections: `compute_ptc` samples without deduplicating the result (and its selection helper is described as balance-weighted sampling); `get_ptc` returns the resulting committee. Consequently, one selected seat for a duplicated validator can yield that validator once in the indexed signer list and a valid signature, while spec fork-choice dispatch then credits every matching seat. This refutation fails.

## Corrected candidate record

- **Spec:** `notify_ptc_messages` at [consensus-specs fork-choice.md, pinned 8a80ffa8, lines 256–267](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md#L256-L267) calls `on_payload_attestation_message` for each `idx` in the indexed attesters. That handler at [lines 1044–1049 and 1071–1073](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md#L1044-L1073) collects every `ptc_index` whose `validator_index` equals the message validator, then writes all those positions.
- **Supporting semantics:** [beacon-chain.md, pinned 8a80ffa8, `compute_ptc` lines 1189–1208](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md#L1189-L1208); [`is_valid_indexed_payload_attestation` lines 1031–1049](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md#L1031-L1049); [`get_indexed_payload_attestation` lines 1318–1336](https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/beacon-chain.md#L1318-L1336). Relevant signature rule: “has a valid aggregate signature”; indexed indices are checked for sorting, with no duplicate-seat-bit constraint.
- **Nimbus:** [fork_choice.nim, pinned 0abc0dd0fc0750a8875c49089d074f7d7197b62c, lines 468–478](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/fork_choice/fork_choice.nim#L468-L478) loops aggregation-bit positions and updates a tally only when that bit is set. The separate wire-message route at [fork_choice_epbs.nim, lines 209–222](https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/fork_choice/fork_choice_epbs.nim#L209-L222) does enumerate the PTC and updates every matching position, which does not repair the block-import route for a node that did not receive the wire message.
- **Lodestar:** [importBlock.ts, pinned e4b4f615, lines 261–280](https://raw.githubusercontent.com/ChainSafe/lodestar/e4b4f615/packages/beacon-node/src/chain/blocks/importBlock.ts#L261-L280) extracts only set bit positions and passes them to fork choice. [protoArray.ts, lines 697–728](https://raw.githubusercontent.com/ChainSafe/lodestar/e4b4f615/packages/fork-choice/src/protoArray/protoArray.ts#L697-L728) records precisely those positions; it receives no validator indices or PTC state to expand duplicates.
- **Condition / trigger:** In a valid Gloas slot, a validator occupies at least two PTC positions. A block at the following slot includes a payload attestation whose aggregation bit is set for one such position and clear for another, with a valid aggregate signature over the deduplicated signer list. A block-importing node that missed the individual wire message records fewer YES or NO votes. If this affects a tally across the strict PTC majority threshold, the node may select a different payload status/head.
- **Other client:** Prysm and Teku’s supplied comparison paths expand the validator to every seat. The pinned sources to compare are [Prysm process_block.go at eb681c1d, lines 625–659](https://github.com/prysmaticlabs/prysm/blob/eb681c1d/beacon-chain/blockchain/process_block.go#L625-L659) and [Teku ForkChoice.java at 2c35574b, lines 1268–1307](https://github.com/Consensys/teku/blob/2c35574b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forkchoice/ForkChoice.java#L1268-L1307). These expand every seat for the validator.
- **Confidence:** High. The different position sets are explicit in the pinned code and the spec’s dispatch semantics.
- **Severity:** Fork-choice divergence (consensus split possible when the resulting quorum decision changes).

## Known-check and test evidence

- **Lodestar PR #9369:** merged May 18, 2026. Its public description says it updates duplicate-validator support across the attestation pool, validation logic, and state views. The pinned block import still consumes only bit positions, so this PR does not address the reviewed path. [PR #9369](https://github.com/ChainSafe/lodestar/pull/9369).
- **Issues / other PRs:** Public repository searches for duplicate PTC block-import behavior in Nimbus and Lodestar did not surface a matching issue or a separate fix. This is a search result, not proof that no such report exists. Nimbus’s publicly listed PRs did not show a relevant fix; the source pin itself confirms the behavior under review.
- **Consensus spec tests:** I did not find a public consensus-spec-tests vector specifically covering duplicate PTC positions / partial bits for one duplicated validator. No such vector was identified by the source and repository searches performed for this review. I did not run tests or access a live network/node.

## URLs read

- https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.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/fork_choice/fork_choice.nim
- https://raw.githubusercontent.com/status-im/nimbus-eth2/0abc0dd0fc0750a8875c49089d074f7d7197b62c/beacon_chain/fork_choice/fork_choice_epbs.nim
- https://raw.githubusercontent.com/ChainSafe/lodestar/e4b4f615/packages/beacon-node/src/chain/blocks/importBlock.ts
- https://raw.githubusercontent.com/ChainSafe/lodestar/e4b4f615/packages/fork-choice/src/protoArray/protoArray.ts
- https://github.com/ChainSafe/lodestar/pull/9369
