@lodestar/fork-choice 1.51.0-dev.fa491bf50f → 1.51.0-dev.fb216e169a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.51.0-dev.fa491bf50f",
14
+ "version": "1.51.0-dev.fb216e169a",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@chainsafe/ssz": "^1.9.0",
43
- "@lodestar/config": "^1.51.0-dev.fa491bf50f",
44
- "@lodestar/params": "^1.51.0-dev.fa491bf50f",
45
- "@lodestar/state-transition": "^1.51.0-dev.fa491bf50f",
46
- "@lodestar/types": "^1.51.0-dev.fa491bf50f",
47
- "@lodestar/utils": "^1.51.0-dev.fa491bf50f"
43
+ "@lodestar/config": "^1.51.0-dev.fb216e169a",
44
+ "@lodestar/params": "^1.51.0-dev.fb216e169a",
45
+ "@lodestar/state-transition": "^1.51.0-dev.fb216e169a",
46
+ "@lodestar/types": "^1.51.0-dev.fb216e169a",
47
+ "@lodestar/utils": "^1.51.0-dev.fb216e169a"
48
48
  },
49
49
  "keywords": [
50
50
  "ethereum",
@@ -52,5 +52,5 @@
52
52
  "beacon",
53
53
  "blockchain"
54
54
  ],
55
- "gitHead": "914f9fa30474e163bf832b4e11aaf63573c770d1"
55
+ "gitHead": "b81476f744c1ac8b7cc16d6fa42f23b77eb0eb0f"
56
56
  }
@@ -6,13 +6,15 @@ export enum InvalidBlockCode {
6
6
  FUTURE_SLOT = "FUTURE_SLOT",
7
7
  FINALIZED_SLOT = "FINALIZED_SLOT",
8
8
  NOT_FINALIZED_DESCENDANT = "NOT_FINALIZED_DESCENDANT",
9
+ PARENT_EXECUTION_INVALID = "PARENT_EXECUTION_INVALID",
9
10
  }
10
11
 
11
12
  export type InvalidBlock =
12
13
  | {code: InvalidBlockCode.UNKNOWN_PARENT; root: RootHex; hash: RootHex | null}
13
14
  | {code: InvalidBlockCode.FUTURE_SLOT; currentSlot: Slot; blockSlot: Slot}
14
15
  | {code: InvalidBlockCode.FINALIZED_SLOT; finalizedSlot: Slot; blockSlot: Slot}
15
- | {code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT; finalizedRoot: RootHex; blockAncestor?: RootHex};
16
+ | {code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT; finalizedRoot: RootHex; blockAncestor?: RootHex}
17
+ | {code: InvalidBlockCode.PARENT_EXECUTION_INVALID; root: RootHex; hash: RootHex | null};
16
18
 
17
19
  export enum InvalidAttestationCode {
18
20
  /**
@@ -693,7 +693,20 @@ export class ForkChoice implements IForkChoice {
693
693
  }
694
694
 
695
695
  // findHead returns the ProtoNode representing the head
696
- const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
696
+ let head: ProtoNode;
697
+ try {
698
+ head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
699
+ } catch (e) {
700
+ // A poisoned tree never recovers, shut the node down instead of failing every head computation
701
+ if (
702
+ e instanceof ProtoArrayError &&
703
+ (e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE ||
704
+ e.type.code === ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS)
705
+ ) {
706
+ this.irrecoverableError = e;
707
+ }
708
+ throw e;
709
+ }
697
710
 
698
711
  this.head = head;
699
712
  return this.head;
@@ -753,6 +766,9 @@ export class ForkChoice implements IForkChoice {
753
766
  *
754
767
  * `justifiedBalances` balances of justified state which is updated synchronously.
755
768
  * This ensures that the forkchoice is never out of sync.
769
+ *
770
+ * A block whose parent payload chain was invalidated is rejected. `executionStatus` is ignored for a gloas
771
+ * block: it has no payload of its own and inherits the status of the parent variant it builds on.
756
772
  */
757
773
  onBlock(
758
774
  block: BeaconBlock,
@@ -781,6 +797,19 @@ export class ForkChoice implements IForkChoice {
781
797
  });
782
798
  }
783
799
 
800
+ // An execution engine verdict can invalidate the parent while this block is being verified. Inserting the
801
+ // block would put a viable node below an invalidated one, or poison the tree if this block is VALID
802
+ if (parentBlock.executionStatus === ExecutionStatus.Invalid) {
803
+ throw new ForkChoiceError({
804
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
805
+ err: {
806
+ code: InvalidBlockCode.PARENT_EXECUTION_INVALID,
807
+ root: parentRootHex,
808
+ hash: parentHashHex,
809
+ },
810
+ });
811
+ }
812
+
784
813
  // Blocks cannot be in the future. If they are, their consideration must be delayed until
