@lodestar/fork-choice 1.51.0-dev.f8eff664d2 → 1.51.0-dev.f921fbf6b7

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.
@@ -122,6 +122,11 @@ export class ProtoArray {
122
122
  * when called with the negative parameter value.
123
123
  */
124
124
  private ptcAttested = new Map<RootHex, BitArray>();
125
+ /**
126
+ * Execution engine inclusion list verdict per block, recorded on payload import from heze.
127
+ * Spec: heze/fork-choice.md#modified-store (payload_inclusion_list_satisfaction)
128
+ */
129
+ private payloadInclusionListSatisfaction = new Map<RootHex, boolean>();
125
130
 
126
131
  constructor({
127
132
  pruneThreshold,
@@ -938,6 +943,27 @@ export class ProtoArray {
938
943
  return this.getParentPayloadStatus(block) === PayloadStatus.FULL;
939
944
  }
940
945
 
946
+ /** Spec: heze/fork-choice.md#new-record_payload_inclusion_list_satisfaction. Returns whether the verdict changed */
947
+ recordPayloadInclusionListSatisfaction(blockRoot: RootHex, satisfied: boolean): boolean {
948
+ const previous = this.payloadInclusionListSatisfaction.get(blockRoot);
949
+ this.payloadInclusionListSatisfaction.set(blockRoot, satisfied);
950
+ return previous !== satisfied;
951
+ }
952
+
953
+ /**
954
+ * Spec: heze/fork-choice.md#new-is_payload_inclusion_list_satisfied
955
+ *
956
+ * False until the payload is delivered. A delivered payload without a verdict is pre-heze, where
957
+ * no inclusion list constraints apply.
958
+ */
959
+ isPayloadInclusionListSatisfied(blockRoot: RootHex): boolean {
960
+ return this.hasPayload(blockRoot) && this.payloadInclusionListSatisfaction.get(blockRoot) !== false;
961
+ }
962
+
963
+ getPayloadInclusionListSatisfaction(blockRoot: RootHex): boolean | null {
964
+ return this.payloadInclusionListSatisfaction.get(blockRoot) ?? null;
965
+ }
966
+
941
967
  /**
942
968
  * Determine if we should extend the payload (prefer FULL over EMPTY)
943
969
  * Spec: gloas/fork-choice.md#new-should_extend_payload
@@ -956,6 +982,10 @@ export class ProtoArray {
956
982
  return false;
957
983
  }
958
984
 
985
+ if (this.payloadInclusionListSatisfaction.get(blockRoot) === false) {
986
+ return false;
987
+ }
988
+
959
989
  // Condition 1: Payload is timely AND data is available
960
990
  if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) {
961
991
  return true;
@@ -989,12 +1019,23 @@ export class ProtoArray {
989
1019
  }
990
1020
 
991
1021
  /**
992
- * Optimistic sync validate till validated latest hash, invalidate any descendant branch
993
- * if invalidate till hash provided. If consensus fails, this will invalidate entire
994
- * 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.
995
1033
  */
996
- // TODO GLOAS: Review usage of this post-gloas
997
- validateLatestHash(execResponse: LVHExecResponse, currentSlot: Slot): void {
1034
+ validateLatestHash(
1035
+ execResponse: LVHExecResponse,
1036
+ currentSlot: Slot,
1037
+ {strict = true}: {strict?: boolean} = {}
1038
+ ): boolean {
998
1039
  // Look reverse because its highly likely node with latestValidExecHash is towards the
999
1040
  // the leaves of the forkchoice
1000
1041
  //
@@ -1005,71 +1046,128 @@ export class ProtoArray {
1005
1046
  // https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167
1006
1047
 
1007
1048
  if (execResponse.executionStatus === ExecutionStatus.Valid) {
1008
- const {latestValidExecHash} = execResponse;
1049
+ const {latestValidExecHash, blockRoot} = execResponse;
1009
1050
  // We use -1 for denoting not found
1010
1051
  let latestValidHashIndex = -1;
1011
1052
 
1012
- for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
1013
- if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
1014
- latestValidHashIndex = nodeIndex;
1015
- // We found the block corresponding to latestValidHashIndex, exit the loop
1016
- 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
+ }
1017
1062
  }
1018
1063
  }
1019
1064
 
1020
1065
  // We are trying to be as forgiving as possible here because ideally latestValidHashIndex
1021
1066
  // should be found in the forkchoice
1022
- if (latestValidHashIndex >= 0) {
1023
- this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
1067
+ if (latestValidHashIndex < 0) {
1068
+ return false;
1024
1069
  }
1025
- } else {
1026
- // In case of invalidation, ideally:
1027
- // i) Find the invalid payload
1028
- // ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
1029
- // iii) Obtain a chain [Last_known_valid_node, ...., LVH]
1030
- //
1031
- // Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
1032
- // gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
1033
- // as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
1034
- // So we currently ignore this chain and hope eventually it gets resolved
1035
- //
1036
- // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
1037
- // if its in fcU.
1038
- //
1039
- const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
1040
- const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
1041
- invalidateFromParentBlockRoot,
1042
- invalidateFromParentBlockHash
1043
- );
1044
- if (invalidateFromParentIndex === undefined) {
1045
- throw Error(
1046
- `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
1047
- );
1070
+ const {executionStatus} = this.nodes[latestValidHashIndex];
1071
+ if (executionStatus === ExecutionStatus.Valid || executionStatus === ExecutionStatus.PreMerge) {
1072
+ return false;
1048
1073
  }
1049
- const latestValidHashIndex =
1050
- latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
1051
- if (latestValidHashIndex === null) {
1052
- /**
1053
- * The LVH (latest valid hash) is null or not found.
1054
- *
1055
- * The spec gives an allowance for the EL being able to return a nullish LVH if it could not
1056
- * "determine" one. There are two interpretations:
1057
- *
1058
- * - "the LVH is unknown" - simply throw and move on. We can't determine which chain to invalidate
1059
- * since we don't know which ancestor is valid.
1060
- *
1061
- * - "the LVH doesn't exist" - this means that the entire ancestor chain is invalid, and should
1062
- * be marked as such.
1063
- *
1064
- * The more robust approach is to treat nullish LVH as "the LVH is unknown" rather than
1065
- * "the LVH doesn't exist". The alternative means that we will poison a valid chain when the
1066
- * EL is lazy (or buggy) with its LVH response.
1067
- */
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
+ }
1087
+ }
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
1068
1124
  throw Error(`Unable to find latestValidExecHash=${latestValidExecHash} in the forkchoice`);
1069
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
+ }
1148
+
1149
+ if (invalidateFromParentIndex <= latestValidHashIndex) {
1150
+ return false;
1151
+ }
1152
+
1153
+ this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
1154
+ return true;
1155
+ }
1070
1156
 
1071
- this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
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;
1072
1169
  }
1170
+ return parentIndex ?? -1;
1073
1171
  }
1074
1172
 
1075
1173
  private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
@@ -1265,6 +1363,15 @@ export class ProtoArray {
1265
1363
 
1266
1364
  // For Gloas: check if from previous slot
1267
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
+ }
1268
1375
  return node.payloadStatus;
1269
1376
  }
1270
1377
 
@@ -1399,6 +1506,7 @@ export class ProtoArray {
1399
1506
  this.payloadTimelinessVotes.delete(root);
1400
1507
  this.ptcAttested.delete(root);
1401
1508
  this.payloadDataAvailabilityVotes.delete(root);
1509
+ this.payloadInclusionListSatisfaction.delete(root);
1402
1510
  }
1403
1511
 
1404
1512
  // Store nodes prior to finalization