@lodestar/fork-choice 1.43.0-dev.ca1fc40294 → 1.43.0-dev.d901f86f98

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.
@@ -1,6 +1,6 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
2
  import {GENESIS_EPOCH, PTC_SIZE} from "@lodestar/params";
3
- import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
+ import {DataAvailabilityStatus, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
4
  import {Epoch, RootHex, Slot} from "@lodestar/types";
5
5
  import {bitCount, toRootHex} from "@lodestar/utils";
6
6
  import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
@@ -109,30 +109,6 @@ export class ProtoArray {
109
109
  null
110
110
  );
111
111
 
112
- // Anchor block PTC votes must be all-true per spec get_forkchoice_store:
113
- // payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))}
114
- // Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/fork-choice.md#modified-get_forkchoice_store
115
- if (protoArray.ptcVotes.has(block.blockRoot)) {
116
- protoArray.ptcVotes.set(block.blockRoot, BitArray.fromBoolArray(Array.from({length: PTC_SIZE}, () => true)));
117
-
118
- // In the spec, we have payload_states = {anchor_root: anchor_state.copy()}
119
- // which means the anchor's "payload" is considered received
120
- // Without FULL, blocks extending FULL from the anchor would be orphaned.
121
- // TODO GLOAS: This is a bug in the spec. Keep this to pass the current spec test
122
- // for now. Need to remove this when we work on v1.7.0-alpha.5
123
- if (block.executionPayloadBlockHash !== null) {
124
- protoArray.onExecutionPayload(
125
- block.blockRoot,
126
- currentSlot,
127
- block.executionPayloadBlockHash,
128
- (block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0,
129
- block.stateRoot,
130
- null,
131
- ExecutionStatus.Valid
132
- );
133
- }
134
- }
135
-
136
112
  return protoArray;
137
113
  }
138
114
 
@@ -238,6 +214,11 @@ export class ProtoArray {
238
214
  return PayloadStatus.FULL;
239
215
  }
240
216
 
217
+ // Genesis block has no parent in the proto array
218
+ if (block.parentRoot === HEX_ZERO_HASH) {
219
+ return PayloadStatus.FULL;
220
+ }
221
+
241
222
  const parentBlock = this.getBlockHexAndBlockHash(block.parentRoot, parentBlockHash);
242
223
  if (parentBlock == null) {
243
224
  throw new ProtoArrayError({
@@ -276,38 +257,43 @@ export class ProtoArray {
276
257
  }
277
258
 
278
259
  /**
279
- * Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
260
+ * Returns the node index of an EMPTY or FULL variant matching block root and block hash.
261
+ * Pre-gloas: checks the single variant. Post-gloas: prefers FULL, falls back to EMPTY.
280
262
  */
281
- getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
263
+ getNodeIndexByRootAndBlockHash(blockRoot: RootHex, blockHash: RootHex): number | undefined {
282
264
  const variantIndices = this.indices.get(blockRoot);
283
265
  if (variantIndices === undefined) {
284
- return null;
266
+ return undefined;
285
267
  }
286
268
 
287
269
  // Pre-Gloas
288
270
  if (!Array.isArray(variantIndices)) {
289
- const node = this.nodes[variantIndices];
290
- return node.executionPayloadBlockHash === blockHash ? node : null;
271
+ return this.nodes[variantIndices].executionPayloadBlockHash === blockHash ? variantIndices : undefined;
291
272
  }
292
273
 
293
- // Post-Gloas, check empty and full variants
274
+ // Post-Gloas, prefer FULL then EMPTY
294
275
  const fullNodeIndex = variantIndices[PayloadStatus.FULL];
295
- if (fullNodeIndex !== undefined) {
296
- const fullNode = this.nodes[fullNodeIndex];
297
- if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
298
- return fullNode;
299
- }
276
+ if (fullNodeIndex !== undefined && this.nodes[fullNodeIndex].executionPayloadBlockHash === blockHash) {
277
+ return fullNodeIndex;
300
278
  }
301
279
 
302
- const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
303
- if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
304
- return emptyNode;
280
+ const emptyNodeIndex = variantIndices[PayloadStatus.EMPTY];
281
+ if (this.nodes[emptyNodeIndex].executionPayloadBlockHash === blockHash) {
282
+ return emptyNodeIndex;
305
283
  }
306
284
 
307
285
  // PENDING is the same to EMPTY so not likely we can return it
308
286
  // also it's only specific for fork-choice
309
287
 
310
- return null;
288
+ return undefined;
289
+ }
290
+
291
+ /**
292
+ * Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
293
+ */
294
+ getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
295
+ const idx = this.getNodeIndexByRootAndBlockHash(blockRoot, blockHash);
296
+ return idx !== undefined ? this.nodes[idx] : null;
311
297
  }
312
298
 
313
299
  /**
@@ -572,9 +558,9 @@ export class ProtoArray {
572
558
  currentSlot: Slot,
573
559
  executionPayloadBlockHash: RootHex,
574
560
  executionPayloadNumber: number,
575
- executionPayloadStateRoot: RootHex,
576
561
  proposerBoostRoot: RootHex | null,
577
- executionStatus: PayloadExecutionStatus
562
+ executionStatus: PayloadExecutionStatus,
563
+ dataAvailabilityStatus: DataAvailabilityStatus
578
564
  ): void {
579
565
  // First check if block exists
580
566
  const variants = this.indices.get(blockRoot);
@@ -616,7 +602,7 @@ export class ProtoArray {
616
602
  });
617
603
  }
618
604
 
619
- // Create FULL variant as a child of PENDING (sibling to EMPTY)
605
+ // Create FULL variant as a child of PENDING (sibling to EMPTY).
620
606
  const fullNode: ProtoNode = {
621
607
  ...pendingNode,
622
608
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
@@ -624,11 +610,10 @@ export class ProtoArray {
624
610
  weight: 0,
625
611
  bestChild: undefined,
626
612
  bestDescendant: undefined,
627
- // TODO GLOAS: handle optimistic sync
628
613
  executionStatus,
629
614
  executionPayloadBlockHash,
630
615
  executionPayloadNumber,
631
- stateRoot: executionPayloadStateRoot,
616
+ dataAvailabilityStatus,
632
617
  };
633
618
 
634
619
  const fullIndex = this.nodes.length;
@@ -637,6 +622,12 @@ export class ProtoArray {
637
622
  // Add FULL variant to the indices array
638
623
  variants[PayloadStatus.FULL] = fullIndex;
639
624
 
625
+ if (executionStatus === ExecutionStatus.Valid) {
626
+ // Walk up from FULL's parent (its own PENDING). FULL is already Valid; the loop breaks
627
+ // immediately if we start at FULL. Same pattern as pre-gloas onBlock at line ~533.
628
+ this.propagateValidExecutionStatusByIndex(pendingIndex);
629
+ }
630
+
640
631
  // Update bestChild for PENDING node (may now prefer FULL over EMPTY)
641
632
  this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
642
633
  }
@@ -665,6 +656,16 @@ export class ProtoArray {
665
656
  }
666
657
  }
667
658
 
659
+ getPTCVotes(blockRootHex: RootHex): BitArray | null {
660
+ const votes = this.ptcVotes.get(blockRootHex);
661
+ if (votes === undefined) {
662
+ // Block not found or not a Gloas block
663
+ return null;
664
+ }
665
+
666
+ return votes;
667
+ }
668
+
668
669
  /**
669
670
  * Check if execution payload for a block is timely
670
671
  * Spec: gloas/fork-choice.md#new-is_payload_timely
@@ -801,11 +802,15 @@ export class ProtoArray {
801
802
  // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
802
803
  // if its in fcU.
803
804
  //
804
- const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
805
- // TODO GLOAS: verify if getting the default/canonical node index is correct here
806
- const invalidateFromParentIndex = this.getDefaultNodeIndex(invalidateFromParentBlockRoot);
805
+ const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
806
+ const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
807
+ invalidateFromParentBlockRoot,
808
+ invalidateFromParentBlockHash
809
+ );
807
810
  if (invalidateFromParentIndex === undefined) {
808
- throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
811
+ throw Error(
812
+ `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
813
+ );
809
814
  }
810
815
  const latestValidHashIndex =
811
816
  latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
@@ -841,12 +846,6 @@ export class ProtoArray {
841
846
  if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
842
847
  break;
843
848
  }
844
- // If PayloadSeparated, that means the node is either PENDING or EMPTY, there could be
845
- // some ancestor still has syncing status.
846
- if (node.executionStatus === ExecutionStatus.PayloadSeparated) {
847
- nodeIndex = node.parent;
848
- continue;
849
- }
850
849
  this.validateNodeByIndex(nodeIndex);
851
850
  nodeIndex = node.parent;
852
851
  }
@@ -944,6 +943,13 @@ export class ProtoArray {
944
943
  invalidNode.executionStatus = ExecutionStatus.Invalid;
945
944
  invalidNode.bestChild = undefined;
946
945
  invalidNode.bestDescendant = undefined;
946
+ // Gloas: PENDING and sibling EMPTY share chain status, flip together
947
+ if (invalidNode.payloadStatus === PayloadStatus.PENDING) {
948
+ const variants = this.indices.get(invalidNode.blockRoot);
949
+ if (Array.isArray(variants)) {
950
+ this.invalidateNodeByIndex(variants[PayloadStatus.EMPTY]);
951
+ }
952
+ }
947
953
 
948
954
  return invalidNode;
949
955
  }
@@ -965,6 +971,13 @@ export class ProtoArray {
965
971
  if (validNode.executionStatus === ExecutionStatus.Syncing) {
966
972
  validNode.executionStatus = ExecutionStatus.Valid;
967
973
  }
974
+ // Gloas: PENDING and sibling EMPTY share chain status, flip together
975
+ if (validNode.payloadStatus === PayloadStatus.PENDING) {
976
+ const variants = this.indices.get(validNode.blockRoot);
977
+ if (Array.isArray(variants)) {
978
+ this.validateNodeByIndex(variants[PayloadStatus.EMPTY]);
979
+ }
980
+ }
968
981
  return validNode;
969
982
  }
970
983