@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.
- package/lib/forkChoice/errors.d.ts +6 -1
- package/lib/forkChoice/errors.d.ts.map +1 -1
- package/lib/forkChoice/errors.js +1 -0
- package/lib/forkChoice/errors.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.js +19 -24
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +24 -19
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +97 -39
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +18 -4
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/metrics.d.ts +4 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +9 -0
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/interface.d.ts +2 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/interface.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +35 -4
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +142 -51
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/errors.ts +3 -1
- package/src/forkChoice/fastConfirmation/utils.ts +21 -20
- package/src/forkChoice/forkChoice.ts +107 -41
- package/src/forkChoice/interface.ts +17 -5
- package/src/metrics.ts +9 -0
- package/src/protoArray/interface.ts +2 -0
- package/src/protoArray/protoArray.ts +164 -56
|
@@ -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
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
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
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
1023
|
-
|
|
1067
|
+
if (latestValidHashIndex < 0) {
|
|
1068
|
+
return false;
|
|
1024
1069
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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
|