@lodestar/fork-choice 1.43.0-dev.2870b59b6a → 1.43.0-dev.2fba242f5d

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.
@@ -24,16 +24,15 @@ export type VoteIndex = number;
24
24
  * - Syncing: EL is syncing, payload validity unknown (optimistic sync)
25
25
  * - PreMerge: Block is from before The Merge, no execution payload exists
26
26
  * - Invalid: Execution payload was invalidated by the EL (post-import status)
27
- * - PayloadSeparated: Gloas beacon block without embedded execution payload.
28
- * The execution payload arrives separately via SignedExecutionPayloadEnvelope.
29
- * Gloas blocks WITH execution payload (FULL variant) use Valid/Invalid/Syncing.
27
+ *
28
+ * For gloas blocks the PENDING/EMPTY variants inherit `executionStatus` from the parent's chain
29
+ * (Valid/Syncing/PreMerge); the FULL variant carries the EL response for this block's own payload.
30
30
  */
31
31
  export enum ExecutionStatus {
32
32
  Valid = "Valid",
33
33
  Syncing = "Syncing",
34
34
  PreMerge = "PreMerge",
35
35
  Invalid = "Invalid",
36
- PayloadSeparated = "PayloadSeparated",
37
36
  }
38
37
 
39
38
  /**
@@ -61,19 +60,21 @@ export type LVHInvalidResponse = {
61
60
  executionStatus: ExecutionStatus.Invalid;
62
61
  latestValidExecHash: RootHex | null;
63
62
  invalidateFromParentBlockRoot: RootHex;
63
+ // EL block hash from invalid block's bid (gloas) or payload's parentHash (pre-gloas).
64
+ // Disambiguates which variant of the parent (FULL vs EMPTY) to invalidate from.
65
+ invalidateFromParentBlockHash: RootHex;
64
66
  };
65
67
  export type LVHExecResponse = LVHValidResponse | LVHInvalidResponse;
66
68
 
67
69
  /**
68
70
  * Any execution status that is not definitively invalid.
69
- * Pre-Gloas: Valid | Syncing | PreMerge
70
- * Post-Gloas: execution status must be PayloadSeparated (beacon block imported before its payload arrives via SignedExecutionPayloadEnvelope)
71
+ * Valid | Syncing | PreMerge
71
72
  */
72
73
  export type BlockExecutionStatus = Exclude<ExecutionStatus, ExecutionStatus.Invalid>;
73
74
 
74
75
  /**
75
76
  * Execution status for a block whose execution payload is present and has been submitted to the EL.
76
- * Used post-Gloas when transitioning a PayloadSeparated block to FULL via onExecutionPayload().
77
+ * Used post-Gloas when transitioning a PENDING block to FULL via onExecutionPayload().
77
78
  */
78
79
  export type PayloadExecutionStatus = ExecutionStatus.Valid | ExecutionStatus.Syncing;
79
80
 
@@ -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,13 +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
-
119
112
  return protoArray;
120
113
  }
121
114
 
@@ -259,38 +252,43 @@ export class ProtoArray {
259
252
  }
260
253
 
261
254
  /**
262
- * 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.
263
257
  */
264
- getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
258
+ getNodeIndexByRootAndBlockHash(blockRoot: RootHex, blockHash: RootHex): number | undefined {
265
259
  const variantIndices = this.indices.get(blockRoot);
266
260
  if (variantIndices === undefined) {
267
- return null;
261
+ return undefined;
268
262
  }
269
263
 
270
264
  // Pre-Gloas
271
265
  if (!Array.isArray(variantIndices)) {
272
- const node = this.nodes[variantIndices];
273
- return node.executionPayloadBlockHash === blockHash ? node : null;
266
+ return this.nodes[variantIndices].executionPayloadBlockHash === blockHash ? variantIndices : undefined;
274
267
  }
275
268
 
276
- // Post-Gloas, check empty and full variants
269
+ // Post-Gloas, prefer FULL then EMPTY
277
270
  const fullNodeIndex = variantIndices[PayloadStatus.FULL];
278
- if (fullNodeIndex !== undefined) {
279
- const fullNode = this.nodes[fullNodeIndex];
280
- if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
281
- return fullNode;
282
- }
271
+ if (fullNodeIndex !== undefined && this.nodes[fullNodeIndex].executionPayloadBlockHash === blockHash) {
272
+ return fullNodeIndex;
283
273
  }
284
274
 
285
- const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
286
- if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
287
- return emptyNode;
275
+ const emptyNodeIndex = variantIndices[PayloadStatus.EMPTY];
276
+ if (this.nodes[emptyNodeIndex].executionPayloadBlockHash === blockHash) {
277
+ return emptyNodeIndex;
288
278
  }
289
279
 
290
280
  // PENDING is the same to EMPTY so not likely we can return it
291
281
  // also it's only specific for fork-choice
292
282
 
293
- 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;
294
292
  }
295
293
 
