@lodestar/fork-choice 1.47.0-dev.6325e4e1fe → 1.47.0-dev.6c2e8e6b0f

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,5 +1,11 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
2
+ import {
3
+ EFFECTIVE_BALANCE_INCREMENT,
4
+ MIN_SEED_LOOKAHEAD,
5
+ SLOTS_PER_EPOCH,
6
+ isForkPostFulu,
7
+ isForkPostGloas,
8
+ } from "@lodestar/params";
3
9
  import {
4
10
  DataAvailabilityStatus,
5
11
  EffectiveBalanceIncrements,
@@ -68,6 +74,8 @@ export type ForkChoiceOpts = {
68
74
  fastConfirmation?: boolean;
69
75
  };
70
76
 
77
+ const EFFECTIVE_BALANCE_INCREMENT_BIGINT = BigInt(EFFECTIVE_BALANCE_INCREMENT);
78
+
71
79
  export enum UpdateHeadOpt {
72
80
  GetCanonicalHead = "getCanonicalHead", // Skip getProposerHead
73
81
  GetProposerHead = "getProposerHead", // With getProposerHead
@@ -147,7 +155,7 @@ export class ForkChoice implements IForkChoice {
147
155
  /** Boost the entire branch with this proposer root as the leaf */
148
156
  private proposerBoostRoot: RootHex | null = null;
149
157
  /** Score to use in proposer boost, evaluated lazily from justified balances */
150
- private justifiedProposerBoostScore: number | null = null;
158
+ private justifiedProposerBoostScore: bigint | null = null;
151
159
  /** The current effective balances */
152
160
  private balances: EffectiveBalanceIncrements;
153
161
  /** Optional fast confirmation rule implementation */
@@ -587,7 +595,7 @@ export class ForkChoice implements IForkChoice {
587
595
  * The structure in line with deltas to propagate boost up the branch
588
596
  * starting from the proposerIndex
589
597
  */
590
- let proposerBoost: {root: RootHex; score: number} | null = null;
598
+ let proposerBoost: {root: RootHex; score: bigint} | null = null;
591
599
  if (this.opts?.proposerBoost && this.proposerBoostRoot) {
592
600
  const proposerBoostScore =
593
601
  this.justifiedProposerBoostScore ??
@@ -635,22 +643,11 @@ export class ForkChoice implements IForkChoice {
635
643
  return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
636
644
  }
637
645
 
638
- /**
639
- * weight is in EFFECTIVE_BALANCE_INCREMENTS not gwei.
640
- * For compliance test use only
641
- */
642
- getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
646
+ /** Returns exact Gwei weights for the compliance test. */
647
+ getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
643
648
  return this.protoArray.getViableHeads(this.fcStore.currentSlot);
644
649
  }
645
650
 
646
- /**
647
- * The cached justified total active balance, in EFFECTIVE_BALANCE_INCREMENT units.
648
- * For compliance test use only
649
- */
650
- getJustifiedTotalActiveBalanceByIncrement(): number {
651
- return this.fcStore.justified.totalBalance;
652
- }
653
-
654
651
  /** This is for the debug API only */
655
652
  getAllNodes(): ProtoNode[] {
656
653
  return this.protoArray.nodes;
@@ -1081,11 +1078,20 @@ export class ForkChoice implements IForkChoice {
1081
1078
  while (this.fcStore.currentSlot < currentSlot) {
1082
1079
  const previousSlot = this.fcStore.currentSlot;
1083
1080
  // Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
1084
- this.onTick(previousSlot + 1);
1081
+ const didUpdateCheckpoints = this.onTick(previousSlot + 1);
1085
1082
  this.queuedAttestationsPreviousSlot = 0;
1086
1083
  // Process any attestations that might now be eligible before running FCR for this slot.
1087
1084
  this.processAttestationQueue();
1088
- this.runFastConfirmation();
1085
+ const didRecomputeHead = this.runFastConfirmation();
1086
+
1087
+ // An epoch-boundary checkpoint pull-up can move the head's dependent root and stale the cached
1088
+ // head before block 0 of the new epoch is imported, making isProposerBoostSameDependentRoot()
1089
+ // wrong for that block. Recompute the head so it reflects the new checkpoint and the queued
1090
+ // votes — unless fast confirmation already did, to avoid a redundant head calculation.
1091
+ if (didUpdateCheckpoints && !didRecomputeHead) {
1092
+ this.updateHead();
1093
+ }
1094
+
1089
1095
  this.validatedAttestationDatas = new Set();
1090
1096
  }
1091
1097
  }
@@ -1636,7 +1642,7 @@ export class ForkChoice implements IForkChoice {
1636
1642
  // the justified state) - fall back to the head state's effective balance
1637
1643
  balance = state.effectiveBalanceIncrements[validatorIndex];
1638
1644
  }
1639
- headWeight += balance;
1645
+ headWeight += BigInt(balance) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
1640
1646
  }
1641
1647
  }
1642
1648
  }
@@ -1666,7 +1672,6 @@ export class ForkChoice implements IForkChoice {
1666
1672
 
1667
1673
  // pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
1668
1674
  const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;
1669
-
1670
1675
  return parentWeight > parentThreshold;
1671
1676
  }
1672
1677
 
@@ -1732,23 +1737,31 @@ export class ForkChoice implements IForkChoice {
1732
1737
  * May need the justified balances of:
1733
1738
  * - unrealizedJustified: Already available in `CheckpointWithBalance`
1734
1739
  * Since this balances are already available the getter is just `() => balances`, without cache interaction
1740
+ *
1741
+ * @returns Whether either checkpoint was updated.
1735
1742
  */
1736
1743
  private updateCheckpoints(
1737
1744
  justifiedCheckpoint: CheckpointWithHex,
1738
1745
  finalizedCheckpoint: CheckpointWithHex,
1739
1746
  getJustifiedBalances: () => JustifiedBalances
1740
- ): void {
1747
+ ): boolean {
1748
+ let updated = false;
1749
+
1741
1750
  // Update justified checkpoint.
1742
1751
  if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
1743
1752
  this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
1744
1753
  this.justifiedProposerBoostScore = null;
1754
+ updated = true;
1745
1755
  }
1746
1756
 
1747
1757
  // Update finalized checkpoint.
1748
1758
  if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
1749
1759
  this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
1750
1760
  this.justifiedProposerBoostScore = null;
1761
+ updated = true;
1751
1762
  }
1763
+
1764
+ return updated;
1752
1765
  }
1753
1766
 
1754
1767
  /**
@@ -2034,8 +2047,10 @@ export class ForkChoice implements IForkChoice {
2034
2047
  * Equivalent to:
2035
2048
  *
2036
2049
  * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_tick
2050
+ *
2051
+ * @returns Whether an epoch-boundary checkpoint was updated.
2037
2052
  */
2038
- private onTick(time: Slot): void {
2053
+ private onTick(time: Slot): boolean {
2039
2054
  const previousSlot = this.fcStore.currentSlot;
2040
2055
 
2041
2056
  if (time > previousSlot + 1) {
@@ -2056,11 +2071,11 @@ export class ForkChoice implements IForkChoice {
2056
2071
 
2057
2072
  // Not a new epoch, return.
2058
2073
  if (computeSlotsSinceEpochStart(time) !== 0) {
2059
- return;
2074
+ return false;
2060
2075
  }
2061
2076
 
2062
- // If a new epoch, pull-up justification and finalization from previous epoch
2063
- this.updateCheckpoints(
2077
+ // If a new epoch, pull-up justification and finalization from previous epoch.
2078
+ return this.updateCheckpoints(
2064
2079
  this.fcStore.unrealizedJustified.checkpoint,
2065
2080
  this.fcStore.unrealizedFinalizedCheckpoint,
2066
2081
  () => this.fcStore.unrealizedJustified.balances
@@ -2086,11 +2101,9 @@ export class ForkChoice implements IForkChoice {
2086
2101
  return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
2087
2102
  }
2088
2103
 
2089
- // No reorg if we are at an epoch boundary
2090
- // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_not_epoch_boundary
2091
- const isAtEpochBoundary = slot % SLOTS_PER_EPOCH === 0;
2092
- if (isAtEpochBoundary) {
2093
- return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.AtEpochBoundary};
2104
+ const isShufflingStable = isForkPostFulu(this.config.getForkName(slot)) || slot % SLOTS_PER_EPOCH !== 0;
2105
+ if (!isShufflingStable) {
2106
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotShufflingStable};
2094
2107
  }
2095
2108
 
2096
2109
  // No reorg if headBlock and parentBlock are not ffg competitive
@@ -2121,10 +2134,11 @@ export class ForkChoice implements IForkChoice {
2121
2134
  return {prelimProposerHead};
2122
2135
  }
2123
2136
 
2124
- private runFastConfirmation(): void {
2137
+ /** Returns whether it recomputed the head, so the caller can avoid a redundant `updateHead()`. */
2138
+ private runFastConfirmation(): boolean {
2125
2139
  const fastConfirmationRule = this.fastConfirmationRule;
2126
2140
  const fastConfirmationContext = this.fastConfirmationContext;
2127
- if (!fastConfirmationRule || !fastConfirmationContext) return;
2141
+ if (!fastConfirmationRule || !fastConfirmationContext) return false;
2128
2142
 
2129
2143
  if (this.fastConfirmationPaused) {
2130
2144
  // Keep consumers on a safe, available root while the rule is paused
@@ -2135,7 +2149,7 @@ export class ForkChoice implements IForkChoice {
2135
2149
  // Runs outside the timed try/catch below; a throw would escape to the clock listener
2136
2150
  this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
2137
2151
  }
2138
- return;
2152
+ return false;
2139
2153
  }
2140
2154
 
2141
2155
  withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
@@ -2158,6 +2172,8 @@ export class ForkChoice implements IForkChoice {
2158
2172
  );
2159
2173
  }
2160
2174
  });
2175
+
2176
+ return true;
2161
2177
  }
2162
2178
 
2163
2179
  private createFastConfirmationContext(): FastConfirmationContext {
@@ -2209,12 +2225,14 @@ export class ForkChoice implements IForkChoice {
2209
2225
  }
2210
2226
  }
2211
2227
 
2212
- // Approximate https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
2228
+ // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
2213
2229
  // Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
2214
- export function getCommitteeFraction(
2230
+ function getCommitteeFraction(
2215
2231
  justifiedTotalActiveBalanceByIncrement: number,
2216
2232
  config: {slotsPerEpoch: number; committeePercent: number}
2217
- ): number {
2218
- const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
2219
- return Math.floor((committeeWeight * config.committeePercent) / 100);
2233
+ ): bigint {
2234
+ const committeeWeightGwei =
2235
+ (BigInt(justifiedTotalActiveBalanceByIncrement) * EFFECTIVE_BALANCE_INCREMENT_BIGINT) /
2236
+ BigInt(config.slotsPerEpoch);
2237
+ return (committeeWeightGwei * BigInt(config.committeePercent)) / 100n;
2220
2238
  }
@@ -54,7 +54,7 @@ export enum NotReorgedReason {
54
54
  HeadBlockIsTimely = "headBlockIsTimely",
55
55
  ParentBlockNotAvailable = "parentBlockNotAvailable",
56
56
  ProposerBoostReorgDisabled = "proposerBoostReorgDisabled",
57
- AtEpochBoundary = "atEpochBoundary",
57
+ NotShufflingStable = "notShufflingStable",
58
58
  NotFFGCompetitive = "notFFGCompetitive",
59
59
  ChainLongUnfinality = "chainLongUnfinality",
60
60
  ParentBlockDistanceMoreThanOneSlot = "parentBlockDistanceMoreThanOneSlot",
@@ -13,7 +13,7 @@ import {IForkChoice} from "./interface.js";
13
13
  * payload may not yet be confirmed canonical, so we report the parent EL block which has
14
14
  * been (the bid commits to extending it).
15
15
  *
16
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.13/specs/bellatrix/fast-confirmation.md#new-get_safe_execution_block_hash
16
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/bellatrix/fast-confirmation.md#new-get_safe_execution_block_hash
17
17
  */
18
18
  export function getSafeExecutionBlockHash(forkChoice: IForkChoice, logger?: Pick<Logger, LogLevel.debug>): RootHex {
19
19
  const confirmedRoot = forkChoice.getConfirmedRoot();
@@ -36,7 +36,7 @@ export function getSafeExecutionBlockHash(forkChoice: IForkChoice, logger?: Pick
36
36
  * Get execution payload hash to report as `finalizedBlockHash` in `engine_forkchoiceUpdated`.
37
37
  * Mirrors `getSafeExecutionBlockHash`: post-Gloas returns the bid `parent_block_hash`.
38
38
  *
39
- * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.13/specs/gloas/fork-choice.md#notify_forkchoice_updated
39
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#notify_forkchoice_updated
40
40
  */
41
41
  export function getFinalizedExecutionBlockHash(forkChoice: IForkChoice): RootHex {
42
42
  return getExecutionBlockHash(forkChoice.getFinalizedBlock());
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export {
17
17
  type IFastConfirmationStore,
18
18
  getFastConfirmationMetrics,
19
19
  } from "./forkChoice/fastConfirmation/fastConfirmationRule.js";
20
- export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt, getCommitteeFraction} from "./forkChoice/forkChoice.js";
20
+ export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
21
21
  export {
22
22
  type AncestorResult,
23
23
  AncestorStatus,
@@ -160,13 +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 */
164
- weight: number;
163
+ /** Total weight in Gwei, ie. attestationScore plus the proposer boost credited to this node */
164
+ weight: bigint;
165
165
  /**
166
- * Weight from attester votes only, excluding proposer boost.
166
+ * Weight in Gwei from attester votes only, excluding proposer boost.
167
167
  * Spec: get_attestation_score
168
168
  */
169
- attestationScore: number;
169
+ attestationScore: bigint;
170
170
  bestChild?: number;
171
171
  bestDescendant?: number;
172
172
  };
@@ -1,5 +1,5 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
- import {GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
2
+ import {EFFECTIVE_BALANCE_INCREMENT, GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
3
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";
@@ -31,7 +31,8 @@ const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
31
31
  * Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
32
32
  * Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
33
33
  */
34
- const boostDeltas = new Array<number>();
34
+ const boostDeltas = new Array<bigint>();
35
+ const EFFECTIVE_BALANCE_INCREMENT_BIGINT = BigInt(EFFECTIVE_BALANCE_INCREMENT);
35
36
 
36
37
  /**
37
38
  * popcount(attended AND NOT yes) — explicit False-vote count.
@@ -52,7 +53,7 @@ export function countNoVotes(attended: BitArray, yes: BitArray): number {
52
53
  }
53
54
 
54
55
  export const DEFAULT_PRUNE_THRESHOLD = 0;
55
- type ProposerBoost = {root: RootHex; score: number};
56
+ type ProposerBoost = {root: RootHex; score: bigint};
56
57
 
57
58
  const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
58
59
 
@@ -379,7 +380,7 @@ export class ProtoArray {
379
380
  }
380
381
 
381
382
  boostDeltas.length = this.nodes.length;
382
- boostDeltas.fill(0);
383
+ boostDeltas.fill(0n);
383
384
 
384
385
  if (
385
386
  justifiedEpoch !== this.justifiedEpoch ||
@@ -416,17 +417,20 @@ export class ProtoArray {
416
417
  // the boost neutral with respect to EMPTY vs FULL selection.
417
418
  const isBoostVariant = isGloasBlock(node) ? node.payloadStatus === PayloadStatus.PENDING : true; // pre-Gloas has only FULL, always boost
418
419
  const currentBoost =
419
- proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0;
420
+ proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0n;
420
421
  const previousBoost =
421
422
  this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot && isBoostVariant
422
423
  ? this.previousProposerBoost.score
423
- : 0;
424
+ : 0n;
424
425
 
425
426
  // If this node's execution status has been marked invalid, then the weight of the node
426
427
  // needs to be taken out of consideration after which the node weight will become 0
427
428
  // for subsequent iterations of applyScoreChanges
428
429
  const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
429
- const attestationDelta = isInvalid ? -node.attestationScore : attestationDeltas[nodeIndex];
430
+ const attestationDelta = isInvalid
431
+ ? -Number(node.attestationScore / EFFECTIVE_BALANCE_INCREMENT_BIGINT)
432
+ : attestationDeltas[nodeIndex];
433
+ const attestationDeltaGwei = BigInt(attestationDelta) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
430
434
  const boostDelta = isInvalid
431
435
  ? // old boost = weight - attestationScore
432
436
  -(node.weight - node.attestationScore)
@@ -434,8 +438,8 @@ export class ProtoArray {
434
438
 
435
439
  // Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
436
440
  // by tracking the two scores apart.
437
- node.attestationScore += attestationDelta;
438
- node.weight += attestationDelta + boostDelta;
441
+ node.attestationScore += attestationDeltaGwei;
442
+ node.weight += attestationDeltaGwei + boostDelta;
439
443
 
440
444
  // Update the parent deltas (if any)
441
445
  const parentIndex = node.parent;
@@ -527,8 +531,8 @@ export class ProtoArray {
527
531
  ...block,
528
532
  parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
529
533
  payloadStatus: PayloadStatus.PENDING,
530
- weight: 0,
531
- attestationScore: 0,
534
+ weight: 0n,
535
+ attestationScore: 0n,
532
536
  bestChild: undefined,
533
537
  bestDescendant: undefined,
534
538
  };
@@ -541,8 +545,8 @@ export class ProtoArray {
541
545
  ...block,
542
546
  parent: pendingIndex, // Points to own PENDING
543
547
  payloadStatus: PayloadStatus.EMPTY,
544
- weight: 0,
545
- attestationScore: 0,
548
+ weight: 0n,
549
+ attestationScore: 0n,
546
550
  bestChild: undefined,
547
551
  bestDescendant: undefined,
548
552
  };
@@ -577,8 +581,8 @@ export class ProtoArray {
577
581
  ...block,
578
582
  parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
579
583
  payloadStatus: PayloadStatus.FULL,
580
- weight: 0,
581
- attestationScore: 0,
584
+ weight: 0n,
585
+ attestationScore: 0n,
582
586
  bestChild: undefined,
583
587
  bestDescendant: undefined,
584
588
  };
@@ -663,8 +667,8 @@ export class ProtoArray {
663
667
  ...pendingNode,
664
668
  parent: pendingIndex, // Points to own PENDING (same as EMPTY)
665
669
  payloadStatus: PayloadStatus.FULL,
666
- weight: 0,
667
- attestationScore: 0,
670
+ weight: 0n,
671
+ attestationScore: 0n,
668
672
  bestChild: undefined,
669
673
  bestDescendant: undefined,
670
674
  executionStatus,
@@ -1496,13 +1500,13 @@ export class ProtoArray {
1496
1500
  childNode.payloadStatus === PayloadStatus.PENDING ||
1497
1501
  childNode.slot + 1 !== currentSlot
1498
1502
  ? childNode.weight
1499
- : 0;
1503
+ : 0n;
1500
1504
  const bestChildEffectiveWeight =
1501
1505
  !isGloasBlock(bestChildNode) ||
1502
1506
  bestChildNode.payloadStatus === PayloadStatus.PENDING ||
1503
1507
  bestChildNode.slot + 1 !== currentSlot
1504
1508
  ? bestChildNode.weight
1505
- : 0;
1509
+ : 0n;
1506
1510
 
1507
1511
  if (childEffectiveWeight !== bestChildEffectiveWeight) {
1508
1512
  // Different effective weights, choose the winner by weight
@@ -1597,8 +1601,8 @@ export class ProtoArray {
1597
1601
  return correctJustified && correctFinalized;
1598
1602
  }
1599
1603
 
1600
- /** Weights are in EFFECTIVE_BALANCE_INCREMENT units (NOT Gwei); callers scale as needed. */
1601
- getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
1604
+ /** Returns exact Gwei weights for the compliance test boundary. */
1605
+ getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
1602
1606
  // Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
1603
1607
  // checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
1604
1608
  // that descends from the justified checkpoint block AND is itself viable for head. Iterating
@@ -1607,7 +1611,7 @@ export class ProtoArray {
1607
1611
  const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
1608
1612
  // Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
1609
1613
  // tree, identified by (root, payload_status, weight) — emit one entry per variant.
1610
- const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] = [];
1614
+ const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] = [];
1611
1615
  for (const node of this.nodes) {
1612
1616
  if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
1613
1617
  continue;
@@ -1624,7 +1628,11 @@ export class ProtoArray {
1624
1628
  continue;
1625
1629
  }
1626
1630
  }
1627
- heads.push({root: node.blockRoot, payloadStatus: node.payloadStatus, weight: node.weight});
1631
+ heads.push({
1632
+ root: node.blockRoot,
1633
+ payloadStatus: node.payloadStatus,
1634
+ weight: node.weight,
1635
+ });
1628
1636
  }
1629
1637
  return heads;
1630
1638
  }