diff --git a/packages/beacon-node/src/chain/errors/proposerPreferences.ts b/packages/beacon-node/src/chain/errors/proposerPreferences.ts index 32804c47f10b..d7be500a858a 100644 --- a/packages/beacon-node/src/chain/errors/proposerPreferences.ts +++ b/packages/beacon-node/src/chain/errors/proposerPreferences.ts @@ -5,6 +5,7 @@ export enum ProposerPreferencesErrorCode { INVALID_EPOCH = "PROPOSER_PREFERENCES_ERROR_INVALID_EPOCH", PROPOSAL_SLOT_PASSED = "PROPOSER_PREFERENCES_ERROR_PROPOSAL_SLOT_PASSED", UNKNOWN_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_UNKNOWN_DEPENDENT_ROOT", + INVALID_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_INVALID_DEPENDENT_ROOT", INVALID_PROPOSER = "PROPOSER_PREFERENCES_ERROR_INVALID_PROPOSER", ALREADY_KNOWN = "PROPOSER_PREFERENCES_ERROR_ALREADY_KNOWN", INVALID_SIGNATURE = "PROPOSER_PREFERENCES_ERROR_INVALID_SIGNATURE", @@ -26,6 +27,11 @@ export type ProposerPreferencesErrorType = proposalSlot: Slot; dependentRoot: RootHex; } + | { + code: ProposerPreferencesErrorCode.INVALID_DEPENDENT_ROOT; + proposalSlot: Slot; + dependentRoot: RootHex; + } | { code: ProposerPreferencesErrorCode.INVALID_PROPOSER; proposalSlot: Slot; diff --git a/packages/beacon-node/src/chain/validation/proposerPreferences.ts b/packages/beacon-node/src/chain/validation/proposerPreferences.ts index 8c2fbe1689ab..43dae26ebb7a 100644 --- a/packages/beacon-node/src/chain/validation/proposerPreferences.ts +++ b/packages/beacon-node/src/chain/validation/proposerPreferences.ts @@ -1,6 +1,7 @@ -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; import { computeEpochAtSlot, + computeStartSlotAtEpoch, createSingleSignatureSetFromComponents, getProposerPreferencesSigningRoot, } from "@lodestar/state-transition"; @@ -70,6 +71,18 @@ export async function validateGossipProposerPreferences( }); } + // [REJECT] Dependent block after the lookahead decision slot (clamped to `GENESIS_SLOT` so early epochs + // don't underflow). Kept after the proposer-resolution IGNORE so state-unavailable roots stay IGNORE (`ignore_dependent_root_state_unavailable`). + const decisionSlot = Math.max(GENESIS_SLOT, computeStartSlotAtEpoch(proposalEpoch - MIN_SEED_LOOKAHEAD) - 1); + const dependentBlock = chain.forkChoice.getBlockHexDefaultStatus(dependentRootHex); + if (dependentBlock !== null && dependentBlock.slot > decisionSlot) { + throw new ProposerPreferencesError(GossipAction.REJECT, { + code: ProposerPreferencesErrorCode.INVALID_DEPENDENT_ROOT, + proposalSlot, + dependentRoot: dependentRootHex, + }); + } + // [REJECT] `is_valid_proposal_slot(state, preferences)` returns True. if (proposers[proposalSlot % SLOTS_PER_EPOCH] !== validatorIndex) { throw new ProposerPreferencesError(GossipAction.REJECT, {