@lodestar/fork-choice 1.45.0-dev.0cff4e6916 → 1.45.0-dev.17a48050e6

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.
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.45.0-dev.0cff4e6916",
14
+ "version": "1.45.0-dev.17a48050e6",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -39,12 +39,12 @@
39
39
  "check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@chainsafe/ssz": "^1.4.0",
43
- "@lodestar/config": "^1.45.0-dev.0cff4e6916",
44
- "@lodestar/params": "^1.45.0-dev.0cff4e6916",
45
- "@lodestar/state-transition": "^1.45.0-dev.0cff4e6916",
46
- "@lodestar/types": "^1.45.0-dev.0cff4e6916",
47
- "@lodestar/utils": "^1.45.0-dev.0cff4e6916"
42
+ "@chainsafe/ssz": "^1.6.2",
43
+ "@lodestar/config": "^1.45.0-dev.17a48050e6",
44
+ "@lodestar/params": "^1.45.0-dev.17a48050e6",
45
+ "@lodestar/state-transition": "^1.45.0-dev.17a48050e6",
46
+ "@lodestar/types": "^1.45.0-dev.17a48050e6",
47
+ "@lodestar/utils": "^1.45.0-dev.17a48050e6"
48
48
  },
49
49
  "keywords": [
50
50
  "ethereum",
@@ -52,5 +52,5 @@
52
52
  "beacon",
53
53
  "blockchain"
54
54
  ],
55
- "gitHead": "0253da9815d85802e62b773620a00b9585c96af1"
55
+ "gitHead": "740e743dd01a58366a3bce4bc843678fd447a6f0"
56
56
  }
@@ -14,6 +14,7 @@ export function createFastConfirmationCache(): FastConfirmationCache {
14
14
  committeeBySlot: new Map(),
15
15
  isDescendantByRootPair: new Map(),
16
16
  voteWeightBySource: new Map(),
17
+ totalActiveBalanceByKey: new Map(),
17
18
  checkpointStateByKey: new Map(),
18
19
  };
19
20
  }
@@ -9,6 +9,8 @@ export type FastConfirmationBalanceSource = {
9
9
  balances: EffectiveBalanceIncrements;
10
10
  };
11
11
 
12
+ export type TotalActiveBalanceCacheKey = IBeaconStateView | EffectiveBalanceIncrements;
13
+
12
14
  export type ForkChoiceStateGetter = (
13
15
  opts: {stateRoot: RootHex; checkpoint?: never} | {stateRoot?: never; checkpoint: CheckpointWithHex}
14
16
  ) => IBeaconStateView | null;
