@lodestar/fork-choice 1.44.0-dev.985999b30c → 1.44.0-dev.a879adb124

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 (70) hide show
  1. package/lib/forkChoice/fastConfirmation/data.d.ts +4 -0
  2. package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -0
  3. package/lib/forkChoice/fastConfirmation/data.js +31 -0
  4. package/lib/forkChoice/fastConfirmation/data.js.map +1 -0
  5. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts +17 -0
  6. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts.map +1 -0
  7. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js +129 -0
  8. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js.map +1 -0
  9. package/lib/forkChoice/fastConfirmation/index.d.ts +4 -0
  10. package/lib/forkChoice/fastConfirmation/index.d.ts.map +1 -0
  11. package/lib/forkChoice/fastConfirmation/index.js +4 -0
  12. package/lib/forkChoice/fastConfirmation/index.js.map +1 -0
  13. package/lib/forkChoice/fastConfirmation/metrics.d.ts +21 -0
  14. package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -0
  15. package/lib/forkChoice/fastConfirmation/metrics.js +42 -0
  16. package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -0
  17. package/lib/forkChoice/fastConfirmation/rules.d.ts +9 -0
  18. package/lib/forkChoice/fastConfirmation/rules.d.ts.map +1 -0
  19. package/lib/forkChoice/fastConfirmation/rules.js +91 -0
  20. package/lib/forkChoice/fastConfirmation/rules.js.map +1 -0
  21. package/lib/forkChoice/fastConfirmation/types.d.ts +101 -0
  22. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -0
  23. package/lib/forkChoice/fastConfirmation/types.js +12 -0
  24. package/lib/forkChoice/fastConfirmation/types.js.map +1 -0
  25. package/lib/forkChoice/fastConfirmation/utils.d.ts +47 -0
  26. package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -0
  27. package/lib/forkChoice/fastConfirmation/utils.js +681 -0
  28. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -0
  29. package/lib/forkChoice/forkChoice.d.ts +23 -3
  30. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  31. package/lib/forkChoice/forkChoice.js +116 -9
  32. package/lib/forkChoice/forkChoice.js.map +1 -1
  33. package/lib/forkChoice/interface.d.ts +19 -7
  34. package/lib/forkChoice/interface.d.ts.map +1 -1
  35. package/lib/forkChoice/interface.js.map +1 -1
  36. package/lib/forkChoice/safeBlocks.d.ts +2 -6
  37. package/lib/forkChoice/safeBlocks.d.ts.map +1 -1
  38. package/lib/forkChoice/safeBlocks.js +15 -7
  39. package/lib/forkChoice/safeBlocks.js.map +1 -1
  40. package/lib/forkChoice/store.d.ts +13 -2
  41. package/lib/forkChoice/store.d.ts.map +1 -1
  42. package/lib/forkChoice/store.js +29 -1
  43. package/lib/forkChoice/store.js.map +1 -1
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +1 -0
  47. package/lib/index.js.map +1 -1
  48. package/lib/metrics.d.ts +12 -1
  49. package/lib/metrics.d.ts.map +1 -1
  50. package/lib/metrics.js +2 -0
  51. package/lib/metrics.js.map +1 -1
  52. package/lib/protoArray/protoArray.d.ts +67 -20
  53. package/lib/protoArray/protoArray.d.ts.map +1 -1
  54. package/lib/protoArray/protoArray.js +170 -38
  55. package/lib/protoArray/protoArray.js.map +1 -1
  56. package/package.json +7 -7
  57. package/src/forkChoice/fastConfirmation/data.ts +43 -0
  58. package/src/forkChoice/fastConfirmation/fastConfirmationRule.ts +159 -0
  59. package/src/forkChoice/fastConfirmation/index.ts +3 -0
  60. package/src/forkChoice/fastConfirmation/metrics.ts +44 -0
  61. package/src/forkChoice/fastConfirmation/rules.ts +124 -0
  62. package/src/forkChoice/fastConfirmation/types.ts +111 -0
  63. package/src/forkChoice/fastConfirmation/utils.ts +968 -0
  64. package/src/forkChoice/forkChoice.ts +150 -10
  65. package/src/forkChoice/interface.ts +36 -7
  66. package/src/forkChoice/safeBlocks.ts +15 -7
  67. package/src/forkChoice/store.ts +34 -1
  68. package/src/index.ts +11 -0
  69. package/src/metrics.ts +3 -1
  70. package/src/protoArray/protoArray.ts +184 -41
@@ -0,0 +1,968 @@
1
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {
3
+ IBeaconStateView,
4
+ computeEpochAtSlot,
5
+ computeSlotsSinceEpochStart,
6
+ computeStartSlotAtEpoch,
7
+ isActiveValidator,
8
+ isStartSlotOfEpoch,
9
+ } from "@lodestar/state-transition";
10
+ import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
11
+ import {Logger, fromHex} from "@lodestar/utils";
12
+ import {ExecutionStatus, ProtoBlock} from "../../protoArray/interface.ts";
13
+ import {CheckpointWithHex, computeTotalBalance, equalCheckpointWithHex} from "../store.ts";
14
+ import {
15
+ type BalanceSourceKey,
16
+ FastConfirmationBalanceSource,
17
+ FastConfirmationCache,
18
+ FastConfirmationContext,
19
+ FastConfirmationSnapshot,
20
+ IFastConfirmationStore,
21
+ } from "./types.ts";
22
+
23
+ // Spec: adjust_committee_weight_estimate_to_ensure_safety
24
+ // https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/fast-confirmation.md#adjust_committee_weight_estimate_to_ensure_safety
25
+ const COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR = 5;
26
+
27
+ const SAFETY_THRESHOLD_UNREACHABLE = Object.freeze({
28
+ threshold: Number.POSITIVE_INFINITY,
29
+ proposerScore: 0,
30
+ maximumSupport: 0,
31
+ supportDiscount: 0,
32
+ adversarialWeight: 0,
33
+ });
34
+
35
+ export function getBlock(ctx: FastConfirmationContext, cache: FastConfirmationCache, root: RootHex): ProtoBlock | null {
36
+ if (cache.blockByRoot.has(root)) {
37
+ return cache.blockByRoot.get(root) ?? null;
38
+ }
39
+ const block = ctx.getBlock(root);
40
+ cache.blockByRoot.set(root, block);
41
+ return block;
42
+ }
43
+
44
+ export function getUnrealizedJustification(
45
+ ctx: FastConfirmationContext,
46
+ cache: FastConfirmationCache,
47
+ blockRoot: RootHex
48
+ ): CheckpointWithHex | null {
49
+ const block = getBlock(ctx, cache, blockRoot);
50
+ if (!block) return null;
51
+ return {
52
+ epoch: block.unrealizedJustifiedEpoch,
53
+ root: fromHex(block.unrealizedJustifiedRoot),
54
+ rootHex: block.unrealizedJustifiedRoot,
55
+ };
56
+ }
57
+
58
+ export function getVotingSource(
59
+ ctx: FastConfirmationContext,
60
+ cache: FastConfirmationCache,
61
+ blockRoot: RootHex
62
+ ): CheckpointWithHex | null {
63
+ const block = getBlock(ctx, cache, blockRoot);
64
+ if (!block) return null;
65
+ const currentEpoch = computeEpochAtSlot(ctx.getCurrentSlot());
66
+ const isFromPrevEpoch = computeEpochAtSlot(block.slot) < currentEpoch;
67
+ const epoch = isFromPrevEpoch ? block.unrealizedJustifiedEpoch : block.justifiedEpoch;
68
+ const rootHex = isFromPrevEpoch ? block.unrealizedJustifiedRoot : block.justifiedRoot;
69
+ return {epoch, root: fromHex(rootHex), rootHex};
70
+ }
71
+
72
+ export function getCheckpointForBlock(
73
+ ctx: FastConfirmationContext,
74
+ blockRoot: RootHex,
75
+ epoch: Epoch
76
+ ): CheckpointWithHex | null {
77
+ try {
78
+ const epochStartSlot = computeStartSlotAtEpoch(epoch);
79
+ const rootHex = ctx.getAncestor(blockRoot, epochStartSlot);
80
+ return {epoch, root: fromHex(rootHex), rootHex};
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ // Spec: `get_ancestor_roots`
87
+ // https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/fast-confirmation.md#get_ancestor_roots
88
+ export function getAncestorRoots(
89
+ ctx: FastConfirmationContext,
90
+ cache: FastConfirmationCache,
91
+ blockRoot: RootHex,
92
+ terminalRoot: RootHex
93
+ ): RootHex[] {
94
+ const cacheKey = `${blockRoot}:${terminalRoot}`;
95
+ if (cache.ancestorRoots.has(cacheKey)) {
96
+ return cache.ancestorRoots.get(cacheKey) ?? [];
97
+ }
98
+
99
+ const terminalBlock = getBlock(ctx, cache, terminalRoot);
100
+ if (!terminalBlock) {
101
+ cache.ancestorRoots.set(cacheKey, null);
102
+ return [];
103
+ }
104
+
105
+ let root = blockRoot;
106
+ const ancestorRoots: RootHex[] = [];
107
+
108
+ let block = getBlock(ctx, cache, root);
109
+ while (block && block.slot > terminalBlock.slot) {
110
+ ancestorRoots.push(root);
111
+ root = block.parentRoot;
112
+
113
+ if (root === terminalRoot) {
114
+ ancestorRoots.reverse();
115
+ cache.ancestorRoots.set(cacheKey, ancestorRoots);
116
+ return ancestorRoots;
117
+ }
118
+
119
+ block = getBlock(ctx, cache, root);
120
+ }
121
+
122
+ cache.ancestorRoots.set(cacheKey, null);
123
+ return [];
124
+ }
125
+
126
+ export function isAncestor(
127
+ ctx: FastConfirmationContext,
128
+ cache: FastConfirmationCache,
129
+ blockRoot: RootHex,
130
+ ancestorRoot: RootHex
131
+ ): boolean {
132
+ const ancestorBlock = getBlock(ctx, cache, ancestorRoot);
133
+ if (!ancestorBlock) return false;
134
+ try {
135
+ return ctx.getAncestor(blockRoot, ancestorBlock.slot) === ancestorRoot;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ export function getHeadState(
142
+ ctx: FastConfirmationContext,
143
+ store: IFastConfirmationStore,
144
+ cache: FastConfirmationCache
145
+ ): IBeaconStateView {
146
+ if (cache.headState !== undefined) return cache.headState;
147
+ const headState = store.stateGetter({stateRoot: ctx.getHead().stateRoot});
148
+ if (!headState) throw new Error(`Head state not found for root ${ctx.getHead().stateRoot}`);
149
+ cache.headState = headState;
150
+ return cache.headState;
151
+ }
152
+
153
+ export function getPulledUpHeadState(
154
+ ctx: FastConfirmationContext,
155
+ store: IFastConfirmationStore,
156
+ cache: FastConfirmationCache
157
+ ): IBeaconStateView {
158
+ if (cache.pulledUpHeadState !== undefined) return cache.pulledUpHeadState;
159
+
160
+ const headState = getHeadState(ctx, store, cache);
161
+ const currentEpoch = computeEpochAtSlot(ctx.getCurrentSlot());
162
+ cache.pulledUpHeadState =
163
+ headState.epoch < currentEpoch ? headState.processSlots(computeStartSlotAtEpoch(currentEpoch)) : headState;
164
+ return cache.pulledUpHeadState;
165
+ }
166
+
167
+ export function getCheckpointState(
168
+ store: IFastConfirmationStore,
169
+ cache: FastConfirmationCache,
170
+ checkpoint: CheckpointWithHex
171
+ ): IBeaconStateView | null {
172
+ const key = `${checkpoint.epoch}:${checkpoint.rootHex}`;
173
+ if (cache.checkpointStateByKey.has(key)) {
174
+ return cache.checkpointStateByKey.get(key) ?? null;
175
+ }
176
+ const state = store.stateGetter({checkpoint});
177
+ cache.checkpointStateByKey.set(key, state ?? null);
178
+ return state ?? null;
179
+ }
180
+
181
+ export function getSlotCommittee(
182
+ cache: FastConfirmationCache,
183
+ state: IBeaconStateView,
184
+ slot: Slot
185
+ ): Set<ValidatorIndex> {
186
+ if (cache.committeeBySlot.has(slot)) {
187
+ return cache.committeeBySlot.get(slot) ?? new Set();
188
+ }
189
+ const epoch = computeEpochAtSlot(slot);
190
+ const committeesCount = state.getBeaconCommitteeCountPerSlot(epoch);
191
+ const participants = new Set<ValidatorIndex>();
192
+ for (let i = 0; i < committeesCount; i++) {
193
+ const committee = state.getBeaconCommittee(slot, i);
194
+ for (const index of committee) {
195
+ participants.add(index);
196
+ }
197
+ }
198
+ cache.committeeBySlot.set(slot, participants);
199
+ return participants;
200
+ }
201
+
202
+ function getSlotRangeParticipants(
203
+ ctx: FastConfirmationContext,
204
+ store: IFastConfirmationStore,
205
+ cache: FastConfirmationCache,
206
+ startSlot: Slot,
207
+ endSlot: Slot
208
+ ): Set<ValidatorIndex> {
209
+ const participants = new Set<ValidatorIndex>();
210
+ const headState = getHeadState(ctx, store, cache);
211
+
212
+ for (let slot = startSlot; slot <= endSlot; slot++) {
213
+ for (const index of getSlotCommittee(cache, headState, slot)) {
214
+ participants.add(index);
215
+ }
216
+ }
217
+
218
+ return participants;
219
+ }
220
+
221
+ function isDescendantCached(
222
+ ctx: FastConfirmationContext,
223
+ cache: FastConfirmationCache,
224
+ ancestorRoot: RootHex,
225
+ descendantRoot: RootHex
226
+ ): boolean {
227
+ const cacheKey = `${ancestorRoot}:${descendantRoot}`;
228
+ if (cache.isDescendantByRootPair.has(cacheKey)) {
229
+ return cache.isDescendantByRootPair.get(cacheKey) ?? false;
230
+ }
231
+
232
+ const isDescendant = ctx.isDescendant(ancestorRoot, descendantRoot);
233
+ cache.isDescendantByRootPair.set(cacheKey, isDescendant);
234
+ return isDescendant;
235
+ }
236
+
237
+ export function getBalanceSource(
238
+ store: IFastConfirmationStore,
239
+ cache: FastConfirmationCache,
240
+ kind: "previous" | "current"
241
+ ): FastConfirmationBalanceSource {
242
+ const checkpoint =
243
+ kind === "previous"
244
+ ? store.previousEpochObservedJustifiedCheckpoint
245
+ : store.currentEpochObservedJustifiedCheckpoint;
246
+ const fallbackBalances =
247
+ kind === "previous" ? store.previousEpochObservedJustifiedBalances : store.currentEpochObservedJustifiedBalances;
248
+ const state = getCheckpointState(store, cache, checkpoint);
249
+ return {
250
+ state,
251
+ balances: state?.effectiveBalanceIncrements ?? fallbackBalances,
252
+ };
253
+ }
254
+
255
+ export function getCurrentBalanceSource(
256
+ store: IFastConfirmationStore,
257
+ cache: FastConfirmationCache
258
+ ): FastConfirmationBalanceSource {
259
+ return getBalanceSource(store, cache, "current");
260
+ }
261
+
262
+ export function getPreviousBalanceSource(
263
+ store: IFastConfirmationStore,
264
+ cache: FastConfirmationCache
265
+ ): FastConfirmationBalanceSource {
266
+ return getBalanceSource(store, cache, "previous");
267
+ }
268
+
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);
276
+ }
277
+
278
+ export function estimateCommitteeWeightBetweenSlots(
279
+ balanceSource: FastConfirmationBalanceSource,
280
+ startSlot: Slot,
281
+ endSlot: Slot
282
+ ): number {
283
+ if (startSlot > endSlot) return 0;
284
+ const totalActiveBalance = getTotalActiveBalance(balanceSource);
285
+ const startEpoch = computeEpochAtSlot(startSlot);
286
+ const endEpoch = computeEpochAtSlot(endSlot);
287
+
288
+ if (isFullValidatorSetCovered(startSlot, endSlot)) {
289
+ return totalActiveBalance;
290
+ }
291
+
292
+ const committeeWeightPerSlot = Math.floor(totalActiveBalance / SLOTS_PER_EPOCH);
293
+
294
+ if (startEpoch === endEpoch) {
295
+ return committeeWeightPerSlot * (endSlot - startSlot + 1);
296
+ }
297
+
298
+ const numSlotsInStartEpoch = SLOTS_PER_EPOCH - computeSlotsSinceEpochStart(startSlot);
299
+ const numSlotsInEndEpoch = computeSlotsSinceEpochStart(endSlot) + 1;
300
+ const remainingSlotsInEndEpoch = SLOTS_PER_EPOCH - numSlotsInEndEpoch;
301
+
302
+ const startEpochWeight = committeeWeightPerSlot * numSlotsInStartEpoch;
303
+ const endEpochWeight = committeeWeightPerSlot * numSlotsInEndEpoch;
304
+ // For ranges that cross exactly one epoch boundary without covering a full epoch,
305
+ // the spec models overlap as:
306
+ // startEpochWeightProRated = startEpochWeight * (1 - numSlotsInEndEpoch / SLOTS_PER_EPOCH)
307
+ // = startEpochWeight * remainingSlotsInEndEpoch / SLOTS_PER_EPOCH
308
+ // We keep the spec's "pro-rate the start epoch" form so integer rounding matches it exactly.
309
+ const startEpochWeightProRated = Math.floor(startEpochWeight / SLOTS_PER_EPOCH) * remainingSlotsInEndEpoch;
310
+
311
+ return adjustCommitteeWeightEstimateToEnsureSafety(startEpochWeightProRated + endEpochWeight);
312
+ }
313
+
314
+ export function adjustCommitteeWeightEstimateToEnsureSafety(estimate: number): number {
315
+ // The spec applies this adjustment in raw Gwei:
316
+ // ceil(estimate_gwei / 1000) * (1000 + factor)
317
+ //
318
+ // Lodestar carries effective balance increments instead, where:
319
+ // 1 increment = EFFECTIVE_BALANCE_INCREMENT = 1e9 Gwei
320
+ //
321
+ // Since each increment is already far larger than 1000 Gwei, the spec's
322
+ // `ceil(... / 1000)` becomes a no-op at our unit scale. The equivalent
323
+ // conservative adjustment in increments is therefore:
324
+ // ceil(estimate_increments * (1000 + factor) / 1000)
325
+ // The `+ 999` is integer ceiling division by 1000.
326
+ return Math.floor((estimate * (1000 + COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR) + 999) / 1000);
327
+ }
328
+
329
+ export function isFullValidatorSetCovered(startSlot: Slot, endSlot: Slot): boolean {
330
+ const startFullEpoch = computeEpochAtSlot(startSlot + (SLOTS_PER_EPOCH - 1));
331
+ const endFullEpoch = computeEpochAtSlot((endSlot + 1) as Slot);
332
+ return startFullEpoch < endFullEpoch;
333
+ }
334
+
335
+ export function computeProposerScore(
336
+ ctx: FastConfirmationContext,
337
+ balanceSource: FastConfirmationBalanceSource
338
+ ): number {
339
+ const totalActiveBalance = getTotalActiveBalance(balanceSource);
340
+ const committeeWeight = Math.floor(totalActiveBalance / SLOTS_PER_EPOCH);
341
+ return Math.floor((committeeWeight * ctx.config.PROPOSER_SCORE_BOOST) / 100);
342
+ }
343
+
344
+ /**
345
+ * Build vote weight map in a single pass over all active validators.
346
+ * Groups validators by their latest vote root, summing their balances.
347
+ * Cached per sourceKey ("current" | "previous").
348
+ */
349
+ function ensureVoteMaps(
350
+ ctx: FastConfirmationContext,
351
+ cache: FastConfirmationCache,
352
+ balanceSource: FastConfirmationBalanceSource,
353
+ sourceKey: BalanceSourceKey
354
+ ): void {
355
+ if (cache.voteWeightBySource.has(sourceKey)) return;
356
+
357
+ const voteMap = new Map<RootHex, number>();
358
+ const balances = balanceSource.balances;
359
+ const state = balanceSource.state;
360
+ const activeIndices = state?.getCurrentShuffling().activeIndices ?? null;
361
+ const equivocating = ctx.getEquivocatingIndices();
362
+ const indices: Iterable<number> = activeIndices ?? balances.keys();
363
+ const isSlashed = state ? (i: ValidatorIndex) => state.getValidator(i).slashed : () => false;
364
+
365
+ for (const i of indices) {
366
+ if (isSlashed(i)) continue;
367
+ if (equivocating.has(i)) continue;
368
+ const weight = balances[i] ?? 0;
369
+ if (weight === 0) continue;
370
+ const msg = ctx.getLatestMessage(i);
371
+ if (!msg) continue;
372
+ voteMap.set(msg.root, (voteMap.get(msg.root) ?? 0) + weight);
373
+ }
374
+
375
+ cache.voteWeightBySource.set(sourceKey, voteMap);
376
+ }
377
+
378
+ export function getAttestationScore(
379
+ ctx: FastConfirmationContext,
380
+ cache: FastConfirmationCache,
381
+ balanceSource: FastConfirmationBalanceSource,
382
+ blockRoot: RootHex,
383
+ sourceKey: BalanceSourceKey
384
+ ): number {
385
+ ensureVoteMaps(ctx, cache, balanceSource, sourceKey);
386
+ const voteMap = cache.voteWeightBySource.get(sourceKey) ?? new Map();
387
+
388
+ let score = 0;
389
+ for (const [voteRoot, weight] of voteMap) {
390
+ if (isDescendantCached(ctx, cache, blockRoot, voteRoot)) {
391
+ score += weight;
392
+ }
393
+ }
394
+
395
+ return score;
396
+ }
397
+
398
+ export function getBlockSupportBetweenSlots(
399
+ ctx: FastConfirmationContext,
400
+ store: IFastConfirmationStore,
401
+ cache: FastConfirmationCache,
402
+ balanceSource: FastConfirmationBalanceSource,
403
+ blockRoot: RootHex,
404
+ startSlot: Slot,
405
+ endSlot: Slot
406
+ ): number {
407
+ if (startSlot > endSlot) return 0;
408
+ const balances = balanceSource.balances;
409
+ const state = balanceSource.state;
410
+ const stateEpoch = state ? computeEpochAtSlot(state.slot) : null;
411
+ const participants = getSlotRangeParticipants(ctx, store, cache, startSlot, endSlot);
412
+ if (participants.size === 0) return 0;
413
+
414
+ const equivocating = ctx.getEquivocatingIndices();
415
+ let score = 0;
416
+ for (const i of participants) {
417
+ if (i >= balances.length) continue;
418
+ const validator = state?.getValidator(i);
419
+ if (validator?.slashed) continue;
420
+ if (validator && stateEpoch !== null && !isActiveValidator(validator, stateEpoch)) continue;
421
+ if (equivocating.has(i)) continue;
422
+ const latestMessage = ctx.getLatestMessage(i);
423
+ if (latestMessage?.root === blockRoot) {
424
+ score += balances[i] ?? 0;
425
+ }
426
+ }
427
+ return score;
428
+ }
429
+
430
+ export function getEquivocationScore(
431
+ ctx: FastConfirmationContext,
432
+ store: IFastConfirmationStore,
433
+ cache: FastConfirmationCache,
434
+ balanceSource: FastConfirmationBalanceSource,
435
+ startSlot: Slot,
436
+ endSlot: Slot
437
+ ): number {
438
+ if (startSlot > endSlot) return 0;
439
+ const balances = balanceSource.balances;
440
+ const state = balanceSource.state;
441
+ const stateEpoch = state ? computeEpochAtSlot(state.slot) : null;
442
+ const participants = getSlotRangeParticipants(ctx, store, cache, startSlot, endSlot);
443
+ if (participants.size === 0) return 0;
444
+
445
+ const equivocating = ctx.getEquivocatingIndices();
446
+ let score = 0;
447
+ for (const i of participants) {
448
+ if (!equivocating.has(i)) continue;
449
+ if (i >= balances.length) continue;
450
+ const validator = state?.getValidator(i);
451
+ if (validator && stateEpoch !== null && !isActiveValidator(validator, stateEpoch)) continue;
452
+ score += balances[i] ?? 0;
453
+ }
454
+ return score;
455
+ }
456
+
457
+ export function computeAdversarialWeight(
458
+ ctx: FastConfirmationContext,
459
+ store: IFastConfirmationStore,
460
+ cache: FastConfirmationCache,
461
+ balanceSource: FastConfirmationBalanceSource,
462
+ startSlot: Slot,
463
+ endSlot: Slot
464
+ ): number {
465
+ const maximumWeight = estimateCommitteeWeightBetweenSlots(balanceSource, startSlot, endSlot);
466
+ // The spec uses raw Gwei and computes `maximum_weight // 100 * threshold`.
467
+ // Lodestar carries effective-balance increments instead, so divide after multiplying to avoid
468
+ // dropping the adversarial budget to zero for small validator sets/minimal presets.
469
+ const maxAdversarialWeight = Math.floor((maximumWeight * ctx.config.CONFIRMATION_BYZANTINE_THRESHOLD) / 100);
470
+ const equivocationScore = getEquivocationScore(ctx, store, cache, balanceSource, startSlot, endSlot);
471
+ return maxAdversarialWeight > equivocationScore ? maxAdversarialWeight - equivocationScore : 0;
472
+ }
473
+
474
+ export function getAdversarialWeight(
475
+ ctx: FastConfirmationContext,
476
+ store: IFastConfirmationStore,
477
+ cache: FastConfirmationCache,
478
+ balanceSource: FastConfirmationBalanceSource,
479
+ blockRoot: RootHex
480
+ ): number {
481
+ const currentSlot = ctx.getCurrentSlot();
482
+ if (currentSlot === 0) return 0;
483
+ const block = getBlock(ctx, cache, blockRoot);
484
+ if (!block) return 0;
485
+ const parentBlock = getBlock(ctx, cache, block.parentRoot);
486
+ if (!parentBlock) return 0;
487
+ const blockEpoch = computeEpochAtSlot(block.slot);
488
+ const parentEpoch = computeEpochAtSlot(parentBlock.slot);
489
+
490
+ if (blockEpoch > parentEpoch) {
491
+ const startSlot = computeStartSlotAtEpoch(blockEpoch);
492
+ return computeAdversarialWeight(ctx, store, cache, balanceSource, startSlot, (currentSlot - 1) as Slot);
493
+ }
494
+ return computeAdversarialWeight(ctx, store, cache, balanceSource, block.slot, (currentSlot - 1) as Slot);
495
+ }
496
+
497
+ export function computeEmptySlotSupportDiscount(
498
+ ctx: FastConfirmationContext,
499
+ store: IFastConfirmationStore,
500
+ cache: FastConfirmationCache,
501
+ balanceSource: FastConfirmationBalanceSource,
502
+ blockRoot: RootHex
503
+ ): number {
504
+ const block = getBlock(ctx, cache, blockRoot);
505
+ if (!block) return 0;
506
+ const parentBlock = getBlock(ctx, cache, block.parentRoot);
507
+ if (!parentBlock) return 0;
508
+
509
+ if (parentBlock.slot + 1 === block.slot) {
510
+ return 0;
511
+ }
512
+
513
+ const parentSupportInEmptySlots = getBlockSupportBetweenSlots(
514
+ ctx,
515
+ store,
516
+ cache,
517
+ balanceSource,
518
+ block.parentRoot,
519
+ (parentBlock.slot + 1) as Slot,
520
+ (block.slot - 1) as Slot
521
+ );
522
+ const adversarialWeight = computeAdversarialWeight(
523
+ ctx,
524
+ store,
525
+ cache,
526
+ balanceSource,
527
+ (parentBlock.slot + 1) as Slot,
528
+ (block.slot - 1) as Slot
529
+ );
530
+
531
+ return parentSupportInEmptySlots > adversarialWeight ? parentSupportInEmptySlots - adversarialWeight : 0;
532
+ }
533
+
534
+ export function computeSafetyThreshold(
535
+ ctx: FastConfirmationContext,
536
+ store: IFastConfirmationStore,
537
+ cache: FastConfirmationCache,
538
+ balanceSource: FastConfirmationBalanceSource,
539
+ blockRoot: RootHex
540
+ ): {
541
+ threshold: number;
542
+ proposerScore: number;
543
+ maximumSupport: number;
544
+ supportDiscount: number;
545
+ adversarialWeight: number;
546
+ } {
547
+ const currentSlot = ctx.getCurrentSlot();
548
+ const block = getBlock(ctx, cache, blockRoot);
549
+ if (!block) {
550
+ return SAFETY_THRESHOLD_UNREACHABLE;
551
+ }
552
+ const parentBlock = getBlock(ctx, cache, block.parentRoot);
553
+ if (!parentBlock) {
554
+ return SAFETY_THRESHOLD_UNREACHABLE;
555
+ }
556
+
557
+ // Spec: compute_safety_threshold(store, block_root, balance_source)
558
+ // Build the threshold from the same terms used in the paper/spec:
559
+ // max possible committee support, proposer boost, empty-slot discount, and adversarial budget.
560
+ const proposerScore = computeProposerScore(ctx, balanceSource);
561
+ const maximumSupport = estimateCommitteeWeightBetweenSlots(
562
+ balanceSource,
563
+ (parentBlock.slot + 1) as Slot,
564
+ (currentSlot - 1) as Slot
565
+ );
566
+ const supportDiscount = computeEmptySlotSupportDiscount(ctx, store, cache, balanceSource, blockRoot);
567
+ const adversarialWeight = getAdversarialWeight(ctx, store, cache, balanceSource, blockRoot);
568
+
569
+ // Spec underflow guard:
570
+ // if the discount alone already exceeds the threshold budget, the safety threshold is zero.
571
+ const threshold =
572
+ supportDiscount > maximumSupport + proposerScore + 2 * adversarialWeight
573
+ ? 0
574
+ : Math.floor((maximumSupport + proposerScore + 2 * adversarialWeight - supportDiscount) / 2);
575
+
576
+ return {threshold, proposerScore, maximumSupport, supportDiscount, adversarialWeight};
577
+ }
578
+
579
+ export function isOneConfirmed(
580
+ ctx: FastConfirmationContext,
581
+ store: IFastConfirmationStore,
582
+ cache: FastConfirmationCache,
583
+ balanceSource: FastConfirmationBalanceSource,
584
+ blockRoot: RootHex,
585
+ sourceKey: BalanceSourceKey,
586
+ logger?: Logger
587
+ ): boolean {
588
+ const currentSlot = ctx.getCurrentSlot();
589
+ if (currentSlot === 0) return false;
590
+ const block = getBlock(ctx, cache, blockRoot);
591
+ if (!block) return false;
592
+ if (block.executionStatus !== ExecutionStatus.Valid && block.executionStatus !== ExecutionStatus.PreMerge) {
593
+ return false;
594
+ }
595
+
596
+ // Spec: is_one_confirmed(store, balance_source, block_root)
597
+ // Compare actual support for this block against the computed LMD-GHOST safety threshold.
598
+ const support = getAttestationScore(ctx, cache, balanceSource, blockRoot, sourceKey);
599
+ const {threshold, proposerScore, maximumSupport, supportDiscount, adversarialWeight} = computeSafetyThreshold(
600
+ ctx,
601
+ store,
602
+ cache,
603
+ balanceSource,
604
+ blockRoot
605
+ );
606
+ const isConfirmed = support > threshold;
607
+ logger?.debug("Fast confirmation one-confirmed evaluation", {
608
+ blockRoot,
609
+ blockSlot: block.slot,
610
+ currentSlot,
611
+ sourceKey,
612
+ support,
613
+ threshold,
614
+ proposerScore,
615
+ maximumSupport,
616
+ supportDiscount,
617
+ adversarialWeight,
618
+ isConfirmed,
619
+ });
620
+
621
+ return isConfirmed;
622
+ }
623
+
624
+ export function getCurrentTarget(ctx: FastConfirmationContext): CheckpointWithHex | null {
625
+ const head = ctx.getHead().blockRoot;
626
+ const currentEpoch = computeEpochAtSlot(ctx.getCurrentSlot());
627
+ return getCheckpointForBlock(ctx, head, currentEpoch);
628
+ }
629
+
630
+ export function getCurrentEpochState(
631
+ ctx: FastConfirmationContext,
632
+ store: IFastConfirmationStore,
633
+ cache: FastConfirmationCache
634
+ ): IBeaconStateView | null {
635
+ return getPulledUpHeadState(ctx, store, cache);
636
+ }
637
+
638
+ export function getCurrentTargetScore(
639
+ ctx: FastConfirmationContext,
640
+ store: IFastConfirmationStore,
641
+ cache: FastConfirmationCache
642
+ ): number {
643
+ const target = getCurrentTarget(ctx);
644
+ const targetState = getCurrentEpochState(ctx, store, cache);
645
+ if (!target || !targetState) return 0;
646
+ const balances = targetState.effectiveBalanceIncrements;
647
+ const activeIndices = targetState.getCurrentShuffling().activeIndices;
648
+ const equivocating = ctx.getEquivocatingIndices();
649
+
650
+ // Group validators by (voteRoot, voteEpoch) to avoid per-validator getCheckpointForBlock calls.
651
+ // On mainnet ~1M validators vote for only ~50 unique (root, epoch) pairs.
652
+ const voteGroups = new Map<RootHex, Map<Epoch, number>>();
653
+ for (const i of activeIndices) {
654
+ if (targetState.getValidator(i).slashed) continue;
655
+ if (equivocating.has(i)) continue;
656
+ const msg = ctx.getLatestMessage(i);
657
+ if (!msg) continue;
658
+ const weight = balances[i] ?? 0;
659
+ if (weight === 0) continue;
660
+ let byEpoch = voteGroups.get(msg.root);
661
+ if (!byEpoch) {
662
+ byEpoch = new Map();
663
+ voteGroups.set(msg.root, byEpoch);
664
+ }
665
+ byEpoch.set(msg.epoch, (byEpoch.get(msg.epoch) ?? 0) + weight);
666
+ }
667
+
668
+ // Check each unique vote group's checkpoint against the target
669
+ let score = 0;
670
+ for (const [root, byEpoch] of voteGroups) {
671
+ for (const [epoch, weight] of byEpoch) {
672
+ const cp = getCheckpointForBlock(ctx, root, epoch);
673
+ if (cp && cp.epoch === target.epoch && cp.rootHex === target.rootHex) {
674
+ score += weight;
675
+ }
676
+ }
677
+ }
678
+ return score;
679
+ }
680
+
681
+ function computeHonestFfgSupport(
682
+ totalActiveBalance: number,
683
+ ffgSupport: number,
684
+ ffgWeightTillNow: number,
685
+ adversarialWeight: number,
686
+ byzantineThreshold: number
687
+ ): number {
688
+ const remainingFfgWeight = totalActiveBalance - ffgWeightTillNow;
689
+ // The spec rounds in raw Gwei as `remaining_ffg_weight // 100 * honest_percent`.
690
+ // In Lodestar this value is in effective-balance increments, so multiply before dividing by 100
691
+ // to preserve the same percentage semantics at our coarser unit scale.
692
+ const remainingHonestFfgWeight = Math.floor((remainingFfgWeight * (100 - byzantineThreshold)) / 100);
693
+ const minHonestFfgSupport = ffgSupport - Math.min(adversarialWeight, ffgSupport);
694
+ return minHonestFfgSupport + remainingHonestFfgWeight;
695
+ }
696
+
697
+ export function computeHonestFfgSupportForCurrentTarget(
698
+ ctx: FastConfirmationContext,
699
+ store: IFastConfirmationStore,
700
+ cache: FastConfirmationCache
701
+ ): number {
702
+ const currentSlot = ctx.getCurrentSlot();
703
+ if (currentSlot === 0) return 0;
704
+ const currentEpoch = computeEpochAtSlot(currentSlot);
705
+ const targetState = getCurrentEpochState(ctx, store, cache);
706
+ if (!targetState) return 0;
707
+ const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
708
+ const ffgSupport = getCurrentTargetScore(ctx, store, cache);
709
+ const tillNowFFGWeight = estimateCommitteeWeightBetweenSlots(
710
+ {state: targetState, balances: targetState.effectiveBalanceIncrements},
711
+ computeStartSlotAtEpoch(currentEpoch),
712
+ (currentSlot - 1) as Slot
713
+ );
714
+ const adversarialWeight = computeAdversarialWeight(
715
+ ctx,
716
+ store,
717
+ cache,
718
+ {state: targetState, balances: targetState.effectiveBalanceIncrements},
719
+ computeStartSlotAtEpoch(currentEpoch),
720
+ (currentSlot - 1) as Slot
721
+ );
722
+ return computeHonestFfgSupport(
723
+ totalActiveBalance,
724
+ ffgSupport,
725
+ tillNowFFGWeight,
726
+ adversarialWeight,
727
+ ctx.config.CONFIRMATION_BYZANTINE_THRESHOLD
728
+ );
729
+ }
730
+
731
+ export function willNoConflictingCheckpointBeJustified(
732
+ ctx: FastConfirmationContext,
733
+ store: IFastConfirmationStore,
734
+ cache: FastConfirmationCache
735
+ ): boolean {
736
+ const target = getCurrentTarget(ctx);
737
+ if (!target) return false;
738
+ if (equalCheckpointWithHex(target, ctx.getUnrealizedJustified().checkpoint)) {
739
+ return true;
740
+ }
741
+ const targetState = getCurrentEpochState(ctx, store, cache);
742
+ if (!targetState) return false;
743
+ const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
744
+ const honestSupport = computeHonestFfgSupportForCurrentTarget(ctx, store, cache);
745
+ return 3 * honestSupport > 1 * totalActiveBalance;
746
+ }
747
+
748
+ export function willCurrentTargetBeJustified(
749
+ ctx: FastConfirmationContext,
750
+ store: IFastConfirmationStore,
751
+ cache: FastConfirmationCache
752
+ ): boolean {
753
+ const targetState = getCurrentEpochState(ctx, store, cache);
754
+ if (!targetState) return false;
755
+ const totalActiveBalance = computeTotalBalance(targetState.getEffectiveBalanceIncrementsZeroInactive());
756
+ const honestSupport = computeHonestFfgSupportForCurrentTarget(ctx, store, cache);
757
+ return 3 * honestSupport >= 2 * totalActiveBalance;
758
+ }
759
+
760
+ export function isConfirmedChainSafe(
761
+ ctx: FastConfirmationContext,
762
+ store: IFastConfirmationStore,
763
+ cache: FastConfirmationCache,
764
+ confirmedRoot: RootHex,
765
+ logger?: Logger
766
+ ): boolean {
767
+ const checkpointInConfirmedChain = getCheckpointForBlock(
768
+ ctx,
769
+ confirmedRoot,
770
+ store.currentEpochObservedJustifiedCheckpoint.epoch
771
+ );
772
+ if (
773
+ checkpointInConfirmedChain === null ||
774
+ !equalCheckpointWithHex(store.currentEpochObservedJustifiedCheckpoint, checkpointInConfirmedChain)
775
+ ) {
776
+ logger?.debug("Fast confirmation chain-safety failed", {
777
+ confirmedRoot,
778
+ reason: "observed_justified_checkpoint_not_in_confirmed_chain",
779
+ observedJustifiedRoot: store.currentEpochObservedJustifiedCheckpoint.rootHex,
780
+ observedJustifiedEpoch: store.currentEpochObservedJustifiedCheckpoint.epoch,
781
+ checkpointRoot: checkpointInConfirmedChain?.rootHex,
782
+ checkpointEpoch: checkpointInConfirmedChain?.epoch,
783
+ });
784
+ return false;
785
+ }
786
+
787
+ const currentEpoch = computeEpochAtSlot(ctx.getCurrentSlot());
788
+ let startRoot: RootHex;
789
+ if (store.currentEpochObservedJustifiedCheckpoint.epoch + 1 >= currentEpoch) {
790
+ startRoot = store.currentEpochObservedJustifiedCheckpoint.rootHex;
791
+ } else {
792
+ let ancestorAtPreviousEpochStartRoot: RootHex;
793
+ try {
794
+ ancestorAtPreviousEpochStartRoot = ctx.getAncestor(
795
+ confirmedRoot,
796
+ computeStartSlotAtEpoch((currentEpoch - 1) as Epoch)
797
+ );
798
+ } catch {
799
+ return false;
800
+ }
801
+ const ancestorAtPreviousEpochStart = getBlock(ctx, cache, ancestorAtPreviousEpochStartRoot);
802
+ if (!ancestorAtPreviousEpochStart) return false;
803
+
804
+ const ancestorEpoch = computeEpochAtSlot(ancestorAtPreviousEpochStart.slot);
805
+
806
+ if (ancestorEpoch + 1 === currentEpoch) {
807
+ startRoot = ancestorAtPreviousEpochStart.parentRoot;
808
+ } else {
809
+ startRoot = ancestorAtPreviousEpochStartRoot;
810
+ }
811
+ }
812
+
813
+ const chainRoots = getAncestorRoots(ctx, cache, confirmedRoot, startRoot);
814
+ const previousBalanceSource = getPreviousBalanceSource(store, cache);
815
+ for (const root of chainRoots) {
816
+ if (!isOneConfirmed(ctx, store, cache, previousBalanceSource, root, "previous", logger)) {
817
+ logger?.debug("Fast confirmation chain-safety failed", {
818
+ confirmedRoot,
819
+ reason: "unconfirmed_block_in_chain",
820
+ blockRoot: root,
821
+ });
822
+ return false;
823
+ }
824
+ }
825
+ return true;
826
+ }
827
+
828
+ // Spec: `find_latest_confirmed_descendant`
829
+ // https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/fast-confirmation.md#find_latest_confirmed_descendant
830
+ export function findLatestConfirmedDescendant(
831
+ snapshot: FastConfirmationSnapshot,
832
+ ctx: FastConfirmationContext,
833
+ store: IFastConfirmationStore,
834
+ cache: FastConfirmationCache,
835
+ latestConfirmedRoot: RootHex,
836
+ logger?: Logger
837
+ ): RootHex {
838
+ const currentEpoch = snapshot.currentEpoch;
839
+ let confirmedRoot = latestConfirmedRoot;
840
+
841
+ const previousSlotVotingSource = getVotingSource(ctx, cache, store.previousSlotHead);
842
+ const prevSlotJustification = getUnrealizedJustification(ctx, cache, store.previousSlotHead);
843
+ const headJustification = snapshot.headUnrealized ?? getUnrealizedJustification(ctx, cache, snapshot.headRoot);
844
+ const currentBalanceSource = getCurrentBalanceSource(store, cache);
845
+
846
+ const confirmedBlock = getBlock(ctx, cache, confirmedRoot);
847
+ const confirmedEpoch = confirmedBlock ? computeEpochAtSlot(confirmedBlock.slot) : null;
848
+ const shouldAdvanceThroughPreviousEpoch =
849
+ confirmedEpoch !== null &&
850
+ confirmedEpoch + 1 === currentEpoch &&
851
+ previousSlotVotingSource !== null &&
852
+ previousSlotVotingSource.epoch + 2 >= currentEpoch &&
853
+ (isStartSlotOfEpoch(snapshot.currentSlot) ||
854
+ (willNoConflictingCheckpointBeJustified(ctx, store, cache) &&
855
+ ((prevSlotJustification !== null && prevSlotJustification.epoch + 1 >= currentEpoch) ||
856
+ (headJustification !== null && headJustification.epoch + 1 >= currentEpoch))));
857
+
858
+ logger?.debug("Fast confirmation descendant search start", {
859
+ latestConfirmedRoot,
860
+ currentEpoch,
861
+ headRoot: snapshot.headRoot,
862
+ shouldAdvanceThroughPreviousEpoch,
863
+ });
864
+
865
+ if (shouldAdvanceThroughPreviousEpoch) {
866
+ const canonicalRoots = getAncestorRoots(ctx, cache, snapshot.headRoot, confirmedRoot);
867
+ for (const blockRoot of canonicalRoots) {
868
+ const block = getBlock(ctx, cache, blockRoot);
869
+ const blockEpoch = block ? computeEpochAtSlot(block.slot) : null;
870
+ const blockSlot = block?.slot;
871
+
872
+ if (blockEpoch === null || blockEpoch === currentEpoch) {
873
+ logger?.debug("Fast confirmation previous-epoch loop stopped", {
874
+ reason: "reached_current_epoch_or_unknown_epoch",
875
+ blockRoot,
876
+ blockSlot,
877
+ blockEpoch,
878
+ });
879
+ break;
880
+ }
881
+ if (!isAncestor(ctx, cache, store.previousSlotHead, blockRoot)) {
882
+ logger?.debug("Fast confirmation previous-epoch loop stopped", {
883
+ reason: "not_ancestor_of_previous_slot_head",
884
+ blockRoot,
885
+ blockSlot,
886
+ blockEpoch,
887
+ previousSlotHead: store.previousSlotHead,
888
+ });
889
+ break;
890
+ }
891
+ const isConfirmed = isOneConfirmed(ctx, store, cache, currentBalanceSource, blockRoot, "current", logger);
892
+ if (!isConfirmed) {
893
+ logger?.debug("Fast confirmation previous-epoch loop stopped", {
894
+ reason: "block_not_one_confirmed",
895
+ blockRoot,
896
+ blockSlot,
897
+ blockEpoch,
898
+ });
899
+ break;
900
+ }
901
+ confirmedRoot = blockRoot;
902
+ logger?.debug("Fast confirmation previous-epoch loop advanced", {confirmedRoot});
903
+ }
904
+ }
905
+
906
+ const shouldAdvanceThroughCurrentEpoch =
907
+ isStartSlotOfEpoch(snapshot.currentSlot) ||
908
+ (headJustification !== null && headJustification.epoch + 1 >= currentEpoch);
909
+
910
+ if (shouldAdvanceThroughCurrentEpoch) {
911
+ const canonicalRoots = getAncestorRoots(ctx, cache, snapshot.headRoot, confirmedRoot);
912
+ let tentativeConfirmedRoot = confirmedRoot;
913
+
914
+ for (const blockRoot of canonicalRoots) {
915
+ const block = getBlock(ctx, cache, blockRoot);
916
+ const blockEpoch = block ? computeEpochAtSlot(block.slot) : null;
917
+ const blockSlot = block?.slot;
918
+ const tentativeBlock = getBlock(ctx, cache, tentativeConfirmedRoot);
919
+ const tentativeEpoch = tentativeBlock ? computeEpochAtSlot(tentativeBlock.slot) : null;
920
+ if (blockEpoch === null || tentativeEpoch === null) break;
921
+
922
+ if (blockEpoch > tentativeEpoch && !willCurrentTargetBeJustified(ctx, store, cache)) {
923
+ logger?.debug("Fast confirmation current-epoch loop stopped", {
924
+ reason: "current_target_not_justified",
925
+ blockRoot,
926
+ blockSlot,
927
+ blockEpoch,
928
+ tentativeEpoch,
929
+ });
930
+ break;
931
+ }
932
+
933
+ const isConfirmed = isOneConfirmed(ctx, store, cache, currentBalanceSource, blockRoot, "current", logger);
934
+ if (!isConfirmed) {
935
+ logger?.debug("Fast confirmation current-epoch loop stopped", {
936
+ reason: "block_not_one_confirmed",
937
+ blockRoot,
938
+ blockSlot,
939
+ blockEpoch,
940
+ });
941
+ break;
942
+ }
943
+ tentativeConfirmedRoot = blockRoot;
944
+ logger?.debug("Fast confirmation current-epoch loop advanced", {tentativeConfirmedRoot});
945
+ }
946
+
947
+ const tentativeBlock = getBlock(ctx, cache, tentativeConfirmedRoot);
948
+ const tentativeEpoch = tentativeBlock ? computeEpochAtSlot(tentativeBlock.slot) : null;
949
+ const tentativeVotingSource = getVotingSource(ctx, cache, tentativeConfirmedRoot);
950
+ if (
951
+ tentativeEpoch !== null &&
952
+ (tentativeEpoch === currentEpoch ||
953
+ (tentativeVotingSource !== null &&
954
+ tentativeVotingSource.epoch + 2 >= currentEpoch &&
955
+ (isStartSlotOfEpoch(snapshot.currentSlot) || willNoConflictingCheckpointBeJustified(ctx, store, cache))))
956
+ ) {
957
+ confirmedRoot = tentativeConfirmedRoot;
958
+ }
959
+ }
960
+
961
+ logger?.debug("Fast confirmation descendant search result", {
962
+ latestConfirmedRoot,
963
+ confirmedRoot,
964
+ shouldAdvanceThroughCurrentEpoch,
965
+ });
966
+
967
+ return confirmedRoot;
968
+ }