Problem
The proposer preferences implementation does not correctly handle epoch boundary reorgs that change the proposerLookahead for the next epoch. When the last block(s) of an epoch get reorged, the RANDAO mix changes, producing different proposer assignments for the next epoch. This can cause valid proposer preferences to be silently dropped.
Root Cause
The RANDAO seed for epoch N+1 proposers depends on the RANDAO mix from epoch N-1 (get_seed uses epoch - MIN_SEED_LOOKAHEAD - 1). Reorging any block in epoch N-1 can change this mix → different seed → different proposer assignments for epoch N+1.
The CL spec gossip conditions are sound (they validate against the current head state), but Lodestar's implementation has gaps in how it caches and evicts preferences.
Issues Found
1. Pool Keying Bug (Critical)
ProposerPreferencesPool uses Map<Slot, SignedProposerPreferences> — keyed by slot only. After a reorg where the proposer for slot S changes from validator V to validator W:
- V's stale preferences occupy the slot key
- W's valid preferences arrive, pass gossip validation, but the pool's
add() sees has(proposalSlot) === true and returns AlreadyKnown
- W's preferences are silently dropped
Fix: Key by (Slot, ValidatorIndex) or allow overwriting when the validatorIndex differs. When looking up preferences for bid matching, verify the entry's validatorIndex against the current head's proposerLookahead.
2. Pool/Cache Eviction on Reorg (Important)
Neither the pool nor the seen cache react to fork choice changes. After an epoch boundary reorg:
- Stale preferences from the old proposer remain in the pool until slot-based pruning
- The seen cache remembers the old proposer's message (no functional impact since dedup is per
validatorIndex, but wastes memory)
Fix: On head change, if the dependent root for the next epoch changed:
- Evict pool entries for next-epoch slots where stored
validatorIndex no longer matches state.proposerLookahead
- Optionally clear seen cache entries for affected slots
3. Validator Re-broadcast on Duty Change (Important)
If a validator's proposer duty for the next epoch disappears (or appears) after a reorg, the validator service must detect this and:
- Re-broadcast preferences for newly assigned slots
- Stop broadcasting preferences for slots it no longer proposes
This is critical for the validator client — without it, the correct post-reorg proposer may never have preferences on the network, meaning builders cannot submit valid bids for that slot. Same pattern as attestation duty dependent root tracking.
4. Downstream Bid Invalidation (Important)
Execution payload bids must match the proposer's fee_recipient and gas_limit. After a reorg changes the proposer:
- Bids built against V's preferences are orphaned
- The bid pool should evict bids for slots where the proposer changed
Relevant Files
packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts
packages/beacon-node/src/chain/seenCache/seenProposerPreferences.ts
packages/beacon-node/src/chain/validation/proposerPreferences.ts
packages/validator/src/services/proposerPreferences.ts
Context
- Spec gossip conditions:
specs/gloas/p2p-interface.md (proposer_preferences topic)
proposer_lookahead computed in process_proposer_lookahead during epoch processing
MIN_SEED_LOOKAHEAD = 1 → next-epoch proposers depend on current epoch boundary state
Problem
The proposer preferences implementation does not correctly handle epoch boundary reorgs that change the
proposerLookaheadfor the next epoch. When the last block(s) of an epoch get reorged, the RANDAO mix changes, producing different proposer assignments for the next epoch. This can cause valid proposer preferences to be silently dropped.Root Cause
The RANDAO seed for epoch N+1 proposers depends on the RANDAO mix from epoch N-1 (
get_seedusesepoch - MIN_SEED_LOOKAHEAD - 1). Reorging any block in epoch N-1 can change this mix → different seed → different proposer assignments for epoch N+1.The CL spec gossip conditions are sound (they validate against the current head state), but Lodestar's implementation has gaps in how it caches and evicts preferences.
Issues Found
1. Pool Keying Bug (Critical)
ProposerPreferencesPoolusesMap<Slot, SignedProposerPreferences>— keyed by slot only. After a reorg where the proposer for slot S changes from validator V to validator W:add()seeshas(proposalSlot) === trueand returnsAlreadyKnownFix: Key by
(Slot, ValidatorIndex)or allow overwriting when thevalidatorIndexdiffers. When looking up preferences for bid matching, verify the entry'svalidatorIndexagainst the current head'sproposerLookahead.2. Pool/Cache Eviction on Reorg (Important)
Neither the pool nor the seen cache react to fork choice changes. After an epoch boundary reorg:
validatorIndex, but wastes memory)Fix: On head change, if the dependent root for the next epoch changed:
validatorIndexno longer matchesstate.proposerLookahead3. Validator Re-broadcast on Duty Change (Important)
If a validator's proposer duty for the next epoch disappears (or appears) after a reorg, the validator service must detect this and:
This is critical for the validator client — without it, the correct post-reorg proposer may never have preferences on the network, meaning builders cannot submit valid bids for that slot. Same pattern as attestation duty dependent root tracking.
4. Downstream Bid Invalidation (Important)
Execution payload bids must match the proposer's
fee_recipientandgas_limit. After a reorg changes the proposer:Relevant Files
packages/beacon-node/src/chain/opPools/proposerPreferencesPool.tspackages/beacon-node/src/chain/seenCache/seenProposerPreferences.tspackages/beacon-node/src/chain/validation/proposerPreferences.tspackages/validator/src/services/proposerPreferences.tsContext
specs/gloas/p2p-interface.md(proposer_preferencestopic)proposer_lookaheadcomputed inprocess_proposer_lookaheadduring epoch processingMIN_SEED_LOOKAHEAD = 1→ next-epoch proposers depend on current epoch boundary state