@lodestar/fork-choice 1.43.0-dev.9c8becae00 → 1.43.0-dev.9f5db5b9c7

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
 
@@ -276,38 +252,43 @@ export class ProtoArray {
276
252
  }
277
253
 
278
254
  /**
279
- * Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
255
+ * Returns the node index of an EMPTY or FULL variant matching block root and block hash.
256
+ * Pre-gloas: checks the single variant. Post-gloas: prefers FULL, falls back to EMPTY.
280
257
  */
281
- getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
258
+ getNodeIndexByRootAndBlockHash(blockRoot: RootHex, blockHash: RootHex): number | undefined {
282
259
  const variantIndices = this.indices.get(blockRoot);
283
260
  if (variantIndices === undefined) {
284
- return null;
261
+ return undefined;
285
262
  }
286
263
 
287
264
  // Pre-Gloas
288
265
  if (!Array.isArray(variantIndices)) {
289
- const node = this.nodes[variantIndices];
290
- return node.executionPayloadBlockHash === blockHash ? node : null;
266
+ return this.nodes[variantIndices].executionPayloadBlockHash === blockHash ? variantIndices : undefined;
291
267
  }
292
268
 
293
- // Post-Gloas, check empty and full variants
269
+ // Post-Gloas, prefer FULL then EMPTY
294
270
  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
- }
271
+ if (fullNodeIndex !== undefined && this.nodes[fullNodeIndex].executionPayloadBlockHash === blockHash) {
272
+ return fullNodeIndex;
300
273
  }
301
274
 
302
- const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
303
- if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
304
- return emptyNode;
275
+ const emptyNodeIndex = variantIndices[PayloadStatus.EMPTY];
276
+ if (this.nodes[emptyNodeIndex].executionPayloadBlockHash === blockHash) {
277
+ return emptyNodeIndex;
305
278
  }
306
279
 
307
280
  // PENDING is the same to EMPTY so not likely we can return it
308
281
  // also it's only specific for fork-choice
309
282
 
310
- return null;
283
+ return undefined;
284
+ }
285
+
286
+ /**
287
+ * Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
288
+ */
289
+ getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
290
+ const idx = this.getNodeIndexByRootAndBlockHash(blockRoot, blockHash);
291
+ return idx !== undefined ? this.nodes[idx] : null;
311
292
  }
312
293
 