785
814
  // the are in the past.
786
815
  //
@@ -942,7 +971,9 @@ export class ForkChoice implements IForkChoice {
942
971
  executionPayloadBlockHash: parentBlockHashFromBid,
943
972
  executionPayloadNumber: parentMeta.number,
944
973
  executionPayloadGasLimit: parentMeta.gasLimit,
945
- executionStatus: this.getPostMergeExecStatus(executionStatus),
974
+ // The parent's default (PENDING) variant does not reflect a SYNCING parent payload, inheriting
975
+ // from the variant matching the bid's parent block hash keeps that payload from being marked VALID
976
+ executionStatus: this.getPostMergeExecStatus(parentBlock.executionStatus),
946
977
  dataAvailabilityStatus,
947
978
  };
948
979
  })()
@@ -1003,14 +1034,8 @@ export class ForkChoice implements IForkChoice {
1003
1034
  *
1004
1035
  * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
1005
1036
  *
1006
- * It only approximates the specification since it does not perform
1007
- * `is_valid_indexed_attestation` since that should already have been called upstream and it's
1008
- * too expensive to call again.
1009
- *
1010
- * ## Notes:
1011
- *
1012
- * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
1013
- * will not be run here.
1037
+ * Does not run `is_valid_indexed_attestation`. The caller must ensure all of its
1038
+ * validity requirements except index ordering: `attestingIndices` may be unsorted.
1014
1039
  */
1015
1040
  onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void {
1016
1041
  // Ignore any attestations to the zero hash.
@@ -1149,6 +1174,19 @@ export class ForkChoice implements IForkChoice {
1149
1174
  executionStatus: PayloadExecutionStatus,
1150
1175
  dataAvailabilityStatus: DataAvailabilityStatus
1151
1176
  ): void {
1177
+ // Same guard as onBlock: the PENDING variant carries the status of the payload chain this block builds on
1178
+ const pendingBlock = this.getBlockHexDefaultStatus(blockRoot);
1179
+ if (pendingBlock?.executionStatus === ExecutionStatus.Invalid) {
1180
+ throw new ForkChoiceError({
1181
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
1182
+ err: {
1183
+ code: InvalidBlockCode.PARENT_EXECUTION_INVALID,
1184
+ root: pendingBlock.parentRoot,
1185
+ hash: pendingBlock.executionPayloadBlockHash,
1186
+ },
1187
+ });
1188
+ }
1189
+
1152
1190
  this.protoArray.onExecutionPayload(
1153
1191
  blockRoot,
1154
1192
  this.fcStore.currentSlot,
@@ -1547,7 +1585,9 @@ export class ForkChoice implements IForkChoice {
1547
1585
 
1548
1586
  /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
1549
1587
  getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] {
1550
- return this.protoArray.nodes.filter((node) => node.parentRoot === parentRoot);
1588
+ return this.protoArray.nodes.filter(
1589
+ (node) => node.parentRoot === parentRoot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)
1590
+ );
1551
1591
  }
1552
1592
 
1553
1593
  /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
@@ -1556,7 +1596,7 @@ export class ForkChoice implements IForkChoice {
1556
1596
  const blocksAtSlot: ProtoBlock[] = [];
1557
1597
  for (let i = 0, len = nodes.length; i < len; i++) {
1558
1598
  const node = nodes[i];
1559
- if (node.slot === slot) {
1599
+ if (node.slot === slot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)) {
1560
1600
  blocksAtSlot.push(node);
1561
1601
  }
1562
1602
  }
@@ -1593,13 +1633,14 @@ export class ForkChoice implements IForkChoice {
1593
1633
  * justified's status as well as validate the terminal conditions if terminal block
1594
1634
  * becomes valid
1595
1635
  */
1596
- validateLatestHash(execResponse: LVHExecResponse): void {
1636
+ validateLatestHash(execResponse: LVHExecResponse, opts?: {strict?: boolean}): boolean {
1597
1637
  try {
1598
- this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot);
1638
+ return this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot, opts);
1599
1639
  } catch (e) {
1600
1640
  if (e instanceof ProtoArrayError && e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE) {
1601
1641
  this.irrecoverableError = e;
1602
1642
  }
1643
+ return false;
1603
1644
  }
1604
1645
  }
1605
1646
 
@@ -155,6 +155,9 @@ export interface IForkChoice {
155
155
  * ## Notes:
156
156
  *
157
157
  * The supplied block **must** pass the `state_transition` function as it will not be run here.
158
+ *
159
+ * Throws `InvalidBlockCode.PARENT_EXECUTION_INVALID` if the parent payload chain was invalidated.
160
+ * `executionStatus` is ignored for a gloas block, it inherits the status of the parent variant it builds on.
158
161
  */
159
162
  onBlock(
160
163
  block: BeaconBlock,
@@ -350,14 +353,17 @@ export interface IForkChoice {
350
353
  * `CheckpointWithHex` / finalized root without a specific payload-status variant in mind.
351
354
  */
352
355
  forwardIterateDescendantsDefaultStatus(blockRoot: RootHex): IterableIterator<ProtoBlock>;
356
+ /** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
353
357
  getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[];
358
+ /** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
354
359
  getBlockSummariesAtSlot(slot: Slot): ProtoBlock[];
355
360
  /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
356
361
  getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult;
357
362
  /**
358
- * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided
363
+ * Apply an execution engine verdict with a latest valid hash, see `ProtoArray.validateLatestHash`.
364
+ * Returns whether any execution status changed. A consensus failure is recorded in `irrecoverableError`.
359
365
  */
360
- validateLatestHash(execResponse: LVHExecResponse): void;
366
+ validateLatestHash(execResponse: LVHExecResponse, opts?: {strict?: boolean}): boolean;
361
367
 
362
368
  /**
363
369
  * A dependent root is the block root of the last block before the state transition that decided a specific shuffling
package/src/metrics.ts CHANGED
@@ -32,6 +32,11 @@ export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
32
32
  name: "beacon_fork_choice_changed_head_total",
33
33
  help: "Count of occasions fork choice has found a new head",
34
34
  }),
35
+ executionInvalidations: register.gauge<{source: string}>({
36
+ name: "beacon_fork_choice_execution_invalidations_total",
37
+ help: "Count of execution engine INVALID verdicts applied to fork choice, by verdict source",
38
+ labelNames: ["source"],
39
+ }),
35
40
  reorg: register.gauge({
36
41
  name: "beacon_fork_choice_reorg_total",
37
42
  help: "Count of occasions fork choice has switched to a different chain",
@@ -55,6 +55,8 @@ export function isGloasBlock(block: ProtoBlock): block is ProtoBlock & {parentBl
55
55
  export type LVHValidResponse = {
56
56
  executionStatus: ExecutionStatus.Valid;
57
57
  latestValidExecHash: RootHex;
58
+ /** Resolves the exact node to validate when several nodes carry the same payload hash (gloas variants) */
59
+ blockRoot?: RootHex;
58
60
  };
59
61
  export type LVHInvalidResponse = {
60
62
  executionStatus: ExecutionStatus.Invalid;
@@ -1019,12 +1019,23 @@ export class ProtoArray {
1019
1019
  }
1020
1020
 
1021
1021
  /**
1022
- * Optimistic sync validate till validated latest hash, invalidate any descendant branch
1023
- * if invalidate till hash provided. If consensus fails, this will invalidate entire
1024
- * forkChoice which will throw on any call to findHead
1022
+ * Apply an execution engine verdict with a latest valid hash (LVH) to the tree. VALID flips Syncing nodes up to the
1023
+ * LVH node. INVALID invalidates the node identified by `invalidateFromParentBlockRoot/Hash` and its ancestors up to
1024
+ * the LVH node (exclusive), then every descendant. Returns whether any status changed.
1025
+ *
1026
+ * A null or unknown LVH means the engine only vouches for the queried payload being invalid: the queried node and
1027
+ * the nodes that inherit that payload (gloas PENDING and EMPTY variants) are invalidated, nothing above them.
1028
+ *
1029
+ * `strict` keeps the consensus failure semantics for verdicts about a payload the engine just processed: a VALID
1030
+ * or PreMerge node on the invalidated chain poisons the tree. Verdicts from forkchoiceUpdated or about an ancestor
1031
+ * chain are applied with `strict: false`: a node the engine already validated is never invalidated and the justified
1032
+ * node is never crossed. If the walk to the LVH meets either, the LVH is treated as unknown.
1025
1033
  */
1026
- // TODO GLOAS: Review usage of this post-gloas
1027
- validateLatestHash(execResponse: LVHExecResponse, currentSlot: Slot): void {
1034
+ validateLatestHash(
1035
+ execResponse: LVHExecResponse,
1036
+ currentSlot: Slot,
1037
+ {strict = true}: {strict?: boolean} = {}
1038
+ ): boolean {
1028
1039
  // Look reverse because its highly likely node with latestValidExecHash is towards the
1029
1040
  // the leaves of the forkchoice
1030
1041
  //
@@ -1035,71 +1046,128 @@ export class ProtoArray {
1035
1046
  // https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167
1036
1047
 
1037
1048
  if (execResponse.executionStatus === ExecutionStatus.Valid) {
1038
- const {latestValidExecHash} = execResponse;
1049
+ const {latestValidExecHash, blockRoot} = execResponse;
1039
1050
  // We use -1 for denoting not found
1040
1051
  let latestValidHashIndex = -1;
1041
1052
 
1042
- for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
1043
- if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
1044
- latestValidHashIndex = nodeIndex;
1045
- // We found the block corresponding to latestValidHashIndex, exit the loop
1046
- break;
1053
+ if (blockRoot !== undefined) {
1054
+ latestValidHashIndex = this.getNodeIndexByRootAndBlockHash(blockRoot, latestValidExecHash) ?? -1;
1055
+ } else {
1056
+ for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
1057
+ if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
1058
+ latestValidHashIndex = nodeIndex;
1059
+ // We found the block corresponding to latestValidHashIndex, exit the loop
1060
+ break;
1061
+ }
1047
1062
  }
1048
1063
  }
1049
1064
 
1050
1065
  // We are trying to be as forgiving as possible here because ideally latestValidHashIndex
1051
1066
  // should be found in the forkchoice
1052
- if (latestValidHashIndex >= 0) {
1053
- this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
1067
+ if (latestValidHashIndex < 0) {
1068
+ return false;
1054
1069
  }
1055
- } else {
1056
- // In case of invalidation, ideally:
1057
- // i) Find the invalid payload
1058
- // ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
1059
- // iii) Obtain a chain [Last_known_valid_node, ...., LVH]
1060
- //
1061
- // Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
1062
- // gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
1063
- // as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
1064
- // So we currently ignore this chain and hope eventually it gets resolved
1065
- //
1066
- // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
1067
- // if its in fcU.
1068
- //
1069
- const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
1070
- const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
1071
- invalidateFromParentBlockRoot,
1072
- invalidateFromParentBlockHash
1073
- );
1074
- if (invalidateFromParentIndex === undefined) {
1075
- throw Error(
1076
- `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
1077
- );
1070
+ const {executionStatus} = this.nodes[latestValidHashIndex];
1071
+ if (executionStatus === ExecutionStatus.Valid || executionStatus === ExecutionStatus.PreMerge) {
1072
+ return false;
1073
+ }
1074
+ if (!strict) {
1075
+ // The walk up flips every Syncing ancestor, a lenient verdict must not contradict an invalidated one
1076
+ let nodeIndex: number | undefined = latestValidHashIndex;
1077
+ while (nodeIndex !== undefined) {
1078
+ const node: ProtoNode = this.nodes[nodeIndex];
1079
+ if (node.executionStatus === ExecutionStatus.Valid || node.executionStatus === ExecutionStatus.PreMerge) {
1080
+ break;
1081
+ }
1082
+ if (node.executionStatus === ExecutionStatus.Invalid) {
1083
+ return false;
1084
+ }
1085
+ nodeIndex = node.parent;
1086
+ }
1078
1087
  }
1079
- const latestValidHashIndex =
1080
- latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
1081
- if (latestValidHashIndex === null) {
1082
- /**
1083
- * The LVH (latest valid hash) is null or not found.
1084
- *
1085
- * The spec gives an allowance for the EL being able to return a nullish LVH if it could not
1086
- * "determine" one. There are two interpretations:
1087
- *
1088
- * - "the LVH is unknown" - simply throw and move on. We can't determine which chain to invalidate
1089
- * since we don't know which ancestor is valid.
1090
- *
1091
- * - "the LVH doesn't exist" - this means that the entire ancestor chain is invalid, and should
1092
- * be marked as such.
1093
- *
1094
- * The more robust approach is to treat nullish LVH as "the LVH is unknown" rather than
1095
- * "the LVH doesn't exist". The alternative means that we will poison a valid chain when the
1096
- * EL is lazy (or buggy) with its LVH response.
1097
- */
1088
+ this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
1089
+ return true;
1090
+ }
1091
+
1092
+ // In case of invalidation, ideally:
1093
+ // i) Find the invalid payload
1094
+ // ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
1095
+ // iii) Obtain a chain [Last_known_valid_node, ...., LVH]
1096
+ //
1097
+ // Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
1098
+ // gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
1099
+ // as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
1100
+ // So we currently ignore this chain and hope eventually it gets resolved
1101
+ //
1102
+ // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
1103
+ // if its in fcU.
1104
+ //
1105
+ const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
1106
+ const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
1107
+ invalidateFromParentBlockRoot,
1108
+ invalidateFromParentBlockHash
1109
+ );
1110
+ if (invalidateFromParentIndex === undefined) {
1111
+ throw Error(
1112
+ `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
1113
+ );
1114
+ }
1115
+ if (!strict && this.nodes[invalidateFromParentIndex].executionStatus !== ExecutionStatus.Syncing) {
1116
+ return false;
1117
+ }
1118
+
1119
+ let latestValidHashIndex =
1120
+ latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
1121
+ if (latestValidHashIndex === null) {
1122
+ if (strict) {
1123
+ // The strict from node is the parent of the rejected payload and may be valid, never guess here
1098
1124
  throw Error(`Unable to find latestValidExecHash=${latestValidExecHash} in the forkchoice`);
1099
1125
  }
1126
+ latestValidHashIndex = this.getUnknownLVHIndex(invalidateFromParentIndex);
1127
+ }
1128
+
1129
+ if (!strict) {
1130
+ // Never invalidate the justified block, the EMPTY variant sharing its chain, or anything at or below it in
1131
+ // the tree. The justified block's own payload (FULL variant) is not part of the checkpoint
1132
+ const justifiedIndex = this.getDefaultNodeIndex(this.justifiedRoot) ?? -1;
1133
+ let nodeIndex: number | undefined = invalidateFromParentIndex;
1134
+ while (nodeIndex !== undefined && nodeIndex > latestValidHashIndex) {
1135
+ const node: ProtoNode = this.nodes[nodeIndex];
1136
+ if (
1137
+ node.executionStatus !== ExecutionStatus.Syncing ||
1138
+ nodeIndex <= justifiedIndex ||
1139
+ (node.blockRoot === this.justifiedRoot && node.payloadStatus === PayloadStatus.EMPTY)
1140
+ ) {
1141
+ // Conflicting LVH: fall back to the unknown LVH bound, but never cross this node
1142
+ latestValidHashIndex = Math.max(nodeIndex, this.getUnknownLVHIndex(invalidateFromParentIndex));
1143
+ break;
1144
+ }
1145
+ nodeIndex = node.parent;
1146
+ }
1147
+ }
1100
1148
 
1101
- this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
1149
+ if (invalidateFromParentIndex <= latestValidHashIndex) {
1150
+ return false;
1102
1151
  }
1152
+
1153
+ this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
1154
+ return true;
1155
+ }
1156
+
1157
+ /**
1158
+ * Upper bound (exclusive) for invalidating from `fromIndex` when the engine gave no usable LVH: the parent of the
1159
+ * last ancestor that still inherits the queried payload. Pre-gloas this is the parent of `fromIndex`. For a gloas
1160
+ * EMPTY or PENDING node it reaches up to the FULL node that owns the payload. -1 when the chain reaches the root.
1161
+ */
1162
+ private getUnknownLVHIndex(fromIndex: number): number {
1163
+ const payloadBlockHash = this.nodes[fromIndex].executionPayloadBlockHash;
1164
+ let nodeIndex = fromIndex;
1165
+ let parentIndex = this.nodes[nodeIndex].parent;
1166
+ while (parentIndex !== undefined && this.nodes[parentIndex].executionPayloadBlockHash === payloadBlockHash) {
1167
+ nodeIndex = parentIndex;
1168
+ parentIndex = this.nodes[nodeIndex].parent;
1169
+ }
1170
+ return parentIndex ?? -1;
1103
1171
  }
1104
1172
 
1105
1173
  private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
@@ -1295,6 +1363,15 @@ export class ProtoArray {
1295
1363
 
1296
1364
  // For Gloas: check if from previous slot
1297
1365
  if (node.slot + 1 !== currentSlot) {
1366
+ // During the block's own slot a payload known to not satisfy the inclusion list
1367
+ // constraints ranks below its EMPTY variant, as it will from the next slot on
1368
+ if (
1369
+ node.slot === currentSlot &&
1370
+ node.payloadStatus === PayloadStatus.FULL &&
1371
+ this.payloadInclusionListSatisfaction.get(node.blockRoot) === false
1372
+ ) {
1373
+ return PayloadStatus.PENDING;
1374
+ }
1298
1375
  return node.payloadStatus;
1299
1376
  }
1300
1377