fix: fix proposer boost reorg by suppressing fcu call during import (#7810)

We need to suppress fcu call during gossip block import when the
incoming block is weak. This is because once notified EL, it will treat
the new block as head and will not reorg back to parent if we try to
reorg.

- Add `shouldOverrideForkChoiceUpdate`
- Call `shouldOverrideForkChoiceUpdate` during gossip block import, and
skip fcu call if result is true
- Modify `predictProposerHead` to be a simple wrapper around
`shouldOverrideForkChoiceUpdate`


Closes #7235

cc. @twoeths @nflaig

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
This commit is contained in:
NC
2025-07-21 15:36:30 -07:00
committed by GitHub
parent ce27087f34
commit 899163f126
9 changed files with 406 additions and 28 deletions

View File

@@ -1,6 +1,12 @@
import {BitArray} from "@chainsafe/ssz";
import {routes} from "@lodestar/api";
import {AncestorStatus, EpochDifference, ForkChoiceError, ForkChoiceErrorCode} from "@lodestar/fork-choice";
import {
AncestorStatus,
EpochDifference,
ForkChoiceError,
ForkChoiceErrorCode,
NotReorgedReason,
} from "@lodestar/fork-choice";
import {
ForkPostAltair,
ForkPostElectra,
@@ -15,6 +21,8 @@ import {
RootCache,
computeEpochAtSlot,
computeStartSlotAtEpoch,
isExecutionStateType,
isStartSlotOfEpoch,
isStateValidatorsNodesPopulated,
} from "@lodestar/state-transition";
import {Attestation, BeaconBlock, altair, capella, electra, phase0, ssz} from "@lodestar/types";
@@ -313,9 +321,64 @@ export async function importBlock(
// Notifying EL of head and finalized updates as below is usually done within the 1st 4s of the slot.
// If there is an advanced payload generation in the next slot, we'll notify EL again 4s before next
// slot via PrepareNextSlotScheduler. There is no harm updating the ELs with same data, it will just ignore it.
// Suppress fcu call if shouldOverrideFcu is true. This only happens if we have proposer boost reorg enabled
// and the block is weak and can potentially be reorged out.
let shouldOverrideFcu = false;
let notOverrideFcuReason = NotReorgedReason.Unknown;
if (opts.isGossipBlock && isExecutionStateType(postState)) {
const proposalSlot = blockSlot + 1;
try {
const proposerIndex = postState.epochCtx.getBeaconProposer(proposalSlot);
const feeRecipient = this.beaconProposerCache.get(proposerIndex);
const {currentSlot} = this.clock;
if (feeRecipient) {
// We would set this to true if
// 1) This is a gossip block
// 2) We are proposer of next slot
// 3) Proposer boost reorg related flag is turned on (this is checked inside the function)
// 4) Block meets the criteria of being re-orged out (this is also checked inside the function)
const result = this.forkChoice.shouldOverrideForkChoiceUpdate(
blockSummary.blockRoot,
this.clock.secFromSlot(currentSlot),
currentSlot
);
shouldOverrideFcu = result.shouldOverrideFcu;
if (!result.shouldOverrideFcu) {
notOverrideFcuReason = result.reason;
}
} else {
notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot;
}
} catch (e) {
if (isStartSlotOfEpoch(proposalSlot)) {
notOverrideFcuReason = NotReorgedReason.NotShufflingStable;
} else {
this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
}
}
}
if (shouldOverrideFcu) {
this.logger.verbose("Weak block detected. Skip fcu call in importBlock", {
blockRoot: blockRootHex,
slot: blockSlot,
});
} else {
this.metrics?.importBlock.notOverrideFcuReason.inc({reason: notOverrideFcuReason});
this.logger.verbose("Strong block detected. Not override fcu call", {
blockRoot: blockRootHex,
slot: blockSlot,
reason: notOverrideFcuReason,
});
}
if (
!this.opts.disableImportExecutionFcU &&
(newHead.blockRoot !== oldHead.blockRoot || currFinalizedEpoch !== prevFinalizedEpoch)
(newHead.blockRoot !== oldHead.blockRoot || currFinalizedEpoch !== prevFinalizedEpoch) &&
!shouldOverrideFcu
) {
/**
* On post BELLATRIX_EPOCH but pre TTD, blocks include empty execution payload with a zero block hash.

View File

@@ -196,6 +196,8 @@ export type ImportBlockOpts = {
seenTimestampSec?: number;
/** Set to true if persist block right at verification time */
eagerPersistBlock?: boolean;
/** Set to true if the importing block is from gossip */
isGossipBlock?: boolean;
};
/**

View File

@@ -841,9 +841,10 @@ export class BeaconChain implements IBeaconChain {
predictProposerHead(slot: Slot): ProtoBlock {
this.metrics?.forkChoice.requests.inc();
const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.predictProposerHead});
const secFromSlot = this.clock.secFromSlot(slot);
try {
return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetPredictedProposerHead, slot}).head;
return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetPredictedProposerHead, secFromSlot, slot}).head;
} catch (e) {
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead});
throw e;

View File

@@ -1,3 +1,4 @@
import {NotReorgedReason} from "@lodestar/fork-choice";
import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
import {BlobsSource, BlockSource} from "../../chain/blocks/types.js";
import {JobQueueItemType} from "../../chain/bls/index.js";
@@ -744,6 +745,11 @@ export function createLodestarMetrics(
help: "Total number of imported blobs by source",
labelNames: ["blobsSource"],
}),
notOverrideFcuReason: register.counter<{reason: NotReorgedReason}>({
name: "lodestar_import_block_not_override_fcu_reason_total",
help: "Reason why the fcu call is not suppressed during block import",
labelNames: ["reason"],
}),
},
engineNotifyNewPayloadResult: register.gauge<{result: ExecutionPayloadStatus}>({
name: "lodestar_execution_engine_notify_new_payload_result_total",

View File

@@ -303,6 +303,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
seenTimestampSec,
// gossip block is validated, we want to process it asap
eagerPersistBlock: true,
isGossipBlock: true,
})
.then(() => {
// Returns the delay between the start of `block.slot` and `current time`

View File

@@ -324,6 +324,21 @@ const forkChoiceTest =
`Invalid proposer head at step ${i}`
);
}
if (step.checks.should_override_forkchoice_update) {
const currentSlot = Math.floor(tickTime / config.SECONDS_PER_SLOT);
const result = chain.forkChoice.shouldOverrideForkChoiceUpdate(
head.blockRoot,
tickTime % config.SECONDS_PER_SLOT,
currentSlot
);
if (result.shouldOverrideFcu === false) {
logger.debug(`Not override fcu reason ${result.reason} at step ${i}`);
}
expect({result: result.shouldOverrideFcu, validator_is_connected: true}).toEqualWithMessage(
step.checks.should_override_forkchoice_update,
`Invalid should override fcu result at step ${i}`
);
}
}
// None of the above
@@ -487,6 +502,10 @@ type Checks = {
finalized_checkpoint?: SpecTestCheckpoint;
proposer_boost_root?: RootHex;
get_proposer_head?: string;
should_override_forkchoice_update?: {
validator_is_connected: boolean;
result: boolean;
};
};
};

View File

@@ -52,6 +52,7 @@ import {
LatestMessage,
NotReorgedReason,
PowBlockHex,
ShouldOverrideForkChoiceUpdateResult,
} from "./interface.js";
import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js";
@@ -70,7 +71,7 @@ export enum UpdateHeadOpt {
export type UpdateAndGetHeadOpt =
| {mode: UpdateHeadOpt.GetCanonicialHead}
| {mode: UpdateHeadOpt.GetProposerHead; secFromSlot: number; slot: Slot}
| {mode: UpdateHeadOpt.GetPredictedProposerHead; slot: Slot};
| {mode: UpdateHeadOpt.GetPredictedProposerHead; secFromSlot: number; slot: Slot};
/**
* Provides an implementation of "Ethereum Consensus -- Beacon Chain Fork Choice":
@@ -207,7 +208,7 @@ export class ForkChoice implements IForkChoice {
const canonicialHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead();
switch (mode) {
case UpdateHeadOpt.GetPredictedProposerHead:
return {head: this.predictProposerHead(canonicialHeadBlock, opt.slot)};
return {head: this.predictProposerHead(canonicialHeadBlock, opt.secFromSlot, opt.slot)};
case UpdateHeadOpt.GetProposerHead: {
const {
proposerHead: head,
@@ -223,6 +224,60 @@ export class ForkChoice implements IForkChoice {
}
}
// Called by `predictProposerHead` and `importBlock`. If the result is not same as blockRoot's block, return true else false
// See https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/bellatrix/fork-choice.md#should_override_forkchoice_update
// Return true if the given block passes all criteria to be re-orged out
// Return false otherwise.
// Note when proposer boost reorg is disabled, it always returns false
shouldOverrideForkChoiceUpdate(
blockRoot: RootHex,
secFromSlot: number,
currentSlot: Slot
): ShouldOverrideForkChoiceUpdateResult {
const headBlock = this.getBlockHex(blockRoot);
if (headBlock === null) {
// should not happen because this block just got imported. Fall back to no-reorg.
return {shouldOverrideFcu: false, reason: NotReorgedReason.HeadBlockNotAvailable};
}
const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
// Skip re-org attempt if proposer boost (reorg) are disabled
if (!proposerBoost || !proposerBoostReorg) {
this.logger?.verbose("Skip shouldOverrideForkChoiceUpdate check since the related flags are disabled", {
slot: currentSlot,
proposerBoost,
proposerBoostReorg,
});
return {shouldOverrideFcu: false, reason: NotReorgedReason.ProposerBoostReorgDisabled};
}
const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
const proposalSlot = headBlock.slot + 1;
// No reorg if parentBlock isn't available
if (parentBlock === undefined) {
return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
}
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
headBlock,
parentBlock,
proposalSlot
);
if (prelimProposerHead === headBlock) {
return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
}
const currentTimeOk =
headBlock.slot === currentSlot || (proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot));
if (!currentTimeOk) {
return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
}
this.logger?.verbose("Block is weak. Should override forkchoice update", {blockRoot, slot: currentSlot});
return {shouldOverrideFcu: true, parentBlock};
}
/**
* Get the proposer boost root
*/
@@ -239,37 +294,32 @@ export class ForkChoice implements IForkChoice {
*
* By calling this function, we assume we are the proposer of next slot
*
* https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/bellatrix/fork-choice.md#should_override_forkchoice_update
*/
predictProposerHead(headBlock: ProtoBlock, currentSlot?: Slot): ProtoBlock {
predictProposerHead(headBlock: ProtoBlock, secFromSlot: number, currentSlot: Slot): ProtoBlock {
// Skip re-org attempt if proposer boost (reorg) are disabled
if (!this.opts?.proposerBoost || !this.opts?.proposerBoostReorg) {
this.logger?.verbose("No proposer boot reorg prediction since the related flags are disabled");
return headBlock;
}
const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
const proposalSlot = headBlock.slot + 1;
currentSlot = currentSlot ?? this.fcStore.currentSlot;
// No reorg if parentBlock isn't available
if (parentBlock === undefined) {
if (headBlock.slot === currentSlot) {
// If head block aka the head cache is current, that means `updateHead` is called during gossip handling,
// that can only happen if shouldOverrideForkChoiceUpdate = false so no reorg
return headBlock;
}
const {prelimProposerHead} = this.getPreliminaryProposerHead(headBlock, parentBlock, proposalSlot);
const blockRoot = headBlock.blockRoot;
const result = this.shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot);
if (prelimProposerHead === headBlock) {
return headBlock;
if (result.shouldOverrideFcu) {
this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
blockRoot,
slot: currentSlot,
});
return result.parentBlock;
}
const currentTimeOk = headBlock.slot === currentSlot;
if (!currentTimeOk) {
return headBlock;
}
this.logger?.info("Current head is weak. Predicting next block to be built on parent of head");
return parentBlock;
return headBlock;
}
/**
@@ -307,10 +357,8 @@ export class ForkChoice implements IForkChoice {
return {proposerHead, isHeadTimely, notReorgedReason: prelimNotReorgedReason};
}
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_proposing_on_time
const proposerReorgCutoff = this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT / 2;
const isProposingOnTime = secFromSlot <= proposerReorgCutoff;
if (!isProposingOnTime) {
// Only re-org if we are proposing on-time
if (!this.isProposingOnTime(secFromSlot)) {
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.NotProposingOnTime};
}
@@ -1133,6 +1181,14 @@ export class ForkChoice implements IForkChoice {
return this.fcStore.currentSlot === block.slot && isBeforeAttestingInterval;
}
/**
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
*/
private isProposingOnTime(secFromSlot: number): boolean {
const proposerReorgCutoff = this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT / 2;
return secFromSlot <= proposerReorgCutoff;
}
private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
if (executionStatus !== ExecutionStatus.PreMerge)
throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);

View File

@@ -63,8 +63,11 @@ export enum NotReorgedReason {
ReorgMoreThanOneSlot = "reorgMoreThanOneSlot",
ProposerBoostNotWornOff = "proposerBoostNotWornOff",
HeadBlockNotWeak = "headBlockNotWeak",
ParentBlockNotStrong = "ParentBlockNotStrong",
ParentBlockNotStrong = "parentBlockNotStrong",
NotProposingOnTime = "notProposingOnTime",
NotProposerOfNextSlot = "notProposerOfNextSlot",
HeadBlockNotAvailable = "headBlockNotAvailable", // Should not happen because head block should be in cache
Unknown = "unknown", // A placeholder in case reason is not provided
}
export type ForkChoiceMetrics = {
@@ -76,6 +79,10 @@ export type ForkChoiceMetrics = {
indices: number;
};
export type ShouldOverrideForkChoiceUpdateResult =
| {shouldOverrideFcu: true; parentBlock: ProtoBlock}
| {shouldOverrideFcu: false; reason: NotReorgedReason};
export interface IForkChoice {
irrecoverableError?: Error;
@@ -107,6 +114,16 @@ export interface IForkChoice {
isHeadTimely?: boolean;
notReorgedReason?: NotReorgedReason;
};
/**
* This is called during block import when proposerBoostReorg is enabled
* fcu call in `importBlock()` will be suppressed if this returns true. It is also
* called by `predictProposerHead()` during `prepareNextSlot()`.
*/
shouldOverrideForkChoiceUpdate(
blockRoot: RootHex,
secFromSlot: number,
currentSlot: Slot
): ShouldOverrideForkChoiceUpdateResult;
/**
* Retrieves all possible chain heads (leaves of fork choice tree).
*/

View File

@@ -0,0 +1,213 @@
import {fromHexString} from "@chainsafe/ssz";
import {config} from "@lodestar/config/default";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {DataAvailabilityStatus} from "@lodestar/state-transition";
import {Slot} from "@lodestar/types";
import {toHex} from "@lodestar/utils";
import {beforeEach, describe, expect, it} from "vitest";
import {NotReorgedReason} from "../../../src/forkChoice/interface.js";
import {ExecutionStatus, ForkChoice, IForkChoiceStore, ProtoArray, ProtoBlock} from "../../../src/index.js";
import {getBlockRoot, getStateRoot} from "../../utils/index.js";
type ProtoBlockWithWeight = ProtoBlock & {weight: number}; // weight of the block itself
describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => {
const genesisSlot = 0;
const genesisEpoch = 0;
const genesisRoot = "0x0000000000000000000000000000000000000000000000000000000000000000";
const parentSlot = genesisSlot + 1;
const headSlot = genesisSlot + 2;
let protoArr: ProtoArray;
const genesisBlock: Omit<ProtoBlock, "targetRoot"> = {
slot: genesisSlot,
stateRoot: getStateRoot(genesisSlot),
parentRoot: toHex(Buffer.alloc(32, 0xff)),
blockRoot: getBlockRoot(genesisSlot),
justifiedEpoch: genesisEpoch,
justifiedRoot: genesisRoot,
finalizedEpoch: genesisEpoch,
finalizedRoot: genesisRoot,
unrealizedJustifiedEpoch: genesisEpoch,
unrealizedJustifiedRoot: genesisRoot,
unrealizedFinalizedEpoch: genesisEpoch,
unrealizedFinalizedRoot: genesisRoot,
executionPayloadBlockHash: null,
executionStatus: ExecutionStatus.PreMerge,
timeliness: false,
dataAvailabilityStatus: DataAvailabilityStatus.PreData,
};
const baseHeadBlock: ProtoBlockWithWeight = {
slot: headSlot,
stateRoot: getStateRoot(headSlot),
parentRoot: getBlockRoot(parentSlot),
blockRoot: getBlockRoot(headSlot),
targetRoot: getBlockRoot(headSlot),
justifiedEpoch: genesisEpoch,
justifiedRoot: genesisRoot,
finalizedEpoch: genesisEpoch,
finalizedRoot: genesisRoot,
unrealizedJustifiedEpoch: genesisEpoch,
unrealizedJustifiedRoot: genesisRoot,
unrealizedFinalizedEpoch: genesisEpoch,
unrealizedFinalizedRoot: genesisRoot,
executionPayloadBlockHash: null,
executionStatus: ExecutionStatus.PreMerge,
timeliness: false,
weight: 29,
dataAvailabilityStatus: DataAvailabilityStatus.PreData,
};
const baseParentHeadBlock: ProtoBlockWithWeight = {
slot: parentSlot,
stateRoot: getStateRoot(parentSlot),
parentRoot: getBlockRoot(genesisSlot),
blockRoot: getBlockRoot(parentSlot),
targetRoot: getBlockRoot(parentSlot),
justifiedEpoch: genesisEpoch,
justifiedRoot: genesisRoot,
finalizedEpoch: genesisEpoch,
finalizedRoot: genesisRoot,
unrealizedJustifiedEpoch: genesisEpoch,
unrealizedJustifiedRoot: genesisRoot,
unrealizedFinalizedEpoch: genesisEpoch,
unrealizedFinalizedRoot: genesisRoot,
executionPayloadBlockHash: null,
executionStatus: ExecutionStatus.PreMerge,
timeliness: false,
weight: 212, // 240 - 29 + 1
dataAvailabilityStatus: DataAvailabilityStatus.PreData,
};
const fcStore: IForkChoiceStore = {
currentSlot: genesisSlot + 1,
justified: {
checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot},
balances: new Uint16Array(Array(32).fill(150)),
totalBalance: 32 * 150,
},
unrealizedJustified: {
checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot},
balances: new Uint16Array(Array(32).fill(150)),
},
finalizedCheckpoint: {
epoch: genesisEpoch,
root: fromHexString(genesisBlock.blockRoot),
rootHex: genesisBlock.blockRoot,
},
unrealizedFinalizedCheckpoint: {
epoch: genesisEpoch,
root: fromHexString(genesisBlock.blockRoot),
rootHex: genesisBlock.blockRoot,
},
justifiedBalancesGetter: () => new Uint16Array(Array(32).fill(150)),
equivocatingIndices: new Set(),
};
const testCases: {
id: string;
parentBlock: ProtoBlockWithWeight;
headBlock: ProtoBlockWithWeight;
expectReorg: boolean;
currentSlot?: Slot;
expectedNotReorgedReason?: NotReorgedReason;
}[] = [
{
id: "Case that meets all conditions to be re-orged",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock},
expectReorg: true,
},
{
id: "No reorg when head block is timely",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, timeliness: true},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely,
},
{
id: "No reorg when proposal slot is at epoch boundary",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, slot: SLOTS_PER_EPOCH * 2 - 1}, // Proposal slot = block slot + 1
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.NotShufflingStable,
},
{
id: "No reorg when the blocks are not ffg competitive",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, unrealizedJustifiedEpoch: 1},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.NotFFGCompetitive,
},
{
id: "No reorg when the blocks are not ffg competitive 2",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, unrealizedJustifiedRoot: "-"},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.NotFFGCompetitive,
},
{
id: "No reorg if long unfinality",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock},
expectReorg: false,
currentSlot: (genesisEpoch + 2) * SLOTS_PER_EPOCH + 1,
expectedNotReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot, // TODO: To make it such that it returns NotReorgedReason.ChainLongUnfinality
},
{
id: "No reorg if reorg spans more than a single slot",
parentBlock: {...baseParentHeadBlock},
headBlock: {...baseHeadBlock, slot: headSlot + 1},
expectReorg: false,
expectedNotReorgedReason: NotReorgedReason.ParentBlockDistanceMoreThanOneSlot,
},
];
beforeEach(() => {
protoArr = ProtoArray.initialize(genesisBlock, genesisSlot);
});
for (const {
id,
parentBlock,
headBlock,
expectReorg,
currentSlot: blockSeenSlot,
expectedNotReorgedReason,
} of testCases) {
it(id, async () => {
protoArr.onBlock(parentBlock, parentBlock.slot);
protoArr.onBlock(headBlock, headBlock.slot);
const secFromSlot = 0;
const currentSlot = blockSeenSlot ?? headBlock.slot;
const forkChoice = new ForkChoice(config, fcStore, protoArr, {
proposerBoost: true,
proposerBoostReorg: true,
});
const result = forkChoice.shouldOverrideForkChoiceUpdate(headBlock.blockRoot, secFromSlot, currentSlot);
expect(result.shouldOverrideFcu).toBe(expectReorg);
if (result.shouldOverrideFcu) {
expect(result.parentBlock.blockRoot).toBe(parentBlock.blockRoot);
} else {
expect(result.reason).toBe(expectedNotReorgedReason);
}
});
}
});