@lodestar/fork-choice 1.51.0-dev.ec493d509d → 1.51.0-dev.efd5633468

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.
@@ -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