diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index a9f1299c4f13..e689954a3bba 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -1,5 +1,5 @@ import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {ProtoBlock, ExecutionStatus} from "@lodestar/fork-choice"; +import {ProtoBlock, MaybeValidExecutionStatus} from "@lodestar/fork-choice"; import {allForks} from "@lodestar/types"; export type FullyVerifiedBlockFlags = { @@ -22,7 +22,7 @@ export type FullyVerifiedBlockFlags = { /** * If the execution payload couldnt be verified because of EL syncing status, used in optimistic sync or for merge block */ - executionStatus?: ExecutionStatus; + executionStatus?: MaybeValidExecutionStatus; }; export type PartiallyVerifiedBlockFlags = FullyVerifiedBlockFlags & { diff --git a/packages/beacon-node/src/chain/blocks/verifyBlock.ts b/packages/beacon-node/src/chain/blocks/verifyBlock.ts index 21f1ff34751d..175143ba6501 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlock.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlock.ts @@ -11,7 +11,13 @@ import { } from "@lodestar/state-transition"; import {bellatrix} from "@lodestar/types"; import {toHexString} from "@chainsafe/ssz"; -import {IForkChoice, ProtoBlock, ExecutionStatus, assertValidTerminalPowBlock} from "@lodestar/fork-choice"; +import { + IForkChoice, + ProtoBlock, + ExecutionStatus, + MaybeValidExecutionStatus, + assertValidTerminalPowBlock, +} from "@lodestar/fork-choice"; import {IChainForkConfig} from "@lodestar/config"; import {ILogger} from "@lodestar/utils"; import {IMetrics} from "../../metrics/index.js"; @@ -128,7 +134,7 @@ export async function verifyBlockStateTransition( chain: VerifyBlockModules, partiallyVerifiedBlock: PartiallyVerifiedBlock, opts: BlockProcessOpts -): Promise<{postState: CachedBeaconStateAllForks; executionStatus: ExecutionStatus}> { +): Promise<{postState: CachedBeaconStateAllForks; executionStatus: MaybeValidExecutionStatus}> { const {block, validProposerSignature, validSignatures} = partiallyVerifiedBlock; // TODO: Skip in process chain segment @@ -185,6 +191,13 @@ export async function verifyBlockStateTransition( } } + // Parent is known to the fork-choice + const parentRoot = toHexString(block.message.parentRoot); + const parentBlock = chain.forkChoice.getBlockHex(parentRoot); + if (!parentBlock) { + throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot}); + } + let executionStatus: ExecutionStatus; if (executionPayloadEnabled) { // TODO: Handle better notifyNewPayload() returning error is syncing @@ -199,10 +212,9 @@ export async function verifyBlockStateTransition( case ExecutePayloadStatus.INVALID: { // If the parentRoot is not same as latestValidHash, then the branch from latestValidHash // to parentRoot needs to be invalidated - const parentHashHex = toHexString(block.message.parentRoot); chain.forkChoice.validateLatestHash( execResult.latestValidHash, - parentHashHex !== execResult.latestValidHash ? parentHashHex : null + parentBlock.executionPayloadBlockHash !== execResult.latestValidHash ? parentRoot : null ); throw new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, @@ -236,16 +248,13 @@ export async function verifyBlockStateTransition( // SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY ahead of the slot of the block being // imported. - const parentRoot = toHexString(block.message.parentRoot); - const parentBlock = chain.forkChoice.getBlockHex(parentRoot); const justifiedBlock = chain.forkChoice.getJustifiedBlock(); if ( - !parentBlock || // Following condition is the !(Not) of the safe import condition - (parentBlock.executionStatus === ExecutionStatus.PreMerge && - justifiedBlock.executionStatus === ExecutionStatus.PreMerge && - block.message.slot + opts.safeSlotsToImportOptimistically > chain.clock.currentSlot) + parentBlock.executionStatus === ExecutionStatus.PreMerge && + justifiedBlock.executionStatus === ExecutionStatus.PreMerge && + block.message.slot + opts.safeSlotsToImportOptimistically > chain.clock.currentSlot ) { throw new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 1ef66a322a82..06373f3ce169 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -202,8 +202,8 @@ export class ExecutionEngineHttp implements IExecutionEngine { } : undefined; - // TODO: propogate latestValidHash to the forkchoice, for now ignore it as we - // currently do not propogate the validation status up the forkchoice + // TODO: propagate latestValidHash to the forkchoice, for now ignore it as we + // currently do not propagate the validation status up the forkchoice const { payloadStatus: {status, latestValidHash: _latestValidHash, validationError}, payloadId, diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index df894662b92b..69c1a9bb4cd5 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -684,14 +684,20 @@ export class ForkChoice implements IForkChoice { } /** - * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidate till hash provided - * TODO: implementation: - * 1. verify is_merge_block if the mergeblock has not yet been validated - * 2. Throw critical error and exit if a block in finalized chain gets invalidated + * Optimistic sync validate till validated latest hash, invalidate any decendant + * branch if invalidate till hash provided + * + * Proxies to protoArray's validateLatestHash and could run extra validations for the + * justified's status as well as validate the terminal conditions if terminal block + * becomes valid */ - validateLatestHash(_latestValidHash: RootHex, _invalidateTillHash: RootHex | null): void { - // Silently ignore for now if all calls were valid - return; + validateLatestHash(latestValidExecHash: RootHex, invalidateTillBlockHash: RootHex | null): void { + this.protoArray.validateLatestHash(latestValidExecHash, invalidateTillBlockHash); + + // Call findHead to validate that the forkChoice consensus has not broken down + // as it is possible for invalidation to invalidate the entire forkChoice if + // the consensus breaks down, which will cause findHead to throw + this.protoArray.findHead(this.fcStore.justifiedCheckpoint.rootHex); } private getPreMergeExecStatus(preCachedData?: OnBlockPrecachedData): ExecutionStatus.PreMerge { diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 499b9f716861..0cea4054a943 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,7 +1,7 @@ import {EffectiveBalanceIncrements} from "@lodestar/state-transition"; import {BeaconStateAllForks} from "@lodestar/state-transition"; import {Epoch, Slot, ValidatorIndex, phase0, allForks, Root, RootHex} from "@lodestar/types"; -import {ProtoBlock, ExecutionStatus} from "../protoArray/interface.js"; +import {ProtoBlock, MaybeValidExecutionStatus} from "../protoArray/interface.js"; import {CheckpointWithHex} from "./store.js"; export type CheckpointHex = { @@ -152,7 +152,7 @@ export type OnBlockPrecachedData = { justifiedBalances?: EffectiveBalanceIncrements; /** Time in seconds when the block was received */ blockDelaySec: number; - executionStatus?: ExecutionStatus; + executionStatus?: MaybeValidExecutionStatus; }; export type LatestMessage = { diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index b4b53454c27f..0683ffff2d99 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -1,5 +1,11 @@ export {ProtoArray} from "./protoArray/protoArray.js"; -export {ProtoBlock, ProtoNode, ExecutionStatus} from "./protoArray/interface.js"; +export { + ProtoBlock, + ProtoNode, + ExecutionStatus, + MaybeValidExecutionStatus, + BlockExecution, +} from "./protoArray/interface.js"; export {ForkChoice, assertValidTerminalPowBlock} from "./forkChoice/forkChoice.js"; export {IForkChoice, OnBlockPrecachedData, PowBlockHex} from "./forkChoice/interface.js"; diff --git a/packages/fork-choice/src/protoArray/errors.ts b/packages/fork-choice/src/protoArray/errors.ts index d7d8e7fb5b13..03f30aedb251 100644 --- a/packages/fork-choice/src/protoArray/errors.ts +++ b/packages/fork-choice/src/protoArray/errors.ts @@ -16,6 +16,9 @@ export enum ProtoArrayErrorCode { INVALID_DELTA_LEN = "PROTO_ARRAY_ERROR_INVALID_DELTA_LEN", REVERTED_FINALIZED_EPOCH = "PROTO_ARRAY_ERROR_REVERTED_FINALIZED_EPOCH", INVALID_BEST_NODE = "PROTO_ARRAY_ERROR_INVALID_BEST_NODE", + INVALID_BLOCK_EXECUTION_STATUS = "PROTO_ARRAY_INVALID_BLOCK_EXECUTION_STATUS", + INVALID_PARENT_EXECUTION_STATUS = "PROTO_ARRAY_INVALID_PARENT_EXECUTION_STATUS", + INVALID_JUSTIFIED_EXECUTION_STATUS = "PROTO_ARRAY_INVALID_JUSTIFIED_EXECUTION_STATUS", } export type ProtoArrayErrorType = @@ -40,7 +43,10 @@ export type ProtoArrayErrorType = headRoot: RootHex; headJustifiedEpoch: Epoch; headFinalizedEpoch: Epoch; - }; + } + | {code: ProtoArrayErrorCode.INVALID_BLOCK_EXECUTION_STATUS; root: RootHex} + | {code: ProtoArrayErrorCode.INVALID_PARENT_EXECUTION_STATUS; root: RootHex; parent: RootHex} + | {code: ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS; root: RootHex}; export class ProtoArrayError extends LodestarError { constructor(type: ProtoArrayErrorType) { diff --git a/packages/fork-choice/src/protoArray/interface.ts b/packages/fork-choice/src/protoArray/interface.ts index 388ee3516bd7..62cee0fd2a86 100644 --- a/packages/fork-choice/src/protoArray/interface.ts +++ b/packages/fork-choice/src/protoArray/interface.ts @@ -17,10 +17,13 @@ export enum ExecutionStatus { Valid = "Valid", Syncing = "Syncing", PreMerge = "PreMerge", + Invalid = "Invalid", } -type BlockExecution = - | {executionPayloadBlockHash: RootHex; executionStatus: ExecutionStatus.Valid | ExecutionStatus.Syncing} +export type MaybeValidExecutionStatus = Exclude; + +export type BlockExecution = + | {executionPayloadBlockHash: RootHex; executionStatus: Exclude} | {executionPayloadBlockHash: null; executionStatus: ExecutionStatus.PreMerge}; /** * A block that is to be applied to the fork choice diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index e3b5191852c4..827364bd3767 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -1,6 +1,6 @@ import {Epoch, RootHex} from "@lodestar/types"; -import {ProtoBlock, ProtoNode, HEX_ZERO_HASH} from "./interface.js"; +import {ProtoBlock, ProtoNode, HEX_ZERO_HASH, ExecutionStatus} from "./interface.js"; import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js"; export const DEFAULT_PRUNE_THRESHOLD = 0; @@ -81,7 +81,7 @@ export class ProtoArray { finalizedRoot, }: { deltas: number[]; - proposerBoost: ProposerBoost | null; + proposerBoost?: ProposerBoost | null; justifiedEpoch: Epoch; justifiedRoot: RootHex; finalizedEpoch: Epoch; @@ -129,7 +129,13 @@ export class ProtoArray { this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot ? this.previousProposerBoost.score : 0; - const nodeDelta = deltas[nodeIndex] + currentBoost - previousBoost; + + // If this node's execution status has been marked invalid, then the weight of the node + // needs to be taken out of consideration + const nodeDelta = + node.executionStatus === ExecutionStatus.Invalid + ? -node.weight + : deltas[nodeIndex] + currentBoost - previousBoost; if (nodeDelta === undefined) { throw new ProtoArrayError({ @@ -190,6 +196,12 @@ export class ProtoArray { if (this.indices.has(block.blockRoot)) { return; } + if (block.executionStatus === ExecutionStatus.Invalid) { + throw new ProtoArrayError({ + code: ProtoArrayErrorCode.INVALID_BLOCK_EXECUTION_STATUS, + root: block.blockRoot, + }); + } const node: ProtoNode = { ...block, @@ -205,6 +217,12 @@ export class ProtoArray { this.nodes.push(node); let parentIndex = node.parent; + // If this node is valid, lets propagate the valid status up the chain + // and throw error if we counter invalid, as this breaks consensus + if (node.executionStatus === ExecutionStatus.Valid && parentIndex !== undefined) { + this.propagateValidExecutionStatusByIndex(parentIndex); + } + let n: ProtoNode | undefined = node; while (parentIndex !== undefined) { this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex); @@ -214,6 +232,163 @@ export class ProtoArray { } } + /** + * Optimistic sync validate till validated latest hash, invalidate any decendant branch + * if invalidate till hash provided. If consensus fails, this will invalidate entire + * forkChoice which will throw on any call to findHead + */ + validateLatestHash(latestValidExecHash: RootHex, invalidateTillBlockHash: RootHex | null): void { + // Look reverse because its highly likely node with latestValidExecHash is towards the + // the leaves of the forkchoice + // + // We can also implement the index to lookup for exec hash => proto block, but it + // still needs to be established properly (though is highly likely) than a unique + // exec hash maps to a unique beacon block. + // For more context on this please checkout the following conversation: + // https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167 + + let latestValidHashIndex = this.nodes.length - 1; + for (; latestValidHashIndex >= 0; latestValidHashIndex--) { + if (this.nodes[latestValidHashIndex].executionPayloadBlockHash === latestValidExecHash) { + // We found the block corresponding to latestValidHashIndex, exit the loop + break; + } + } + + // If we found block corresponding to latestValidExecHash, validate it and its parents + if (latestValidHashIndex >= 0) { + this.propagateValidExecutionStatusByIndex(latestValidHashIndex); + } + + if (invalidateTillBlockHash !== null) { + const invalidateTillIndex = this.indices.get(invalidateTillBlockHash); + if (invalidateTillIndex === undefined) { + throw Error(`Unable to find invalidateTillBlockHash=${invalidateTillBlockHash} in forkChoice`); + } + this.propagateInValidExecutionStatusByIndex(invalidateTillIndex, latestValidHashIndex); + } + } + + propagateValidExecutionStatusByIndex(validNodeIndex: number): void { + let node: ProtoNode | undefined = this.getNodeFromIndex(validNodeIndex); + // propagate till we keep encountering syncing status + while ( + node !== undefined && + node.executionStatus !== ExecutionStatus.Valid && + node.executionStatus !== ExecutionStatus.PreMerge + ) { + switch (node?.executionStatus) { + // If parent's execution is Invalid, we can't accept this block + // It should only happen on the leaf's parent, because we can't + // have a non invalid's parent as valid, if this happens it + // is a critical failure. So a chec + case ExecutionStatus.Invalid: + throw new ProtoArrayError({ + code: ProtoArrayErrorCode.INVALID_PARENT_EXECUTION_STATUS, + root: node.blockRoot, + parent: node.parentRoot, + }); + + case ExecutionStatus.Syncing: + // If the node is valid and its parent is marked syncing, we need to + // propagate the execution status up + node.executionStatus = ExecutionStatus.Valid; + break; + + default: + } + + node = node.parent !== undefined ? this.getNodeByIndex(node.parent) : undefined; + } + } + + /** + * Do a two pass invalidation: + * 1. we go up and mark all nodes invalid and then + * 2. we need do iterate down and mark all children of invalid nodes invalid + */ + + propagateInValidExecutionStatusByIndex(invalidateTillIndex: number, latestValidHashIndex: number): void { + /* + * invalidateAll is a flag which indicates detection of consensus failure + * if latestValidHashIndex has to be strictly parent of invalidateTillIndex + * else its consensus failure already! + */ + let invalidateAll = false; + let invalidateIndex: number | undefined = invalidateTillIndex; + + // If we haven't found latestValidHashIndex i.e. its -1, and this was an + // invalidation call back (because of presense of invalidateTillBlockHash), + // 1. It could implies whatever we have in forkchoice downwards from the last + // PreMerge block is all invalid. + // 2. If there is INVALID block and any of its ancestors is NOT_VALIDATED then EL + // can return dummy LVH + // 3. If the terminal block itself is invalid LVH will be 0x000...00 and will + // result into -1 + // + // So, like other clients, we have to be forgiving, and just invalidate the + // invalidateTillIndex. + // + // More context here in R & D discord conversation regarding how other clients are + // handling this: + // https://discord.com/channels/595666850260713488/892088344438255616/994279955036905563 + + if (latestValidHashIndex < 0) { + // This will just allow invalidation of invalidateTillIndex + latestValidHashIndex = invalidateTillIndex - 1; + } + + // Lets go invalidating, ideally latestValidHashIndex should be the parent of + // invalidateTillIndex and we should reach latestValidHashIndex by iterating + // to parents, but it might not lead to parent beacause of the conditions + // describe above + // + // So we will also be forgiving like other clients, and keep invalidating only till + // we hop on/before latestValidHashIndex by following parent + while (invalidateIndex !== undefined && invalidateIndex > latestValidHashIndex) { + const invalidNode = this.getNodeFromIndex(invalidateIndex); + if ( + invalidNode.executionStatus !== ExecutionStatus.Syncing && + invalidNode.executionStatus !== ExecutionStatus.Invalid + ) { + // This is another catastrophe, and indicates consensus failure + // the entire forkchoice should be invalidated + invalidateAll = true; + } + invalidNode.executionStatus = ExecutionStatus.Invalid; + // Time to propagate up + invalidateIndex = invalidNode.parent; + } + + // Pass 2: mark all children of invalid nodes as invalid + let nodeIndex = 1; + while (nodeIndex < this.nodes.length) { + const node = this.getNodeFromIndex(nodeIndex); + const parent = node.parent !== undefined ? this.getNodeByIndex(node.parent) : undefined; + // Only invalidate if this is post merge, and either parent is invalid or the + // concensus has failed + if ( + node.executionStatus !== ExecutionStatus.PreMerge && + (invalidateAll || parent?.executionStatus === ExecutionStatus.Invalid) + ) { + node.executionStatus = ExecutionStatus.Invalid; + node.bestChild = undefined; + node.bestDescendant = undefined; + } + nodeIndex++; + } + + // update the forkchoice + this.applyScoreChanges({ + deltas: Array.from({length: this.indices.size}, () => 0), + proposerBoost: this.previousProposerBoost, + justifiedEpoch: this.justifiedEpoch, + justifiedRoot: this.justifiedRoot, + finalizedEpoch: this.finalizedEpoch, + finalizedRoot: this.finalizedRoot, + }); + } + /** * Follows the best-descendant links to find the best-block (i.e., head-block). */ @@ -234,6 +409,13 @@ export class ProtoArray { }); } + if (justifiedNode.executionStatus === ExecutionStatus.Invalid) { + throw new ProtoArrayError({ + code: ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS, + root: justifiedNode.blockRoot, + }); + } + const bestDescendantIndex = justifiedNode.bestDescendant ?? justifiedIndex; const bestNode = this.nodes[bestDescendantIndex]; @@ -482,6 +664,7 @@ export class ProtoArray { * head. */ nodeIsViableForHead(node: ProtoNode): boolean { + if (node.executionStatus === ExecutionStatus.Invalid) return false; const correctJustified = (node.justifiedEpoch === this.justifiedEpoch && node.justifiedRoot === this.justifiedRoot) || this.justifiedEpoch === 0; @@ -672,7 +855,7 @@ export class ProtoArray { return this.indices.size; } - private getNodeFromIndex(index: number): ProtoNode { + getNodeFromIndex(index: number): ProtoNode { const node = this.nodes[index]; if (node === undefined) { throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index}); diff --git a/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts new file mode 100644 index 000000000000..2a39fd108450 --- /dev/null +++ b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts @@ -0,0 +1,248 @@ +import {expect} from "chai"; +import {ProtoArray, ExecutionStatus, MaybeValidExecutionStatus, BlockExecution} from "../../../src/index.js"; + +type ValidationTestCase = { + root: string; + bestChild?: string; + bestDescendant?: string; + executionStatus: ExecutionStatus | undefined; +}; + +describe("executionStatus updates", () => { + const blocks: {slot: number; root: string; parent: string; executionStatus: MaybeValidExecutionStatus}[] = [ + {slot: 1, root: "1A", parent: "0", executionStatus: ExecutionStatus.Syncing}, + {slot: 2, root: "2A", parent: "1A", executionStatus: ExecutionStatus.Syncing}, + {slot: 3, root: "3A", parent: "2A", executionStatus: ExecutionStatus.Syncing}, + {slot: 2, root: "2B", parent: "1A", executionStatus: ExecutionStatus.Syncing}, + {slot: 2, root: "3B", parent: "2B", executionStatus: ExecutionStatus.Syncing}, + {slot: 2, root: "2C", parent: "none", executionStatus: ExecutionStatus.Syncing}, + {slot: 3, root: "3C", parent: "2C", executionStatus: ExecutionStatus.Syncing}, + ]; + + const fc = ProtoArray.initialize({ + slot: 0, + stateRoot: "-", + parentRoot: "-", + blockRoot: "0", + + justifiedEpoch: 0, + justifiedRoot: "-", + finalizedEpoch: 0, + finalizedRoot: "-", + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, + }); + + function collectProtoarrayValidationStatus(): ValidationTestCase[] { + const expectedForkChoice: ValidationTestCase[] = []; + + for (const block of blocks) { + const blockNode = fc.getNode(block.root); + const bestChild = + blockNode?.bestChild !== undefined ? fc.getNodeFromIndex(blockNode.bestChild).blockRoot : undefined; + const bestDescendant = + blockNode?.bestDescendant !== undefined ? fc.getNodeFromIndex(blockNode.bestDescendant).blockRoot : undefined; + expectedForkChoice.push({ + root: block.root, + bestChild, + bestDescendant, + executionStatus: blockNode?.executionStatus, + }); + } + return expectedForkChoice; + } + + for (const block of blocks) { + const executionData = (block.executionStatus === ExecutionStatus.PreMerge + ? {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge} + : {executionPayloadBlockHash: block.root, executionStatus: block.executionStatus}) as BlockExecution; + fc.onBlock({ + slot: block.slot, + blockRoot: block.root, + parentRoot: block.parent, + stateRoot: "-", + targetRoot: "-", + + justifiedEpoch: 0, + justifiedRoot: "-", + finalizedEpoch: 0, + finalizedRoot: "-", + + ...executionData, + }); + } + + const deltas = Array.from({length: fc.nodes.length}, () => 0); + fc.applyScoreChanges({ + deltas, + proposerBoost: null, + justifiedEpoch: 0, + justifiedRoot: "-", + finalizedEpoch: 0, + finalizedRoot: "-", + }); + + const preValidationForkChoice = collectProtoarrayValidationStatus(); + + it("preValidation forkchoice setup should be correct", () => { + expect(preValidationForkChoice).to.be.deep.equal([ + {root: "1A", bestChild: "2B", bestDescendant: "3B", executionStatus: ExecutionStatus.Syncing}, + {root: "2A", bestChild: "3A", bestDescendant: "3A", executionStatus: ExecutionStatus.Syncing}, + {root: "3A", bestChild: undefined, bestDescendant: undefined, executionStatus: ExecutionStatus.Syncing}, + {root: "2B", bestChild: "3B", bestDescendant: "3B", executionStatus: ExecutionStatus.Syncing}, + {root: "3B", bestChild: undefined, bestDescendant: undefined, executionStatus: ExecutionStatus.Syncing}, + {root: "2C", bestChild: "3C", bestDescendant: "3C", executionStatus: ExecutionStatus.Syncing}, + {root: "3C", bestChild: undefined, bestDescendant: undefined, executionStatus: ExecutionStatus.Syncing}, + ]); + }); + + // Invalidate 3C but validate 2C with parent none which is not present in forkchoice + fc.validateLatestHash("2C", "3C"); + + const invalidate3CValidate2CForkChoice = collectProtoarrayValidationStatus(); + it("correcly invalidate 3C and validate 2C only", () => { + expect(invalidate3CValidate2CForkChoice).to.be.deep.equal([ + { + root: "1A", + bestChild: "2B", + bestDescendant: "3B", + executionStatus: "Syncing", + }, + { + root: "2A", + bestChild: "3A", + bestDescendant: "3A", + executionStatus: "Syncing", + }, + { + root: "3A", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Syncing", + }, + { + root: "2B", + bestChild: "3B", + bestDescendant: "3B", + executionStatus: "Syncing", + }, + { + root: "3B", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Syncing", + }, + { + root: "2C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Valid", + }, + { + root: "3C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Invalid", + }, + ]); + }); + + // Validate 3B, 2B, 1A (premerge) + fc.validateLatestHash("3B", null); + const validate3B2B1A = collectProtoarrayValidationStatus(); + it("Validate 3B, 2B, 1A", () => { + expect(validate3B2B1A).to.be.deep.equal([ + { + root: "1A", + bestChild: "2B", + bestDescendant: "3B", + executionStatus: "Valid", + }, + { + root: "2A", + bestChild: "3A", + bestDescendant: "3A", + executionStatus: "Syncing", + }, + { + root: "3A", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Syncing", + }, + { + root: "2B", + bestChild: "3B", + bestDescendant: "3B", + executionStatus: "Valid", + }, + { + root: "3B", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Valid", + }, + { + root: "2C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Valid", + }, + { + root: "3C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Invalid", + }, + ]); + }); + + fc.validateLatestHash("1A", "3A"); + const invalidate3A2A = collectProtoarrayValidationStatus(); + it("Invalidate 3A, 2A with 2A loosing its bestChild, bestDescendant", () => { + expect(invalidate3A2A).to.be.deep.equal([ + { + root: "1A", + bestChild: "2B", + bestDescendant: "3B", + executionStatus: "Valid", + }, + { + root: "2A", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Invalid", + }, + { + root: "3A", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Invalid", + }, + { + root: "2B", + bestChild: "3B", + bestDescendant: "3B", + executionStatus: "Valid", + }, + { + root: "3B", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Valid", + }, + { + root: "2C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Valid", + }, + { + root: "3C", + bestChild: undefined, + bestDescendant: undefined, + executionStatus: "Invalid", + }, + ]); + }); +});