From d4e105d65cd7baa6c5cae4eabd9a6164f25ef6f8 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 27 Aug 2026 11:14:48 +0100 Subject: [PATCH 1/3] feat: reorg weak head when the proposer equivocated --- .../fork-choice/src/forkChoice/forkChoice.ts | 45 ++++++++++-- .../fork-choice/src/protoArray/protoArray.ts | 21 ++++-- .../unit/forkChoice/getProposerHead.test.ts | 71 ++++++++++++++++++- specrefs/.ethspecify.yml | 3 - specrefs/functions.yml | 16 +++-- 5 files changed, 136 insertions(+), 20 deletions(-) diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 0c29660706d2..6b22c9c63b17 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -462,7 +462,8 @@ export class ForkChoice implements IForkChoice { * https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg * This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()` * - * Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#get_proposer_head + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-get_proposer_head */ getProposerHead( headBlock: ProtoBlock, @@ -493,6 +494,29 @@ export class ForkChoice implements IForkChoice { return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable}; } + // Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead() + const currentTimeOk = headBlock.slot + 1 === slot; + const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot; + + // Re-org more aggressively if there is a proposer equivocation in the previous slot, skipping the + // regular reorg conditions. Any known equivocation counts here, timely or not. + if ( + currentTimeOk && + isProposerBoostWornOff && + this.isProposerEquivocation(headBlock) && + this.isHeadWeak(headBlock.blockRoot) + ) { + this.logger?.verbose("Performing single-slot reorg to remove weak head of equivocating proposer", { + slot, + proposerHead: parentBlock.blockRoot, + weakHead: headBlock.blockRoot, + proposerIndex: headBlock.proposerIndex, + }); + proposerHead = parentBlock; + + return {proposerHead, isHeadTimely}; + } + const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot); if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) { @@ -505,14 +529,11 @@ export class ForkChoice implements IForkChoice { } // No reorg if attempted reorg is more than a single slot - // Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead() - const currentTimeOk = headBlock.slot + 1 === slot; if (!currentTimeOk) { return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot}; } // No reorg if proposer boost is still in effect - const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot; if (!isProposerBoostWornOff) { return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff}; } @@ -1756,7 +1777,21 @@ export class ForkChoice implements IForkChoice { // Parent is weak and from the previous slot: apply boost only if there are no early // equivocations, ie. no other PTC-timely block at the parent's slot from the same proposer. - return !this.protoArray.hasEquivocatingBlock(parentBlock.proposerIndex, parentBlock.slot, parentBlock.blockRoot); + return !this.protoArray.hasEquivocatingBlock( + parentBlock.proposerIndex, + parentBlock.slot, + parentBlock.blockRoot, + true + ); + } + + /** + * Return true if another block at the same slot from the same proposer is known to fork choice. + * + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation + */ + private isProposerEquivocation(block: ProtoBlock): boolean { + return this.protoArray.hasEquivocatingBlock(block.proposerIndex, block.slot, block.blockRoot, false); } /** diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index e43baabd2816..d29323e23bd7 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -2008,15 +2008,19 @@ export class ProtoArray { } /** - * Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex` and - * is PTC-timely. Used by `should_apply_proposer_boost` to detect proposer equivocations. - * - * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost + * Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex`. + * `should_apply_proposer_boost` only counts PTC-timely blocks (`ptcTimelyOnly`), `is_proposer_equivocation` + * counts any known block. * * Iterates unique block roots (via the canonical variant) since `slot`, `proposerIndex` and * `ptcTimeliness` are block-level properties identical across payload-status variants. */ - hasEquivocatingBlock(proposerIndex: ValidatorIndex, slot: Slot, excludeRoot: RootHex): boolean { + hasEquivocatingBlock( + proposerIndex: ValidatorIndex, + slot: Slot, + excludeRoot: RootHex, + ptcTimelyOnly: boolean + ): boolean { for (const root of this.indices.keys()) { if (root === excludeRoot) { continue; @@ -2026,7 +2030,12 @@ export class ProtoArray { continue; } const node = this.nodes[nodeIndex]; - if (node !== undefined && node.slot === slot && node.proposerIndex === proposerIndex && node.ptcTimeliness) { + if ( + node !== undefined && + node.slot === slot && + node.proposerIndex === proposerIndex && + (node.ptcTimeliness || !ptcTimelyOnly) + ) { return true; } } diff --git a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts index 627d2a4f3885..91a38c518010 100644 --- a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts @@ -195,11 +195,22 @@ describe("Forkchoice / GetProposerHead", () => { stateGetter: () => null, }; + /** Another block at the head slot from the same proposer, ie. a proposer equivocation */ + const equivocatingHeadBlock: ProtoBlockWithWeight = { + ...baseHeadBlock, + stateRoot: getStateRoot(headSlot + 100), + blockRoot: getBlockRoot(headSlot + 100), + targetRoot: getBlockRoot(headSlot + 100), + weight: 0, + }; + // head block's weight < 30 is considered weak. parent block's total weight > 240 is considered strong const testCases: { id: string; parentBlock: ProtoBlockWithWeight; headBlock: ProtoBlockWithWeight; + /** Imported alongside the head, to simulate an equivocation */ + siblingBlock?: ProtoBlockWithWeight; expectReorg: boolean; currentSlot?: Slot; secFromSlot?: number; @@ -294,6 +305,60 @@ describe("Forkchoice / GetProposerHead", () => { secFromSlot: config.getProposerReorgCutoffMs(ForkName.phase0) / 1000 + 1, expectedNotReorgedReason: NotReorgedReason.NotProposingOnTime, }, + { + id: "Reorg weak equivocating head even if head is timely", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true}, + siblingBlock: equivocatingHeadBlock, + expectReorg: true, + }, + { + id: "Reorg weak equivocating head even if parent is weak", + parentBlock: {...baseParentHeadBlock, weight: 211}, + headBlock: {...baseHeadBlock}, + siblingBlock: equivocatingHeadBlock, + expectReorg: true, + }, + { + id: "Reorg weak equivocating head even if not proposing on time", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock}, + siblingBlock: equivocatingHeadBlock, + expectReorg: true, + secFromSlot: config.getProposerReorgCutoffMs(ForkName.phase0) / 1000 + 1, + }, + { + id: "Reorg weak equivocating head regardless of the equivocating block's PTC timeliness", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true}, + siblingBlock: {...equivocatingHeadBlock, ptcTimeliness: true}, + expectReorg: true, + }, + { + id: "No equivocation reorg if head is strong", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true, weight: 30}, + siblingBlock: equivocatingHeadBlock, + expectReorg: false, + expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely, + }, + { + id: "No equivocation reorg if current slot is more than one slot from head block", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock}, + siblingBlock: equivocatingHeadBlock, + expectReorg: false, + currentSlot: headSlot + 2, + expectedNotReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot, + }, + { + id: "No equivocation reorg if the other block at the head slot is from a different proposer", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true}, + siblingBlock: {...equivocatingHeadBlock, proposerIndex: 1}, + expectReorg: false, + expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely, + }, ]; beforeEach(() => { @@ -304,6 +369,7 @@ describe("Forkchoice / GetProposerHead", () => { id, parentBlock, headBlock, + siblingBlock, expectReorg, currentSlot: proposalSlot, secFromSlot, @@ -313,11 +379,14 @@ describe("Forkchoice / GetProposerHead", () => { it(`${id}`, async () => { protoArr.onBlock(parentBlock, parentBlock.slot, null); protoArr.onBlock(headBlock, headBlock.slot, null); + if (siblingBlock) { + protoArr.onBlock(siblingBlock, siblingBlock.slot, null); + } const currentSlot = proposalSlot ?? headBlock.slot + 1; const currentSecFromSlot = secFromSlot ?? 0; protoArr.applyScoreChanges({ - attestationDeltas: [0, parentBlock.weight, headBlock.weight], + attestationDeltas: [0, parentBlock.weight, headBlock.weight, ...(siblingBlock ? [siblingBlock.weight] : [])], proposerBoost: null, justifiedEpoch: genesisEpoch, justifiedRoot: genesisRoot, diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 53a719b22d28..ee4992098018 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -158,7 +158,6 @@ exceptions: - get_weight#phase0 - is_candidate_block#phase0 - is_proposer#phase0 - - is_proposer_equivocation#phase0 - max_compressed_len#phase0 - max_message_size#phase0 - record_block_timeliness#phase0 @@ -313,8 +312,6 @@ exceptions: - process_proposer_slashing#gloas - process_slot#gloas - process_withdrawals#gloas - - record_block_timeliness#gloas - - should_apply_proposer_boost#gloas - update_builder_pending_withdrawals#gloas - update_latest_messages#gloas - update_next_withdrawal_builder_index#gloas diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 67f1ad8c5254..7fb8f436c649 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -5251,7 +5251,7 @@ - name: get_proposer_head#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" + search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#get_proposer_head" spec: | def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: @@ -5372,7 +5372,7 @@ - name: get_proposer_head#gloas sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" + search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-get_proposer_head" spec: | def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: @@ -7555,7 +7555,9 @@ - name: is_proposer_equivocation#phase0 - sources: [] + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation" spec: | def is_proposer_equivocation(store: Store, root: Root) -> bool: @@ -12140,7 +12142,9 @@ - name: record_block_timeliness#gloas - sources: [] + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-record_block_timeliness" spec: | def record_block_timeliness(store: Store, root: Root) -> None: @@ -12253,7 +12257,9 @@ - name: should_apply_proposer_boost#gloas - sources: [] + sources: + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost" spec: | def should_apply_proposer_boost(store: Store) -> bool: From 62a75910d95643fb8d6ef227e9527d8432459eb1 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 27 Aug 2026 11:35:25 +0100 Subject: [PATCH 2/3] mirror the equivocation branch in shouldOverrideForkChoiceUpdate --- .../fork-choice/src/forkChoice/forkChoice.ts | 18 ++++++-- .../shouldOverrideForkChoiceUpdate.test.ts | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 6b22c9c63b17..123ddc37534d 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -365,6 +365,21 @@ export class ForkChoice implements IForkChoice { return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable}; } + const currentTimeOk = + headBlock.slot === currentSlot || + (proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot)); + + // Mirror the proposer equivocation branch of getProposerHead(). The head slot's attestations are + // still queued at this point so the head is assumed weak, same as for the regular branch below. + if (currentTimeOk && this.isProposerEquivocation(headBlock)) { + this.logger?.verbose("Head proposer equivocated. Should override forkchoice update", { + blockRoot: headBlock.blockRoot, + slot: currentSlot, + proposerIndex: headBlock.proposerIndex, + }); + return {shouldOverrideFcu: true, parentBlock}; + } + const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead( headBlock, parentBlock, @@ -375,9 +390,6 @@ export class ForkChoice implements IForkChoice { return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown}; } - const currentTimeOk = - headBlock.slot === currentSlot || - (proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot)); if (!currentTimeOk) { return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot}; } diff --git a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts index 6356dc3fa972..ec83829dd708 100644 --- a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts @@ -195,10 +195,21 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { stateGetter: () => null, }; + /** Another block at the head slot from the same proposer, ie. a proposer equivocation */ + const equivocatingHeadBlock: ProtoBlockWithWeight = { + ...baseHeadBlock, + stateRoot: getStateRoot(headSlot + 100), + blockRoot: getBlockRoot(headSlot + 100), + targetRoot: getBlockRoot(headSlot + 100), + weight: 0, + }; + const testCases: { id: string; parentBlock: ProtoBlockWithWeight; headBlock: ProtoBlockWithWeight; + /** Imported alongside the head, to simulate an equivocation */ + siblingBlock?: ProtoBlockWithWeight; expectReorg: boolean; currentSlot?: Slot; expectedNotReorgedReason?: NotReorgedReason; @@ -260,6 +271,37 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { expectReorg: false, expectedNotReorgedReason: NotReorgedReason.ParentBlockDistanceMoreThanOneSlot, }, + { + id: "Reorg equivocating head even if head is timely", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true}, + siblingBlock: equivocatingHeadBlock, + expectReorg: true, + }, + { + id: "Reorg equivocating head even if reorg spans more than a single slot", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, slot: headSlot + 1}, + siblingBlock: {...equivocatingHeadBlock, slot: headSlot + 1}, + expectReorg: true, + }, + { + id: "No equivocation reorg if current slot is more than one slot from head block", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock}, + siblingBlock: equivocatingHeadBlock, + expectReorg: false, + currentSlot: headSlot + 2, + expectedNotReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot, + }, + { + id: "No equivocation reorg if the other block at the head slot is from a different proposer", + parentBlock: {...baseParentHeadBlock}, + headBlock: {...baseHeadBlock, timeliness: true}, + siblingBlock: {...equivocatingHeadBlock, proposerIndex: 1}, + expectReorg: false, + expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely, + }, ]; beforeEach(() => { @@ -270,6 +312,7 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { id, parentBlock, headBlock, + siblingBlock, expectReorg, currentSlot: blockSeenSlot, expectedNotReorgedReason, @@ -278,6 +321,9 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { it(id, async () => { protoArr.onBlock(parentBlock, parentBlock.slot, null); protoArr.onBlock(headBlock, headBlock.slot, null); + if (siblingBlock) { + protoArr.onBlock(siblingBlock, siblingBlock.slot, null); + } const secFromSlot = 0; const currentSlot = blockSeenSlot ?? headBlock.slot; From 2d72796b1100ff7dcb19465b33c700f371d7e6bd Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 27 Aug 2026 11:45:17 +0100 Subject: [PATCH 3/3] comment the ptcTimelyOnly flag and point specrefs at the implementations --- .../fork-choice/src/forkChoice/forkChoice.ts | 4 ++++ specrefs/functions.yml | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 123ddc37534d..d0ab95940e38 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1793,6 +1793,8 @@ export class ForkChoice implements IForkChoice { parentBlock.proposerIndex, parentBlock.slot, parentBlock.blockRoot, + // Only a sibling seen before the PTC deadline counts. A late released one might not have been + // seen by the proposer, so it cannot be expected to reorg it and is not denied the boost for it. true ); } @@ -1803,6 +1805,8 @@ export class ForkChoice implements IForkChoice { * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation */ private isProposerEquivocation(block: ProtoBlock): boolean { + // Any known sibling counts, timely or not. Timeliness only matters for withholding the boost from + // the next proposer, the reorg itself is safe to attempt whenever the equivocation is visible. return this.protoArray.hasEquivocatingBlock(block.proposerIndex, block.slot, block.blockRoot, false); } diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 7fb8f436c649..d33b29894176 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -5251,7 +5251,8 @@ - name: get_proposer_head#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#get_proposer_head" + search: '^\s+getProposerHead\(' + regex: true spec: | def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: @@ -5372,7 +5373,8 @@ - name: get_proposer_head#gloas sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-get_proposer_head" + search: '^\s+getProposerHead\(' + regex: true spec: | def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: @@ -7557,7 +7559,8 @@ - name: is_proposer_equivocation#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation" + search: '^\s+private isProposerEquivocation\(' + regex: true spec: | def is_proposer_equivocation(store: Store, root: Root) -> bool: @@ -12144,7 +12147,11 @@ - name: record_block_timeliness#gloas sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-record_block_timeliness" + search: '^\s+protected isBlockTimely\(' + regex: true + - file: packages/fork-choice/src/forkChoice/forkChoice.ts + search: '^\s+protected isBlockPtcTimely\(' + regex: true spec: | def record_block_timeliness(store: Store, root: Root) -> None: @@ -12259,7 +12266,8 @@ - name: should_apply_proposer_boost#gloas sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost" + search: '^\s+private shouldApplyProposerBoost\(' + regex: true spec: | def should_apply_proposer_boost(store: Store) -> bool: