@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.
- package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/data.js +1 -0
- package/lib/forkChoice/fastConfirmation/data.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/metrics.d.ts +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/metrics.js +4 -0
- package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/types.d.ts +3 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/types.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.d.ts +3 -3
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.js +27 -19
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +46 -7
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +178 -55
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +18 -6
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js +1 -1
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +1 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.d.ts +3 -3
- package/lib/protoArray/computeDeltas.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.js +31 -27
- package/lib/protoArray/computeDeltas.js.map +1 -1
- package/lib/protoArray/interface.d.ts +6 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +28 -3
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +106 -18
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +9 -9
- package/src/forkChoice/fastConfirmation/data.ts +1 -0
- package/src/forkChoice/fastConfirmation/metrics.ts +4 -0
- package/src/forkChoice/fastConfirmation/types.ts +4 -0
- package/src/forkChoice/fastConfirmation/utils.ts +39 -16
- package/src/forkChoice/forkChoice.ts +193 -57
- package/src/forkChoice/interface.ts +15 -16
- package/src/index.ts +1 -1
- package/src/protoArray/computeDeltas.ts +33 -29
- package/src/protoArray/interface.ts +6 -0
- package/src/protoArray/protoArray.ts +117 -22
|
@@ -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(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
2
|
+
import {MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
3
3
|
import {
|
|
4
4
|
DataAvailabilityStatus,
|
|
5
5
|
EffectiveBalanceIncrements,
|
|
@@ -152,6 +152,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
152
152
|
/** Optional fast confirmation rule implementation */
|
|
153
153
|
private readonly fastConfirmationRule?: IFastConfirmationRule;
|
|
154
154
|
private readonly fastConfirmationContext?: FastConfirmationContext;
|
|
155
|
+
private fastConfirmationPaused = false;
|
|
155
156
|
/**
|
|
156
157
|
* Instantiates a Fork Choice from some existing components
|
|
157
158
|
*
|
|
@@ -180,6 +181,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
180
181
|
if (this.opts?.fastConfirmation) {
|
|
181
182
|
this.fastConfirmationRule = new FastConfirmationRule(this.fcStore, metrics, this.logger);
|
|
182
183
|
this.fastConfirmationContext = this.createFastConfirmationContext();
|
|
184
|
+
metrics?.fastConfirmation.paused.set(0);
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
metrics?.forkChoice.votes.addCollect(() => {
|
|
@@ -230,6 +232,47 @@ export class ForkChoice implements IForkChoice {
|
|
|
230
232
|
return this.getBlockHexDefaultStatus(this.getConfirmedRoot());
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
resumeFastConfirmation(): void {
|
|
236
|
+
this.toggleFastConfirmation(false);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pauseFastConfirmation(): void {
|
|
240
|
+
this.toggleFastConfirmation(true);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private toggleFastConfirmation(paused: boolean): void {
|
|
244
|
+
if (!this.fastConfirmationRule) return;
|
|
245
|
+
if (paused === this.fastConfirmationPaused) return;
|
|
246
|
+
this.fastConfirmationPaused = paused;
|
|
247
|
+
if (paused) {
|
|
248
|
+
// Pin immediately: block imports report the safe block hash to the EL before the next slot tick
|
|
249
|
+
this.fcStore.confirmedRoot = this.fcStore.finalizedCheckpoint.rootHex;
|
|
250
|
+
try {
|
|
251
|
+
this.notifyConfirmedRoot();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// Callers run in clock/network handler context with no catch above
|
|
254
|
+
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this.metrics?.fastConfirmation.paused.set(paused ? 1 : 0);
|
|
258
|
+
this.logger?.info(paused ? "Paused fast confirmation" : "Resumed fast confirmation", {
|
|
259
|
+
slot: this.fcStore.currentSlot,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private notifyConfirmedRoot(): void {
|
|
264
|
+
const confirmedRoot = this.fcStore.confirmedRoot;
|
|
265
|
+
const confirmedBlock = this.getBlockHexDefaultStatus(confirmedRoot);
|
|
266
|
+
if (confirmedBlock === null) {
|
|
267
|
+
throw new Error(`Fast confirmation produced root not in protoArray: ${confirmedRoot}`);
|
|
268
|
+
}
|
|
269
|
+
this.fcStore.notifyFastConfirmation?.({
|
|
270
|
+
block: confirmedRoot,
|
|
271
|
+
slot: confirmedBlock.slot,
|
|
272
|
+
currentSlot: this.fcStore.currentSlot,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
/**
|
|
234
277
|
*
|
|
235
278
|
* A multiplexer to wrap around the traditional `updateHead()` according to the scenario
|
|
@@ -455,26 +498,12 @@ export class ForkChoice implements IForkChoice {
|
|
|
455
498
|
}
|
|
456
499
|
|
|
457
500
|
// No reorg if headBlock is "not weak" ie. headBlock's weight exceeds (REORG_HEAD_WEIGHT_THRESHOLD = 20)% of total attester weight
|
|
458
|
-
|
|
459
|
-
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
460
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
461
|
-
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
|
|
462
|
-
});
|
|
463
|
-
const headNode = this.protoArray.getNode(headBlock.blockRoot, headBlock.payloadStatus);
|
|
464
|
-
// If headNode is unavailable, give up reorg
|
|
465
|
-
if (headNode === undefined || headNode.weight >= reorgThreshold) {
|
|
501
|
+
if (!this.isHeadWeak(headBlock.blockRoot)) {
|
|
466
502
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
|
|
467
503
|
}
|
|
468
504
|
|
|
469
505
|
// No reorg if parentBlock is "not strong" ie. parentBlock's weight is less than or equal to (REORG_PARENT_WEIGHT_THRESHOLD = 160)% of total attester weight
|
|
470
|
-
|
|
471
|
-
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
472
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
473
|
-
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
|
|
474
|
-
});
|
|
475
|
-
const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentBlock.payloadStatus);
|
|
476
|
-
// If parentNode is unavailable, give up reorg
|
|
477
|
-
if (parentNode === undefined || parentNode.weight <= parentThreshold) {
|
|
506
|
+
if (!this.isParentStrong(parentBlock.blockRoot)) {
|
|
478
507
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
|
|
479
508
|
}
|
|
480
509
|
|
|
@@ -517,7 +546,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
517
546
|
|
|
518
547
|
const timer = computeDeltasMetrics?.duration.startTimer();
|
|
519
548
|
const {
|
|
520
|
-
|
|
549
|
+
attestationDeltas,
|
|
521
550
|
equivocatingValidators,
|
|
522
551
|
oldInactiveValidators,
|
|
523
552
|
newInactiveValidators,
|
|
@@ -533,8 +562,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
533
562
|
);
|
|
534
563
|
timer?.();
|
|
535
564
|
|
|
536
|
-
computeDeltasMetrics?.deltasCount.set(
|
|
537
|
-
computeDeltasMetrics?.zeroDeltasCount.set(
|
|
565
|
+
computeDeltasMetrics?.deltasCount.set(attestationDeltas.length);
|
|
566
|
+
computeDeltasMetrics?.zeroDeltasCount.set(attestationDeltas.filter((d) => d === 0).length);
|
|
538
567
|
computeDeltasMetrics?.equivocatingValidators.set(equivocatingValidators);
|
|
539
568
|
computeDeltasMetrics?.oldInactiveValidators.set(oldInactiveValidators);
|
|
540
569
|
computeDeltasMetrics?.newInactiveValidators.set(newInactiveValidators);
|
|
@@ -560,7 +589,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
560
589
|
|
|
561
590
|
const currentSlot = this.fcStore.currentSlot;
|
|
562
591
|
this.protoArray.applyScoreChanges({
|
|
563
|
-
|
|
592
|
+
attestationDeltas,
|
|
564
593
|
proposerBoost,
|
|
565
594
|
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
566
595
|
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
@@ -585,11 +614,31 @@ export class ForkChoice implements IForkChoice {
|
|
|
585
614
|
return this.protoArray.nodes.filter((node) => node.slot > windowStart).length;
|
|
586
615
|
}
|
|
587
616
|
|
|
617
|
+
getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
|
|
618
|
+
return this.protoArray.getPayloadRevealCounts(fromSlot, toSlot);
|
|
619
|
+
}
|
|
620
|
+
|
|
588
621
|
/** Very expensive function, iterates the entire ProtoArray. Called only in debug API */
|
|
589
622
|
getHeads(): ProtoBlock[] {
|
|
590
623
|
return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
|
|
591
624
|
}
|
|
592
625
|
|
|
626
|
+
/**
|
|
627
|
+
* weight is in EFFECTIVE_BALANCE_INCREMENTS not gwei.
|
|
628
|
+
* For compliance test use only
|
|
629
|
+
*/
|
|
630
|
+
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
631
|
+
return this.protoArray.getViableHeads(this.fcStore.currentSlot);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The cached justified total active balance, in EFFECTIVE_BALANCE_INCREMENT units.
|
|
636
|
+
* For compliance test use only
|
|
637
|
+
*/
|
|
638
|
+
getJustifiedTotalActiveBalanceByIncrement(): number {
|
|
639
|
+
return this.fcStore.justified.totalBalance;
|
|
640
|
+
}
|
|
641
|
+
|
|
593
642
|
/** This is for the debug API only */
|
|
594
643
|
getAllNodes(): ProtoNode[] {
|
|
595
644
|
return this.protoArray.nodes;
|
|
@@ -628,11 +677,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
628
677
|
blockDelaySec: number,
|
|
629
678
|
currentSlot: Slot,
|
|
630
679
|
executionStatus: BlockExecutionStatus,
|
|
631
|
-
dataAvailabilityStatus: DataAvailabilityStatus
|
|
632
|
-
// The expected proposer index on the canonical chain we are following.
|
|
633
|
-
// Calculated by our head state. We use it as part of the proposer
|
|
634
|
-
// boost decision making. No boost will be set if this is null.
|
|
635
|
-
expectedProposerIndex: ValidatorIndex | null
|
|
680
|
+
dataAvailabilityStatus: DataAvailabilityStatus
|
|
636
681
|
): ProtoBlock {
|
|
637
682
|
const {parentRoot, slot} = block;
|
|
638
683
|
const parentRootHex = toRootHex(parentRoot);
|
|
@@ -707,8 +752,6 @@ export class ForkChoice implements IForkChoice {
|
|
|
707
752
|
isTimely &&
|
|
708
753
|
// only boost the first block we see
|
|
709
754
|
this.proposerBoostRoot === null &&
|
|
710
|
-
expectedProposerIndex !== null &&
|
|
711
|
-
block.proposerIndex === expectedProposerIndex &&
|
|
712
755
|
this.isProposerBoostSameDependentRoot(this.head.blockRoot, parentRootHex);
|
|
713
756
|
// Candidate boost root used for protoArray.onBlock's best-child weighting. Committed to the
|
|
714
757
|
// store only after the insertion succeeds.
|
|
@@ -901,14 +944,15 @@ export class ForkChoice implements IForkChoice {
|
|
|
901
944
|
|
|
902
945
|
this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
|
|
903
946
|
|
|
904
|
-
//
|
|
947
|
+
// Determine which variant the attestation supports
|
|
948
|
+
//
|
|
949
|
+
// Pre-gloas: payload is always present, vote goes to FULL.
|
|
905
950
|
// Post-gloas:
|
|
906
|
-
//
|
|
907
|
-
//
|
|
951
|
+
// - block.slot < message.slot: EMPTY if data.index is 0 and FULL if data.index is 1.
|
|
952
|
+
// - else: PENDING
|
|
953
|
+
//
|
|
954
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-get_supported_node
|
|
908
955
|
let payloadStatus: PayloadStatus;
|
|
909
|
-
|
|
910
|
-
// We need to retrieve block to check if it's Gloas and to compare slot
|
|
911
|
-
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-is_supporting_vote
|
|
912
956
|
const block = this.getBlockHexDefaultStatus(blockRootHex);
|
|
913
957
|
|
|
914
958
|
if (block && isGloasBlock(block)) {
|
|
@@ -1500,11 +1544,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
1500
1544
|
}
|
|
1501
1545
|
|
|
1502
1546
|
/**
|
|
1503
|
-
* Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition,
|
|
1504
|
-
* consensus-specs
|
|
1505
|
-
* proposer-shuffling dependent root for the current epoch as the canonical head
|
|
1506
|
-
* the block was imported. This withholds the boost from a block built on a different
|
|
1507
|
-
* branch than the head.
|
|
1547
|
+
* Spec: phase0/fork-choice.md#update_proposer_boost_root (`is_same_dependent_root` condition, added in
|
|
1548
|
+
* https://github.com/ethereum/consensus-specs/pull/5306). Proposer boost is only granted when the imported
|
|
1549
|
+
* block shares the same proposer-shuffling dependent root for the current epoch as the canonical head
|
|
1550
|
+
* computed before the block was imported. This withholds the boost from a block built on a different
|
|
1551
|
+
* shuffling branch than the head.
|
|
1508
1552
|
*
|
|
1509
1553
|
* The block is not yet in the proto-array when this runs, so its dependent root is traced from
|
|
1510
1554
|
* its parent
|
|
@@ -1527,6 +1571,93 @@ export class ForkChoice implements IForkChoice {
|
|
|
1527
1571
|
return headDependentRoot === blockDependentRoot;
|
|
1528
1572
|
}
|
|
1529
1573
|
|
|
1574
|
+
/**
|
|
1575
|
+
* Return true if the block is "weak" ie. its weight is below REORG_HEAD_WEIGHT_THRESHOLD of the
|
|
1576
|
+
* total attester weight per slot.
|
|
1577
|
+
*
|
|
1578
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/phase0/fork-choice.md#is_head_weak
|
|
1579
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-is_head_weak
|
|
1580
|
+
*/
|
|
1581
|
+
private isHeadWeak(blockRoot: RootHex): boolean {
|
|
1582
|
+
// The default variant is PENDING for gloas, FULL pre-gloas. PENDING is the variant gloas measures
|
|
1583
|
+
// support on, ie. support for the beacon block root regardless of its payload status.
|
|
1584
|
+
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
|
|
1585
|
+
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
|
|
1586
|
+
if (node === undefined) {
|
|
1587
|
+
// this is called for head so we should always have this in forkchoice, otherwise we have a serious error
|
|
1588
|
+
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1592
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1593
|
+
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
if (!isForkPostGloas(this.config.getForkName(node.slot))) {
|
|
1597
|
+
return node.weight < reorgThreshold;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
let headWeight = node.attestationScore;
|
|
1601
|
+
|
|
1602
|
+
const {equivocatingIndices} = this.fcStore;
|
|
1603
|
+
// Equivocators are extremely rare (none in normal operation), and with none the added weight is
|
|
1604
|
+
// always 0. Return before fetching the state and walking the block's committees.
|
|
1605
|
+
if (equivocatingIndices.size > 0) {
|
|
1606
|
+
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
|
|
1607
|
+
// Only ever called on the head, so the state is always cached.
|
|
1608
|
+
// A miss is a broken invariant, not a recoverable state.
|
|
1609
|
+
if (state === null) {
|
|
1610
|
+
throw new ForkChoiceError({
|
|
1611
|
+
code: ForkChoiceErrorCode.BEACON_STATE_ERROR,
|
|
1612
|
+
error: new Error(`Missing state for isHeadWeak, blockRoot=${blockRoot} stateRoot=${node.stateRoot}`),
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const epoch = computeEpochAtSlot(node.slot);
|
|
1617
|
+
for (let index = 0; index < state.getBeaconCommitteeCountPerSlot(epoch); index++) {
|
|
1618
|
+
for (const validatorIndex of state.getBeaconCommittee(node.slot, index)) {
|
|
1619
|
+
if (equivocatingIndices.has(validatorIndex)) {
|
|
1620
|
+
// the spec specifies to use effective_balance of the justified state
|
|
1621
|
+
let balance = this.fcStore.justified.balances[validatorIndex];
|
|
1622
|
+
if (!balance) {
|
|
1623
|
+
// 0 (zeroed by getEffectiveBalanceIncrementsZeroInactive) or undefined (validator not in
|
|
1624
|
+
// the justified state) - fall back to the head state's effective balance
|
|
1625
|
+
balance = state.effectiveBalanceIncrements[validatorIndex];
|
|
1626
|
+
}
|
|
1627
|
+
headWeight += balance;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return headWeight < reorgThreshold;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Return true if the parent block is "strong" ie. its weight exceeds REORG_PARENT_WEIGHT_THRESHOLD
|
|
1638
|
+
* of the total attester weight per slot.
|
|
1639
|
+
*
|
|
1640
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_parent_strong
|
|
1641
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/fork-choice.md#modified-is_parent_strong
|
|
1642
|
+
*/
|
|
1643
|
+
private isParentStrong(parentRoot: RootHex): boolean {
|
|
1644
|
+
const node = this.protoArray.getNodeDefaultStatus(parentRoot);
|
|
1645
|
+
// If parentNode is unavailable, give up reorg
|
|
1646
|
+
if (node === undefined) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1651
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1652
|
+
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
|
|
1656
|
+
const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;
|
|
1657
|
+
|
|
1658
|
+
return parentWeight > parentThreshold;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1530
1661
|
/**
|
|
1531
1662
|
* Return true if the block is timely for the current slot.
|
|
1532
1663
|
* Child class can overwrite this for testing purpose.
|
|
@@ -1943,11 +2074,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
1943
2074
|
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
|
|
1944
2075
|
}
|
|
1945
2076
|
|
|
1946
|
-
// No reorg if we are at epoch boundary
|
|
1947
|
-
// https://github.com/ethereum/consensus-specs/blob/v1.
|
|
1948
|
-
const
|
|
1949
|
-
if (
|
|
1950
|
-
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.
|
|
2077
|
+
// No reorg if we are at an epoch boundary
|
|
2078
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_not_epoch_boundary
|
|
2079
|
+
const isAtEpochBoundary = slot % SLOTS_PER_EPOCH === 0;
|
|
2080
|
+
if (isAtEpochBoundary) {
|
|
2081
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.AtEpochBoundary};
|
|
1951
2082
|
}
|
|
1952
2083
|
|
|
1953
2084
|
// No reorg if headBlock and parentBlock are not ffg competitive
|
|
@@ -1979,9 +2110,23 @@ export class ForkChoice implements IForkChoice {
|
|
|
1979
2110
|
}
|
|
1980
2111
|
|
|
1981
2112
|
private runFastConfirmation(): void {
|
|
1982
|
-
|
|
1983
|
-
|
|
2113
|
+
const fastConfirmationRule = this.fastConfirmationRule;
|
|
2114
|
+
const fastConfirmationContext = this.fastConfirmationContext;
|
|
2115
|
+
if (!fastConfirmationRule || !fastConfirmationContext) return;
|
|
1984
2116
|
|
|
2117
|
+
if (this.fastConfirmationPaused) {
|
|
2118
|
+
// Keep consumers on a safe, available root while the rule is paused
|
|
2119
|
+
this.fcStore.confirmedRoot = this.fcStore.finalizedCheckpoint.rootHex;
|
|
2120
|
+
try {
|
|
2121
|
+
this.notifyConfirmedRoot();
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
// Runs outside the timed try/catch below; a throw would escape to the clock listener
|
|
2124
|
+
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
2125
|
+
}
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
|
|
1985
2130
|
try {
|
|
1986
2131
|
withObservedDuration(
|
|
1987
2132
|
this.metrics?.fastConfirmation.stepsDuration.startTimer({
|
|
@@ -1990,18 +2135,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1990
2135
|
() => this.updateHead()
|
|
1991
2136
|
);
|
|
1992
2137
|
|
|
1993
|
-
const result =
|
|
2138
|
+
const result = fastConfirmationRule.onSlotStartAfterPastAttestationsApplied(fastConfirmationContext);
|
|
1994
2139
|
this.fcStore.confirmedRoot = result.confirmedRoot;
|
|
1995
|
-
|
|
1996
|
-
const confirmedBlock = this.getBlockHexDefaultStatus(result.confirmedRoot);
|
|
1997
|
-
if (confirmedBlock === null) {
|
|
1998
|
-
throw new Error(`Fast confirmation produced root not in protoArray: ${result.confirmedRoot}`);
|
|
1999
|
-
}
|
|
2000
|
-
this.fcStore.notifyFastConfirmation?.({
|
|
2001
|
-
block: result.confirmedRoot,
|
|
2002
|
-
slot: confirmedBlock.slot,
|
|
2003
|
-
currentSlot: this.fcStore.currentSlot,
|
|
2004
|
-
});
|
|
2140
|
+
this.notifyConfirmedRoot();
|
|
2005
2141
|
} catch (err) {
|
|
2006
2142
|
this.logger?.debug(
|
|
2007
2143
|
"Fast confirmation failed",
|
|
@@ -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,
|
|
@@ -62,7 +53,7 @@ export enum NotReorgedReason {
|
|
|
62
53
|
HeadBlockIsTimely = "headBlockIsTimely",
|
|
63
54
|
ParentBlockNotAvailable = "parentBlockNotAvailable",
|
|
64
55
|
ProposerBoostReorgDisabled = "proposerBoostReorgDisabled",
|
|
65
|
-
|
|
56
|
+
AtEpochBoundary = "atEpochBoundary",
|
|
66
57
|
NotFFGCompetitive = "notFFGCompetitive",
|
|
67
58
|
ChainLongUnfinality = "chainLongUnfinality",
|
|
68
59
|
ParentBlockDistanceMoreThanOneSlot = "parentBlockDistanceMoreThanOneSlot",
|
|
@@ -90,7 +81,7 @@ export interface IForkChoice {
|
|
|
90
81
|
* ## Specification
|
|
91
82
|
*
|
|
92
83
|
* Modified for Gloas to return ProtoNode instead of just root:
|
|
93
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
84
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#modified-get_ancestor
|
|
94
85
|
*
|
|
95
86
|
* Pre-Gloas: Returns (root, PAYLOAD_STATUS_FULL)
|
|
96
87
|
* Gloas: Returns (root, payloadStatus) based on actual node state
|
|
@@ -109,6 +100,10 @@ export interface IForkChoice {
|
|
|
109
100
|
getHead(): ProtoBlock;
|
|
110
101
|
getConfirmedRoot(): RootHex;
|
|
111
102
|
getConfirmedBlock(): ProtoBlock | null;
|
|
103
|
+
/** Resume the fast confirmation rule; restarts from the finalized root on the next slot tick */
|
|
104
|
+
resumeFastConfirmation(): void;
|
|
105
|
+
/** Pause the fast confirmation rule (e.g. while syncing); pins the confirmed root to the finalized root */
|
|
106
|
+
pauseFastConfirmation(): void;
|
|
112
107
|
updateAndGetHead(mode: UpdateAndGetHeadOpt): {
|
|
113
108
|
head: ProtoBlock;
|
|
114
109
|
isHeadTimely?: boolean;
|
|
@@ -160,8 +155,7 @@ export interface IForkChoice {
|
|
|
160
155
|
blockDelaySec: number,
|
|
161
156
|
currentSlot: Slot,
|
|
162
157
|
executionStatus: BlockExecutionStatus,
|
|
163
|
-
dataAvailabilityStatus: DataAvailabilityStatus
|
|
164
|
-
expectedProposerIndex: ValidatorIndex | null
|
|
158
|
+
dataAvailabilityStatus: DataAvailabilityStatus
|
|
165
159
|
): ProtoBlock;
|
|
166
160
|
/**
|
|
167
161
|
* Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
|
|
@@ -196,7 +190,7 @@ export interface IForkChoice {
|
|
|
196
190
|
*
|
|
197
191
|
* ## Specification
|
|
198
192
|
*
|
|
199
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
193
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#new-notify_ptc_messages
|
|
200
194
|
*/
|
|
201
195
|
notifyPtcMessages(
|
|
202
196
|
blockRoot: RootHex,
|
|
@@ -211,7 +205,7 @@ export interface IForkChoice {
|
|
|
211
205
|
*
|
|
212
206
|
* ## Specification
|
|
213
207
|
*
|
|
214
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
208
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/fork-choice.md#new-on_execution_payload_envelope
|
|
215
209
|
*
|
|
216
210
|
* @param blockRoot - The beacon block root for which the payload arrived
|
|
217
211
|
* @param executionPayloadBlockHash - The block hash of the execution payload
|
|
@@ -252,6 +246,11 @@ export interface IForkChoice {
|
|
|
252
246
|
hasPayloadUnsafe(blockRoot: Root): boolean;
|
|
253
247
|
hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
|
|
254
248
|
getSlotsPresent(windowStart: number): number;
|
|
249
|
+
/**
|
|
250
|
+
* Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed
|
|
251
|
+
* payload (FULL variant exists). Used by the builder circuit breaker.
|
|
252
|
+
*/
|
|
253
|
+
getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number};
|
|
255
254
|
getPTCVotes(blockRootHex: RootHex): (boolean | null)[] | null;
|
|
256
255
|
/** Raw PTC vote tallies for the debug fork choice endpoint; `null` for pre-Gloas roots. */
|
|
257
256
|
getPTCVoteCounts(blockRootHex: RootHex): {
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
type IFastConfirmationStore,
|
|
18
18
|
getFastConfirmationMetrics,
|
|
19
19
|
} from "./forkChoice/fastConfirmation/fastConfirmationRule.ts";
|
|
20
|
-
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
|
|
20
|
+
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt, getCommitteeFraction} from "./forkChoice/forkChoice.js";
|
|
21
21
|
export {
|
|
22
22
|
type AncestorResult,
|
|
23
23
|
AncestorStatus,
|