From c0e352c7bcee0c24b2c974a76e7cbbd764414f29 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:44:47 -0700 Subject: [PATCH] Do not compare proposer index in proposer boost --- .../src/chain/blocks/importBlock.ts | 7 +- .../test/spec/utils/gossipValidation.ts | 7 +- .../unit/chain/forkChoice/forkChoice.test.ts | 106 ++++-------------- .../fork-choice/src/forkChoice/forkChoice.ts | 18 +-- .../fork-choice/src/forkChoice/interface.ts | 14 +-- .../src/stateView/beaconStateView.ts | 8 -- .../src/stateView/interface.ts | 1 - .../src/stateView/nativeBeaconStateView.ts | 10 -- 8 files changed, 30 insertions(+), 141 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 59527fd706df..74b89ec590a5 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -124,18 +124,13 @@ export async function importBlock( executionStatus = parentBlock.executionStatus; } - // getBeaconProposerOrNull will return null if head state is more than one epoch away - // from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer - const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot); - const blockSummary = this.forkChoice.onBlock( block.message, postState, blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - expectedProposerIndex + dataAvailabilityStatus ); // This adds the state necessary to process the next block diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index 448cad42b2e1..961400e7fb76 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -475,7 +475,6 @@ export async function runGossipValidationTest( } const postState = computePostState(parentState, signedBlock, fork); - const expectedProposerIndex: number | null = chain.getHeadState().getBeaconProposerOrNull(slot); if (blockEntry.failed) { // payload_status === "VALID" (filtered above) @@ -487,8 +486,7 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Valid, - getDataAvailabilityStatusForFork(fork), - expectedProposerIndex + getDataAvailabilityStatusForFork(fork) ); blockStatesByRoot.set(blockRootHex, postState); continue; @@ -503,8 +501,7 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Syncing, - getDataAvailabilityStatusForFork(fork), - expectedProposerIndex + getDataAvailabilityStatusForFork(fork) ); blockStatesByRoot.set(blockRootHex, postState); invalidateImportedBlock(chain, blockRootHex, parentRootHex); diff --git a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts index f0a4aead962c..8fec524b96ee 100644 --- a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts +++ b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts @@ -107,8 +107,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); forkChoice.onBlock( orphanedBlock.message, @@ -116,8 +115,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); let head = forkChoice.getHead(); expect(head.slot).toBe(orphanedBlock.message.slot); @@ -127,8 +125,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); // tie break condition causes head to be orphaned block (based on hex root comparison) head = forkChoice.getHead(); @@ -139,8 +136,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); head = forkChoice.getHead(); // without vote, head gets stuck at orphaned block @@ -200,75 +196,19 @@ describe("LodestarForkChoice", () => { const currentSlot = 128; forkChoice.updateTime(currentSlot); - forkChoice.onBlock( - block08.message, - state08, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); - forkChoice.onBlock( - block12.message, - state12, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); - forkChoice.onBlock( - block16.message, - state16, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); - forkChoice.onBlock( - block20.message, - state20, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); - forkChoice.onBlock( - block24.message, - state24, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); - forkChoice.onBlock( - block28.message, - state28, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); + forkChoice.onBlock(block08.message, state08, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); + forkChoice.onBlock(block12.message, state12, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); + forkChoice.onBlock(block16.message, state16, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); + forkChoice.onBlock(block20.message, state20, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); + forkChoice.onBlock(block24.message, state24, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); + forkChoice.onBlock(block28.message, state28, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); expect(forkChoice.getAllAncestorBlocks(hashBlock(block16.message), PayloadStatus.FULL)).toHaveLength(4); expect(forkChoice.getAllAncestorBlocks(hashBlock(block24.message), PayloadStatus.FULL)).toHaveLength(6); expect(forkChoice.getBlockHexDefaultStatus(hashBlock(block08.message))).not.toBeNull(); expect(forkChoice.getBlockHexDefaultStatus(hashBlock(block12.message))).not.toBeNull(); expect(forkChoice.hasBlockHex(hashBlock(block08.message))).toBe(true); expect(forkChoice.hasBlockHex(hashBlock(block12.message))).toBe(true); - forkChoice.onBlock( - block32.message, - state32, - blockDelaySec, - currentSlot, - executionStatus, - dataAvailabilityStatus, - null - ); + forkChoice.onBlock(block32.message, state32, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus); forkChoice.prune(hashBlock(block16.message)); expect(forkChoice.getAllAncestorBlocks(hashBlock(block16.message), PayloadStatus.FULL).length).toBeWithMessage( 1, @@ -304,8 +244,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); forkChoice.onBlock( orphanedBlock.message, @@ -313,8 +252,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); forkChoice.onBlock( parentBlock.message, @@ -322,8 +260,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); forkChoice.onBlock( childBlock.message, @@ -331,8 +268,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); const childBlockRoot = toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(childBlock.message)); // the old way to get non canonical blocks @@ -379,8 +315,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockW.message.slot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); // X @@ -392,8 +327,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockX.message.slot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); // Y, same epoch to X @@ -405,8 +339,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockY.message.slot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); // Y and Z are candidates for new head, make more attestations on Y @@ -447,8 +380,7 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockZ.message.slot, executionStatus, - dataAvailabilityStatus, - null + dataAvailabilityStatus ); const head = forkChoice.updateHead(); diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 9ea39c53ce29..b4a1cd73c747 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -632,11 +632,7 @@ export class ForkChoice implements IForkChoice { blockDelaySec: number, currentSlot: Slot, executionStatus: BlockExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus, - // The expected proposer index on the canonical chain we are following. - // Calculated by our head state. We use it as part of the proposer - // boost decision making. No boost will be set if this is null. - expectedProposerIndex: ValidatorIndex | null + dataAvailabilityStatus: DataAvailabilityStatus ): ProtoBlock { const {parentRoot, slot} = block; const parentRootHex = toRootHex(parentRoot); @@ -711,8 +707,6 @@ export class ForkChoice implements IForkChoice { isTimely && // only boost the first block we see this.proposerBoostRoot === null && - expectedProposerIndex !== null && - block.proposerIndex === expectedProposerIndex && this.isProposerBoostSameDependentRoot(this.head.blockRoot, parentRootHex); // Candidate boost root used for protoArray.onBlock's best-child weighting. Committed to the // store only after the insertion succeeds. @@ -1505,11 +1499,11 @@ export class ForkChoice implements IForkChoice { } /** - * Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition, - * consensus-specs #5306). Proposer boost is only granted when the imported block shares the same - * proposer-shuffling dependent root for the current epoch as the canonical head computed before - * the block was imported. This withholds the boost from a block built on a different shuffling - * branch than the head. + * Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition, added in + * https://github.com/ethereum/consensus-specs/pull/5306). Proposer boost is only granted when the imported + * block shares the same proposer-shuffling dependent root for the current epoch as the canonical head + * computed before the block was imported. This withholds the boost from a block built on a different + * shuffling branch than the head. * * The block is not yet in the proto-array when this runs, so its dependent root is traced from * its parent diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 63b5cd983291..40acc201652b 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,14 +1,5 @@ import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition"; -import { - AttesterSlashing, - BeaconBlock, - Epoch, - IndexedAttestation, - Root, - RootHex, - Slot, - ValidatorIndex, -} from "@lodestar/types"; +import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types"; import { BlockExecutionStatus, LVHExecResponse, @@ -160,8 +151,7 @@ export interface IForkChoice { blockDelaySec: number, currentSlot: Slot, executionStatus: BlockExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus, - expectedProposerIndex: ValidatorIndex | null + dataAvailabilityStatus: DataAvailabilityStatus ): ProtoBlock; /** * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`. diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 3c27e85f2ed9..bf467ae4b88d 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -515,14 +515,6 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { return this.cachedState.epochCtx.getBeaconProposer(slot); } - getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null { - try { - return this.cachedState.epochCtx.getBeaconProposer(slot); - } catch { - return null; - } - } - computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { return computeAnchorCheckpoint(this.config, this.cachedState); } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index b0425279d05c..6097eabd0a8f 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -92,7 +92,6 @@ export interface IBeaconStateView { currentProposers: ValidatorIndex[]; nextProposers: ValidatorIndex[]; getBeaconProposer(slot: Slot): ValidatorIndex; - getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null; // Validators and balances effectiveBalanceIncrements: EffectiveBalanceIncrements; diff --git a/packages/state-transition/src/stateView/nativeBeaconStateView.ts b/packages/state-transition/src/stateView/nativeBeaconStateView.ts index 60b8019776c3..4b807fc06061 100644 --- a/packages/state-transition/src/stateView/nativeBeaconStateView.ts +++ b/packages/state-transition/src/stateView/nativeBeaconStateView.ts @@ -128,8 +128,6 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork { private readonly _getBeaconCommitteeCountPerSlot = new Map(); private readonly _getShufflingDecisionRoot = new Map(); private readonly _getBeaconProposer = new Map(); - // getBeaconProposerOrNull can return null, so use .has() to distinguish "not cached" from "cached null" - private readonly _getBeaconProposerOrNull = new Map(); private readonly _getValidator = new Map(); private readonly _getBalance = new Map(); private readonly _getIndexedSyncCommitteeAtEpoch = new Map(); @@ -411,14 +409,6 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork { return cached; } - getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null { - if (!this._getBeaconProposerOrNull.has(slot)) { - this._getBeaconProposerOrNull.set(slot, this.binding.getBeaconProposerOrNull(slot)); - } - // biome-ignore lint/style/noNonNullAssertion: has() check guarantees a value - return this._getBeaconProposerOrNull.get(slot)!; - } - // Validators and balances get effectiveBalanceIncrements(): EffectiveBalanceIncrements {