@lodestar/fork-choice 1.41.0-dev.20f622cc52 → 1.41.0-dev.241ad7952a

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,7 +1,8 @@
1
+ import {BitArray} from "@chainsafe/ssz";
1
2
  import {GENESIS_EPOCH, PTC_SIZE} from "@lodestar/params";
2
3
  import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
4
  import {Epoch, RootHex, Slot} from "@lodestar/types";
4
- import {toRootHex} from "@lodestar/utils";
5
+ import {bitCount, toRootHex} from "@lodestar/utils";
5
6
  import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
6
7
  import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
7
8
  import {
@@ -60,14 +61,14 @@ export class ProtoArray {
60
61
  private previousProposerBoost: ProposerBoost | null = null;
61
62
 
62
63
  /**
63
- * PTC (Payload Timeliness Committee) votes per block
64
- * Maps block root to boolean array of size PTC_SIZE (from params: 512 mainnet, 2 minimal)
64
+ * PTC (Payload Timeliness Committee) votes per block as bitvectors
65
+ * Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal)
65
66
  * Spec: gloas/fork-choice.md#modified-store (line 148)
66
67
  *
67
- * ptcVotes[blockRoot][i] = true if PTC member i voted payload_present=true
68
+ * Bit i is set if PTC member i voted payload_present=true
68
69
  * Used by is_payload_timely() to determine if payload is timely
69
70
  */
70
- private ptcVotes = new Map<RootHex, boolean[]>();
71
+ private ptcVotes = new Map<RootHex, BitArray>();
71
72
 
72
73
  constructor({
73
74
  pruneThreshold,
@@ -168,6 +169,26 @@ export class ProtoArray {
168
169
  return PayloadStatus.PENDING;
169
170
  }
170
171
 
172
+ /**
173
+ * Get the node index for the default/canonical variant in a single hash lookup.
174
+ * - Pre-Gloas blocks: returns the FULL variant index
175
+ * - Gloas blocks: returns the PENDING variant index
176
+ */
177
+ getDefaultNodeIndex(blockRoot: RootHex): number | undefined {
178
+ const variantOrArr = this.indices.get(blockRoot);
179
+ if (variantOrArr == null) {
180
+ return undefined;
181
+ }
182
+
183
+ // Pre-Gloas: value is the index directly
184
+ if (!Array.isArray(variantOrArr)) {
185
+ return variantOrArr;
186
+ }
187
+
188
+ // Gloas: PENDING is the canonical variant
189
+ return variantOrArr[PayloadStatus.PENDING];
190
+ }
191
+
171
192
  /**
172
193
  * Determine which parent payload status a block extends
173
194
  * Spec: gloas/fork-choice.md#new-get_parent_payload_status
@@ -370,6 +391,7 @@ export class ProtoArray {
370
391
  // We _must_ perform these functions separate from the weight-updating loop above to ensure
371
392
  // that we have a fully coherent set of weights before updating parent
372
393
  // best-child/descendant.
394
+ const proposerBoostRoot = proposerBoost?.root ?? null;
373
395
  for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
374
396
  const node = this.nodes[nodeIndex];
375
397
  if (node === undefined) {
@@ -382,7 +404,7 @@ export class ProtoArray {
382
404
  // If the node has a parent, try to update its best-child and best-descendant.
383
405
  const parentIndex = node.parent;
384
406
  if (parentIndex !== undefined) {
385
- this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, proposerBoost?.root ?? null);
407
+ this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, proposerBoostRoot);
386
408
  }
387
409
  }
388
410
  // Update the previous proposer boost
@@ -476,7 +498,7 @@ export class ProtoArray {
476
498
 
477
499
  // Initialize PTC votes for this block (all false initially)
478
500
  // Spec: gloas/fork-choice.md#modified-on_block (line 645)
479
- this.ptcVotes.set(block.blockRoot, new Array(PTC_SIZE).fill(false));
501
+ this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
480
502
  } else {
481
503
  // Pre-Gloas: Only create FULL node (payload embedded in block)
482
504
  const node: ProtoNode = {
@@ -605,8 +627,7 @@ export class ProtoArray {
605
627
  throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
606
628
  }
607
629
 
608
- // Update the vote
609
- votes[ptcIndex] = payloadPresent;
630
+ votes.set(ptcIndex, payloadPresent);
610
631
  }
611
632
  }
612
633
 
@@ -636,7 +657,7 @@ export class ProtoArray {
636
657
  }
637
658
 
638
659
  // Count votes for payload_present=true
639
- const yesVotes = votes.filter((v) => v).length;
660
+ const yesVotes = bitCount(votes.uint8Array);
640
661
  return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
641
662
  }
642
663
 
@@ -676,8 +697,8 @@ export class ProtoArray {
676
697
 
677
698
  // Get proposer boost block
678
699
  // We don't care about variant here, just need proposer boost block info
679
- const defaultStatus = this.getDefaultVariant(proposerBoostRoot);
680
- const proposerBoostBlock = defaultStatus !== undefined ? this.getNode(proposerBoostRoot, defaultStatus) : undefined;
700
+ const proposerBoostIndex = this.getDefaultNodeIndex(proposerBoostRoot);
701
+ const proposerBoostBlock = proposerBoostIndex !== undefined ? this.getNodeByIndex(proposerBoostIndex) : undefined;
681
702
  if (!proposerBoostBlock) {
682
703
  // Proposer boost block not found, default to extending payload
683
704
  return true;
@@ -745,12 +766,8 @@ export class ProtoArray {
745
766
  // if its in fcU.
746
767
  //
747
768
  const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
748
- // TODO GLOAS: verify if getting default variant is correct here
749
- const defaultStatus = this.getDefaultVariant(invalidateFromParentBlockRoot);
750
- const invalidateFromParentIndex =
751
- defaultStatus !== undefined
752
- ? this.getNodeIndexByRootAndStatus(invalidateFromParentBlockRoot, defaultStatus)
753
- : undefined;
769
+ // TODO GLOAS: verify if getting the default/canonical node index is correct here
770
+ const invalidateFromParentIndex = this.getDefaultNodeIndex(invalidateFromParentBlockRoot);
754
771
  if (invalidateFromParentIndex === undefined) {
755
772
  throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
756
773
  }
@@ -963,9 +980,7 @@ export class ProtoArray {
963
980
  }
964
981
 
965
982
  // Get canonical node: FULL for pre-Gloas, PENDING for Gloas
966
- const defaultStatus = this.getDefaultVariant(justifiedRoot);
967
- const justifiedIndex =
968
- defaultStatus !== undefined ? this.getNodeIndexByRootAndStatus(justifiedRoot, defaultStatus) : undefined;
983
+ const justifiedIndex = this.getDefaultNodeIndex(justifiedRoot);
969
984
  if (justifiedIndex === undefined) {
970
985
  throw new ProtoArrayError({
971
986
  code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN,
@@ -1477,11 +1492,8 @@ export class ProtoArray {
1477
1492
  * For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
1478
1493
  * For pre-Gloas blocks: returns FULL variants
1479
1494
  */
1480
- *iterateAncestorNodes(blockRoot: RootHex): IterableIterator<ProtoNode> {
1481
- // Get canonical node: FULL for pre-Gloas, PENDING for Gloas
1482
- const defaultStatus = this.getDefaultVariant(blockRoot);
1483
- const startIndex =
1484
- defaultStatus !== undefined ? this.getNodeIndexByRootAndStatus(blockRoot, defaultStatus) : undefined;
1495
+ *iterateAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): IterableIterator<ProtoNode> {
1496
+ const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
1485
1497
  if (startIndex === undefined) {
1486
1498
  return;
1487
1499
  }
@@ -1520,11 +1532,8 @@ export class ProtoArray {
1520
1532
  * For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
1521
1533
  * For pre-Gloas blocks: returns FULL variants
1522
1534
  */
1523
- getAllAncestorNodes(blockRoot: RootHex): ProtoNode[] {
1524
- // Get canonical node: FULL for pre-Gloas, PENDING for Gloas
1525
- const defaultStatus = this.getDefaultVariant(blockRoot);
1526
- const startIndex =
1527
- defaultStatus !== undefined ? this.getNodeIndexByRootAndStatus(blockRoot, defaultStatus) : undefined;
1535
+ getAllAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoNode[] {
1536
+ const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
1528
1537
  if (startIndex === undefined) {
1529
1538
  return [];
1530
1539
  }
@@ -1537,12 +1546,10 @@ export class ProtoArray {
1537
1546
  });
1538
1547
  }
1539
1548
 
1540
- // Include starting node if node is pre-gloas
1541
- // Reason why we exclude post-gloas is because node is always default variant (PENDING)
1542
- // which we want to exclude.
1549
+ // Exclude PENDING variant from returned ancestors.
1543
1550
  const nodes: ProtoNode[] = [];
1544
1551
 
1545
- if (!isGloasBlock(node)) {
1552
+ if (node.payloadStatus !== PayloadStatus.PENDING) {
1546
1553
  nodes.push(node);
1547
1554
  }
1548
1555
 
@@ -1567,13 +1574,8 @@ export class ProtoArray {
1567
1574
  * For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
1568
1575
  * For pre-Gloas blocks: returns FULL variants
1569
1576
  */
1570
- getAllNonAncestorNodes(blockRoot: RootHex): ProtoNode[] {
1571
- // Get canonical node: FULL for pre-Gloas, PENDING for Gloas
1572
- const defaultStatus = this.getDefaultVariant(blockRoot);
1573
- if (defaultStatus === undefined) {
1574
- return [];
1575
- }
1576
- const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, defaultStatus);
1577
+ getAllNonAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoNode[] {
1578
+ const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
1577
1579
  if (startIndex === undefined) {
1578
1580
  return [];
1579
1581
  }
@@ -1613,11 +1615,11 @@ export class ProtoArray {
1613
1615
  * For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
1614
1616
  * For pre-Gloas blocks: returns FULL variants
1615
1617
  */
1616
- getAllAncestorAndNonAncestorNodes(blockRoot: RootHex): {ancestors: ProtoNode[]; nonAncestors: ProtoNode[]} {
1617
- // Get canonical node: FULL for pre-Gloas, PENDING for Gloas
1618
- const defaultStatus = this.getDefaultVariant(blockRoot);
1619
- const startIndex =
1620
- defaultStatus !== undefined ? this.getNodeIndexByRootAndStatus(blockRoot, defaultStatus) : undefined;
1618
+ getAllAncestorAndNonAncestorNodes(
1619
+ blockRoot: RootHex,
1620
+ payloadStatus: PayloadStatus
1621
+ ): {ancestors: ProtoNode[]; nonAncestors: ProtoNode[]} {
1622
+ const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
1621
1623
  if (startIndex === undefined) {
1622
1624
  return {ancestors: [], nonAncestors: []};
1623
1625
  }
@@ -1667,12 +1669,7 @@ export class ProtoArray {
1667
1669
  * Uses default variant (PENDING for Gloas, FULL for pre-Gloas)
1668
1670
  */
1669
1671
  hasBlock(blockRoot: RootHex): boolean {
1670
- const defaultVariant = this.getDefaultVariant(blockRoot);
1671
- if (defaultVariant === undefined) {
1672
- return false;
1673
- }
1674
- const index = this.getNodeIndexByRootAndStatus(blockRoot, defaultVariant);
1675
- return index !== undefined;
1672
+ return this.getDefaultNodeIndex(blockRoot) !== undefined;
1676
1673
  }
1677
1674
 
1678
1675
  /**
@@ -1735,26 +1732,28 @@ export class ProtoArray {
1735
1732
  /**
1736
1733
  * Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
1737
1734
  * Always returns `false` if either input roots are unknown.
1738
- * Still returns `true` if `ancestorRoot` === `descendantRoot` (and the roots are known)
1735
+ * Still returns `true` if `ancestorRoot` === `descendantRoot` and payload statuses match.
1739
1736
  */
1740
- isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
1741
- // We use the default variant (PENDING for Gloas, FULL for pre-Gloas)
1742
- // We cannot use FULL/EMPTY variants for Gloas because they may not be canonical
1743
- const defaultStatus = this.getDefaultVariant(ancestorRoot);
1744
- const ancestorNode = defaultStatus !== undefined ? this.getNode(ancestorRoot, defaultStatus) : undefined;
1737
+ isDescendant(
1738
+ ancestorRoot: RootHex,
1739
+ ancestorPayloadStatus: PayloadStatus,
1740
+ descendantRoot: RootHex,
1741
+ descendantPayloadStatus: PayloadStatus
1742
+ ): boolean {
1743
+ const ancestorNode = this.getNode(ancestorRoot, ancestorPayloadStatus);
1745
1744
  if (!ancestorNode) {
1746
1745
  return false;
1747
1746
  }
1748
1747
 
1749
- if (ancestorRoot === descendantRoot) {
1748
+ if (ancestorRoot === descendantRoot && ancestorPayloadStatus === descendantPayloadStatus) {
1750
1749
  return true;
1751
1750
  }
1752
1751
 
1753
- for (const node of this.iterateAncestorNodes(descendantRoot)) {
1752
+ for (const node of this.iterateAncestorNodes(descendantRoot, descendantPayloadStatus)) {
1754
1753
  if (node.slot < ancestorNode.slot) {
1755
1754
  return false;
1756
1755
  }
1757
- if (node.blockRoot === ancestorNode.blockRoot) {
1756
+ if (node.blockRoot === ancestorNode.blockRoot && node.payloadStatus === ancestorNode.payloadStatus) {
1758
1757
  return true;
1759
1758
  }
1760
1759
  }