@lodestar/fork-choice 1.45.0-dev.51a1c44b27 → 1.45.0-dev.6568180f96

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.
Files changed (49) hide show
  1. package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -1
  2. package/lib/forkChoice/fastConfirmation/data.js +1 -0
  3. package/lib/forkChoice/fastConfirmation/data.js.map +1 -1
  4. package/lib/forkChoice/fastConfirmation/metrics.d.ts +1 -0
  5. package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -1
  6. package/lib/forkChoice/fastConfirmation/metrics.js +4 -0
  7. package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -1
  8. package/lib/forkChoice/fastConfirmation/types.d.ts +3 -0
  9. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
  10. package/lib/forkChoice/fastConfirmation/types.js.map +1 -1
  11. package/lib/forkChoice/fastConfirmation/utils.d.ts +3 -3
  12. package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
  13. package/lib/forkChoice/fastConfirmation/utils.js +27 -19
  14. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
  15. package/lib/forkChoice/forkChoice.d.ts +46 -7
  16. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  17. package/lib/forkChoice/forkChoice.js +178 -55
  18. package/lib/forkChoice/forkChoice.js.map +1 -1
  19. package/lib/forkChoice/interface.d.ts +18 -6
  20. package/lib/forkChoice/interface.d.ts.map +1 -1
  21. package/lib/forkChoice/interface.js +1 -1
  22. package/lib/forkChoice/interface.js.map +1 -1
  23. package/lib/index.d.ts +1 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +1 -1
  26. package/lib/index.js.map +1 -1
  27. package/lib/metrics.d.ts +1 -0
  28. package/lib/metrics.d.ts.map +1 -1
  29. package/lib/protoArray/computeDeltas.d.ts +3 -3
  30. package/lib/protoArray/computeDeltas.d.ts.map +1 -1
  31. package/lib/protoArray/computeDeltas.js +31 -27
  32. package/lib/protoArray/computeDeltas.js.map +1 -1
  33. package/lib/protoArray/interface.d.ts +6 -0
  34. package/lib/protoArray/interface.d.ts.map +1 -1
  35. package/lib/protoArray/protoArray.d.ts +28 -3
  36. package/lib/protoArray/protoArray.d.ts.map +1 -1
  37. package/lib/protoArray/protoArray.js +106 -18
  38. package/lib/protoArray/protoArray.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/forkChoice/fastConfirmation/data.ts +1 -0
  41. package/src/forkChoice/fastConfirmation/metrics.ts +4 -0
  42. package/src/forkChoice/fastConfirmation/types.ts +4 -0
  43. package/src/forkChoice/fastConfirmation/utils.ts +39 -16
  44. package/src/forkChoice/forkChoice.ts +193 -57
  45. package/src/forkChoice/interface.ts +15 -16
  46. package/src/index.ts +1 -1
  47. package/src/protoArray/computeDeltas.ts +33 -29
  48. package/src/protoArray/interface.ts +6 -0
  49. package/src/protoArray/protoArray.ts +117 -22
@@ -4,10 +4,10 @@ import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
4
4
  import {NULL_VOTE_INDEX, VoteIndex} from "./interface.js";
5
5
 
6
6
  // reuse arrays to avoid memory reallocation and gc
7
- const deltas = new Array<number>();
7
+ const attestationDeltas = new Array<number>();
8
8
 
