@lodestar/fork-choice 1.35.0-dev.955e9f89ed → 1.35.0-dev.98d359db41

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.
@@ -0,0 +1,1648 @@
1
+ import {ChainConfig, ChainForkConfig} from "@lodestar/config";
2
+ import {INTERVALS_PER_SLOT, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
3
+ import {
4
+ CachedBeaconStateAllForks,
5
+ DataAvailabilityStatus,
6
+ EffectiveBalanceIncrements,
7
+ ZERO_HASH,
8
+ computeEpochAtSlot,
9
+ computeSlotsSinceEpochStart,
10
+ computeStartSlotAtEpoch,
11
+ getAttesterSlashableIndices,
12
+ isExecutionBlockBodyType,
13
+ isExecutionEnabled,
14
+ isExecutionStateType,
15
+ } from "@lodestar/state-transition";
16
+ import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
17
+ import {
18
+ AttesterSlashing,
19
+ BeaconBlock,
20
+ Epoch,
21
+ IndexedAttestation,
22
+ Root,
23
+ RootHex,
24
+ Slot,
25
+ ValidatorIndex,
26
+ bellatrix,
27
+ phase0,
28
+ ssz,
29
+ } from "@lodestar/types";
30
+ import {Logger, MapDef, fromHex, toRootHex} from "@lodestar/utils";
31
+ import {ForkChoiceMetrics} from "../metrics.js";
32
+ import {computeDeltas} from "../protoArray/computeDeltas.js";
33
+ import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";
34
+ import {
35
+ ExecutionStatus,
36
+ HEX_ZERO_HASH,
37
+ LVHExecResponse,
38
+ MaybeValidExecutionStatus,
39
+ ProtoBlock,
40
+ ProtoNode,
41
+ VoteTracker,
42
+ } from "../protoArray/interface.js";
43
+ import {ProtoArray} from "../protoArray/protoArray.js";
44
+ import {ForkChoiceError, ForkChoiceErrorCode, InvalidAttestationCode, InvalidBlockCode} from "./errors.js";
45
+ import {
46
+ AncestorResult,
47
+ AncestorStatus,
48
+ EpochDifference,
49
+ IForkChoice,
50
+ LatestMessage,
51
+ NotReorgedReason,
52
+ PowBlockHex,
53
+ ShouldOverrideForkChoiceUpdateResult,
54
+ } from "./interface.js";
55
+ import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js";
56
+
57
+ export type ForkChoiceOpts = {
58
+ proposerBoost?: boolean;
59
+ proposerBoostReorg?: boolean;
60
+ computeUnrealized?: boolean;
61
+ };
62
+
63
+ export enum UpdateHeadOpt {
64
+ GetCanonicalHead = "getCanonicalHead", // Skip getProposerHead
65
+ GetProposerHead = "getProposerHead", // With getProposerHead
66
+ GetPredictedProposerHead = "getPredictedProposerHead", // With predictProposerHead
67
+ }
68
+
69
+ export type UpdateAndGetHeadOpt =
70
+ | {mode: UpdateHeadOpt.GetCanonicalHead}
71
+ | {mode: UpdateHeadOpt.GetProposerHead; secFromSlot: number; slot: Slot}
72
+ | {mode: UpdateHeadOpt.GetPredictedProposerHead; secFromSlot: number; slot: Slot};
73
+
74
+ /**
75
+ * Provides an implementation of "Ethereum Consensus -- Beacon Chain Fork Choice":
76
+ *
77
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#fork-choice
78
+ *
79
+ * ## Detail
80
+ *
81
+ * This class wraps `ProtoArray` and provides:
82
+ *
83
+ * - Management of validators latest messages and balances
84
+ * - Management of the justified/finalized checkpoints as seen by fork choice
85
+ * - Queuing of attestations from the current slot
86
+ *
87
+ * This class MUST be used with the following considerations:
88
+ *
89
+ * - Time is not updated automatically, updateTime MUST be called every slot
90
+ */
91
+ export class ForkChoice implements IForkChoice {
92
+ irrecoverableError?: Error;
93
+ /**
94
+ * Votes currently tracked in the protoArray
95
+ * Indexed by validator index
96
+ * Each vote contains the latest message and previous message
97
+ */
98
+ private readonly votes: VoteTracker[] = [];
99
+
100
+ /**
101
+ * Attestations that arrived at the current slot and must be queued for later processing.
102
+ * NOT currently tracked in the protoArray
103
+ */
104
+ private readonly queuedAttestations: MapDef<Slot, MapDef<RootHex, Set<ValidatorIndex>>> = new MapDef(
105
+ () => new MapDef(() => new Set())
106
+ );
107
+
108
+ /**
109
+ * It's inconsistent to count number of queued attestations at different intervals of slot.
110
+ * Instead of that, we count number of queued attestations at the previous slot.
111
+ */
112
+ private queuedAttestationsPreviousSlot = 0;
113
+
114
+ // Note: as of Jun 2022 Lodestar metrics show that 100% of the times updateHead() is called, synced = false.
115
+ // Because we are processing attestations from gossip, recomputing scores is always necessary
116
+ // /** Avoid having to compute deltas all the times. */
117
+ // private synced = false;
118
+
119
+ /** Cached head */
120
+ private head: ProtoBlock;
121
+ /**
122
+ * Only cache attestation data root hex if it's tree backed since it's available.
123
+ **/
124
+ private validatedAttestationDatas = new Set<string>();
125
+ /** Boost the entire branch with this proposer root as the leaf */
126
+ private proposerBoostRoot: RootHex | null = null;
127
+ /** Score to use in proposer boost, evaluated lazily from justified balances */
128
+ private justifiedProposerBoostScore: number | null = null;
129
+ /** The current effective balances */
130
+ private balances: EffectiveBalanceIncrements;
131
+ /**
132
+ * Instantiates a Fork Choice from some existing components
133
+ *
134
+ * This is useful if the existing components have been loaded from disk after a process restart.
135
+ */
136
+ constructor(
137
+ private readonly config: ChainForkConfig,
138
+ private readonly fcStore: IForkChoiceStore,
139
+ /** The underlying representation of the block DAG. */
140
+ private readonly protoArray: ProtoArray,
141
+ readonly metrics: ForkChoiceMetrics | null,
142
+ private readonly opts?: ForkChoiceOpts,
143
+ private readonly logger?: Logger
144
+ ) {
145
+ this.head = this.updateHead();
146
+ this.balances = this.fcStore.justified.balances;
147
+
148
+ metrics?.forkChoice.votes.addCollect(() => {
149
+ metrics.forkChoice.votes.set(this.votes.length);
150
+ metrics.forkChoice.queuedAttestations.set(this.queuedAttestationsPreviousSlot);
151
+ metrics.forkChoice.validatedAttestationDatas.set(this.validatedAttestationDatas.size);
152
+ metrics.forkChoice.balancesLength.set(this.balances.length);
153
+ metrics.forkChoice.nodes.set(this.protoArray.nodes.length);
154
+ metrics.forkChoice.indices.set(this.protoArray.indices.size);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Returns the block root of an ancestor of `blockRoot` at the given `slot`.
160
+ * (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
161
+ *
162
+ * NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
163
+ *
164
+ * ### Specification
165
+ *
166
+ * Equivalent to:
167
+ *
168
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
169
+ */
170
+ getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex {
171
+ return this.protoArray.getAncestor(blockRoot, ancestorSlot);
172
+ }
173
+
174
+ /**
175
+ * Get the cached head root
176
+ */
177
+ getHeadRoot(): RootHex {
178
+ return this.getHead().blockRoot;
179
+ }
180
+
181
+ /**
182
+ * Get the cached head
183
+ */
184
+ getHead(): ProtoBlock {
185
+ return this.head;
186
+ }
187
+
188
+ /**
189
+ *
190
+ * A multiplexer to wrap around the traditional `updateHead()` according to the scenario
191
+ * Scenarios as follow:
192
+ * Prepare to propose in the next slot: getHead() -> predictProposerHead()
193
+ * Proposing in the current slot: updateHead() -> getProposerHead()
194
+ * Others eg. initializing forkchoice, importBlock: updateHead()
195
+ *
196
+ * Only `GetProposerHead` returns additional field `isHeadTimely` and `notReorgedReason` for metrics purpose
197
+ */
198
+ updateAndGetHead(opt: UpdateAndGetHeadOpt): {
199
+ head: ProtoBlock;
200
+ isHeadTimely?: boolean;
201
+ notReorgedReason?: NotReorgedReason;
202
+ } {
203
+ const {mode} = opt;
204
+
205
+ const canonicalHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead();
206
+ switch (mode) {
207
+ case UpdateHeadOpt.GetPredictedProposerHead:
208
+ return {head: this.predictProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot)};
209
+ case UpdateHeadOpt.GetProposerHead: {
210
+ const {
211
+ proposerHead: head,
212
+ isHeadTimely,
213
+ notReorgedReason,
214
+ } = this.getProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot);
215
+ return {head, isHeadTimely, notReorgedReason};
216
+ }
217
+ case UpdateHeadOpt.GetCanonicalHead:
218
+ return {head: canonicalHeadBlock};
219
+ default:
220
+ return {head: canonicalHeadBlock};
221
+ }
222
+ }
223
+
224
+ // Called by `predictProposerHead` and `importBlock`. If the result is not same as blockRoot's block, return true else false
225
+ // See https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/bellatrix/fork-choice.md#should_override_forkchoice_update
226
+ // Return true if the given block passes all criteria to be re-orged out
227
+ // Return false otherwise.
228
+ // Note when proposer boost reorg is disabled, it always returns false
229
+ shouldOverrideForkChoiceUpdate(
230
+ blockRoot: RootHex,
231
+ secFromSlot: number,
232
+ currentSlot: Slot
233
+ ): ShouldOverrideForkChoiceUpdateResult {
234
+ const headBlock = this.getBlockHex(blockRoot);
235
+ if (headBlock === null) {
236
+ // should not happen because this block just got imported. Fall back to no-reorg.
237
+ return {shouldOverrideFcu: false, reason: NotReorgedReason.HeadBlockNotAvailable};
238
+ }
239
+ const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
240
+ // Skip re-org attempt if proposer boost (reorg) are disabled
241
+ if (!proposerBoost || !proposerBoostReorg) {
242
+ this.logger?.verbose("Skip shouldOverrideForkChoiceUpdate check since the related flags are disabled", {
243
+ slot: currentSlot,
244
+ proposerBoost,
245
+ proposerBoostReorg,
246
+ });
247
+ return {shouldOverrideFcu: false, reason: NotReorgedReason.ProposerBoostReorgDisabled};
248
+ }
249
+
250
+ const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
251
+ const proposalSlot = headBlock.slot + 1;
252
+
253
+ // No reorg if parentBlock isn't available
254
+ if (parentBlock === undefined) {
255
+ return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
256
+ }
257
+
258
+ const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
259
+ headBlock,
260
+ parentBlock,
261
+ proposalSlot
262
+ );
263
+
264
+ if (prelimProposerHead === headBlock) {
265
+ return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
266
+ }
267
+
268
+ const currentTimeOk =
269
+ headBlock.slot === currentSlot || (proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot));
270
+ if (!currentTimeOk) {
271
+ return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
272
+ }
273
+
274
+ this.logger?.verbose("Block is weak. Should override forkchoice update", {blockRoot, slot: currentSlot});
275
+ return {shouldOverrideFcu: true, parentBlock};
276
+ }
277
+
278
+ /**
279
+ * Get the proposer boost root
280
+ */
281
+ getProposerBoostRoot(): RootHex {
282
+ return this.proposerBoostRoot ?? HEX_ZERO_HASH;
283
+ }
284
+
285
+ /**
286
+ * To predict the proposer head of the next slot. That is, to predict if proposer-boost-reorg could happen.
287
+ * Reason why we can't be certain is because information of the head block is not fully available yet
288
+ * since the current slot hasn't ended especially the attesters' votes.
289
+ *
290
+ * There is a chance we mispredict.
291
+ *
292
+ * By calling this function, we assume we are the proposer of next slot
293
+ *
294
+ */
295
+ predictProposerHead(headBlock: ProtoBlock, secFromSlot: number, currentSlot: Slot): ProtoBlock {
296
+ const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
297
+ // Skip re-org attempt if proposer boost (reorg) are disabled
298
+ if (!proposerBoost || !proposerBoostReorg) {
299
+ this.logger?.verbose("No proposer boost reorg prediction since the related flags are disabled", {
300
+ slot: currentSlot,
301
+ proposerBoost,
302
+ proposerBoostReorg,
303
+ });
304
+ return headBlock;
305
+ }
306
+
307
+ const blockRoot = headBlock.blockRoot;
308
+ const result = this.shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot);
309
+
310
+ if (result.shouldOverrideFcu) {
311
+ this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
312
+ slot: currentSlot,
313
+ proposerHead: result.parentBlock.blockRoot,
314
+ weakHead: blockRoot,
315
+ });
316
+ return result.parentBlock;
317
+ }
318
+
319
+ this.logger?.verbose("Current head is strong. Predicting next block to be built on head", {
320
+ slot: currentSlot,
321
+ head: headBlock.blockRoot,
322
+ reason: result.reason,
323
+ });
324
+
325
+ return headBlock;
326
+ }
327
+
328
+ /**
329
+ *
330
+ * This function takes in the canonical head block and determine the proposer head (canonical head block or its parent)
331
+ * https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg
332
+ * This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()`
333
+ *
334
+ * Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head
335
+ */
336
+ getProposerHead(
337
+ headBlock: ProtoBlock,
338
+ secFromSlot: number,
339
+ slot: Slot
340
+ ): {proposerHead: ProtoBlock; isHeadTimely: boolean; notReorgedReason?: NotReorgedReason} {
341
+ const isHeadTimely = headBlock.timeliness;
342
+ let proposerHead = headBlock;
343
+
344
+ // Skip re-org attempt if proposer boost (reorg) are disabled
345
+ const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
346
+ if (!proposerBoost || !proposerBoostReorg) {
347
+ this.logger?.verbose("No proposer boost reorg attempt since the related flags are disabled", {
348
+ slot,
349
+ proposerBoost,
350
+ proposerBoostReorg,
351
+ });
352
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostReorgDisabled};
353
+ }
354
+
355
+ const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
356
+
357
+ // No reorg if parentBlock isn't available
358
+ if (parentBlock === undefined) {
359
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable};
360
+ }
361
+
362
+ const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot);
363
+
364
+ if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) {
365
+ return {proposerHead, isHeadTimely, notReorgedReason: prelimNotReorgedReason};
366
+ }
367
+
368
+ // Only re-org if we are proposing on-time
369
+ if (!this.isProposingOnTime(secFromSlot)) {
370
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.NotProposingOnTime};
371
+ }
372
+
373
+ // No reorg if attempted reorg is more than a single slot
374
+ // Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
375
+ const currentTimeOk = headBlock.slot + 1 === slot;
376
+ if (!currentTimeOk) {
377
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot};
378
+ }
379
+
380
+ // No reorg if proposer boost is still in effect
381
+ const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
382
+ if (!isProposerBoostWornOff) {
383
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff};
384
+ }
385
+
386
+ // No reorg if headBlock is "not weak" ie. headBlock's weight exceeds (REORG_HEAD_WEIGHT_THRESHOLD = 20)% of total attester weight
387
+ // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_weak
388
+ const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
389
+ slotsPerEpoch: SLOTS_PER_EPOCH,
390
+ committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
391
+ });
392
+ const headNode = this.protoArray.getNode(headBlock.blockRoot);
393
+ // If headNode is unavailable, give up reorg
394
+ if (headNode === undefined || headNode.weight >= reorgThreshold) {
395
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
396
+ }
397
+
398
+ // 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
399
+ // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#is_parent_strong
400
+ const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
401
+ slotsPerEpoch: SLOTS_PER_EPOCH,
402
+ committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
403
+ });
404
+ const parentNode = this.protoArray.getNode(parentBlock.blockRoot);
405
+ // If parentNode is unavailable, give up reorg
406
+ if (parentNode === undefined || parentNode.weight <= parentThreshold) {
407
+ return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
408
+ }
409
+
410
+ // Reorg if all above checks fail
411
+ this.logger?.verbose("Performing single-slot reorg to remove current weak head", {
412
+ slot,
413
+ proposerHead: parentBlock.blockRoot,
414
+ weakHead: headBlock.blockRoot,
415
+ });
416
+ proposerHead = parentBlock;
417
+
418
+ return {proposerHead, isHeadTimely};
419
+ }
420
+
421
+ /**
422
+ * Run the fork choice rule to determine the head.
423
+ * Update the head cache.
424
+ *
425
+ * Very expensive function (400ms / run as of Aug 2021). Call when the head really needs to be re-calculated.
426
+ *
427
+ * ## Specification
428
+ *
429
+ * Is equivalent to:
430
+ *
431
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_head
432
+ */
433
+ updateHead(): ProtoBlock {
434
+ // balances is not changed but votes are changed
435
+
436
+ // NOTE: In current Lodestar metrics, 100% of forkChoiceRequests this.synced = false.
437
+ // No need to cache computeDeltas()
438
+ //
439
+ // TODO: In current Lodestar metrics, 100% of forkChoiceRequests result in a changed head.
440
+ // No need to cache the head anymore
441
+
442
+ // Check if scores need to be calculated/updated
443
+ const oldBalances = this.balances;
444
+ const newBalances = this.fcStore.justified.balances;
445
+ const deltas = computeDeltas(
446
+ this.protoArray.nodes.length,
447
+ this.votes,
448
+ oldBalances,
449
+ newBalances,
450
+ this.fcStore.equivocatingIndices
451
+ );
452
+ this.balances = newBalances;
453
+ /**
454
+ * The structure in line with deltas to propagate boost up the branch
455
+ * starting from the proposerIndex
456
+ */
457
+ let proposerBoost: {root: RootHex; score: number} | null = null;
458
+ if (this.opts?.proposerBoost && this.proposerBoostRoot) {
459
+ const proposerBoostScore =
460
+ this.justifiedProposerBoostScore ??
461
+ getCommitteeFraction(this.fcStore.justified.totalBalance, {
462
+ slotsPerEpoch: SLOTS_PER_EPOCH,
463
+ committeePercent: this.config.PROPOSER_SCORE_BOOST,
464
+ });
465
+ proposerBoost = {root: this.proposerBoostRoot, score: proposerBoostScore};
466
+ this.justifiedProposerBoostScore = proposerBoostScore;
467
+ }
468
+
469
+ const currentSlot = this.fcStore.currentSlot;
470
+ this.protoArray.applyScoreChanges({
471
+ deltas,
472
+ proposerBoost,
473
+ justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
474
+ justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
475
+ finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
476
+ finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
477
+ currentSlot,
478
+ });
479
+
480
+ const headRoot = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
481
+ const headIndex = this.protoArray.indices.get(headRoot);
482
+ if (headIndex === undefined) {
483
+ throw new ForkChoiceError({
484
+ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
485
+ root: headRoot,
486
+ });
487
+ }
488
+ const headNode = this.protoArray.nodes[headIndex];
489
+ if (headNode === undefined) {
490
+ throw new ForkChoiceError({
491
+ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
492
+ root: headRoot,
493
+ });
494
+ }
495
+
496
+ this.head = headNode;
497
+ return this.head;
498
+ }
499
+
500
+ /**
501
+ * An iteration over protoArray to get present slots, to be called preemptively
502
+ * from prepareNextSlot to prevent delay on produceBlindedBlock
503
+ * @param windowStart is the slot after which (excluding) to provide present slots
504
+ */
505
+ getSlotsPresent(windowStart: number): number {
506
+ return this.protoArray.nodes.filter((node) => node.slot > windowStart).length;
507
+ }
508
+
509
+ /** Very expensive function, iterates the entire ProtoArray. Called only in debug API */
510
+ getHeads(): ProtoBlock[] {
511
+ return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
512
+ }
513
+
514
+ /** This is for the debug API only */
515
+ getAllNodes(): ProtoNode[] {
516
+ return this.protoArray.nodes;
517
+ }
518
+
519
+ getFinalizedCheckpoint(): CheckpointWithHex {
520
+ return this.fcStore.finalizedCheckpoint;
521
+ }
522
+
523
+ getJustifiedCheckpoint(): CheckpointWithHex {
524
+ return this.fcStore.justified.checkpoint;
525
+ }
526
+
527
+ /**
528
+ * Add `block` to the fork choice DAG.
529
+ *
530
+ * ## Specification
531
+ *
532
+ * Approximates:
533
+ *
534
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_block
535
+ *
536
+ * It only approximates the specification since it does not run the `state_transition` check.
537
+ * That should have already been called upstream and it's too expensive to call again.
538
+ *
539
+ * ## Notes:
540
+ *
541
+ * The supplied block **must** pass the `state_transition` function as it will not be run here.
542
+ *
543
+ * `justifiedBalances` balances of justified state which is updated synchronously.
544
+ * This ensures that the forkchoice is never out of sync.
545
+ */
546
+ onBlock(
547
+ block: BeaconBlock,
548
+ state: CachedBeaconStateAllForks,
549
+ blockDelaySec: number,
550
+ currentSlot: Slot,
551
+ executionStatus: MaybeValidExecutionStatus,
552
+ dataAvailabilityStatus: DataAvailabilityStatus
553
+ ): ProtoBlock {
554
+ const {parentRoot, slot} = block;
555
+ const parentRootHex = toRootHex(parentRoot);
556
+ // Parent block must be known
557
+ const parentBlock = this.protoArray.getBlock(parentRootHex);
558
+ if (!parentBlock) {
559
+ throw new ForkChoiceError({
560
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
561
+ err: {
562
+ code: InvalidBlockCode.UNKNOWN_PARENT,
563
+ root: parentRootHex,
564
+ },
565
+ });
566
+ }
567
+
568
+ // Blocks cannot be in the future. If they are, their consideration must be delayed until
569
+ // the are in the past.
570
+ //
571
+ // Note: presently, we do not delay consideration. We just drop the block.
572
+ if (slot > this.fcStore.currentSlot) {
573
+ throw new ForkChoiceError({
574
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
575
+ err: {
576
+ code: InvalidBlockCode.FUTURE_SLOT,
577
+ currentSlot: this.fcStore.currentSlot,
578
+ blockSlot: slot,
579
+ },
580
+ });
581
+ }
582
+
583
+ // Check that block is later than the finalized epoch slot (optimization to reduce calls to
584
+ // get_ancestor).
585
+ const finalizedSlot = computeStartSlotAtEpoch(this.fcStore.finalizedCheckpoint.epoch);
586
+ if (slot <= finalizedSlot) {
587
+ throw new ForkChoiceError({
588
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
589
+ err: {
590
+ code: InvalidBlockCode.FINALIZED_SLOT,
591
+ finalizedSlot,
592
+ blockSlot: slot,
593
+ },
594
+ });
595
+ }
596
+
597
+ // Check block is a descendant of the finalized block at the checkpoint finalized slot.
598
+ const blockAncestorRoot = this.getAncestor(parentRootHex, finalizedSlot);
599
+ const finalizedRoot = this.fcStore.finalizedCheckpoint.rootHex;
600
+ if (blockAncestorRoot !== finalizedRoot) {
601
+ throw new ForkChoiceError({
602
+ code: ForkChoiceErrorCode.INVALID_BLOCK,
603
+ err: {
604
+ code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT,
605
+ finalizedRoot,
606
+ blockAncestor: blockAncestorRoot,
607
+ },
608
+ });
609
+ }
610
+
611
+ const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
612
+ const blockRootHex = toRootHex(blockRoot);
613
+
614
+ // Assign proposer score boost if the block is timely
615
+ // before attesting interval = before 1st interval
616
+ const isTimely = this.isBlockTimely(block, blockDelaySec);
617
+ if (
618
+ this.opts?.proposerBoost &&
619
+ isTimely &&
620
+ // only boost the first block we see
621
+ this.proposerBoostRoot === null
622
+ ) {
623
+ this.proposerBoostRoot = blockRootHex;
624
+ }
625
+
626
+ // As per specs, we should be validating here the terminal conditions of
627
+ // the PoW if this were a merge transition block.
628
+ // (https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/fork-choice.md#on_block)
629
+ //
630
+ // However this check has been moved to the `verifyBlockStateTransition` in
631
+ // `packages/beacon-node/src/chain/blocks/verifyBlock.ts` as:
632
+ //
633
+ // 1. Its prudent to fail fast and not try importing a block in forkChoice.
634
+ // 2. Also the data to run such a validation is readily available there.
635
+
636
+ const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint);
637
+ const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint);
638
+ const stateJustifiedEpoch = justifiedCheckpoint.epoch;
639
+
640
+ // Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if
641
+ // the justified checkpoint changes
642
+ this.updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, () =>
643
+ this.fcStore.justifiedBalancesGetter(justifiedCheckpoint, state)
644
+ );
645
+
646
+ const blockEpoch = computeEpochAtSlot(slot);
647
+
648
+ // same logic to compute_pulled_up_tip in the spec, making it inline because of reusing variables
649
+ // If the parent checkpoints are already at the same epoch as the block being imported,
650
+ // it's impossible for the unrealized checkpoints to differ from the parent's. This
651
+ // holds true because:
652
+ //
653
+ // 1. A child block cannot have lower FFG checkpoints than its parent.
654
+ // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`.
655
+ // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`.
656
+ //
657
+ // This is an optimization. It should reduce the amount of times we run
658
+ // `process_justification_and_finalization` by approximately 1/3rd when the chain is
659
+ // performing optimally.
660
+ let unrealizedJustifiedCheckpoint: CheckpointWithHex;
661
+ let unrealizedFinalizedCheckpoint: CheckpointWithHex;
662
+ if (this.opts?.computeUnrealized) {
663
+ if (
664
+ parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
665
+ parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch
666
+ ) {
667
+ // reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet
668
+ unrealizedJustifiedCheckpoint = {
669
+ epoch: parentBlock.unrealizedJustifiedEpoch,
670
+ root: fromHex(parentBlock.unrealizedJustifiedRoot),
671
+ rootHex: parentBlock.unrealizedJustifiedRoot,
672
+ };
673
+ unrealizedFinalizedCheckpoint = {
674
+ epoch: parentBlock.unrealizedFinalizedEpoch,
675
+ root: fromHex(parentBlock.unrealizedFinalizedRoot),
676
+ rootHex: parentBlock.unrealizedFinalizedRoot,
677
+ };
678
+ } else {
679
+ // compute new, happens 2/3 first blocks of epoch as monitored in mainnet
680
+ const unrealized = computeUnrealizedCheckpoints(state);
681
+ unrealizedJustifiedCheckpoint = toCheckpointWithHex(unrealized.justifiedCheckpoint);
682
+ unrealizedFinalizedCheckpoint = toCheckpointWithHex(unrealized.finalizedCheckpoint);
683
+ }
684
+ } else {
685
+ unrealizedJustifiedCheckpoint = justifiedCheckpoint;
686
+ unrealizedFinalizedCheckpoint = finalizedCheckpoint;
687
+ }
688
+
689
+ // Un-realized checkpoints
690
+ // Update best known unrealized justified & finalized checkpoints
691
+ this.updateUnrealizedCheckpoints(unrealizedJustifiedCheckpoint, unrealizedFinalizedCheckpoint, () =>
692
+ this.fcStore.justifiedBalancesGetter(unrealizedJustifiedCheckpoint, state)
693
+ );
694
+
695
+ // If block is from past epochs, try to update store's justified & finalized checkpoints right away
696
+ if (blockEpoch < computeEpochAtSlot(currentSlot)) {
697
+ this.updateCheckpoints(unrealizedJustifiedCheckpoint, unrealizedFinalizedCheckpoint, () =>
698
+ this.fcStore.justifiedBalancesGetter(unrealizedJustifiedCheckpoint, state)
699
+ );
700
+ }
701
+
702
+ const targetSlot = computeStartSlotAtEpoch(blockEpoch);
703
+ const targetRoot = slot === targetSlot ? blockRoot : state.blockRoots.get(targetSlot % SLOTS_PER_HISTORICAL_ROOT);
704
+
705
+ // This does not apply a vote to the block, it just makes fork choice aware of the block so
706
+ // it can still be identified as the head even if it doesn't have any votes.
707
+ const protoBlock: ProtoBlock = {
708
+ slot: slot,
709
+ blockRoot: blockRootHex,
710
+ parentRoot: parentRootHex,
711
+ targetRoot: toRootHex(targetRoot),
712
+ stateRoot: toRootHex(block.stateRoot),
713
+ timeliness: isTimely,
714
+
715
+ justifiedEpoch: stateJustifiedEpoch,
716
+ justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root),
717
+ finalizedEpoch: finalizedCheckpoint.epoch,
718
+ finalizedRoot: toRootHex(state.finalizedCheckpoint.root),
719
+ unrealizedJustifiedEpoch: unrealizedJustifiedCheckpoint.epoch,
720
+ unrealizedJustifiedRoot: unrealizedJustifiedCheckpoint.rootHex,
721
+ unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch,
722
+ unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
723
+
724
+ ...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
725
+ ? {
726
+ executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
727
+ executionPayloadNumber: block.body.executionPayload.blockNumber,
728
+ executionStatus: this.getPostMergeExecStatus(executionStatus),
729
+ dataAvailabilityStatus,
730
+ }
731
+ : {
732
+ executionPayloadBlockHash: null,
733
+ executionStatus: this.getPreMergeExecStatus(executionStatus),
734
+ dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
735
+ }),
736
+ };
737
+
738
+ this.protoArray.onBlock(protoBlock, currentSlot);
739
+
740
+ return protoBlock;
741
+ }
742
+
743
+ /**
744
+ * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
745
+ *
746
+ * ## Specification
747
+ *
748
+ * Approximates:
749
+ *
750
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
751
+ *
752
+ * It only approximates the specification since it does not perform
753
+ * `is_valid_indexed_attestation` since that should already have been called upstream and it's
754
+ * too expensive to call again.
755
+ *
756
+ * ## Notes:
757
+ *
758
+ * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
759
+ * will not be run here.
760
+ */
761
+ onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void {
762
+ // Ignore any attestations to the zero hash.
763
+ //
764
+ // This is an edge case that results from the spec aliasing the zero hash to the genesis
765
+ // block. Attesters may attest to the zero hash if they have never seen a block.
766
+ //
767
+ // We have two options here:
768
+ //
769
+ // 1. Apply all zero-hash attestations to the genesis block.
770
+ // 2. Ignore all attestations to the zero hash.
771
+ //
772
+ // (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is
773
+ // fine because votes to the genesis block are not useful; all validators implicitly attest
774
+ // to genesis just by being present in the chain.
775
+ const attestationData = attestation.data;
776
+ const {slot, beaconBlockRoot} = attestationData;
777
+ const blockRootHex = toRootHex(beaconBlockRoot);
778
+ const targetEpoch = attestationData.target.epoch;
779
+ if (ssz.Root.equals(beaconBlockRoot, ZERO_HASH)) {
780
+ return;
781
+ }
782
+
783
+ this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
784
+
785
+ if (slot < this.fcStore.currentSlot) {
786
+ for (const validatorIndex of attestation.attestingIndices) {
787
+ if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
788
+ this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
789
+ }
790
+ }
791
+ } else {
792
+ // The spec declares:
793
+ //
794
+ // ```
795
+ // Attestations can only affect the fork choice of subsequent slots.
796
+ // Delay consideration in the fork choice until their slot is in the past.
797
+ // ```
798
+ const byRoot = this.queuedAttestations.getOrDefault(slot);
799
+ const validatorIndices = byRoot.getOrDefault(blockRootHex);
800
+ for (const validatorIndex of attestation.attestingIndices) {
801
+ if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
802
+ validatorIndices.add(validatorIndex);
803
+ }
804
+ }
805
+ }
806
+ }
807
+
808
+ /**
809
+ * Small different from the spec:
810
+ * We already call is_slashable_attestation_data() and is_valid_indexed_attestation
811
+ * in state transition so no need to do it again
812
+ */
813
+ onAttesterSlashing(attesterSlashing: AttesterSlashing): void {
814
+ // TODO: we already call in in state-transition, find a way not to recompute it again
815
+ const intersectingIndices = getAttesterSlashableIndices(attesterSlashing);
816
+ for (const validatorIndex of intersectingIndices) {
817
+ this.fcStore.equivocatingIndices.add(validatorIndex);
818
+ }
819
+ }
820
+
821
+ getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined {
822
+ const vote = this.votes[validatorIndex];
823
+ if (vote === undefined) {
824
+ return undefined;
825
+ }
826
+ return {
827
+ epoch: vote.nextEpoch,
828
+ root: vote.nextIndex === null ? HEX_ZERO_HASH : this.protoArray.nodes[vote.nextIndex].blockRoot,
829
+ };
830
+ }
831
+
832
+ /**
833
+ * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
834
+ * This should only be called once per slot because:
835
+ * - calling this multiple times in the same slot does not update `votes`
836
+ * - new attestations in the current slot must stay in the queue
837
+ * - new attestations in the old slots are applied to the `votes` already
838
+ * - also side effect of this function is `validatedAttestationDatas` reseted
839
+ */
840
+ updateTime(currentSlot: Slot): void {
841
+ if (this.fcStore.currentSlot >= currentSlot) return;
842
+ while (this.fcStore.currentSlot < currentSlot) {
843
+ const previousSlot = this.fcStore.currentSlot;
844
+ // Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
845
+ this.onTick(previousSlot + 1);
846
+ }
847
+
848
+ this.queuedAttestationsPreviousSlot = 0;
849
+ // Process any attestations that might now be eligible.
850
+ this.processAttestationQueue();
851
+ this.validatedAttestationDatas = new Set();
852
+ }
853
+
854
+ getTime(): Slot {
855
+ return this.fcStore.currentSlot;
856
+ }
857
+
858
+ /** Returns `true` if the block is known **and** a descendant of the finalized root. */
859
+ hasBlock(blockRoot: Root): boolean {
860
+ return this.hasBlockHex(toRootHex(blockRoot));
861
+ }
862
+ /** Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. */
863
+ getBlock(blockRoot: Root): ProtoBlock | null {
864
+ return this.getBlockHex(toRootHex(blockRoot));
865
+ }
866
+
867
+ /**
868
+ * Returns `true` if the block is known **and** a descendant of the finalized root.
869
+ */
870
+ hasBlockHex(blockRoot: RootHex): boolean {
871
+ const node = this.protoArray.getNode(blockRoot);
872
+ if (node === undefined) {
873
+ return false;
874
+ }
875
+
876
+ return this.protoArray.isFinalizedRootOrDescendant(node);
877
+ }
878
+
879
+ /**
880
+ * Same to hasBlock but without checking if the block is a descendant of the finalized root.
881
+ */
882
+ hasBlockUnsafe(blockRoot: Root): boolean {
883
+ return this.hasBlockHexUnsafe(toRootHex(blockRoot));
884
+ }
885
+
886
+ /**
887
+ * Same to hasBlockHex but without checking if the block is a descendant of the finalized root.
888
+ */
889
+ hasBlockHexUnsafe(blockRoot: RootHex): boolean {
890
+ return this.protoArray.hasBlock(blockRoot);
891
+ }
892
+
893
+ /**
894
+ * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
895
+ */
896
+ getBlockHex(blockRoot: RootHex): ProtoBlock | null {
897
+ const node = this.protoArray.getNode(blockRoot);
898
+ if (!node) {
899
+ return null;
900
+ }
901
+
902
+ if (!this.protoArray.isFinalizedRootOrDescendant(node)) {
903
+ return null;
904
+ }
905
+
906
+ return {
907
+ ...node,
908
+ };
909
+ }
910
+
911
+ getJustifiedBlock(): ProtoBlock {
912
+ const block = this.getBlockHex(this.fcStore.justified.checkpoint.rootHex);
913
+ if (!block) {
914
+ throw new ForkChoiceError({
915
+ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
916
+ root: this.fcStore.justified.checkpoint.rootHex,
917
+ });
918
+ }
919
+ return block;
920
+ }
921
+
922
+ getFinalizedBlock(): ProtoBlock {
923
+ const block = this.getBlockHex(this.fcStore.finalizedCheckpoint.rootHex);
924
+ if (!block) {
925
+ throw new ForkChoiceError({
926
+ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
927
+ root: this.fcStore.finalizedCheckpoint.rootHex,
928
+ });
929
+ }
930
+ return block;
931
+ }
932
+
933
+ /**
934
+ * Returns true if the `descendantRoot` has an ancestor with `ancestorRoot`.
935
+ *
936
+ * Always returns `false` if either input roots are unknown.
937
+ * Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
938
+ */
939
+ isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
940
+ return this.protoArray.isDescendant(ancestorRoot, descendantRoot);
941
+ }
942
+
943
+ /**
944
+ * All indices in votes are relative to proto array so always keep it up to date
945
+ */
946
+ prune(finalizedRoot: RootHex): ProtoBlock[] {
947
+ const prunedNodes = this.protoArray.maybePrune(finalizedRoot);
948
+ const prunedCount = prunedNodes.length;
949
+ for (let i = 0; i < this.votes.length; i++) {
950
+ const vote = this.votes[i];
951
+ // validator has never voted
952
+ if (vote === undefined) {
953
+ continue;
954
+ }
955
+
956
+ if (vote.currentIndex !== null) {
957
+ if (vote.currentIndex >= prunedCount) {
958
+ vote.currentIndex -= prunedCount;
959
+ } else {
960
+ // the vote was for a pruned proto node
961
+ vote.currentIndex = null;
962
+ }
963
+ }
964
+
965
+ if (vote.nextIndex !== null) {
966
+ if (vote.nextIndex >= prunedCount) {
967
+ vote.nextIndex -= prunedCount;
968
+ } else {
969
+ // the vote was for a pruned proto node
970
+ vote.nextIndex = null;
971
+ }
972
+ }
973
+ }
974
+ return prunedNodes;
975
+ }
976
+
977
+ setPruneThreshold(threshold: number): void {
978
+ this.protoArray.pruneThreshold = threshold;
979
+ }
980
+
981
+ /**
982
+ * Iterates backwards through block summaries, starting from a block root.
983
+ * Return only the non-finalized blocks.
984
+ */
985
+ iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<ProtoBlock> {
986
+ return this.protoArray.iterateAncestorNodes(blockRoot);
987
+ }
988
+
989
+ /**
990
+ * Returns all blocks backwards starting from a block root.
991
+ * Return only the non-finalized blocks.
992
+ */
993
+ getAllAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
994
+ const blocks = this.protoArray.getAllAncestorNodes(blockRoot);
995
+ // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
996
+ return blocks.slice(0, blocks.length - 1);
997
+ }
998
+
999
+ /**
1000
+ * The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
1001
+ */
1002
+ getAllNonAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
1003
+ return this.protoArray.getAllNonAncestorNodes(blockRoot);
1004
+ }
1005
+
1006
+ /**
1007
+ * Returns both ancestor and non-ancestor blocks in a single traversal.
1008
+ */
1009
+ getAllAncestorAndNonAncestorBlocks(blockRoot: RootHex): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]} {
1010
+ const {ancestors, nonAncestors} = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot);
1011
+
1012
+ return {
1013
+ // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
1014
+ ancestors: ancestors.slice(0, ancestors.length - 1),
1015
+ nonAncestors,
1016
+ };
1017
+ }
1018
+
1019
+ getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null {
1020
+ if (slot > this.head.slot) {
1021
+ return null;
1022
+ }
1023
+
1024
+ if (slot === this.head.slot) {
1025
+ return this.head;
1026
+ }
1027
+
1028
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
1029
+ if (block.slot === slot) {
1030
+ return block;
1031
+ }
1032
+ }
1033
+ return null;
1034
+ }
1035
+
1036
+ getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null {
1037
+ if (slot >= this.head.slot) {
1038
+ return this.head;
1039
+ }
1040
+
1041
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
1042
+ if (slot >= block.slot) {
1043
+ return block;
1044
+ }
1045
+ }
1046
+ return null;
1047
+ }
1048
+
1049
+ /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
1050
+ forwarditerateAncestorBlocks(): ProtoBlock[] {
1051
+ return this.protoArray.nodes;
1052
+ }
1053
+
1054
+ *forwardIterateDescendants(blockRoot: RootHex): IterableIterator<ProtoBlock> {
1055
+ const rootsInChain = new Set([blockRoot]);
1056
+
1057
+ const blockIndex = this.protoArray.indices.get(blockRoot);
1058
+ if (blockIndex === undefined) {
1059
+ throw new ForkChoiceError({
1060
+ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
1061
+ root: blockRoot,
1062
+ });
1063
+ }
1064
+
1065
+ for (let i = blockIndex + 1; i < this.protoArray.nodes.length; i++) {
1066
+ const node = this.protoArray.nodes[i];
1067
+ if (rootsInChain.has(node.parentRoot)) {
1068
+ rootsInChain.add(node.blockRoot);
1069
+ yield node;
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
1075
+ getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] {
1076
+ return this.protoArray.nodes.filter((node) => node.parentRoot === parentRoot);
1077
+ }
1078
+
1079
+ /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
1080
+ getBlockSummariesAtSlot(slot: Slot): ProtoBlock[] {
1081
+ const nodes = this.protoArray.nodes;
1082
+ const blocksAtSlot: ProtoBlock[] = [];
1083
+ for (let i = 0, len = nodes.length; i < len; i++) {
1084
+ const node = nodes[i];
1085
+ if (node.slot === slot) {
1086
+ blocksAtSlot.push(node);
1087
+ }
1088
+ }
1089
+ return blocksAtSlot;
1090
+ }
1091
+
1092
+ /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
1093
+ getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult {
1094
+ const prevNode = this.protoArray.getNode(prevBlock.blockRoot);
1095
+ const newNode = this.protoArray.getNode(newBlock.blockRoot);
1096
+ if (!prevNode || !newNode) {
1097
+ return {code: AncestorStatus.BlockUnknown};
1098
+ }
1099
+
1100
+ const commonAncestor = this.protoArray.getCommonAncestor(prevNode, newNode);
1101
+ // No common ancestor, should never happen. Return null to not throw
1102
+ if (!commonAncestor) {
1103
+ return {code: AncestorStatus.NoCommonAncenstor};
1104
+ }
1105
+
1106
+ // If common node is one of both nodes, then they are direct descendants, return null
1107
+ if (commonAncestor.blockRoot === prevNode.blockRoot || commonAncestor.blockRoot === newNode.blockRoot) {
1108
+ return {code: AncestorStatus.Descendant};
1109
+ }
1110
+
1111
+ return {code: AncestorStatus.CommonAncestor, depth: Math.max(newNode.slot, prevNode.slot) - commonAncestor.slot};
1112
+ }
1113
+
1114
+ /**
1115
+ * Optimistic sync validate till validated latest hash, invalidate any descendant
1116
+ * branch if invalidate till hash provided
1117
+ *
1118
+ * Proxies to protoArray's validateLatestHash and could run extra validations for the
1119
+ * justified's status as well as validate the terminal conditions if terminal block
1120
+ * becomes valid
1121
+ */
1122
+ validateLatestHash(execResponse: LVHExecResponse): void {
1123
+ try {
1124
+ this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot);
1125
+ } catch (e) {
1126
+ if (e instanceof ProtoArrayError && e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE) {
1127
+ this.irrecoverableError = e;
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * A dependent root is the block root of the last block before the state transition that decided a specific shuffling
1134
+ *
1135
+ * For proposer shuffling with 0 epochs of lookahead = previous immediate epoch transition
1136
+ * For attester shuffling with 1 epochs of lookahead = last epoch's epoch transition
1137
+ *
1138
+ * ```
1139
+ * epoch: 0 1 2 3 4
1140
+ * |-------|-------|=======|-------|
1141
+ * dependent root A -------------^
1142
+ * dependent root B -----^
1143
+ * ```
1144
+ * - proposer shuffling for a block in epoch 2: dependent root A (EpochDifference = 0)
1145
+ * - attester shuffling for a block in epoch 2: dependent root B (EpochDifference = 1)
1146
+ */
1147
+ getDependentRoot(block: ProtoBlock, epochDifference: EpochDifference): RootHex {
1148
+ // The navigation at the end of the while loop will always progress backwards,
1149
+ // jumping to a block with a strictly less slot number. So the condition `blockEpoch < atEpoch`
1150
+ // is guaranteed to happen. Given the use of target blocks for faster navigation, it will take
1151
+ // at most `2 * (blockEpoch - atEpoch + 1)` iterations to find the dependent root.
1152
+
1153
+ const beforeSlot = block.slot - (block.slot % SLOTS_PER_EPOCH) - epochDifference * SLOTS_PER_EPOCH;
1154
+
1155
+ // Special case close to genesis block, return the genesis block root
1156
+ if (beforeSlot <= 0) {
1157
+ const genesisBlock = this.protoArray.nodes[0];
1158
+ if (genesisBlock === undefined || genesisBlock.slot !== 0) {
1159
+ throw Error("Genesis block not available");
1160
+ }
1161
+ return genesisBlock.blockRoot;
1162
+ }
1163
+
1164
+ const finalizedSlot = this.getFinalizedBlock().slot;
1165
+ while (block.slot >= finalizedSlot) {
1166
+ // Dependant root must be in epoch less than `beforeSlot`
1167
+ if (block.slot < beforeSlot) {
1168
+ return block.blockRoot;
1169
+ }
1170
+
1171
+ // Skip one last jump if there's no skipped slot at first slot of the epoch
1172
+ if (block.slot === beforeSlot) {
1173
+ return block.parentRoot;
1174
+ }
1175
+
1176
+ block =
1177
+ block.blockRoot === block.targetRoot
1178
+ ? // For the first slot of the epoch, a block is it's own target
1179
+ this.protoArray.getBlockReadonly(block.parentRoot)
1180
+ : // else we can navigate much faster jumping to the target block
1181
+ this.protoArray.getBlockReadonly(block.targetRoot);
1182
+ }
1183
+
1184
+ throw Error(`Not found dependent root for block slot ${block.slot}, epoch difference ${epochDifference}`);
1185
+ }
1186
+
1187
+ /**
1188
+ * Return true if the block is timely for the current slot.
1189
+ * Child class can overwrite this for testing purpose.
1190
+ */
1191
+ protected isBlockTimely(block: BeaconBlock, blockDelaySec: number): boolean {
1192
+ const isBeforeAttestingInterval = blockDelaySec < this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT;
1193
+ return this.fcStore.currentSlot === block.slot && isBeforeAttestingInterval;
1194
+ }
1195
+
1196
+ /**
1197
+ * https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
1198
+ */
1199
+ private isProposingOnTime(secFromSlot: number): boolean {
1200
+ const proposerReorgCutoff = this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT / 2;
1201
+ return secFromSlot <= proposerReorgCutoff;
1202
+ }
1203
+
1204
+ private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
1205
+ if (executionStatus !== ExecutionStatus.PreMerge)
1206
+ throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);
1207
+ return executionStatus;
1208
+ }
1209
+
1210
+ private getPreMergeDataStatus(dataAvailabilityStatus: DataAvailabilityStatus): DataAvailabilityStatus.PreData {
1211
+ if (dataAvailabilityStatus !== DataAvailabilityStatus.PreData)
1212
+ throw Error(
1213
+ `Invalid pre-merge data status: expected: ${DataAvailabilityStatus.PreData}, got ${dataAvailabilityStatus}`
1214
+ );
1215
+ return dataAvailabilityStatus;
1216
+ }
1217
+
1218
+ private getPostMergeExecStatus(
1219
+ executionStatus: MaybeValidExecutionStatus
1220
+ ): ExecutionStatus.Valid | ExecutionStatus.Syncing {
1221
+ if (executionStatus === ExecutionStatus.PreMerge)
1222
+ throw Error(
1223
+ `Invalid post-merge execution status: expected: ${ExecutionStatus.Syncing} or ${ExecutionStatus.Valid} , got ${executionStatus}`
1224
+ );
1225
+ return executionStatus;
1226
+ }
1227
+
1228
+ /**
1229
+ * Why `getJustifiedBalances` getter?
1230
+ * - updateCheckpoints() is called in both on_block and on_tick.
1231
+ * - Our cache strategy to get justified balances is incomplete, it can't regen all possible states.
1232
+ * - If the justified state is not available it will get one that is "closest" to the justified checkpoint.
1233
+ * - As a last resort fallback the state that references the new justified checkpoint is close or equal to the
1234
+ * desired justified state. However, the state is available only in the on_block handler
1235
+ * - `getJustifiedBalances` makes the dynamics of justified balances cache easier to reason about
1236
+ *
1237
+ * **`on_block`**:
1238
+ * May need the justified balances of:
1239
+ * - justifiedCheckpoint
1240
+ * - unrealizedJustifiedCheckpoint
1241
+ * These balances are not immediately available so the getter calls a cache fn `() => cache.getBalances()`
1242
+ *
1243
+ * **`on_tick`**
1244
+ * May need the justified balances of:
1245
+ * - unrealizedJustified: Already available in `CheckpointHexWithBalance`
1246
+ * Since this balances are already available the getter is just `() => balances`, without cache interaction
1247
+ */
1248
+ private updateCheckpoints(
1249
+ justifiedCheckpoint: CheckpointWithHex,
1250
+ finalizedCheckpoint: CheckpointWithHex,
1251
+ getJustifiedBalances: () => JustifiedBalances
1252
+ ): void {
1253
+ // Update justified checkpoint.
1254
+ if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
1255
+ this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
1256
+ this.justifiedProposerBoostScore = null;
1257
+ }
1258
+
1259
+ // Update finalized checkpoint.
1260
+ if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
1261
+ this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
1262
+ this.justifiedProposerBoostScore = null;
1263
+ }
1264
+ }
1265
+
1266
+ /**
1267
+ * Update unrealized checkpoints in store if necessary
1268
+ */
1269
+ private updateUnrealizedCheckpoints(
1270
+ unrealizedJustifiedCheckpoint: CheckpointWithHex,
1271
+ unrealizedFinalizedCheckpoint: CheckpointWithHex,
1272
+ getJustifiedBalances: () => JustifiedBalances
1273
+ ): void {
1274
+ if (unrealizedJustifiedCheckpoint.epoch > this.fcStore.unrealizedJustified.checkpoint.epoch) {
1275
+ this.fcStore.unrealizedJustified = {
1276
+ checkpoint: unrealizedJustifiedCheckpoint,
1277
+ balances: getJustifiedBalances(),
1278
+ };
1279
+ }
1280
+ if (unrealizedFinalizedCheckpoint.epoch > this.fcStore.unrealizedFinalizedCheckpoint.epoch) {
1281
+ this.fcStore.unrealizedFinalizedCheckpoint = unrealizedFinalizedCheckpoint;
1282
+ }
1283
+ }
1284
+
1285
+ /**
1286
+ * Validates the `indexed_attestation` for application to fork choice.
1287
+ *
1288
+ * ## Specification
1289
+ *
1290
+ * Equivalent to:
1291
+ *
1292
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#validate_on_attestation
1293
+ */
1294
+ private validateOnAttestation(
1295
+ indexedAttestation: IndexedAttestation,
1296
+ slot: Slot,
1297
+ blockRootHex: string,
1298
+ targetEpoch: Epoch,
1299
+ attDataRoot: string,
1300
+ // forceImport attestation even if too old, mostly used in spec tests
1301
+ forceImport?: boolean
1302
+ ): void {
1303
+ // There is no point in processing an attestation with an empty bitfield. Reject
1304
+ // it immediately.
1305
+ //
1306
+ // This is not in the specification, however it should be transparent to other nodes. We
1307
+ // return early here to avoid wasting precious resources verifying the rest of it.
1308
+ if (!indexedAttestation.attestingIndices.length) {
1309
+ throw new ForkChoiceError({
1310
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1311
+ err: {
1312
+ code: InvalidAttestationCode.EMPTY_AGGREGATION_BITFIELD,
1313
+ },
1314
+ });
1315
+ }
1316
+
1317
+ if (!this.validatedAttestationDatas.has(attDataRoot)) {
1318
+ this.validateAttestationData(indexedAttestation.data, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
1319
+ }
1320
+ }
1321
+
1322
+ private validateAttestationData(
1323
+ attestationData: phase0.AttestationData,
1324
+ slot: Slot,
1325
+ beaconBlockRootHex: string,
1326
+ targetEpoch: Epoch,
1327
+ attDataRoot: string,
1328
+ // forceImport attestation even if too old, mostly used in spec tests
1329
+ forceImport?: boolean
1330
+ ): void {
1331
+ const epochNow = computeEpochAtSlot(this.fcStore.currentSlot);
1332
+ const targetRootHex = toRootHex(attestationData.target.root);
1333
+
1334
+ // Attestation must be from the current of previous epoch.
1335
+ if (targetEpoch > epochNow) {
1336
+ throw new ForkChoiceError({
1337
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1338
+ err: {
1339
+ code: InvalidAttestationCode.FUTURE_EPOCH,
1340
+ attestationEpoch: targetEpoch,
1341
+ currentEpoch: epochNow,
1342
+ },
1343
+ });
1344
+ }
1345
+
1346
+ if (!forceImport && targetEpoch + 1 < epochNow) {
1347
+ throw new ForkChoiceError({
1348
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1349
+ err: {
1350
+ code: InvalidAttestationCode.PAST_EPOCH,
1351
+ attestationEpoch: targetEpoch,
1352
+ currentEpoch: epochNow,
1353
+ },
1354
+ });
1355
+ }
1356
+
1357
+ if (targetEpoch !== computeEpochAtSlot(slot)) {
1358
+ throw new ForkChoiceError({
1359
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1360
+ err: {
1361
+ code: InvalidAttestationCode.BAD_TARGET_EPOCH,
1362
+ target: targetEpoch,
1363
+ slot,
1364
+ },
1365
+ });
1366
+ }
1367
+
1368
+ // Attestation target must be for a known block.
1369
+ //
1370
+ // We do not delay the block for later processing to reduce complexity and DoS attack
1371
+ // surface.
1372
+ if (!this.protoArray.hasBlock(targetRootHex)) {
1373
+ throw new ForkChoiceError({
1374
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1375
+ err: {
1376
+ code: InvalidAttestationCode.UNKNOWN_TARGET_ROOT,
1377
+ root: targetRootHex,
1378
+ },
1379
+ });
1380
+ }
1381
+
1382
+ // Load the block for `attestation.data.beacon_block_root`.
1383
+ //
1384
+ // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork
1385
+ // choice. Any known, non-finalized block should be in fork choice, so this check
1386
+ // immediately filters out attestations that attest to a block that has not been processed.
1387
+ //
1388
+ // Attestations must be for a known block. If the block is unknown, we simply drop the
1389
+ // attestation and do not delay consideration for later.
1390
+ const block = this.protoArray.getBlock(beaconBlockRootHex);
1391
+ if (!block) {
1392
+ throw new ForkChoiceError({
1393
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1394
+ err: {
1395
+ code: InvalidAttestationCode.UNKNOWN_HEAD_BLOCK,
1396
+ beaconBlockRoot: beaconBlockRootHex,
1397
+ },
1398
+ });
1399
+ }
1400
+
1401
+ // If an attestation points to a block that is from an earlier slot than the attestation,
1402
+ // then all slots between the block and attestation must be skipped. Therefore if the block
1403
+ // is from a prior epoch to the attestation, then the target root must be equal to the root
1404
+ // of the block that is being attested to.
1405
+ const expectedTargetHex = targetEpoch > computeEpochAtSlot(block.slot) ? beaconBlockRootHex : block.targetRoot;
1406
+
1407
+ if (expectedTargetHex !== targetRootHex) {
1408
+ throw new ForkChoiceError({
1409
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1410
+ err: {
1411
+ code: InvalidAttestationCode.INVALID_TARGET,
1412
+ attestation: targetRootHex,
1413
+ local: expectedTargetHex,
1414
+ },
1415
+ });
1416
+ }
1417
+
1418
+ // Attestations must not be for blocks in the future. If this is the case, the attestation
1419
+ // should not be considered.
1420
+ if (block.slot > slot) {
1421
+ throw new ForkChoiceError({
1422
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1423
+ err: {
1424
+ code: InvalidAttestationCode.ATTESTS_TO_FUTURE_BLOCK,
1425
+ block: block.slot,
1426
+ attestation: slot,
1427
+ },
1428
+ });
1429
+ }
1430
+
1431
+ this.validatedAttestationDatas.add(attDataRoot);
1432
+ }
1433
+
1434
+ /**
1435
+ * Add a validator's latest message to the tracked votes
1436
+ */
1437
+ private addLatestMessage(validatorIndex: ValidatorIndex, nextEpoch: Epoch, nextRoot: RootHex): void {
1438
+ const vote = this.votes[validatorIndex];
1439
+ // should not happen, attestation is validated before this step
1440
+ const nextIndex = this.protoArray.indices.get(nextRoot);
1441
+ if (nextIndex === undefined) {
1442
+ throw new Error(`Could not find proto index for nextRoot ${nextRoot}`);
1443
+ }
1444
+
1445
+ if (vote === undefined) {
1446
+ this.votes[validatorIndex] = {
1447
+ currentIndex: null,
1448
+ nextIndex,
1449
+ nextEpoch,
1450
+ };
1451
+ } else if (nextEpoch > vote.nextEpoch) {
1452
+ vote.nextIndex = nextIndex;
1453
+ vote.nextEpoch = nextEpoch;
1454
+ }
1455
+ // else its an old vote, don't count it
1456
+ }
1457
+
1458
+ /**
1459
+ * Processes and removes from the queue any queued attestations which may now be eligible for
1460
+ * processing due to the slot clock incrementing.
1461
+ */
1462
+ private processAttestationQueue(): void {
1463
+ const currentSlot = this.fcStore.currentSlot;
1464
+ for (const [slot, byRoot] of this.queuedAttestations.entries()) {
1465
+ const targetEpoch = computeEpochAtSlot(slot);
1466
+ if (slot < currentSlot) {
1467
+ this.queuedAttestations.delete(slot);
1468
+ for (const [blockRoot, validatorIndices] of byRoot.entries()) {
1469
+ const blockRootHex = blockRoot;
1470
+ for (const validatorIndex of validatorIndices) {
1471
+ // equivocatingIndices was checked in onAttestation
1472
+ this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
1473
+ }
1474
+
1475
+ if (slot === currentSlot - 1) {
1476
+ this.queuedAttestationsPreviousSlot += validatorIndices.size;
1477
+ }
1478
+ }
1479
+ } else {
1480
+ break;
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ /**
1486
+ * Called whenever the current time increases.
1487
+ *
1488
+ * ## Specification
1489
+ *
1490
+ * Equivalent to:
1491
+ *
1492
+ * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_tick
1493
+ */
1494
+ private onTick(time: Slot): void {
1495
+ const previousSlot = this.fcStore.currentSlot;
1496
+
1497
+ if (time > previousSlot + 1) {
1498
+ throw new ForkChoiceError({
1499
+ code: ForkChoiceErrorCode.INCONSISTENT_ON_TICK,
1500
+ previousSlot,
1501
+ time,
1502
+ });
1503
+ }
1504
+
1505
+ // Update store time
1506
+ this.fcStore.currentSlot = time;
1507
+ // Reset proposer boost if this is a new slot.
1508
+ if (this.proposerBoostRoot) {
1509
+ // Since previous weight was boosted, we need would now need to recalculate the scores without the boost
1510
+ this.proposerBoostRoot = null;
1511
+ }
1512
+
1513
+ // Not a new epoch, return.
1514
+ if (computeSlotsSinceEpochStart(time) !== 0) {
1515
+ return;
1516
+ }
1517
+
1518
+ // If a new epoch, pull-up justification and finalization from previous epoch
1519
+ this.updateCheckpoints(
1520
+ this.fcStore.unrealizedJustified.checkpoint,
1521
+ this.fcStore.unrealizedFinalizedCheckpoint,
1522
+ () => this.fcStore.unrealizedJustified.balances
1523
+ );
1524
+ }
1525
+
1526
+ /**
1527
+ *
1528
+ * Common logic of get_proposer_head() and should_override_forkchoice_update()
1529
+ * No one should be calling this function except these two
1530
+ *
1531
+ */
1532
+ private getPreliminaryProposerHead(
1533
+ headBlock: ProtoBlock,
1534
+ parentBlock: ProtoBlock,
1535
+ slot: Slot
1536
+ ): {prelimProposerHead: ProtoBlock; prelimNotReorgedReason?: NotReorgedReason} {
1537
+ let prelimProposerHead = headBlock;
1538
+ // No reorg if headBlock is on time
1539
+ // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_late
1540
+ const isHeadLate = !headBlock.timeliness;
1541
+ if (!isHeadLate) {
1542
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
1543
+ }
1544
+
1545
+ // No reorg if we are at epoch boundary where proposer shuffling could change
1546
+ // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_shuffling_stable
1547
+ const isShufflingStable = slot % SLOTS_PER_EPOCH !== 0;
1548
+ if (!isShufflingStable) {
1549
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotShufflingStable};
1550
+ }
1551
+
1552
+ // No reorg if headBlock and parentBlock are not ffg competitive
1553
+ // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_ffg_competitive
1554
+ const {unrealizedJustifiedEpoch: headBlockCpEpoch, unrealizedJustifiedRoot: headBlockCpRoot} = headBlock;
1555
+ const {unrealizedJustifiedEpoch: parentBlockCpEpoch, unrealizedJustifiedRoot: parentBlockCpRoot} = parentBlock;
1556
+ const isFFGCompetitive = headBlockCpEpoch === parentBlockCpEpoch && headBlockCpRoot === parentBlockCpRoot;
1557
+ if (!isFFGCompetitive) {
1558
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotFFGCompetitive};
1559
+ }
1560
+
1561
+ // No reorg if chain is not finalizing within REORG_MAX_EPOCHS_SINCE_FINALIZATION
1562
+ // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_finalization_ok
1563
+ const epochsSinceFinalization = computeEpochAtSlot(slot) - this.getFinalizedCheckpoint().epoch;
1564
+ const isFinalizationOk = epochsSinceFinalization <= this.config.REORG_MAX_EPOCHS_SINCE_FINALIZATION;
1565
+ if (!isFinalizationOk) {
1566
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.ChainLongUnfinality};
1567
+ }
1568
+
1569
+ // No reorg if this reorg spans more than a single slot
1570
+ const parentSlotOk = parentBlock.slot + 1 === headBlock.slot;
1571
+ if (!parentSlotOk) {
1572
+ return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.ParentBlockDistanceMoreThanOneSlot};
1573
+ }
1574
+
1575
+ prelimProposerHead = parentBlock;
1576
+
1577
+ return {prelimProposerHead};
1578
+ }
1579
+ }
1580
+
1581
+ /**
1582
+ * This function checks the terminal pow conditions on the merge block as
1583
+ * specified in the config either via TTD or TBH. This function is part of
1584
+ * forkChoice because if the merge block was previously imported as syncing
1585
+ * and the EL eventually signals it catching up via validateLatestHash
1586
+ * the specs mandates validating terminal conditions on the previously
1587
+ * imported merge block.
1588
+ */
1589
+ export function assertValidTerminalPowBlock(
1590
+ config: ChainConfig,
1591
+ block: bellatrix.BeaconBlock,
1592
+ preCachedData: {
1593
+ executionStatus: ExecutionStatus.Syncing | ExecutionStatus.Valid;
1594
+ powBlock?: PowBlockHex | null;
1595
+ powBlockParent?: PowBlockHex | null;
1596
+ }
1597
+ ): void {
1598
+ if (!ssz.Root.equals(config.TERMINAL_BLOCK_HASH, ZERO_HASH)) {
1599
+ if (computeEpochAtSlot(block.slot) < config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH)
1600
+ throw Error(`Terminal block activation epoch ${config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH} not reached`);
1601
+
1602
+ // powBock.blockHash is hex, so we just pick the corresponding root
1603
+ if (!ssz.Root.equals(block.body.executionPayload.parentHash, config.TERMINAL_BLOCK_HASH))
1604
+ throw new Error(
1605
+ `Invalid terminal block hash, expected: ${toRootHex(config.TERMINAL_BLOCK_HASH)}, actual: ${toRootHex(
1606
+ block.body.executionPayload.parentHash
1607
+ )}`
1608
+ );
1609
+ } else {
1610
+ // If no TERMINAL_BLOCK_HASH override, check ttd
1611
+
1612
+ // Delay powBlock checks if the payload execution status is unknown because of
1613
+ // syncing response in notifyNewPayload call while verifying
1614
+ if (preCachedData?.executionStatus === ExecutionStatus.Syncing) return;
1615
+
1616
+ const {powBlock, powBlockParent} = preCachedData;
1617
+ if (!powBlock) throw Error("onBlock preCachedData must include powBlock");
1618
+ // if powBlock is genesis don't assert powBlockParent
1619
+ if (!powBlockParent && powBlock.parentHash !== HEX_ZERO_HASH)
1620
+ throw Error("onBlock preCachedData must include powBlockParent");
1621
+
1622
+ const isTotalDifficultyReached = powBlock.totalDifficulty >= config.TERMINAL_TOTAL_DIFFICULTY;
1623
+ // If we don't have powBlockParent here, powBlock is the genesis and as we would have errored above
1624
+ // we can mark isParentTotalDifficultyValid as valid
1625
+ const isParentTotalDifficultyValid =
1626
+ !powBlockParent || powBlockParent.totalDifficulty < config.TERMINAL_TOTAL_DIFFICULTY;
1627
+ if (!isTotalDifficultyReached) {
1628
+ throw Error(
1629
+ `Invalid terminal POW block: total difficulty not reached expected >= ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlock.totalDifficulty}`
1630
+ );
1631
+ }
1632
+
1633
+ if (!isParentTotalDifficultyValid) {
1634
+ throw Error(
1635
+ `Invalid terminal POW block parent: expected < ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlockParent.totalDifficulty}`
1636
+ );
1637
+ }
1638
+ }
1639
+ }
1640
+ // Approximate https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#calculate_committee_fraction
1641
+ // Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
1642
+ export function getCommitteeFraction(
1643
+ justifiedTotalActiveBalanceByIncrement: number,
1644
+ config: {slotsPerEpoch: number; committeePercent: number}
1645
+ ): number {
1646
+ const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
1647
+ return Math.floor((committeeWeight * config.committeePercent) / 100);
1648
+ }