313
294
  /**
@@ -572,9 +553,9 @@ export class ProtoArray {
572
553
  currentSlot: Slot,
573
554
  executionPayloadBlockHash: RootHex,
574
555
  executionPayloadNumber: number,
575
- executionPayloadStateRoot: RootHex,
576
556
  proposerBoostRoot: RootHex | null,
577
- executionStatus: PayloadExecutionStatus
557
+ executionStatus: PayloadExecutionStatus,
558
+ dataAvailabilityStatus: DataAvailabilityStatus
578
559
  ): void {
579
560
  // First check if block exists
580
561
  const variants = this.indices.get(blockRoot);
@@ -616,7 +597,7 @@ export class ProtoArray {
616
597
  });
617
598
  }
618
599
 
619
- // Create FULL variant as a child of PENDING (sibling to EMPTY)
600
+ // Create FULL variant as a child of PENDING (sibling to EMPTY).
620
601
  const fullNode: ProtoNode = {
621
602
  ...pendingNode,
622
603
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
@@ -624,11 +605,10 @@ export class ProtoArray {
624
605
  weight: 0,
625
606
  bestChild: undefined,
626
607
  bestDescendant: undefined,
627
- // TODO GLOAS: handle optimistic sync
628
608
  executionStatus,
629
609
  executionPayloadBlockHash,
630
610
  executionPayloadNumber,
631
- stateRoot: executionPayloadStateRoot,
611
+ dataAvailabilityStatus,
632
612
  };
633
613
 
634
614
  const fullIndex = this.nodes.length;
@@ -637,6 +617,12 @@ export class ProtoArray {
637
617
  // Add FULL variant to the indices array
638
618
  variants[PayloadStatus.FULL] = fullIndex;
639
619
 
620
+ if (executionStatus === ExecutionStatus.Valid) {
621
+ // Walk up from FULL's parent (its own PENDING). FULL is already Valid; the loop breaks
622
+ // immediately if we start at FULL. Same pattern as pre-gloas onBlock at line ~533.
623
+ this.propagateValidExecutionStatusByIndex(pendingIndex);
624
+ }
625
+
640
626
  // Update bestChild for PENDING node (may now prefer FULL over EMPTY)
641
627
  this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
642
628
  }
@@ -665,6 +651,16 @@ export class ProtoArray {
665
651
  }
666
652
  }
667
653
 
654
+ getPTCVotes(blockRootHex: RootHex): BitArray | null {
655
+ const votes = this.ptcVotes.get(blockRootHex);
656
+ if (votes === undefined) {
657
+ // Block not found or not a Gloas block
658
+ return null;
659
+ }
660
+
661
+ return votes;
662
+ }
663
+
668
664
  /**
669
665
  * Check if execution payload for a block is timely
670
666
  * Spec: gloas/fork-choice.md#new-is_payload_timely
@@ -801,11 +797,15 @@ export class ProtoArray {
801
797
  // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
802
798
  // if its in fcU.
803
799
  //
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);
800
+ const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
801
+ const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
802
+ invalidateFromParentBlockRoot,
803
+ invalidateFromParentBlockHash
804
+ );
807
805
  if (invalidateFromParentIndex === undefined) {
808
- throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
806
+ throw Error(
807
+ `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
808
+ );
809
809
  }
810
810
  const latestValidHashIndex =
811
811
  latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
@@ -841,12 +841,6 @@ export class ProtoArray {
841
841
  if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
842
842
  break;
843
843
  }
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
844
  this.validateNodeByIndex(nodeIndex);
851
845
  nodeIndex = node.parent;
852
846
  }
@@ -944,6 +938,13 @@ export class ProtoArray {
944
938
  invalidNode.executionStatus = ExecutionStatus.Invalid;
945
939
  invalidNode.bestChild = undefined;
946
940
  invalidNode.bestDescendant = undefined;
941
+ // Gloas: PENDING and sibling EMPTY share chain status, flip together
942
+ if (invalidNode.payloadStatus === PayloadStatus.PENDING) {
943
+ const variants = this.indices.get(invalidNode.blockRoot);
944
+ if (Array.isArray(variants)) {
945
+ this.invalidateNodeByIndex(variants[PayloadStatus.EMPTY]);
946
+ }
947
+ }
947
948
 
948
949
  return invalidNode;
949
950
  }
@@ -965,6 +966,13 @@ export class ProtoArray {
965
966
  if (validNode.executionStatus === ExecutionStatus.Syncing) {
966
967
  validNode.executionStatus = ExecutionStatus.Valid;
967
968
  }
969
+ // Gloas: PENDING and sibling EMPTY share chain status, flip together
970
+ if (validNode.payloadStatus === PayloadStatus.PENDING) {
971
+ const variants = this.indices.get(validNode.blockRoot);
972
+ if (Array.isArray(variants)) {
973
+ this.validateNodeByIndex(variants[PayloadStatus.EMPTY]);
974
+ }
975
+ }
968
976
  return validNode;
969
977
  }
970
978
 
@@ -1676,10 +1684,9 @@ export class ProtoArray {
1676
1684
  const ancestors: ProtoNode[] = [];
1677
1685
  const nonAncestors: ProtoNode[] = [];
1678
1686
 
1679
- // Include starting node if it's not PENDING (i.e., pre-Gloas or EMPTY/FULL variant post-Gloas)
1680
- if (node.payloadStatus !== PayloadStatus.PENDING) {
1681
- ancestors.push(node);
1682
- }
1687
+ // caller of this method may pass default status
1688
+ // this is the only node that we accept PENDING
1689
+ ancestors.push(node);
1683
1690
 
1684
1691
  let nodeIndex = startIndex;
1685
1692
  while (node.parent !== undefined) {