@lodestar/fork-choice 1.35.0-dev.fcf8d024ea → 1.35.0-dev.feed916580

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1076 +0,0 @@
1
- import {GENESIS_EPOCH} from "@lodestar/params";
2
- import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
- import {Epoch, RootHex, Slot} from "@lodestar/types";
4
- import {toRootHex} from "@lodestar/utils";
5
- import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
6
- import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
7
- import {ExecutionStatus, HEX_ZERO_HASH, LVHExecResponse, ProtoBlock, ProtoNode} from "./interface.js";
8
-
9
- export const DEFAULT_PRUNE_THRESHOLD = 0;
10
- type ProposerBoost = {root: RootHex; score: number};
11
-
12
- const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
13
-
14
- export class ProtoArray {
15
- // Do not attempt to prune the tree unless it has at least this many nodes.
16
- // Small prunes simply waste time
17
- pruneThreshold: number;
18
- justifiedEpoch: Epoch;
19
- justifiedRoot: RootHex;
20
- finalizedEpoch: Epoch;
21
- finalizedRoot: RootHex;
22
- nodes: ProtoNode[] = [];
23
- indices = new Map<RootHex, number>();
24
- lvhError?: LVHExecError;
25
-
26
- private previousProposerBoost: ProposerBoost | null = null;
27
-
28
- constructor({
29
- pruneThreshold,
30
- justifiedEpoch,
31
- justifiedRoot,
32
- finalizedEpoch,
33
- finalizedRoot,
34
- }: {
35
- pruneThreshold: number;
36
- justifiedEpoch: Epoch;
37
- justifiedRoot: RootHex;
38
- finalizedEpoch: Epoch;
39
- finalizedRoot: RootHex;
40
- }) {
41
- this.pruneThreshold = pruneThreshold;
42
- this.justifiedEpoch = justifiedEpoch;
43
- this.justifiedRoot = justifiedRoot;
44
- this.finalizedEpoch = finalizedEpoch;
45
- this.finalizedRoot = finalizedRoot;
46
- }
47
-
48
- static initialize(block: Omit<ProtoBlock, "targetRoot">, currentSlot: Slot): ProtoArray {
49
- const protoArray = new ProtoArray({
50
- pruneThreshold: DEFAULT_PRUNE_THRESHOLD,
51
- justifiedEpoch: block.justifiedEpoch,
52
- justifiedRoot: block.justifiedRoot,
53
- finalizedEpoch: block.finalizedEpoch,
54
- finalizedRoot: block.finalizedRoot,
55
- });
56
- protoArray.onBlock(
57
- {
58
- ...block,
59
- // We are using the blockROot as the targetRoot, since it always lies on an epoch boundary
60
- targetRoot: block.blockRoot,
61
- } as ProtoBlock,
62
- currentSlot
63
- );
64
- return protoArray;
65
- }
66
-
67
- /**
68
- * Iterate backwards through the array, touching all nodes and their parents and potentially
69
- * the best-child of each parent.
70
- *
71
- * The structure of the `self.nodes` array ensures that the child of each node is always
72
- * touched before its parent.
73
- *
74
- * For each node, the following is done:
75
- *
76
- * - Update the node's weight with the corresponding delta.
77
- * - Back-propagate each node's delta to its parents delta.
78
- * - Compare the current node with the parents best-child, updating it if the current node
79
- * should become the best child.
80
- * - If required, update the parents best-descendant with the current node or its best-descendant.
81
- */
82
- applyScoreChanges({
83
- deltas,
84
- proposerBoost,
85
- justifiedEpoch,
86
- justifiedRoot,
87
- finalizedEpoch,
88
- finalizedRoot,
89
- currentSlot,
90
- }: {
91
- deltas: number[];
92
- proposerBoost: ProposerBoost | null;
93
- justifiedEpoch: Epoch;
94
- justifiedRoot: RootHex;
95
- finalizedEpoch: Epoch;
96
- finalizedRoot: RootHex;
97
- currentSlot: Slot;
98
- }): void {
99
- if (deltas.length !== this.indices.size) {
100
- throw new ProtoArrayError({
101
- code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
102
- deltas: deltas.length,
103
- indices: this.indices.size,
104
- });
105
- }
106
-
107
- if (
108
- justifiedEpoch !== this.justifiedEpoch ||
109
- finalizedEpoch !== this.finalizedEpoch ||
110
- justifiedRoot !== this.justifiedRoot ||
111
- finalizedRoot !== this.finalizedRoot
112
- ) {
113
- this.justifiedEpoch = justifiedEpoch;
114
- this.finalizedEpoch = finalizedEpoch;
115
- this.justifiedRoot = justifiedRoot;
116
- this.finalizedRoot = finalizedRoot;
117
- }
118
-
119
- // Iterate backwards through all indices in this.nodes
120
- for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
121
- const node = this.nodes[nodeIndex];
122
- if (node === undefined) {
123
- throw new ProtoArrayError({
124
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
125
- index: nodeIndex,
126
- });
127
- }
128
-
129
- // There is no need to adjust the balances or manage parent of the zero hash since it
130
- // is an alias to the genesis block. The weight applied to the genesis block is
131
- // irrelevant as we _always_ choose it and it's impossible for it to have a parent.
132
- if (node.blockRoot === HEX_ZERO_HASH) {
133
- continue;
134
- }
135
-
136
- const currentBoost = proposerBoost && proposerBoost.root === node.blockRoot ? proposerBoost.score : 0;
137
- const previousBoost =
138
- this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot
139
- ? this.previousProposerBoost.score
140
- : 0;
141
-
142
- // If this node's execution status has been marked invalid, then the weight of the node
143
- // needs to be taken out of consideration after which the node weight will become 0
144
- // for subsequent iterations of applyScoreChanges
145
- const nodeDelta =
146
- node.executionStatus === ExecutionStatus.Invalid
147
- ? -node.weight
148
- : deltas[nodeIndex] + currentBoost - previousBoost;
149
-
150
- // Apply the delta to the node
151
- node.weight += nodeDelta;
152
-
153
- // Update the parent delta (if any)
154
- const parentIndex = node.parent;
155
- if (parentIndex !== undefined) {
156
- const parentDelta = deltas[parentIndex];
157
- if (parentDelta === undefined) {
158
- throw new ProtoArrayError({
159
- code: ProtoArrayErrorCode.INVALID_PARENT_DELTA,
160
- index: parentIndex,
161
- });
162
- }
163
-
164
- // back-propagate the nodes delta to its parent
165
- deltas[parentIndex] += nodeDelta;
166
- }
167
- }
168
-
169
- // A second time, iterate backwards through all indices in `this.nodes`.
170
- //
171
- // We _must_ perform these functions separate from the weight-updating loop above to ensure
172
- // that we have a fully coherent set of weights before updating parent
173
- // best-child/descendant.
174
- for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
175
- const node = this.nodes[nodeIndex];
176
- if (node === undefined) {
177
- throw new ProtoArrayError({
178
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
179
- index: nodeIndex,
180
- });
181
- }
182
-
183
- // If the node has a parent, try to update its best-child and best-descendant.
184
- const parentIndex = node.parent;
185
- if (parentIndex !== undefined) {
186
- this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot);
187
- }
188
- }
189
- // Update the previous proposer boost
190
- this.previousProposerBoost = proposerBoost;
191
- }
192
-
193
- /**
194
- * Register a block with the fork choice.
195
- *
196
- * It is only sane to supply an undefined parent for the genesis block
197
- */
198
- onBlock(block: ProtoBlock, currentSlot: Slot): void {
199
- // If the block is already known, simply ignore it
200
- if (this.indices.has(block.blockRoot)) {
201
- return;
202
- }
203
- if (block.executionStatus === ExecutionStatus.Invalid) {
204
- throw new ProtoArrayError({
205
- code: ProtoArrayErrorCode.INVALID_BLOCK_EXECUTION_STATUS,
206
- root: block.blockRoot,
207
- });
208
- }
209
-
210
- const node: ProtoNode = {
211
- ...block,
212
- parent: this.indices.get(block.parentRoot),
213
- weight: 0,
214
- bestChild: undefined,
215
- bestDescendant: undefined,
216
- };
217
-
218
- const nodeIndex = this.nodes.length;
219
-
220
- this.indices.set(node.blockRoot, nodeIndex);
221
- this.nodes.push(node);
222
-
223
- // If this node is valid, lets propagate the valid status up the chain
224
- // and throw error if we counter invalid, as this breaks consensus
225
- if (node.parent !== undefined) {
226
- this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot);
227
-
228
- if (node.executionStatus === ExecutionStatus.Valid) {
229
- this.propagateValidExecutionStatusByIndex(node.parent);
230
- }
231
- }
232
- }
233
-
234
- /**
235
- * Optimistic sync validate till validated latest hash, invalidate any descendant branch
236
- * if invalidate till hash provided. If consensus fails, this will invalidate entire
237
- * forkChoice which will throw on any call to findHead
238
- */
239
- validateLatestHash(execResponse: LVHExecResponse, currentSlot: Slot): void {
240
- // Look reverse because its highly likely node with latestValidExecHash is towards the
241
- // the leaves of the forkchoice
242
- //
243
- // We can also implement the index to lookup for exec hash => proto block, but it
244
- // still needs to be established properly (though is highly likely) than a unique
245
- // exec hash maps to a unique beacon block.
246
- // For more context on this please checkout the following conversation:
247
- // https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167
248
-
249
- if (execResponse.executionStatus === ExecutionStatus.Valid) {
250
- const {latestValidExecHash} = execResponse;
251
- // We use -1 for denoting not found
252
- let latestValidHashIndex = -1;
253
-
254
- for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
255
- if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
256
- latestValidHashIndex = nodeIndex;
257
- // We found the block corresponding to latestValidHashIndex, exit the loop
258
- break;
259
- }
260
- }
261
-
262
- // We are trying to be as forgiving as possible here because ideally latestValidHashIndex
263
- // should be found in the forkchoice
264
- if (latestValidHashIndex >= 0) {
265
- this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
266
- }
267
- } else {
268
- // In case of invalidation, ideally:
269
- // i) Find the invalid payload
270
- // ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
271
- // iii) Obtain a chain [Last_known_valid_node, ...., LVH]
272
- //
273
- // Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
274
- // gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
275
- // as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
276
- // So we currently ignore this chain and hope eventually it gets resolved
277
- //
278
- // Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
279
- // if its in fcU.
280
- //
281
- const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
282
- const invalidateFromParentIndex = this.indices.get(invalidateFromParentBlockRoot);
283
- if (invalidateFromParentIndex === undefined) {
284
- throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
285
- }
286
- const latestValidHashIndex =
287
- latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
288
- if (latestValidHashIndex === null) {
289
- /**
290
- * The LVH (latest valid hash) is null or not found.
291
- *
292
- * The spec gives an allowance for the EL being able to return a nullish LVH if it could not
293
- * "determine" one. There are two interpretations:
294
- *
295
- * - "the LVH is unknown" - simply throw and move on. We can't determine which chain to invalidate
296
- * since we don't know which ancestor is valid.
297
- *
298
- * - "the LVH doesn't exist" - this means that the entire ancestor chain is invalid, and should
299
- * be marked as such.
300
- *
301
- * The more robust approach is to treat nullish LVH as "the LVH is unknown" rather than
302
- * "the LVH doesn't exist". The alternative means that we will poison a valid chain when the
303
- * EL is lazy (or buggy) with its LVH response.
304
- */
305
- throw Error(`Unable to find latestValidExecHash=${latestValidExecHash} in the forkchoice`);
306
- }
307
-
308
- this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
309
- }
310
- }
311
-
312
- private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
313
- let nodeIndex: number | undefined = validNodeIndex;
314
- // propagate till we keep encountering syncing status
315
- while (nodeIndex !== undefined) {
316
- const node = this.getNodeFromIndex(nodeIndex);
317
- if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
318
- break;
319
- }
320
- this.validateNodeByIndex(nodeIndex);
321
- nodeIndex = node.parent;
322
- }
323
- }
324
-
325
- /**
326
- * Do a two pass invalidation:
327
- * 1. we go up and mark all nodes invalid and then
328
- * 2. we need do iterate down and mark all children of invalid nodes invalid
329
- *
330
- * latestValidHashIndex === undefined implies invalidate only invalidateTillIndex
331
- * latestValidHashIndex === -1 implies invalidate all post merge blocks
332
- * latestValidHashIndex >=0 implies invalidate the chain upwards from invalidateTillIndex
333
- */
334
-
335
- private propagateInValidExecutionStatusByIndex(
336
- invalidateFromParentIndex: number,
337
- latestValidHashIndex: number,
338
- currentSlot: Slot
339
- ): void {
340
- // Pass 1: mark invalidateFromParentIndex and its parents invalid
341
- let invalidateIndex: number | undefined = invalidateFromParentIndex;
342
- while (invalidateIndex !== undefined && invalidateIndex > latestValidHashIndex) {
343
- const invalidNode = this.invalidateNodeByIndex(invalidateIndex);
344
- invalidateIndex = invalidNode.parent;
345
- }
346
-
347
- // Pass 2: mark all children of invalid nodes as invalid
348
- for (let nodeIndex = 0; nodeIndex < this.nodes.length; nodeIndex++) {
349
- const node = this.getNodeFromIndex(nodeIndex);
350
- const parent = node.parent !== undefined ? this.getNodeByIndex(node.parent) : undefined;
351
- // Only invalidate if this is post merge, and either parent is invalid or the
352
- // consensus has failed
353
- if (parent?.executionStatus === ExecutionStatus.Invalid) {
354
- // check and flip node status to invalid
355
- this.invalidateNodeByIndex(nodeIndex);
356
- }
357
- }
358
-
359
- // update the forkchoice as the invalidation can change the entire forkchoice DAG
360
- this.applyScoreChanges({
361
- deltas: Array.from({length: this.nodes.length}, () => 0),
362
- proposerBoost: this.previousProposerBoost,
363
- justifiedEpoch: this.justifiedEpoch,
364
- justifiedRoot: this.justifiedRoot,
365
- finalizedEpoch: this.finalizedEpoch,
366
- finalizedRoot: this.finalizedRoot,
367
- currentSlot,
368
- });
369
- }
370
-
371
- private getNodeIndexFromLVH(latestValidExecHash: RootHex, ancestorFromIndex: number): number | null {
372
- let nodeIndex: number | undefined = ancestorFromIndex;
373
- while (nodeIndex !== undefined && nodeIndex >= 0) {
374
- const node = this.getNodeFromIndex(nodeIndex);
375
- if (
376
- (node.executionStatus === ExecutionStatus.PreMerge && latestValidExecHash === ZERO_HASH_HEX) ||
377
- node.executionPayloadBlockHash === latestValidExecHash
378
- ) {
379
- break;
380
- }
381
- nodeIndex = node.parent;
382
- }
383
- return nodeIndex !== undefined ? nodeIndex : null;
384
- }
385
-
386
- private invalidateNodeByIndex(nodeIndex: number): ProtoNode {
387
- const invalidNode = this.getNodeFromIndex(nodeIndex);
388
-
389
- // If node to be invalidated is pre-merge or valid,it is a catastrophe,
390
- // and indicates consensus failure and a non recoverable damage.
391
- //
392
- // There is no further processing that can be done.
393
- // Just assign error for marking proto-array perma damaged and throw!
394
- if (
395
- invalidNode.executionStatus === ExecutionStatus.Valid ||
396
- invalidNode.executionStatus === ExecutionStatus.PreMerge
397
- ) {
398
- const lvhCode =
399
- invalidNode.executionStatus === ExecutionStatus.Valid
400
- ? LVHExecErrorCode.ValidToInvalid
401
- : LVHExecErrorCode.PreMergeToInvalid;
402
-
403
- this.lvhError = {
404
- lvhCode,
405
- blockRoot: invalidNode.blockRoot,
406
- execHash: invalidNode.executionPayloadBlockHash ?? ZERO_HASH_HEX,
407
- };
408
- throw new ProtoArrayError({
409
- code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
410
- ...this.lvhError,
411
- });
412
- }
413
-
414
- invalidNode.executionStatus = ExecutionStatus.Invalid;
415
- invalidNode.bestChild = undefined;
416
- invalidNode.bestDescendant = undefined;
417
-
418
- return invalidNode;
419
- }
420
-
421
- private validateNodeByIndex(nodeIndex: number): ProtoNode {
422
- const validNode = this.getNodeFromIndex(nodeIndex);
423
- if (validNode.executionStatus === ExecutionStatus.Invalid) {
424
- this.lvhError = {
425
- lvhCode: LVHExecErrorCode.InvalidToValid,
426
- blockRoot: validNode.blockRoot,
427
- execHash: validNode.executionPayloadBlockHash,
428
- };
429
- throw new ProtoArrayError({
430
- code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
431
- ...this.lvhError,
432
- });
433
- }
434
-
435
- if (validNode.executionStatus === ExecutionStatus.Syncing) {
436
- validNode.executionStatus = ExecutionStatus.Valid;
437
- }
438
- return validNode;
439
- }
440
-
441
- /**
442
- * Follows the best-descendant links to find the best-block (i.e., head-block).
443
- */
444
- findHead(justifiedRoot: RootHex, currentSlot: Slot): RootHex {
445
- if (this.lvhError) {
446
- throw new ProtoArrayError({
447
- code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
448
- ...this.lvhError,
449
- });
450
- }
451
-
452
- const justifiedIndex = this.indices.get(justifiedRoot);
453
- if (justifiedIndex === undefined) {
454
- throw new ProtoArrayError({
455
- code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN,
456
- root: justifiedRoot,
457
- });
458
- }
459
-
460
- const justifiedNode = this.nodes[justifiedIndex];
461
- if (justifiedNode === undefined) {
462
- throw new ProtoArrayError({
463
- code: ProtoArrayErrorCode.INVALID_JUSTIFIED_INDEX,
464
- index: justifiedIndex,
465
- });
466
- }
467
-
468
- if (justifiedNode.executionStatus === ExecutionStatus.Invalid) {
469
- throw new ProtoArrayError({
470
- code: ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS,
471
- root: justifiedNode.blockRoot,
472
- });
473
- }
474
-
475
- const bestDescendantIndex = justifiedNode.bestDescendant ?? justifiedIndex;
476
-
477
- const bestNode = this.nodes[bestDescendantIndex];
478
- if (bestNode === undefined) {
479
- throw new ProtoArrayError({
480
- code: ProtoArrayErrorCode.INVALID_BEST_DESCENDANT_INDEX,
481
- index: bestDescendantIndex,
482
- });
483
- }
484
-
485
- /**
486
- * Perform a sanity check that the node is indeed valid to be the head
487
- * The justified node is always considered viable for head per spec:
488
- * def get_head(store: Store) -> Root:
489
- * blocks = get_filtered_block_tree(store)
490
- * head = store.justified_checkpoint.root
491
- */
492
- if (bestDescendantIndex !== justifiedIndex && !this.nodeIsViableForHead(bestNode, currentSlot)) {
493
- throw new ProtoArrayError({
494
- code: ProtoArrayErrorCode.INVALID_BEST_NODE,
495
- startRoot: justifiedRoot,
496
- justifiedEpoch: this.justifiedEpoch,
497
- finalizedEpoch: this.finalizedEpoch,
498
- headRoot: justifiedNode.blockRoot,
499
- headJustifiedEpoch: justifiedNode.justifiedEpoch,
500
- headFinalizedEpoch: justifiedNode.finalizedEpoch,
501
- });
502
- }
503
-
504
- return bestNode.blockRoot;
505
- }
506
-
507
- /**
508
- * Update the tree with new finalization information. The tree is only actually pruned if both
509
- * of the two following criteria are met:
510
- *
511
- * - The supplied finalized epoch and root are different to the current values.
512
- * - The number of nodes in `self` is at least `self.prune_threshold`.
513
- *
514
- * # Errors
515
- *
516
- * Returns errors if:
517
- *
518
- * - The finalized epoch is less than the current one.
519
- * - The finalized epoch is equal to the current one, but the finalized root is different.
520
- * - There is some internal error relating to invalid indices inside `this`.
521
- */
522
- maybePrune(finalizedRoot: RootHex): ProtoBlock[] {
523
- const finalizedIndex = this.indices.get(finalizedRoot);
524
- if (finalizedIndex === undefined) {
525
- throw new ProtoArrayError({
526
- code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN,
527
- root: finalizedRoot,
528
- });
529
- }
530
-
531
- if (finalizedIndex < this.pruneThreshold) {
532
- // Pruning at small numbers incurs more cost than benefit
533
- return [];
534
- }
535
-
536
- // Remove the this.indices key/values for all the to-be-deleted nodes
537
- for (let nodeIndex = 0; nodeIndex < finalizedIndex; nodeIndex++) {
538
- const node = this.nodes[nodeIndex];
539
- if (node === undefined) {
540
- throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index: nodeIndex});
541
- }
542
- this.indices.delete(node.blockRoot);
543
- }
544
-
545
- // Store nodes prior to finalization
546
- const removed = this.nodes.slice(0, finalizedIndex);
547
- // Drop all the nodes prior to finalization
548
- this.nodes = this.nodes.slice(finalizedIndex);
549
-
550
- // Adjust the indices map
551
- for (const [key, value] of this.indices.entries()) {
552
- if (value < finalizedIndex) {
553
- throw new ProtoArrayError({
554
- code: ProtoArrayErrorCode.INDEX_OVERFLOW,
555
- value: "indices",
556
- });
557
- }
558
- this.indices.set(key, value - finalizedIndex);
559
- }
560
-
561
- // Iterate through all the existing nodes and adjust their indices to match the new layout of this.nodes
562
- for (let i = 0, len = this.nodes.length; i < len; i++) {
563
- const node = this.nodes[i];
564
- const parentIndex = node.parent;
565
- if (parentIndex !== undefined) {
566
- // If node.parent is less than finalizedIndex, set it to undefined
567
- node.parent = parentIndex < finalizedIndex ? undefined : parentIndex - finalizedIndex;
568
- }
569
- const bestChild = node.bestChild;
570
- if (bestChild !== undefined) {
571
- if (bestChild < finalizedIndex) {
572
- throw new ProtoArrayError({
573
- code: ProtoArrayErrorCode.INDEX_OVERFLOW,
574
- value: "bestChild",
575
- });
576
- }
577
- node.bestChild = bestChild - finalizedIndex;
578
- }
579
- const bestDescendant = node.bestDescendant;
580
- if (bestDescendant !== undefined) {
581
- if (bestDescendant < finalizedIndex) {
582
- throw new ProtoArrayError({
583
- code: ProtoArrayErrorCode.INDEX_OVERFLOW,
584
- value: "bestDescendant",
585
- });
586
- }
587
- node.bestDescendant = bestDescendant - finalizedIndex;
588
- }
589
- }
590
- return removed;
591
- }
592
-
593
- /**
594
- * Observe the parent at `parent_index` with respect to the child at `child_index` and
595
- * potentially modify the `parent.best_child` and `parent.best_descendant` values.
596
- *
597
- * ## Detail
598
- *
599
- * There are four outcomes:
600
- *
601
- * - The child is already the best child but it's now invalid due to a FFG change and should be removed.
602
- * - The child is already the best child and the parent is updated with the new
603
- * best-descendant.
604
- * - The child is not the best child but becomes the best child.
605
- * - The child is not the best child and does not become the best child.
606
- */
607
- maybeUpdateBestChildAndDescendant(parentIndex: number, childIndex: number, currentSlot: Slot): void {
608
- const childNode = this.nodes[childIndex];
609
- if (childNode === undefined) {
610
- throw new ProtoArrayError({
611
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
612
- index: childIndex,
613
- });
614
- }
615
-
616
- const parentNode = this.nodes[parentIndex];
617
- if (parentNode === undefined) {
618
- throw new ProtoArrayError({
619
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
620
- index: parentIndex,
621
- });
622
- }
623
-
624
- const childLeadsToViableHead = this.nodeLeadsToViableHead(childNode, currentSlot);
625
-
626
- // These three variables are aliases to the three options that we may set the
627
- // parent.bestChild and parent.bestDescendent to.
628
- //
629
- // Aliases are used to assist readability.
630
- type ChildAndDescendant = [number | undefined, number | undefined];
631
- const changeToNull: ChildAndDescendant = [undefined, undefined];
632
- const changeToChild: ChildAndDescendant = [childIndex, childNode.bestDescendant ?? childIndex];
633
- const noChange: ChildAndDescendant = [parentNode.bestChild, parentNode.bestDescendant];
634
-
635
- let newChildAndDescendant: ChildAndDescendant;
636
- const bestChildIndex = parentNode.bestChild;
637
- if (bestChildIndex !== undefined) {
638
- if (bestChildIndex === childIndex && !childLeadsToViableHead) {
639
- // the child is already the best-child of the parent but its not viable for the head
640
- // so remove it
641
- newChildAndDescendant = changeToNull;
642
- } else if (bestChildIndex === childIndex) {
643
- // the child is the best-child already
644
- // set it again to ensure that the best-descendent of the parent is updated
645
- newChildAndDescendant = changeToChild;
646
- } else {
647
- const bestChildNode = this.nodes[bestChildIndex];
648
- if (bestChildNode === undefined) {
649
- throw new ProtoArrayError({
650
- code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX,
651
- index: bestChildIndex,
652
- });
653
- }
654
-
655
- const bestChildLeadsToViableHead = this.nodeLeadsToViableHead(bestChildNode, currentSlot);
656
-
657
- if (childLeadsToViableHead && !bestChildLeadsToViableHead) {
658
- // the child leads to a viable head, but the current best-child doesn't
659
- newChildAndDescendant = changeToChild;
660
- } else if (!childLeadsToViableHead && bestChildLeadsToViableHead) {
661
- // the best child leads to a viable head but the child doesn't
662
- newChildAndDescendant = noChange;
663
- } else if (childNode.weight === bestChildNode.weight) {
664
- // tie-breaker of equal weights by root
665
- if (childNode.blockRoot >= bestChildNode.blockRoot) {
666
- newChildAndDescendant = changeToChild;
667
- } else {
668
- newChildAndDescendant = noChange;
669
- }
670
- } else {
671
- // choose the winner by weight
672
- if (childNode.weight >= bestChildNode.weight) {
673
- newChildAndDescendant = changeToChild;
674
- } else {
675
- newChildAndDescendant = noChange;
676
- }
677
- }
678
- }
679
- } else if (childLeadsToViableHead) {
680
- // There is no current best-child and the child is viable.
681
- newChildAndDescendant = changeToChild;
682
- } else {
683
- // There is no current best-child but the child is not viable.
684
- newChildAndDescendant = noChange;
685
- }
686
-
687
- parentNode.bestChild = newChildAndDescendant[0];
688
- parentNode.bestDescendant = newChildAndDescendant[1];
689
- }
690
-
691
- /**
692
- * Indicates if the node itself is viable for the head, or if it's best descendant is viable
693
- * for the head.
694
- */
695
- nodeLeadsToViableHead(node: ProtoNode, currentSlot: Slot): boolean {
696
- let bestDescendantIsViableForHead: boolean;
697
- const bestDescendantIndex = node.bestDescendant;
698
- if (bestDescendantIndex !== undefined) {
699
- const bestDescendantNode = this.nodes[bestDescendantIndex];
700
- if (bestDescendantNode === undefined) {
701
- throw new ProtoArrayError({
702
- code: ProtoArrayErrorCode.INVALID_BEST_DESCENDANT_INDEX,
703
- index: bestDescendantIndex,
704
- });
705
- }
706
- bestDescendantIsViableForHead = this.nodeIsViableForHead(bestDescendantNode, currentSlot);
707
- } else {
708
- bestDescendantIsViableForHead = false;
709
- }
710
-
711
- return bestDescendantIsViableForHead || this.nodeIsViableForHead(node, currentSlot);
712
- }
713
-
714
- /**
715
- * This is the equivalent to the `filter_block_tree` function in the Ethereum Consensus spec:
716
- *
717
- * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#filter_block_tree
718
- *
719
- * Any node that has a different finalized or justified epoch should not be viable for the
720
- * head.
721
- */
722
- nodeIsViableForHead(node: ProtoNode, currentSlot: Slot): boolean {
723
- // If node has invalid executionStatus, it can't be a viable head
724
- if (node.executionStatus === ExecutionStatus.Invalid) {
725
- return false;
726
- }
727
- const currentEpoch = computeEpochAtSlot(currentSlot);
728
-
729
- // If block is from a previous epoch, filter using unrealized justification & finalization information
730
- // If block is from the current epoch, filter using the head state's justification & finalization information
731
- const isFromPrevEpoch = computeEpochAtSlot(node.slot) < currentEpoch;
732
- const votingSourceEpoch = isFromPrevEpoch ? node.unrealizedJustifiedEpoch : node.justifiedEpoch;
733
-
734
- // The voting source should be at the same height as the store's justified checkpoint or
735
- // not more than two epochs ago
736
- const correctJustified =
737
- this.justifiedEpoch === GENESIS_EPOCH ||
738
- votingSourceEpoch === this.justifiedEpoch ||
739
- votingSourceEpoch + 2 >= currentEpoch;
740
-
741
- const correctFinalized = this.finalizedEpoch === 0 || this.isFinalizedRootOrDescendant(node);
742
- return correctJustified && correctFinalized;
743
- }
744
-
745
- /**
746
- * Return `true` if `node` is equal to or a descendant of the finalized node.
747
- * This function helps improve performance of nodeIsViableForHead a lot by avoiding
748
- * the loop inside `getAncestors`.
749
- */
750
- isFinalizedRootOrDescendant(node: ProtoNode): boolean {
751
- // The finalized and justified checkpoints represent a list of known
752
- // ancestors of `node` that are likely to coincide with the store's
753
- // finalized checkpoint.
754
- if (
755
- (node.finalizedEpoch === this.finalizedEpoch && node.finalizedRoot === this.finalizedRoot) ||
756
- (node.justifiedEpoch === this.finalizedEpoch && node.justifiedRoot === this.finalizedRoot) ||
757
- (node.unrealizedFinalizedEpoch === this.finalizedEpoch && node.unrealizedFinalizedRoot === this.finalizedRoot) ||
758
- (node.unrealizedJustifiedEpoch === this.finalizedEpoch && node.unrealizedJustifiedRoot === this.finalizedRoot)
759
- ) {
760
- return true;
761
- }
762
-
763
- const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch);
764
- return this.finalizedEpoch === 0 || this.finalizedRoot === this.getAncestorOrNull(node.blockRoot, finalizedSlot);
765
- }
766
-
767
- /**
768
- * Same to getAncestor but it may return null instead of throwing error
769
- */
770
- getAncestorOrNull(blockRoot: RootHex, ancestorSlot: Slot): RootHex | null {
771
- try {
772
- return this.getAncestor(blockRoot, ancestorSlot);
773
- } catch (_) {
774
- return null;
775
- }
776
- }
777
-
778
- /**
779
- * Returns the block root of an ancestor of `blockRoot` at the given `slot`.
780
- * (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
781
- *
782
- * NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
783
- *
784
- * ### Specification
785
- *
786
- * Equivalent to:
787
- *
788
- * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
789
- */
790
- getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex {
791
- const block = this.getBlock(blockRoot);
792
- if (!block) {
793
- throw new ForkChoiceError({
794
- code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
795
- root: blockRoot,
796
- });
797
- }
798
-
799
- if (block.slot > ancestorSlot) {
800
- // Search for a slot that is lte the target slot.
801
- // We check for lower slots to account for skip slots.
802
- for (const node of this.iterateAncestorNodes(blockRoot)) {
803
- if (node.slot <= ancestorSlot) {
804
- return node.blockRoot;
805
- }
806
- }
807
- throw new ForkChoiceError({
808
- code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
809
- descendantRoot: blockRoot,
810
- ancestorSlot,
811
- });
812
- }
813
- // Root is older or equal than queried slot, thus a skip slot. Return most recent root prior to slot.
814
- return blockRoot;
815
- }
816
-
817
- /**
818
- * Iterate from a block root backwards over nodes
819
- */
820
- *iterateAncestorNodes(blockRoot: RootHex): IterableIterator<ProtoNode> {
821
- const startIndex = this.indices.get(blockRoot);
822
- if (startIndex === undefined) {
823
- return;
824
- }
825
-
826
- const node = this.nodes[startIndex];
827
- if (node === undefined) {
828
- throw new ProtoArrayError({
829
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
830
- index: startIndex,
831
- });
832
- }
833
-
834
- yield* this.iterateAncestorNodesFromNode(node);
835
- }
836
-
837
- /**
838
- * Iterate from a block root backwards over nodes
839
- */
840
- *iterateAncestorNodesFromNode(node: ProtoNode): IterableIterator<ProtoNode> {
841
- while (node.parent !== undefined) {
842
- node = this.getNodeFromIndex(node.parent);
843
- yield node;
844
- }
845
- }
846
-
847
- /**
848
- * Get all nodes from a block root backwards
849
- */
850
- getAllAncestorNodes(blockRoot: RootHex): ProtoNode[] {
851
- const startIndex = this.indices.get(blockRoot);
852
- if (startIndex === undefined) {
853
- return [];
854
- }
855
-
856
- let node = this.nodes[startIndex];
857
- if (node === undefined) {
858
- throw new ProtoArrayError({
859
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
860
- index: startIndex,
861
- });
862
- }
863
-
864
- const nodes = [node];
865
-
866
- while (node.parent !== undefined) {
867
- node = this.getNodeFromIndex(node.parent);
868
- nodes.push(node);
869
- }
870
-
871
- return nodes;
872
- }
873
-
874
- /**
875
- * The opposite of iterateNodes.
876
- * iterateNodes is to find ancestor nodes of a blockRoot.
877
- * this is to find non-ancestor nodes of a blockRoot.
878
- */
879
- getAllNonAncestorNodes(blockRoot: RootHex): ProtoNode[] {
880
- const startIndex = this.indices.get(blockRoot);
881
- if (startIndex === undefined) {
882
- return [];
883
- }
884
-
885
- let node = this.nodes[startIndex];
886
- if (node === undefined) {
887
- throw new ProtoArrayError({
888
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
889
- index: startIndex,
890
- });
891
- }
892
- const result: ProtoNode[] = [];
893
- let nodeIndex = startIndex;
894
- while (node.parent !== undefined) {
895
- const parentIndex = node.parent;
896
- node = this.getNodeFromIndex(parentIndex);
897
- // nodes between nodeIndex and parentIndex means non-ancestor nodes
898
- result.push(...this.getNodesBetween(nodeIndex, parentIndex));
899
- nodeIndex = parentIndex;
900
- }
901
- result.push(...this.getNodesBetween(nodeIndex, 0));
902
- return result;
903
- }
904
-
905
- /**
906
- * Returns both ancestor and non-ancestor nodes in a single traversal.
907
- */
908
- getAllAncestorAndNonAncestorNodes(blockRoot: RootHex): {ancestors: ProtoNode[]; nonAncestors: ProtoNode[]} {
909
- const startIndex = this.indices.get(blockRoot);
910
- if (startIndex === undefined) {
911
- return {ancestors: [], nonAncestors: []};
912
- }
913
-
914
- let node = this.nodes[startIndex];
915
- if (node === undefined) {
916
- throw new ProtoArrayError({
917
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
918
- index: startIndex,
919
- });
920
- }
921
-
922
- const ancestors: ProtoNode[] = [];
923
- const nonAncestors: ProtoNode[] = [];
924
-
925
- let nodeIndex = startIndex;
926
- while (node.parent !== undefined) {
927
- ancestors.push(node);
928
-
929
- const parentIndex = node.parent;
930
- node = this.getNodeFromIndex(parentIndex);
931
-
932
- // Nodes between nodeIndex and parentIndex are non-ancestor nodes
933
- nonAncestors.push(...this.getNodesBetween(nodeIndex, parentIndex));
934
- nodeIndex = parentIndex;
935
- }
936
-
937
- ancestors.push(node);
938
- nonAncestors.push(...this.getNodesBetween(nodeIndex, 0));
939
-
940
- return {ancestors, nonAncestors};
941
- }
942
-
943
- hasBlock(blockRoot: RootHex): boolean {
944
- return this.indices.has(blockRoot);
945
- }
946
-
947
- getNode(blockRoot: RootHex): ProtoNode | undefined {
948
- const blockIndex = this.indices.get(blockRoot);
949
- if (blockIndex === undefined) {
950
- return undefined;
951
- }
952
- return this.getNodeByIndex(blockIndex);
953
- }
954
-
955
- /** Return MUTABLE ProtoBlock for blockRoot (spreads properties) */
956
- getBlock(blockRoot: RootHex): ProtoBlock | undefined {
957
- const node = this.getNode(blockRoot);
958
- if (!node) {
959
- return undefined;
960
- }
961
- return {
962
- ...node,
963
- };
964
- }
965
-
966
- /** Return NON-MUTABLE ProtoBlock for blockRoot (does not spread properties) */
967
- getBlockReadonly(blockRoot: RootHex): ProtoBlock {
968
- const node = this.getNode(blockRoot);
969
- if (!node) {
970
- throw Error(`No block for root ${blockRoot}`);
971
- }
972
- return node;
973
- }
974
-
975
- /**
976
- * Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
977
- * Always returns `false` if either input roots are unknown.
978
- * Still returns `true` if `ancestorRoot` === `descendantRoot` (and the roots are known)
979
- */
980
- isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
981
- const ancestorNode = this.getNode(ancestorRoot);
982
- if (!ancestorNode) {
983
- return false;
984
- }
985
-
986
- if (ancestorRoot === descendantRoot) {
987
- return true;
988
- }
989
-
990
- for (const node of this.iterateAncestorNodes(descendantRoot)) {
991
- if (node.slot < ancestorNode.slot) {
992
- return false;
993
- }
994
- if (node.blockRoot === ancestorNode.blockRoot) {
995
- return true;
996
- }
997
- }
998
- return false;
999
- }
1000
-
1001
- /**
1002
- * Returns a common ancestor for nodeA or nodeB or null if there's none
1003
- */
1004
- getCommonAncestor(nodeA: ProtoNode, nodeB: ProtoNode): ProtoNode | null {
1005
- while (true) {
1006
- // If nodeA is higher than nodeB walk up nodeA tree
1007
- if (nodeA.slot > nodeB.slot) {
1008
- if (nodeA.parent === undefined) {
1009
- return null;
1010
- }
1011
-
1012
- nodeA = this.getNodeFromIndex(nodeA.parent);
1013
- }
1014
-
1015
- // If nodeB is higher than nodeA walk up nodeB tree
1016
- else if (nodeA.slot < nodeB.slot) {
1017
- if (nodeB.parent === undefined) {
1018
- return null;
1019
- }
1020
-
1021
- nodeB = this.getNodeFromIndex(nodeB.parent);
1022
- }
1023
-
1024
- // If both node trees are at the same height, if same root == common ancestor.
1025
- // Otherwise, keep walking up until there's a match or no parent.
1026
- else {
1027
- if (nodeA.blockRoot === nodeB.blockRoot) {
1028
- return nodeA;
1029
- }
1030
-
1031
- if (nodeA.parent === undefined || nodeB.parent === undefined) {
1032
- return null;
1033
- }
1034
-
1035
- nodeA = this.getNodeFromIndex(nodeA.parent);
1036
- nodeB = this.getNodeFromIndex(nodeB.parent);
1037
- }
1038
- }
1039
- }
1040
-
1041
- length(): number {
1042
- return this.indices.size;
1043
- }
1044
-
1045
- private getNodeFromIndex(index: number): ProtoNode {
1046
- const node = this.nodes[index];
1047
- if (node === undefined) {
1048
- throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index});
1049
- }
1050
- return node;
1051
- }
1052
-
1053
- private getNodeByIndex(blockIndex: number): ProtoNode | undefined {
1054
- const node = this.nodes[blockIndex];
1055
- if (node === undefined) {
1056
- return undefined;
1057
- }
1058
-
1059
- return node;
1060
- }
1061
-
1062
- private getNodesBetween(upperIndex: number, lowerIndex: number): ProtoNode[] {
1063
- const result = [];
1064
- for (let index = upperIndex - 1; index > lowerIndex; index--) {
1065
- const node = this.nodes[index];
1066
- if (node === undefined) {
1067
- throw new ProtoArrayError({
1068
- code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
1069
- index,
1070
- });
1071
- }
1072
- result.push(node);
1073
- }
1074
- return result;
1075
- }
1076
- }