9
9
  export type DeltasResult = {
10
- deltas: number[];
10
+ attestationDeltas: number[];
11
11
  equivocatingValidators: number;
12
12
  // inactive validators before beacon node started
13
13
  oldInactiveValidators: number;
@@ -19,9 +19,9 @@ export type DeltasResult = {
19
19
  };
20
20
 
21
21
  /**
22
- * Returns a list of `deltas`, where there is one delta for each of the indices in `indices`
22
+ * Returns a list of `attestationDeltas`, where there is one delta for each of the indices in `indices`
23
23
  *
24
- * The deltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
24
+ * The attestationDeltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
25
25
  *
26
26
  * ## Errors
27
27
  *
@@ -46,8 +46,8 @@ export function computeDeltas(
46
46
  throw new Error(`numProtoNodes must be less than NULL_VOTE_INDEX: ${numProtoNodes} >= ${NULL_VOTE_INDEX}`);
47
47
  }
48
48
 
49
- deltas.length = numProtoNodes;
50
- deltas.fill(0);
49
+ attestationDeltas.length = numProtoNodes;
50
+ attestationDeltas.fill(0);
51
51
 
52
52
  // avoid creating new variables in the loop to potentially reduce GC pressure
53
53
  let oldBalance: number, newBalance: number;
@@ -67,6 +67,27 @@ export function computeDeltas(
67
67
  currentIndex = voteCurrentIndices[vIndex];
68
68
  nextIndex = voteNextIndices[vIndex];
69
69
 
70
+ // This equivocator check MUST be the first branch in the loop body.
71
+ // Handle equivocating (attester-slashed) validators before the no-live-vote check so the sorted
72
+ // cursor always advances; a jammed cursor would skip the discount for higher-index equivocators.
73
+ if (vIndex === equivocatingValidatorIndex) {
74
+ // this function could be called multiple times but we only want to process slashing validator for 1 time
75
+ if (currentIndex !== NULL_VOTE_INDEX) {
76
+ if (currentIndex >= numProtoNodes) {
77
+ throw new ProtoArrayError({
78
+ code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
79
+ index: currentIndex,
80
+ });
81
+ }
82
+ oldBalance = oldBalances[vIndex] ?? 0;
83
+ attestationDeltas[currentIndex] -= oldBalance;
84
+ }
85
+ voteCurrentIndices[vIndex] = NULL_VOTE_INDEX;
86
+ equivocatingIndex++;
87
+ equivocatingValidatorIndex = equivocatingArray[equivocatingIndex];
88
+ continue;
89
+ }
90
+
70
91
  // There is no need to create a score change if the validator has never voted or both of their
71
92
  // votes are for the zero hash (genesis block)
72
93
  if (currentIndex === NULL_VOTE_INDEX && nextIndex === NULL_VOTE_INDEX) {
@@ -85,23 +106,6 @@ export function computeDeltas(
85
106
  // on-boarded fewer validators than the prior fork.
86
107
  newBalance = newBalances === oldBalances ? oldBalance : (newBalances[vIndex] ?? 0);
87
108
 
88
- if (vIndex === equivocatingValidatorIndex) {
89
- // this function could be called multiple times but we only want to process slashing validator for 1 time
90
- if (currentIndex !== NULL_VOTE_INDEX) {
91
- if (currentIndex >= numProtoNodes) {
92
- throw new ProtoArrayError({
93
- code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
94
- index: currentIndex,
95
- });
96
- }
97
- deltas[currentIndex] -= oldBalance;
98
- }
99
- voteCurrentIndices[vIndex] = NULL_VOTE_INDEX;
100
- equivocatingIndex++;
101
- equivocatingValidatorIndex = equivocatingArray[equivocatingIndex];
102
- continue;
103
- }
104
-
105
109
  if (oldBalance === 0 && newBalance === 0) {
106
110
  newInactiveValidators++;
107
111
  continue;
@@ -121,7 +125,7 @@ export function computeDeltas(
121
125
  });
122
126
  }
123
127
 
124
- deltas[currentIndex] -= oldBalance;
128
+ attestationDeltas[currentIndex] -= oldBalance;
125
129
  }
126
130
 
127
131
  // We ignore the vote if it is not known in `indices .
@@ -134,7 +138,7 @@ export function computeDeltas(
134
138
  });
135
139
  }
136
140
 
137
- deltas[nextIndex] += newBalance;
141
+ attestationDeltas[nextIndex] += newBalance;
138
142
  }
139
143
  voteCurrentIndices[vIndex] = nextIndex;
140
144
  newVoteValidators++;
@@ -143,13 +147,13 @@ export function computeDeltas(
143
147
  }
144
148
  } // end validator loop
145
149
 
146
- if (deltas.length !== numProtoNodes) {
147
- // deltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
148
- throw new Error(`deltas length mismatch: expected ${numProtoNodes}, got ${deltas.length}`);
150
+ if (attestationDeltas.length !== numProtoNodes) {
151
+ // attestationDeltas array could be growed in the loop, especially if we mistakenly set the [NULL_VOTE_INDEX] to it , just to be safe
152
+ throw new Error(`attestationDeltas length mismatch: expected ${numProtoNodes}, got ${attestationDeltas.length}`);
149
153
  }
150
154
 
151
155
  return {
152
- deltas,
156
+ attestationDeltas,
153
157
  equivocatingValidators,
154
158
  oldInactiveValidators,
155
159
  newInactiveValidators,
@@ -160,7 +160,13 @@ export type ProtoBlock = BlockExtraMeta & {
160
160
  */
161
161
  export type ProtoNode = ProtoBlock & {
162
162
  parent?: number;
163
+ /** Total weight, ie. attestationScore plus the proposer boost credited to this node */
163
164
  weight: number;
165
+ /**
166
+ * Weight from attester votes only, excluding proposer boost.
167
+ * Spec: get_attestation_score
168
+ */
169
+ attestationScore: number;
164
170
  bestChild?: number;
165
171
  bestDescendant?: number;
166
172
  };
@@ -27,6 +27,12 @@ const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
27
27
  */
28
28
  const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
29
29
 
30
+ /**
31
+ * Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
32
+ * Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
33
+ */
34
+ const boostDeltas = new Array<number>();
35
+
30
36
  /**
31
37
  * popcount(attended AND NOT yes) — explicit False-vote count.
32
38
  * Excludes PTC members who didn't attest (the None state).
@@ -348,7 +354,7 @@ export class ProtoArray {
348
354
  * - If required, update the parents best-descendant with the current node or its best-descendant.
349
355
  */
350
356
  applyScoreChanges({
351
- deltas,
357
+ attestationDeltas,
352
358
  proposerBoost,
353
359
  justifiedEpoch,
354
360
  justifiedRoot,
@@ -356,7 +362,7 @@ export class ProtoArray {
356
362
  finalizedRoot,
357
363
  currentSlot,
358
364
  }: {
359
- deltas: number[];
365
+ attestationDeltas: number[];
360
366
  proposerBoost: ProposerBoost | null;
361
367
  justifiedEpoch: Epoch;
362
368
  justifiedRoot: RootHex;
@@ -364,14 +370,17 @@ export class ProtoArray {
364
370
  finalizedRoot: RootHex;
365
371
  currentSlot: Slot;
366
372
  }): void {
367
- if (deltas.length !== this.nodes.length) {
373
+ if (attestationDeltas.length !== this.nodes.length) {
368
374
  throw new ProtoArrayError({
369
375
  code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
370
- deltas: deltas.length,
376
+ deltas: attestationDeltas.length,
371
377
  indices: this.nodes.length,
372
378
  });
373
379
  }
374
380
 
381
+ boostDeltas.length = this.nodes.length;
382
+ boostDeltas.fill(0);
383
+
375
384
  if (
376
385
  justifiedEpoch !== this.justifiedEpoch ||
377
386
  finalizedEpoch !== this.finalizedEpoch ||
@@ -416,18 +425,22 @@ export class ProtoArray {
416
425
  // If this node's execution status has been marked invalid, then the weight of the node
417
426
  // needs to be taken out of consideration after which the node weight will become 0
418
427
  // for subsequent iterations of applyScoreChanges
419
- const nodeDelta =
420
- node.executionStatus === ExecutionStatus.Invalid
421
- ? -node.weight
422
- : deltas[nodeIndex] + currentBoost - previousBoost;
423
-
424
- // Apply the delta to the node
425
- node.weight += nodeDelta;
426
-
427
- // Update the parent delta (if any)
428
+ const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
429
+ const attestationDelta = isInvalid ? -node.attestationScore : attestationDeltas[nodeIndex];
430
+ const boostDelta = isInvalid
431
+ ? // old boost = weight - attestationScore
432
+ -(node.weight - node.attestationScore)
433
+ : boostDeltas[nodeIndex] + currentBoost - previousBoost;
434
+
435
+ // Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
436
+ // by tracking the two scores apart.
437
+ node.attestationScore += attestationDelta;
438
+ node.weight += attestationDelta + boostDelta;
439
+
440
+ // Update the parent deltas (if any)
428
441
  const parentIndex = node.parent;
429
442
  if (parentIndex !== undefined) {
430
- const parentDelta = deltas[parentIndex];
443
+ const parentDelta = attestationDeltas[parentIndex];
431
444
  if (parentDelta === undefined) {
432
445
  throw new ProtoArrayError({
433
446
  code: ProtoArrayErrorCode.INVALID_PARENT_DELTA,
@@ -435,8 +448,9 @@ export class ProtoArray {
435
448
  });
436
449
  }
437
450
 
438
- // back-propagate the nodes delta to its parent
439
- deltas[parentIndex] += nodeDelta;
451
+ // back-propagate the nodes deltas to its parent
452
+ attestationDeltas[parentIndex] += attestationDelta;
453
+ boostDeltas[parentIndex] += boostDelta;
440
454
  }
441
455
  }
442
456
 
@@ -514,6 +528,7 @@ export class ProtoArray {
514
528
  parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
515
529
  payloadStatus: PayloadStatus.PENDING,
516
530
  weight: 0,
531
+ attestationScore: 0,
517
532
  bestChild: undefined,
518
533
  bestDescendant: undefined,
519
534
  };
@@ -527,6 +542,7 @@ export class ProtoArray {
527
542
  parent: pendingIndex, // Points to own PENDING
528
543
  payloadStatus: PayloadStatus.EMPTY,
529
544
  weight: 0,
545
+ attestationScore: 0,
530
546
  bestChild: undefined,
531
547
  bestDescendant: undefined,
532
548
  };
@@ -562,6 +578,7 @@ export class ProtoArray {
562
578
  parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
563
579
  payloadStatus: PayloadStatus.FULL,
564
580
  weight: 0,
581
+ attestationScore: 0,
565
582
  bestChild: undefined,
566
583
  bestDescendant: undefined,
567
584
  };
@@ -647,6 +664,7 @@ export class ProtoArray {
647
664
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
648
665
  payloadStatus: PayloadStatus.FULL,
649
666
  weight: 0,
667
+ attestationScore: 0,
650
668
  bestChild: undefined,
651
669
  bestDescendant: undefined,
652
670
  executionStatus,
@@ -672,6 +690,28 @@ export class ProtoArray {
672
690
  this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
673
691
  }
674
692
 
693
+ /**
694
+ * Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed
695
+ * payload (FULL variant exists). Used by the builder circuit breaker.
696
+ */
697
+ getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
698
+ let blocksPresent = 0;
699
+ let payloadsRevealed = 0;
700
+ // Full scan, nodes are in import order not slot order (an old block can be imported after newer
701
+ // ones during sync or reorg resolution), so we cannot stop early on an out-of-window slot
702
+ for (const node of this.nodes) {
703
+ // Count each gloas block once via its PENDING variant, pre-gloas nodes are FULL only
704
+ if (node.slot < fromSlot || node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) {
705
+ continue;
706
+ }
707
+ blocksPresent++;
708
+ if (this.hasPayload(node.blockRoot)) {
709
+ payloadsRevealed++;
710
+ }
711
+ }
712
+ return {blocksPresent, payloadsRevealed};
713
+ }
714
+
675
715
  /**
676
716
  * Update PTC votes for multiple validators attesting to a block
677
717
  * Spec: gloas/fork-choice.md#new-notify_ptc_messages
@@ -1030,7 +1070,7 @@ export class ProtoArray {
1030
1070
 
1031
1071
  // update the forkchoice as the invalidation can change the entire forkchoice DAG
1032
1072
  this.applyScoreChanges({
1033
- deltas: Array.from({length: this.nodes.length}, () => 0),
1073
+ attestationDeltas: Array.from({length: this.nodes.length}, () => 0),
1034
1074
  proposerBoost: this.previousProposerBoost,
1035
1075
  justifiedEpoch: this.justifiedEpoch,
1036
1076
  justifiedRoot: this.justifiedRoot,
@@ -1541,6 +1581,38 @@ export class ProtoArray {
1541
1581
  return correctJustified && correctFinalized;
1542
1582
  }
1543
1583
 
1584
+ /** Weights are in EFFECTIVE_BALANCE_INCREMENT units (NOT Gwei); callers scale as needed. */
1585
+ getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
1586
+ // Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
1587
+ // checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
1588
+ // that descends from the justified checkpoint block AND is itself viable for head. Iterating
1589
+ // all nodes without the justified-descendant filter would wrongly include FFG-viable leaves
1590
+ // that hang off the finalized checkpoint on a branch not under the current justified checkpoint.
1591
+ const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
1592
+ // Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
1593
+ // tree, identified by (root, payload_status, weight) — emit one entry per variant.
1594
+ const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] = [];
1595
+ for (const node of this.nodes) {
1596
+ if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
1597
+ continue;
1598
+ }
1599
+ if (this.justifiedEpoch !== GENESIS_EPOCH) {
1600
+ // Same-root short-circuit: every payload-status variant of the justified block itself is
1601
+ // in the filtered tree, but `isDescendant` from the default (PENDING) variant does not
1602
+ // reach the sibling EMPTY/FULL variants of the same root.
1603
+ const descendsFromJustified =
1604
+ node.blockRoot === this.justifiedRoot ||
1605
+ (justifiedVariant !== undefined &&
1606
+ this.isDescendant(this.justifiedRoot, justifiedVariant, node.blockRoot, node.payloadStatus));
1607
+ if (!descendsFromJustified) {
1608
+ continue;
1609
+ }
1610
+ }
1611
+ heads.push({root: node.blockRoot, payloadStatus: node.payloadStatus, weight: node.weight});
1612
+ }
1613
+ return heads;
1614
+ }
1615
+
1544
1616
  /**
1545
1617
  * Return `true` if `node` is equal to or a descendant of the finalized node.
1546
1618
  * This function helps improve performance of nodeIsViableForHead a lot by avoiding
@@ -1584,7 +1656,7 @@ export class ProtoArray {
1584
1656
  * ### Specification
1585
1657
  *
1586
1658
  * Modified for Gloas to return node identifier instead of just root:
1587
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#modified-get_ancestor
1659
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-get_ancestor
1588
1660
  *
1589
1661
  * Pre-Gloas: Returns (root, PAYLOAD_STATUS_FULL)
1590
1662
  * Gloas: Returns (root, payloadStatus) based on actual node state
@@ -1898,6 +1970,19 @@ export class ProtoArray {
1898
1970
  return this.getNodeByIndex(blockIndex);
1899
1971
  }
1900
1972
 
1973
+ /**
1974
+ * Return ProtoNode for the default/canonical variant in a single hash lookup
1975
+ * - Pre-Gloas blocks: the FULL variant
1976
+ * - Gloas blocks: the PENDING variant
1977
+ */
1978
+ getNodeDefaultStatus(blockRoot: RootHex): ProtoNode | undefined {
1979
+ const nodeIndex = this.getDefaultNodeIndex(blockRoot);
1980
+ if (nodeIndex === undefined) {
1981
+ return undefined;
1982
+ }
1983
+ return this.getNodeByIndex(nodeIndex);
1984
+ }
1985
+
1901
1986
  /**
1902
1987
  * Return MUTABLE ProtoBlock for blockRoot with explicit payload status
1903
1988
  *
@@ -1941,6 +2026,11 @@ export class ProtoArray {
1941
2026
  * Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
1942
2027
  * Always returns `false` if either input roots are unknown.
1943
2028
  * Still returns `true` if `ancestorRoot` === `descendantRoot` and payload statuses match.
2029
+ *
2030
+ * Gloas payload-status matching: a `PENDING` ancestor matches any payload variant
2031
+ * (PENDING/EMPTY/FULL) of the same block, so this can also return `true` for the same
2032
+ * root when statuses differ (e.g. ancestor `PENDING`, descendant `EMPTY`/`FULL`).
2033
+ * `EMPTY` and `FULL` are mutually exclusive siblings and never match each other.
1944
2034
  */
1945
2035
  isDescendant(
1946
2036
  ancestorRoot: RootHex,
@@ -1958,11 +2048,16 @@ export class ProtoArray {
1958
2048
  }
1959
2049
 
1960
2050
  for (const node of this.iterateAncestorNodes(descendantRoot, descendantPayloadStatus)) {
1961
- if (node.slot < ancestorNode.slot) {
1962
- return false;
2051
+ if (node.blockRoot === ancestorNode.blockRoot) {
2052
+ // Gloas is_ancestor: a PENDING ancestor matches any payload variant of the same block.
2053
+ return (
2054
+ node.payloadStatus === ancestorNode.payloadStatus || ancestorNode.payloadStatus === PayloadStatus.PENDING
2055
+ );
1963
2056
  }
1964
- if (node.blockRoot === ancestorNode.blockRoot && node.payloadStatus === ancestorNode.payloadStatus) {
1965
- return true;
2057
+ // Ancestors are iterated in decreasing slot, so once we reach the ancestor's slot
2058
+ // without a root match it cannot be in this chain.
2059
+ if (node.slot <= ancestorNode.slot) {
2060
+ return false;
1966
2061
  }
1967
2062
  }
1968
2063
  return false;