diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index d30965b08d21..ed5c66e6634d 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -116,13 +116,19 @@ 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 + 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 3e3c6677486e..8651ef3ed396 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -624,11 +624,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..448cad42b2e1 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -475,6 +475,7 @@ 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) @@ -486,7 +487,8 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Valid, - getDataAvailabilityStatusForFork(fork) + getDataAvailabilityStatusForFork(fork), + expectedProposerIndex ); blockStatesByRoot.set(blockRootHex, postState); continue; @@ -501,7 +503,8 @@ export async function runGossipValidationTest( 0, slot, ExecutionStatus.Syncing, - getDataAvailabilityStatusForFork(fork) + getDataAvailabilityStatusForFork(fork), + expectedProposerIndex ); 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 8ea7c3a4d0c2..29d512c5e256 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, + null ); forkChoice.onBlock( orphanedBlock.message, @@ -114,7 +115,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); let head = forkChoice.getHead(); expect(head.slot).toBe(orphanedBlock.message.slot); @@ -124,7 +126,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); // 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, + null ); 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, + 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 + ); 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, + null + ); 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, + null ); forkChoice.onBlock( orphanedBlock.message, @@ -251,7 +312,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); forkChoice.onBlock( parentBlock.message, @@ -259,7 +321,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); forkChoice.onBlock( childBlock.message, @@ -267,7 +330,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, currentSlot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); 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, + null ); // X @@ -326,7 +391,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockX.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); // Y, same epoch to X @@ -338,7 +404,8 @@ describe("LodestarForkChoice", () => { blockDelaySec, blockY.message.slot, executionStatus, - dataAvailabilityStatus + dataAvailabilityStatus, + null ); // 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, + null ); const head = forkChoice.updateHead(); diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 3efa9c117cf0..4e06c8317e54 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -601,7 +601,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); @@ -674,7 +678,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 1c936adf5db1..fc4d7cdc28cd 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`. diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 7be17881c753..8acaa7271f96 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -782,14 +782,17 @@ export class EpochCache { */ getBeaconProposer(slot: Slot): ValidatorIndex { const epoch = computeEpochAtSlot(slot); - if (epoch !== this.currentShuffling.epoch) { - throw new EpochCacheError({ - code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH, - currentEpoch: this.currentShuffling.epoch, - requestedEpoch: epoch, - }); + if (epoch === this.currentShuffling.epoch) { + return this.proposers[slot % SLOTS_PER_EPOCH]; + } + if (epoch === this.nextEpoch) { + return this.getBeaconProposersNextEpoch()[slot % SLOTS_PER_EPOCH]; } - return this.proposers[slot % SLOTS_PER_EPOCH]; + throw new EpochCacheError({ + code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH, + currentEpoch: this.currentShuffling.epoch, + requestedEpoch: epoch, + }); } getBeaconProposers(): ValidatorIndex[] { diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 5fcdea7258f0..88c8046549c3 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -498,6 +498,14 @@ 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 7f5fd7006939..5dead7e6dffa 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -89,6 +89,7 @@ export interface IBeaconStateView { currentProposers: ValidatorIndex[]; nextProposers: ValidatorIndex[]; getBeaconProposer(slot: Slot): ValidatorIndex; + getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null; // Validators and balances effectiveBalanceIncrements: EffectiveBalanceIncrements;