296
294
  /**
@@ -555,9 +553,9 @@ export class ProtoArray {
555
553
  currentSlot: Slot,
556
554
  executionPayloadBlockHash: RootHex,
557
555
  executionPayloadNumber: number,
558
- executionPayloadStateRoot: RootHex,
559
556
  proposerBoostRoot: RootHex | null,
560
- executionStatus: PayloadExecutionStatus
557
+ executionStatus: PayloadExecutionStatus,
558
+ dataAvailabilityStatus: DataAvailabilityStatus
561
559
  ): void {
562
560
  // First check if block exists
563
561
  const variants = this.indices.get(blockRoot);
@@ -599,7 +597,7 @@ export class ProtoArray {
599
597
  });
600
598
  }
601
599
 
602
- // Create FULL variant as a child of PENDING (sibling to EMPTY)
600
+ // Create FULL variant as a child of PENDING (sibling to EMPTY).
603
601
  const fullNode: ProtoNode = {
604
602
  ...pendingNode,
605
603
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
@@ -607,11 +605,10 @@ export class ProtoArray {
607
605
  weight: 0,
608
606
  bestChild: undefined,
609
607
  bestDescendant: undefined,
610
- // TODO GLOAS: handle optimistic sync
611
608
  executionStatus,
612
609
  executionPayloadBlockHash,
613
610
  executionPayloadNumber,
614
- stateRoot: executionPayloadStateRoot,
611
+ dataAvailabilityStatus,
615
612
  };
616
613
 
617
614
  const fullIndex = this.nodes.length;
@@ -620,6 +617,12 @@ export class ProtoArray {
620
617
  // Add FULL variant to the indices array
621
618
  variants[PayloadStatus.FULL] = fullIndex;
622
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
+
623
626
  // Update bestChild for PENDING node (may now prefer FULL over EMPTY)
624
627
  this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
625
628
  }
@@ -648,6 +651,16 @@ export class ProtoArray {
648
651
  }
649
652
  }
650
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
+
651
664
  /**
652
665
  * Check if execution payload for a block is timely
653
666
  * Spec: gloas/fork-choice.md#new-is_payload_timely
@@ -690,7 +703,7 @@ export class ProtoArray {
690
703
  * Determine if we should extend the payload (prefer FULL over EMPTY)
691
704
  * Spec: gloas/fork-choice.md#new-should_extend_payload
692
705
  *
693
- * Returns true if:
706
+ * Returns true if payload is verified (FULL variant exists) AND:
694
707
  * 1. Payload is timely, OR
695
708
  * 2. No proposer boost root (empty/zero hash), OR
696
709
  * 3. Proposer boost root's parent is not this block, OR
@@ -700,6 +713,10 @@ export class ProtoArray {
700
713
  * @param proposerBoostRoot - Current proposer boost root (from ForkChoice)
701
714
  */
702
715
  shouldExtendPayload(blockRoot: RootHex, proposerBoostRoot: RootHex | null): boolean {
716
+ if (!this.hasPayload(blockRoot)) {
717
+ return false;
718
+ }
719
+
703
720
  // Condition 1: Payload is timely
704
721
  if (this.isPayloadTimely(blockRoot)) {
705
722
  return true;
@@ -780,11 +797,15 @@ export class ProtoArray {
780
797
  // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
781
798
  // if its in fcU.
782
799
  //
783
- const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
784
- // TODO GLOAS: verify if getting the default/canonical node index is correct here
785
- const invalidateFromParentIndex = this.getDefaultNodeIndex(invalidateFromParentBlockRoot);
800
+ const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
801
+ const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
802
+ invalidateFromParentBlockRoot,
803
+ invalidateFromParentBlockHash
804
+ );
786
805
  if (invalidateFromParentIndex === undefined) {
787
- throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
806
+ throw Error(
807
+ `Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
808
+ );
788
809
  }
789
810
  const latestValidHashIndex =
790
811
  latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
@@ -820,12 +841,6 @@ export class ProtoArray {
820
841
  if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
821
842
  break;
822
843
  }
823
- // If PayloadSeparated, that means the node is either PENDING or EMPTY, there could be
824
- // some ancestor still has syncing status.
825
- if (node.executionStatus === ExecutionStatus.PayloadSeparated) {
826
- nodeIndex = node.parent;
827
- continue;
828
- }
829
844
  this.validateNodeByIndex(nodeIndex);
830
845
  nodeIndex = node.parent;
831
846
  }
@@ -923,6 +938,13 @@ export class ProtoArray {
923
938
  invalidNode.executionStatus = ExecutionStatus.Invalid;
924
939
  invalidNode.bestChild = undefined;
925
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
+ }
926
948
 
927
949
  return invalidNode;
928
950
  }
@@ -944,6 +966,13 @@ export class ProtoArray {
944
966
  if (validNode.executionStatus === ExecutionStatus.Syncing) {
945
967
  validNode.executionStatus = ExecutionStatus.Valid;
946
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
+ }
947
976
  return validNode;
948
977
  }
949
978
 
@@ -1655,10 +1684,9 @@ export class ProtoArray {
1655
1684
  const ancestors: ProtoNode[] = [];
1656
1685
  const nonAncestors: ProtoNode[] = [];
1657
1686
 
1658
- // Include starting node if it's not PENDING (i.e., pre-Gloas or EMPTY/FULL variant post-Gloas)
1659
- if (node.payloadStatus !== PayloadStatus.PENDING) {
1660
- ancestors.push(node);
1661
- }
1687
+ // caller of this method may pass default status
1688
+ // this is the only node that we accept PENDING
1689
+ ancestors.push(node);
1662
1690
 
1663
1691
  let nodeIndex = startIndex;
1664
1692
  while (node.parent !== undefined) {