Skip to content

Commit 055b83c

Browse files
ensi321claude
andauthored
feat: only give proposer boost to canonical proposer (#9313)
closes #9231 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6c85077 commit 055b83c

9 files changed

Lines changed: 138 additions & 38 deletions

File tree

packages/beacon-node/src/chain/blocks/importBlock.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,19 @@ export async function importBlock(
116116
}
117117
executionStatus = parentBlock.executionStatus;
118118
}
119+
120+
// getBeaconProposerOrNull will return null if head state is more than one epoch away
121+
// from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer
122+
const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot);
123+
119124
const blockSummary = this.forkChoice.onBlock(
120125
block.message,
121126
postState,
122127
blockDelaySec,
123128
currentSlot,
124129
executionStatus,
125-
dataAvailabilityStatus
130+
dataAvailabilityStatus,
131+
expectedProposerIndex
126132
);
127133

128134
// This adds the state necessary to process the next block

packages/beacon-node/test/spec/presets/fork_choice.test.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,6 @@ const forkChoiceTest =
624624
// integrated
625625
shouldSkip: (_testcase, name, _index) =>
626626
name.includes("invalid_incorrect_proof") ||
627-
// TODO GLOAS: Proposer boost specs have been changed retroactively in v1.7.0-alpha.1,
628-
// and these tests are failing until we update our implementation.
629-
name.includes("voting_source_beyond_two_epoch") ||
630-
name.includes("justified_update_always_if_better") ||
631-
name.includes("justified_update_not_realized_finality") ||
632627
// TODO GLOAS: These tests will be unskipped by https://github.com/ChainSafe/lodestar/pull/9233
633628
(name.includes("gloas") &&
634629
(name.includes("simple_attempted_reorg_without_enough_ffg_votes") ||

packages/beacon-node/test/spec/utils/gossipValidation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ export async function runGossipValidationTest(
475475
}
476476

477477
const postState = computePostState(parentState, signedBlock, fork);
478+
const expectedProposerIndex: number | null = chain.getHeadState().getBeaconProposerOrNull(slot);
478479

479480
if (blockEntry.failed) {
480481
// payload_status === "VALID" (filtered above)
@@ -486,7 +487,8 @@ export async function runGossipValidationTest(
486487
0,
487488
slot,
488489
ExecutionStatus.Valid,
489-
getDataAvailabilityStatusForFork(fork)
490+
getDataAvailabilityStatusForFork(fork),
491+
expectedProposerIndex
490492
);
491493
blockStatesByRoot.set(blockRootHex, postState);
492494
continue;
@@ -501,7 +503,8 @@ export async function runGossipValidationTest(
501503
0,
502504
slot,
503505
ExecutionStatus.Syncing,
504-
getDataAvailabilityStatusForFork(fork)
506+
getDataAvailabilityStatusForFork(fork),
507+
expectedProposerIndex
505508
);
506509
blockStatesByRoot.set(blockRootHex, postState);
507510
invalidateImportedBlock(chain, blockRootHex, parentRootHex);

packages/beacon-node/test/unit/chain/forkChoice/forkChoice.test.ts

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,17 @@ describe("LodestarForkChoice", () => {
106106
blockDelaySec,
107107
currentSlot,
108108
executionStatus,
109-
dataAvailabilityStatus
109+
dataAvailabilityStatus,
110+
null
110111
);
111112
forkChoice.onBlock(
112113
orphanedBlock.message,
113114
orphanedState,
114115
blockDelaySec,
115116
currentSlot,
116117
executionStatus,
117-
dataAvailabilityStatus
118+
dataAvailabilityStatus,
119+
null
118120
);
119121
let head = forkChoice.getHead();
120122
expect(head.slot).toBe(orphanedBlock.message.slot);
@@ -124,7 +126,8 @@ describe("LodestarForkChoice", () => {
124126
blockDelaySec,
125127
currentSlot,
126128
executionStatus,
127-
dataAvailabilityStatus
129+
dataAvailabilityStatus,
130+
null
128131
);
129132
// tie break condition causes head to be orphaned block (based on hex root comparison)
130133
head = forkChoice.getHead();
@@ -135,7 +138,8 @@ describe("LodestarForkChoice", () => {
135138
blockDelaySec,
136139
currentSlot,
137140
executionStatus,
138-
dataAvailabilityStatus
141+
dataAvailabilityStatus,
142+
null
139143
);
140144
head = forkChoice.getHead();
141145
// without vote, head gets stuck at orphaned block
@@ -195,19 +199,75 @@ describe("LodestarForkChoice", () => {
195199
const currentSlot = 128;
196200
forkChoice.updateTime(currentSlot);
197201

198-
forkChoice.onBlock(block08.message, state08, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
199-
forkChoice.onBlock(block12.message, state12, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
200-
forkChoice.onBlock(block16.message, state16, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
201-
forkChoice.onBlock(block20.message, state20, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
202-
forkChoice.onBlock(block24.message, state24, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
203-
forkChoice.onBlock(block28.message, state28, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
202+
forkChoice.onBlock(
203+
block08.message,
204+
state08,
205+
blockDelaySec,
206+
currentSlot,
207+
executionStatus,
208+
dataAvailabilityStatus,
209+
null
210+
);
211+
forkChoice.onBlock(
212+
block12.message,
213+
state12,
214+
blockDelaySec,
215+
currentSlot,
216+
executionStatus,
217+
dataAvailabilityStatus,
218+
null
219+
);
220+
forkChoice.onBlock(
221+
block16.message,
222+
state16,
223+
blockDelaySec,
224+
currentSlot,
225+
executionStatus,
226+
dataAvailabilityStatus,
227+
null
228+
);
229+
forkChoice.onBlock(
230+
block20.message,
231+
state20,
232+
blockDelaySec,
233+
currentSlot,
234+
executionStatus,
235+
dataAvailabilityStatus,
236+
null
237+
);
238+
forkChoice.onBlock(
239+
block24.message,
240+
state24,
241+
blockDelaySec,
242+
currentSlot,
243+
executionStatus,
244+
dataAvailabilityStatus,
245+
null
246+
);
247+
forkChoice.onBlock(
248+
block28.message,
249+
state28,
250+
blockDelaySec,
251+
currentSlot,
252+
executionStatus,
253+
dataAvailabilityStatus,
254+
null
255+
);
204256
expect(forkChoice.getAllAncestorBlocks(hashBlock(block16.message), PayloadStatus.FULL)).toHaveLength(4);
205257
expect(forkChoice.getAllAncestorBlocks(hashBlock(block24.message), PayloadStatus.FULL)).toHaveLength(6);
206258
expect(forkChoice.getBlockHexDefaultStatus(hashBlock(block08.message))).not.toBeNull();
207259
expect(forkChoice.getBlockHexDefaultStatus(hashBlock(block12.message))).not.toBeNull();
208260
expect(forkChoice.hasBlockHex(hashBlock(block08.message))).toBe(true);
209261
expect(forkChoice.hasBlockHex(hashBlock(block12.message))).toBe(true);
210-
forkChoice.onBlock(block32.message, state32, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus);
262+
forkChoice.onBlock(
263+
block32.message,
264+
state32,
265+
blockDelaySec,
266+
currentSlot,
267+
executionStatus,
268+
dataAvailabilityStatus,
269+
null
270+
);
211271
forkChoice.prune(hashBlock(block16.message));
212272
expect(forkChoice.getAllAncestorBlocks(hashBlock(block16.message), PayloadStatus.FULL).length).toBeWithMessage(
213273
1,
@@ -243,31 +303,35 @@ describe("LodestarForkChoice", () => {
243303
blockDelaySec,
244304
currentSlot,
245305
executionStatus,
246-
dataAvailabilityStatus
306+
dataAvailabilityStatus,
307+
null
247308
);
248309
forkChoice.onBlock(
249310
orphanedBlock.message,
250311
orphanedState,
251312
blockDelaySec,
252313
currentSlot,
253314
executionStatus,
254-
dataAvailabilityStatus
315+
dataAvailabilityStatus,
316+
null
255317
);
256318
forkChoice.onBlock(
257319
parentBlock.message,
258320
parentState,
259321
blockDelaySec,
260322
currentSlot,
261323
executionStatus,
262-
dataAvailabilityStatus
324+
dataAvailabilityStatus,
325+
null
263326
);
264327
forkChoice.onBlock(
265328
childBlock.message,
266329
childState,
267330
blockDelaySec,
268331
currentSlot,
269332
executionStatus,
270-
dataAvailabilityStatus
333+
dataAvailabilityStatus,
334+
null
271335
);
272336
const childBlockRoot = toHexString(ssz.phase0.BeaconBlock.hashTreeRoot(childBlock.message));
273337
// the old way to get non canonical blocks
@@ -314,7 +378,8 @@ describe("LodestarForkChoice", () => {
314378
blockDelaySec,
315379
blockW.message.slot,
316380
executionStatus,
317-
dataAvailabilityStatus
381+
dataAvailabilityStatus,
382+
null
318383
);
319384

320385
// X
@@ -326,7 +391,8 @@ describe("LodestarForkChoice", () => {
326391
blockDelaySec,
327392
blockX.message.slot,
328393
executionStatus,
329-
dataAvailabilityStatus
394+
dataAvailabilityStatus,
395+
null
330396
);
331397

332398
// Y, same epoch to X
@@ -338,7 +404,8 @@ describe("LodestarForkChoice", () => {
338404
blockDelaySec,
339405
blockY.message.slot,
340406
executionStatus,
341-
dataAvailabilityStatus
407+
dataAvailabilityStatus,
408+
null
342409
);
343410

344411
// Y and Z are candidates for new head, make more attestations on Y
@@ -379,7 +446,8 @@ describe("LodestarForkChoice", () => {
379446
blockDelaySec,
380447
blockZ.message.slot,
381448
executionStatus,
382-
dataAvailabilityStatus
449+
dataAvailabilityStatus,
450+
null
383451
);
384452

385453
const head = forkChoice.updateHead();

packages/fork-choice/src/forkChoice/forkChoice.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,11 @@ export class ForkChoice implements IForkChoice {
601601
blockDelaySec: number,
602602
currentSlot: Slot,
603603
executionStatus: BlockExecutionStatus,
604-
dataAvailabilityStatus: DataAvailabilityStatus
604+
dataAvailabilityStatus: DataAvailabilityStatus,
605+
// The expected proposer index on the canonical chain we are following.
606+
// Calculated by our head state. We use it as part of the proposer
607+
// boost decision making. No boost will be set if this is null.
608+
expectedProposerIndex: ValidatorIndex | null
605609
): ProtoBlock {
606610
const {parentRoot, slot} = block;
607611
const parentRootHex = toRootHex(parentRoot);
@@ -674,7 +678,9 @@ export class ForkChoice implements IForkChoice {
674678
this.opts?.proposerBoost &&
675679
isTimely &&
676680
// only boost the first block we see
677-
this.proposerBoostRoot === null
681+
this.proposerBoostRoot === null &&
682+
expectedProposerIndex !== null &&
683+
block.proposerIndex === expectedProposerIndex
678684
) {
679685
this.proposerBoostRoot = blockRootHex;
680686
}

packages/fork-choice/src/forkChoice/interface.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
2-
import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types";
2+
import {
3+
AttesterSlashing,
4+
BeaconBlock,
5+
Epoch,
6+
IndexedAttestation,
7+
Root,
8+
RootHex,
9+
Slot,
10+
ValidatorIndex,
11+
} from "@lodestar/types";
312
import {
413
BlockExecutionStatus,
514
LVHExecResponse,
@@ -145,7 +154,8 @@ export interface IForkChoice {
145154
blockDelaySec: number,
146155
currentSlot: Slot,
147156
executionStatus: BlockExecutionStatus,
148-
dataAvailabilityStatus: DataAvailabilityStatus
157+
dataAvailabilityStatus: DataAvailabilityStatus,
158+
expectedProposerIndex: ValidatorIndex | null
149159
): ProtoBlock;
150160
/**
151161
* Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.

packages/state-transition/src/cache/epochCache.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -782,14 +782,17 @@ export class EpochCache {
782782
*/
783783
getBeaconProposer(slot: Slot): ValidatorIndex {
784784
const epoch = computeEpochAtSlot(slot);
785-
if (epoch !== this.currentShuffling.epoch) {
786-
throw new EpochCacheError({
787-
code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH,
788-
currentEpoch: this.currentShuffling.epoch,
789-
requestedEpoch: epoch,
790-
});
785+
if (epoch === this.currentShuffling.epoch) {
786+
return this.proposers[slot % SLOTS_PER_EPOCH];
787+
}
788+
if (epoch === this.nextEpoch) {
789+
return this.getBeaconProposersNextEpoch()[slot % SLOTS_PER_EPOCH];
791790
}
792-
return this.proposers[slot % SLOTS_PER_EPOCH];
791+
throw new EpochCacheError({
792+
code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH,
793+
currentEpoch: this.currentShuffling.epoch,
794+
requestedEpoch: epoch,
795+
});
793796
}
794797

795798
getBeaconProposers(): ValidatorIndex[] {

packages/state-transition/src/stateView/beaconStateView.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
498498
return this.cachedState.epochCtx.getBeaconProposer(slot);
499499
}
500500

501+
getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null {
502+
try {
503+
return this.cachedState.epochCtx.getBeaconProposer(slot);
504+
} catch {
505+
return null;
506+
}
507+
}
508+
501509
computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} {
502510
return computeAnchorCheckpoint(this.config, this.cachedState);
503511
}

packages/state-transition/src/stateView/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface IBeaconStateView {
8989
currentProposers: ValidatorIndex[];
9090
nextProposers: ValidatorIndex[];
9191
getBeaconProposer(slot: Slot): ValidatorIndex;
92+
getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null;
9293

9394
// Validators and balances
9495
effectiveBalanceIncrements: EffectiveBalanceIncrements;

0 commit comments

Comments
 (0)