diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index cdd8cb3109cc..bdc72a6b4e61 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -739,13 +739,7 @@ const forkChoiceTest = (name.includes("simple_attempted_reorg_without_enough_ffg_votes") || name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") || name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch") || - name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || - // TODO: re-enable after "apply proposer boost if dependent roots match" (consensus-specs #5306) - // is implemented. New behavior in v1.7.0-alpha.9; Lodestar still applies the boost - // unconditionally. Only the altair vectors exercise the changed condition (other forks pass). - name.endsWith("altair/fork_choice/on_block/pyspec_tests/justified_update_always_if_better") || - name.endsWith("altair/fork_choice/on_block/pyspec_tests/justified_update_not_realized_finality") || - name.endsWith("altair/fork_choice/get_head/pyspec_tests/voting_source_beyond_two_epoch"), + name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))), }, }; }; diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index bd0923a11093..51d358000c4d 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; import { DataAvailabilityStatus, EffectiveBalanceIncrements, @@ -707,7 +707,8 @@ export class ForkChoice implements IForkChoice { // only boost the first block we see this.proposerBoostRoot === null && expectedProposerIndex !== null && - block.proposerIndex === expectedProposerIndex + block.proposerIndex === expectedProposerIndex && + this.isProposerBoostSameDependentRoot(this.head.blockRoot, parentRootHex) ) { this.proposerBoostRoot = blockRootHex; } @@ -1494,6 +1495,34 @@ export class ForkChoice implements IForkChoice { throw Error(`Not found dependent root for block slot ${block.slot}, epoch difference ${epochDifference}`); } + /** + * 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. + * + * The block is not yet in the proto-array when this runs, so its dependent root is traced from + * its parent + */ + private isProposerBoostSameDependentRoot(headRootHex: RootHex, blockParentRootHex: RootHex): boolean { + const epoch = computeEpochAtSlot(this.fcStore.currentSlot); + // Genesis block parent + if (epoch <= MIN_SEED_LOOKAHEAD) { + return true; + } + + const dependentSlot = computeStartSlotAtEpoch(epoch - MIN_SEED_LOOKAHEAD) - 1; + const headDependentRoot = this.protoArray.getAncestorOrNull(headRootHex, dependentSlot)?.blockRoot; + const blockDependentRoot = this.protoArray.getAncestorOrNull(blockParentRootHex, dependentSlot)?.blockRoot; + // On lookup failure, we lean on the conservative side and withold the boost + if (headDependentRoot === undefined || blockDependentRoot === undefined) { + return false; + } + + return headDependentRoot === blockDependentRoot; + } + /** * Return true if the block is timely for the current slot. * Child class can overwrite this for testing purpose.