@@ -109,6 +111,8 @@ export type FastConfirmationCache = {
109
111
  isDescendantByRootPair: Map<string, boolean>;
110
112
  /** voteRoot -> totalWeight, keyed by sourceKey */
111
113
  voteWeightBySource: Map<BalanceSourceKey, Map<RootHex, number>>;
114
+ /** total active balance, keyed by the balance source's state or balances reference */
115
+ totalActiveBalanceByKey: Map<TotalActiveBalanceCacheKey, number>;
112
116
  headState?: IBeaconStateView;
113
117
  pulledUpHeadState?: IBeaconStateView;
114
118
  checkpointStateByKey: Map<string, IBeaconStateView | null>;
@@ -18,6 +18,7 @@ import {
18
18
  FastConfirmationContext,
19
19
  FastConfirmationSnapshot,
20
20
  IFastConfirmationStore,
21
+ type TotalActiveBalanceCacheKey,
21
22
  } from "./types.ts";
22
23
 
23
24
  // Spec: adjust_committee_weight_estimate_to_ensure_safety
@@ -266,22 +267,34 @@ export function getPreviousBalanceSource(
266
267
  return getBalanceSource(store, cache, "previous");
267
268
  }
268
269
 
269
- export function getTotalActiveBalance(balanceSource: FastConfirmationBalanceSource): number {
270
- if (balanceSource.state) {
271
- return computeTotalBalance(balanceSource.state.getEffectiveBalanceIncrementsZeroInactive());
272
- }
273
- // Fallback balances come from the justified-balance path and already zero inactive
274
- // validators, so summing them gives the active justified total for this balance source.
275
- return computeTotalBalance(balanceSource.balances);
270
+ export function getTotalActiveBalance(
271
+ balanceSource: FastConfirmationBalanceSource,
272
+ cache: FastConfirmationCache
273
+ ): number {
274
+ // Invariant per balance source within a run, but read many times per is_one_confirmed evaluation
275
+ // across the epoch-boundary chain walk; memoize so the full validator-set scan runs once per run.
276
+ const key: TotalActiveBalanceCacheKey = balanceSource.state ?? balanceSource.balances;
277
+ const cached = cache.totalActiveBalanceByKey.get(key);
278
+ if (cached !== undefined) return cached;
279
+
280
+ const total = balanceSource.state
281
+ ? computeTotalBalance(balanceSource.state.getEffectiveBalanceIncrementsZeroInactive())
282
+ : // Fallback balances come from the justified-balance path and already zero inactive
283
+ // validators, so summing them gives the active justified total for this balance source.
284
+ computeTotalBalance(balanceSource.balances);
285
+
286
+ cache.totalActiveBalanceByKey.set(key, total);
287
+ return total;
276
288
  }
277
289
 
278
290
  export function estimateCommitteeWeightBetweenSlots(
279
291
  balanceSource: FastConfirmationBalanceSource,
292
+ cache: FastConfirmationCache,
280
293
  startSlot: Slot,
281
294
  endSlot: Slot
282
295
  ): number {
283
296
  if (startSlot > endSlot) return 0;
284
- const totalActiveBalance = getTotalActiveBalance(balanceSource);
297
+ const totalActiveBalance = getTotalActiveBalance(balanceSource, cache);
285
298
  const startEpoch = computeEpochAtSlot(startSlot);
286
299
  const endEpoch = computeEpochAtSlot(endSlot);
287
300
 
@@ -334,9 +347,10 @@ export function isFullValidatorSetCovered(startSlot: Slot, endSlot: Slot): boole
334
347
 
335
348
  export function computeProposerScore(
336
349
  ctx: FastConfirmationContext,
337
- balanceSource: FastConfirmationBalanceSource
350
+ balanceSource: FastConfirmationBalanceSource,
351
+ cache: FastConfirmationCache
338
352
  ): number {
339
- const totalActiveBalance = getTotalActiveBalance(balanceSource);
353
+ const totalActiveBalance = getTotalActiveBalance(balanceSource, cache);
340
354
  const committeeWeight = Math.floor(totalActiveBalance / SLOTS_PER_EPOCH);
341
355
  return Math.floor((committeeWeight * ctx.config.PROPOSER_SCORE_BOOST) / 100);
342
356
  }
@@ -469,7 +483,7 @@ export function computeAdversarialWeight(
469
483
  startSlot: Slot,
470
484
  endSlot: Slot
471
485
  ): number {
472
- const maximumWeight = estimateCommitteeWeightBetweenSlots(balanceSource, startSlot, endSlot);
486
+ const maximumWeight = estimateCommitteeWeightBetweenSlots(balanceSource, cache, startSlot, endSlot);
473
487
  // The spec uses raw Gwei and computes `maximum_weight // 100 * threshold`.
474
488
  // Lodestar carries effective-balance increments instead, so divide after multiplying to avoid
475
489
  // dropping the adversarial budget to zero for small validator sets/minimal presets.
@@ -564,9 +578,10 @@ export function computeSafetyThreshold(
564
578
  // Spec: compute_safety_threshold(store, block_root, balance_source)
565
579
  // Build the threshold from the same terms used in the paper/spec:
566
580
  // max possible committee support, proposer boost, empty-slot discount, and adversarial budget.
567
- const proposerScore = computeProposerScore(ctx, balanceSource);
581
+ const proposerScore = computeProposerScore(ctx, balanceSource, cache);
568
582
  const maximumSupport = estimateCommitteeWeightBetweenSlots(
569
583
  balanceSource,
584
+ cache,
570
585
  (parentBlock.slot + 1) as Slot,
571
586
  (currentSlot - 1) as Slot
572
587
  );
@@ -711,10 +726,12 @@ export function computeHonestFfgSupportForCurrentTarget(
711
726
  const currentEpoch = computeEpochAtSlot(currentSlot);
712
727
  const targetState = getCurrentEpochState(ctx, store, cache);
713
728
  if (!targetState) return 0;
714
- const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
729
+ const targetBalanceSource = {state: targetState, balances: targetState.effectiveBalanceIncrements};
730
+ const totalActiveBalance = getTotalActiveBalance(targetBalanceSource, cache);
715
731
  const ffgSupport = getCurrentTargetScore(ctx, store, cache);
716
732
  const tillNowFFGWeight = estimateCommitteeWeightBetweenSlots(
717
- {state: targetState, balances: targetState.effectiveBalanceIncrements},
733
+ targetBalanceSource,
734
+ cache,
718
735
  computeStartSlotAtEpoch(currentEpoch),
719
736
  (currentSlot - 1) as Slot
720
737
  );
@@ -747,7 +764,10 @@ export function willNoConflictingCheckpointBeJustified(
747
764
  }
748
765
  const targetState = getCurrentEpochState(ctx, store, cache);
749
766
  if (!targetState) return false;
750
- const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
767
+ const totalActiveBalance = getTotalActiveBalance(
768
+ {state: targetState, balances: targetState.effectiveBalanceIncrements},
769
+ cache
770
+ );
751
771
  const honestSupport = computeHonestFfgSupportForCurrentTarget(ctx, store, cache);
752
772
  return 3 * honestSupport > 1 * totalActiveBalance;
753
773
  }
@@ -759,7 +779,10 @@ export function willCurrentTargetBeJustified(
759
779
  ): boolean {
760
780
  const targetState = getCurrentEpochState(ctx, store, cache);
761
781
  if (!targetState) return false;
762
- const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
782
+ const totalActiveBalance = getTotalActiveBalance(
783
+ {state: targetState, balances: targetState.effectiveBalanceIncrements},
784
+ cache
785
+ );
763
786
  const honestSupport = computeHonestFfgSupportForCurrentTarget(ctx, store, cache);
764
787
  return 3 * honestSupport >= 2 * totalActiveBalance;
765
788
  }
@@ -589,6 +589,10 @@ export class ForkChoice implements IForkChoice {
589
589
  return this.protoArray.nodes.filter((node) => node.slot > windowStart).length;
590
590
  }
591
591
 
592
+ getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
593
+ return this.protoArray.getPayloadRevealCounts(fromSlot, toSlot);
594
+ }
595
+
592
596
  /** Very expensive function, iterates the entire ProtoArray. Called only in debug API */
593
597
  getHeads(): ProtoBlock[] {
594
598
  return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
@@ -632,11 +636,7 @@ export class ForkChoice implements IForkChoice {
632
636
  blockDelaySec: number,
633
637
  currentSlot: Slot,
634
638
  executionStatus: BlockExecutionStatus,
635
- dataAvailabilityStatus: DataAvailabilityStatus,
636
- // The expected proposer index on the canonical chain we are following.
637
- // Calculated by our head state. We use it as part of the proposer
638
- // boost decision making. No boost will be set if this is null.
639
- expectedProposerIndex: ValidatorIndex | null
639
+ dataAvailabilityStatus: DataAvailabilityStatus
640
640
  ): ProtoBlock {
641
641
  const {parentRoot, slot} = block;
642
642
  const parentRootHex = toRootHex(parentRoot);
@@ -711,8 +711,6 @@ export class ForkChoice implements IForkChoice {
711
711
  isTimely &&
712
712
  // only boost the first block we see
713
713
  this.proposerBoostRoot === null &&
714
- expectedProposerIndex !== null &&
715
- block.proposerIndex === expectedProposerIndex &&
716
714
  this.isProposerBoostSameDependentRoot(this.head.blockRoot, parentRootHex);
717
715
  // Candidate boost root used for protoArray.onBlock's best-child weighting. Committed to the
718
716
  // store only after the insertion succeeds.
@@ -1505,11 +1503,11 @@ export class ForkChoice implements IForkChoice {
1505
1503
  }
1506
1504
 
1507
1505
  /**
1508
- * Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition,
1509
- * consensus-specs #5306). Proposer boost is only granted when the imported block shares the same
1510
- * proposer-shuffling dependent root for the current epoch as the canonical head computed before
1511
- * the block was imported. This withholds the boost from a block built on a different shuffling
1512
- * branch than the head.
1506
+ * Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition, added in
1507
+ * https://github.com/ethereum/consensus-specs/pull/5306). Proposer boost is only granted when the imported
1508
+ * block shares the same proposer-shuffling dependent root for the current epoch as the canonical head
1509
+ * computed before the block was imported. This withholds the boost from a block built on a different
1510
+ * shuffling branch than the head.
1513
1511
  *
1514
1512
  * The block is not yet in the proto-array when this runs, so its dependent root is traced from
1515
1513
  * its parent
@@ -1,14 +1,5 @@
1
1
  import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
2
- import {
3
- AttesterSlashing,
4
- BeaconBlock,
5
- Epoch,
6
- IndexedAttestation,
7
- Root,
8
- RootHex,
9
- Slot,
10
- ValidatorIndex,
11
- } from "@lodestar/types";
2
+ import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types";
12
3
  import {
13
4
  BlockExecutionStatus,
14
5
  LVHExecResponse,
@@ -160,8 +151,7 @@ export interface IForkChoice {
160
151
  blockDelaySec: number,
161
152
  currentSlot: Slot,
162
153
  executionStatus: BlockExecutionStatus,
163
- dataAvailabilityStatus: DataAvailabilityStatus,
164
- expectedProposerIndex: ValidatorIndex | null
154
+ dataAvailabilityStatus: DataAvailabilityStatus
165
155
  ): ProtoBlock;
166
156
  /**
167
157
  * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
@@ -252,6 +242,11 @@ export interface IForkChoice {
252
242
  hasPayloadUnsafe(blockRoot: Root): boolean;
253
243
  hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
254
244
  getSlotsPresent(windowStart: number): number;
245
+ /**
246
+ * Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed
247
+ * payload (FULL variant exists). Used by the builder circuit breaker.
248
+ */
249
+ getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number};
255
250
  getPTCVotes(blockRootHex: RootHex): (boolean | null)[] | null;
256
251
  /** Raw PTC vote tallies for the debug fork choice endpoint; `null` for pre-Gloas roots. */
257
252
  getPTCVoteCounts(blockRootHex: RootHex): {
@@ -672,6 +672,35 @@ export class ProtoArray {
672
672
  this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
673
673
  }
674
674
 
675
+ /**
676
+ * Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed
677
+ * payload (FULL variant exists). Used by the builder circuit breaker.
678
+ */
679
+ getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
680
+ let blocksPresent = 0;
681
+ let payloadsRevealed = 0;
682
+ // Iterate backward as recent blocks are at the end of the nodes array. Only break on block
683
+ // nodes below the window since FULL variants are appended late when the envelope is imported
684
+ for (let i = this.nodes.length - 1; i >= 0; i--) {
685
+ const node = this.nodes[i];
686
+ if (node.slot < fromSlot) {
687
+ if (isGloasBlock(node) && node.payloadStatus === PayloadStatus.FULL) {
688
+ continue;
689
+ }
690
+ break;
691
+ }
692
+ // Count each gloas block once via its PENDING variant, pre-gloas nodes are FULL only
693
+ if (node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) {
694
+ continue;
695
+ }
696
+ blocksPresent++;
697
+ if (this.hasPayload(node.blockRoot)) {
698
+ payloadsRevealed++;
699
+ }
700
+ }
701
+ return {blocksPresent, payloadsRevealed};
702
+ }
703
+
675
704
  /**
676
705
  * Update PTC votes for multiple validators attesting to a block
677
706
  * Spec: gloas/fork-choice.md#new-notify_ptc_messages