diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 8cd27f99f7ac..2d4f5412f193 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -116,13 +116,23 @@ export async function importBlock( } executionStatus = parentBlock.executionStatus; } + + let expectedProposerIndex: number | null = null; + const headState = this.getHeadState(); + if (headState.epoch === blockEpoch) { + expectedProposerIndex = headState.currentProposers[blockSlot % SLOTS_PER_EPOCH]; + } else if (headState.epoch + 1 === blockEpoch) { + expectedProposerIndex = headState.nextProposers[blockSlot % SLOTS_PER_EPOCH]; + } + const blockSummary = this.forkChoice.onBlock( block.message, postState, blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + expectedProposerIndex ); // This adds the state necessary to process the next block 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 1a19815d05b1..ed642270e4e1 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -622,11 +622,6 @@ const forkChoiceTest = // integrated shouldSkip: (_testcase, name, _index) => name.includes("invalid_incorrect_proof") || - // TODO GLOAS: Proposer boost specs have been changed retroactively in v1.7.0-alpha.1, - // and these tests are failing until we update our implementation. - name.includes("voting_source_beyond_two_epoch") || - name.includes("justified_update_always_if_better") || - name.includes("justified_update_not_realized_finality") || // TODO GLOAS: These tests will be unskipped by https://github.com/ChainSafe/lodestar/pull/9233 (name.includes("gloas") && (name.includes("simple_attempted_reorg_without_enough_ffg_votes") || diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index 961400e7fb76..22416a457dd9 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -9,7 +9,7 @@ import {chainConfigFromJson, chainConfigTypes, createBeaconConfig} from "@lodest import {getConfig} from "@lodestar/config/test-utils"; import {ExecutionStatus} from "@lodestar/fork-choice"; import {testLogger} from "@lodestar/logger/test-utils"; -import {ForkName} from "@lodestar/params"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import { BeaconStateAllForks, BeaconStateView, @@ -486,7 +486,8 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Valid, - getDataAvailabilityStatusForFork(fork) + getDataAvailabilityStatusForFork(fork), + getHeadProposerIndex(chain.getHeadState(), slot) ); blockStatesByRoot.set(blockRootHex, postState); continue; @@ -501,7 +502,8 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Syncing, - getDataAvailabilityStatusForFork(fork) + getDataAvailabilityStatusForFork(fork), + getHeadProposerIndex(chain.getHeadState(), slot) ); blockStatesByRoot.set(blockRootHex, postState); invalidateImportedBlock(chain, blockRootHex, parentRootHex); @@ -727,6 +729,17 @@ async function validateMessageForTopic( } } +function getHeadProposerIndex(headState: IBeaconStateView, slot: number): number | null { + const slotEpoch = computeEpochAtSlot(slot); + if (headState.epoch === slotEpoch) { + return headState.currentProposers[slot % SLOTS_PER_EPOCH]; + } + if (headState.epoch + 1 === slotEpoch) { + return headState.nextProposers[slot % SLOTS_PER_EPOCH]; + } + return null; +} + function rejectOnInvalidSerializedBytes(fn: () => T): T { try { return fn(); 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 8ea7c3a4d0c2..c4dd183e6af2 100644 --- a/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts +++ b/packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts @@ -106,7 +106,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + targetBlock.message.proposerIndex ); forkChoice.onBlock( orphanedBlock.message, @@ -114,7 +115,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + orphanedBlock.message.proposerIndex ); let head = forkChoice.getHead(); expect(head.slot).toBe(orphanedBlock.message.slot); @@ -124,7 +126,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + parentBlock.message.proposerIndex ); // tie break condition causes head to be orphaned block (based on hex root comparison) head = forkChoice.getHead(); @@ -135,7 +138,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + childBlock.message.proposerIndex ); head = forkChoice.getHead(); // without vote, head gets stuck at orphaned block @@ -195,19 +199,75 @@ describe("LodestarForkChoice", () => { const currentSlot = 128; forkChoice.updateTime(currentSlot); - 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); + forkChoice.onBlock( + block08.message, + state08, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block08.message.proposerIndex + ); + forkChoice.onBlock( + block12.message, + state12, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block12.message.proposerIndex + ); + forkChoice.onBlock( + block16.message, + state16, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block16.message.proposerIndex + ); + forkChoice.onBlock( + block20.message, + state20, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block20.message.proposerIndex + ); + forkChoice.onBlock( + block24.message, + state24, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block24.message.proposerIndex + ); + forkChoice.onBlock( + block28.message, + state28, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block28.message.proposerIndex + ); 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); + forkChoice.onBlock( + block32.message, + state32, + blockDelaySec, + currentSlot, + executionStatus, + dataAvailabilityStatus, + block32.message.proposerIndex + ); forkChoice.prune(hashBlock(block16.message)); expect(forkChoice.getAllAncestorBlocks(hashBlock(block16.message), PayloadStatus.FULL).length).toBeWithMessage( 1, @@ -243,7 +303,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + targetBlock.message.proposerIndex ); forkChoice.onBlock( orphanedBlock.message, @@ -251,7 +312,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + orphanedBlock.message.proposerIndex ); forkChoice.onBlock( parentBlock.message, @@ -259,7 +321,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + parentBlock.message.proposerIndex ); forkChoice.onBlock( childBlock.message, @@ -267,7 +330,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + childBlock.message.proposerIndex ); const childBlockRoot = toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(childBlock.message)); // the old way to get non canonical blocks @@ -314,7 +378,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockW.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + blockW.message.proposerIndex ); // X @@ -326,7 +391,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockX.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + blockX.message.proposerIndex ); // Y, same epoch to X @@ -338,7 +404,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockY.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + blockY.message.proposerIndex ); // Y and Z are candidates for new head, make more attestations on Y @@ -379,7 +446,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockZ.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + blockZ.message.proposerIndex ); const head = forkChoice.updateHead(); diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index abca3df86437..03e57d95178c 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -596,7 +596,11 @@ export class ForkChoice implements IForkChoice { blockDelaySec: number, currentSlot: Slot, executionStatus: BlockExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus + 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 ): ProtoBlock { const {parentRoot, slot} = block; const parentRootHex = toRootHex(parentRoot); @@ -669,7 +673,9 @@ export class ForkChoice implements IForkChoice { this.opts?.proposerBoost && isTimely && // only boost the first block we see - this.proposerBoostRoot === null + this.proposerBoostRoot === null && + expectedProposerIndex !== null && + block.proposerIndex === expectedProposerIndex ) { this.proposerBoostRoot = blockRootHex; } diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 534f5fa51524..2b2104e89832 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,5 +1,14 @@ import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition"; -import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types"; +import { + AttesterSlashing, + BeaconBlock, + Epoch, + IndexedAttestation, + Root, + RootHex, + Slot, + ValidatorIndex, +} from "@lodestar/types"; import { BlockExecutionStatus, LVHExecResponse, @@ -145,7 +154,8 @@ export interface IForkChoice { blockDelaySec: number, currentSlot: Slot, executionStatus: BlockExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus + dataAvailabilityStatus: DataAvailabilityStatus, + expectedProposerIndex: ValidatorIndex | null ): ProtoBlock; /** * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.