@lodestar/fork-choice 1.45.0-dev.f535421f29 → 1.45.0-dev.f6c521a123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/data.js +1 -0
- package/lib/forkChoice/fastConfirmation/data.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/metrics.d.ts +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/metrics.js +4 -0
- package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/types.d.ts +3 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/types.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.d.ts +3 -3
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.js +27 -19
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +46 -7
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +171 -50
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +15 -3
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js +1 -1
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +1 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.d.ts +3 -3
- package/lib/protoArray/computeDeltas.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.js +31 -27
- package/lib/protoArray/computeDeltas.js.map +1 -1
- package/lib/protoArray/interface.d.ts +6 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +27 -2
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +105 -17
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +9 -9
- package/src/forkChoice/fastConfirmation/data.ts +1 -0
- package/src/forkChoice/fastConfirmation/metrics.ts +4 -0
- package/src/forkChoice/fastConfirmation/types.ts +4 -0
- package/src/forkChoice/fastConfirmation/utils.ts +39 -16
- package/src/forkChoice/forkChoice.ts +186 -51
- package/src/forkChoice/interface.ts +12 -13
- package/src/index.ts +1 -1
- package/src/protoArray/computeDeltas.ts +33 -29
- package/src/protoArray/interface.ts +6 -0
- package/src/protoArray/protoArray.ts +116 -21
|
@@ -27,6 +27,12 @@ const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
|
27
27
|
*/
|
|
28
28
|
const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
|
|
32
|
+
* Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
|
|
33
|
+
*/
|
|
34
|
+
const boostDeltas = new Array<number>();
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* popcount(attended AND NOT yes) — explicit False-vote count.
|
|
32
38
|
* Excludes PTC members who didn't attest (the None state).
|
|
@@ -348,7 +354,7 @@ export class ProtoArray {
|
|
|
348
354
|
* - If required, update the parents best-descendant with the current node or its best-descendant.
|
|
349
355
|
*/
|
|
350
356
|
applyScoreChanges({
|
|
351
|
-
|
|
357
|
+
attestationDeltas,
|
|
352
358
|
proposerBoost,
|
|
353
359
|
justifiedEpoch,
|
|
354
360
|
justifiedRoot,
|
|
@@ -356,7 +362,7 @@ export class ProtoArray {
|
|
|
356
362
|
finalizedRoot,
|
|
357
363
|
currentSlot,
|
|
358
364
|
}: {
|
|
359
|
-
|
|
365
|
+
attestationDeltas: number[];
|
|
360
366
|
proposerBoost: ProposerBoost | null;
|
|
361
367
|
justifiedEpoch: Epoch;
|
|
362
368
|
justifiedRoot: RootHex;
|
|
@@ -364,14 +370,17 @@ export class ProtoArray {
|
|
|
364
370
|
finalizedRoot: RootHex;
|
|
365
371
|
currentSlot: Slot;
|
|
366
372
|
}): void {
|
|
367
|
-
if (
|
|
373
|
+
if (attestationDeltas.length !== this.nodes.length) {
|
|
368
374
|
throw new ProtoArrayError({
|
|
369
375
|
code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
|
|
370
|
-
deltas:
|
|
376
|
+
deltas: attestationDeltas.length,
|
|
371
377
|
indices: this.nodes.length,
|
|
372
378
|
});
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
boostDeltas.length = this.nodes.length;
|
|
382
|
+
boostDeltas.fill(0);
|
|
383
|
+
|
|
375
384
|
if (
|
|
376
385
|
justifiedEpoch !== this.justifiedEpoch ||
|
|
377
386
|
finalizedEpoch !== this.finalizedEpoch ||
|
|
@@ -416,18 +425,22 @@ export class ProtoArray {
|
|
|
416
425
|
// If this node's execution status has been marked invalid, then the weight of the node
|
|
417
426
|
// needs to be taken out of consideration after which the node weight will become 0
|
|
418
427
|
// for subsequent iterations of applyScoreChanges
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
+
const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
|
|
429
|
+
const attestationDelta = isInvalid ? -node.attestationScore : attestationDeltas[nodeIndex];
|
|
430
|
+
const boostDelta = isInvalid
|
|
431
|
+
? // old boost = weight - attestationScore
|
|
432
|
+
-(node.weight - node.attestationScore)
|
|
433
|
+
: boostDeltas[nodeIndex] + currentBoost - previousBoost;
|
|
434
|
+
|
|
435
|
+
// Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
|
|
436
|
+
// by tracking the two scores apart.
|
|
437
|
+
node.attestationScore += attestationDelta;
|
|
438
|
+
node.weight += attestationDelta + boostDelta;
|
|
439
|
+
|
|
440
|
+
// Update the parent deltas (if any)
|
|
428
441
|
const parentIndex = node.parent;
|
|
429
442
|
if (parentIndex !== undefined) {
|
|
430
|
-
const parentDelta =
|
|
443
|
+
const parentDelta = attestationDeltas[parentIndex];
|
|
431
444
|
if (parentDelta === undefined) {
|
|
432
445
|
throw new ProtoArrayError({
|
|
433
446
|
code: ProtoArrayErrorCode.INVALID_PARENT_DELTA,
|
|
@@ -435,8 +448,9 @@ export class ProtoArray {
|
|
|
435
448
|
});
|
|
436
449
|
}
|
|
437
450
|
|
|
438
|
-
// back-propagate the nodes
|
|
439
|
-
|
|
451
|
+
// back-propagate the nodes deltas to its parent
|
|
452
|
+
attestationDeltas[parentIndex] += attestationDelta;
|
|
453
|
+
boostDeltas[parentIndex] += boostDelta;
|
|
440
454
|
}
|
|
441
455
|
}
|
|
442
456
|
|
|
@@ -514,6 +528,7 @@ export class ProtoArray {
|
|
|
514
528
|
parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
|
|
515
529
|
payloadStatus: PayloadStatus.PENDING,
|
|
516
530
|
weight: 0,
|
|
531
|
+
attestationScore: 0,
|
|
517
532
|
bestChild: undefined,
|
|
518
533
|
bestDescendant: undefined,
|
|
519
534
|
};
|
|
@@ -527,6 +542,7 @@ export class ProtoArray {
|
|
|
527
542
|
parent: pendingIndex, // Points to own PENDING
|
|
528
543
|
payloadStatus: PayloadStatus.EMPTY,
|
|
529
544
|
weight: 0,
|
|
545
|
+
attestationScore: 0,
|
|
530
546
|
bestChild: undefined,
|
|
531
547
|
bestDescendant: undefined,
|
|
532
548
|
};
|
|
@@ -562,6 +578,7 @@ export class ProtoArray {
|
|
|
562
578
|
parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
|
|
563
579
|
payloadStatus: PayloadStatus.FULL,
|
|
564
580
|
weight: 0,
|
|
581
|
+
attestationScore: 0,
|
|
565
582
|
bestChild: undefined,
|
|
566
583
|
bestDescendant: undefined,
|
|
567
584
|
};
|
|
@@ -647,6 +664,7 @@ export class ProtoArray {
|
|
|
647
664
|
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
648
665
|
payloadStatus: PayloadStatus.FULL,
|
|
649
666
|
weight: 0,
|
|
667
|
+
attestationScore: 0,
|
|
650
668
|
bestChild: undefined,
|
|
651
669
|
bestDescendant: undefined,
|
|
652
670
|
executionStatus,
|
|
@@ -672,6 +690,28 @@ export class ProtoArray {
|
|
|
672
690
|
this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
|
|
673
691
|
}
|
|
674
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed
|
|
695
|
+
* payload (FULL variant exists). Used by the builder circuit breaker.
|
|
696
|
+
*/
|
|
697
|
+
getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
|
|
698
|
+
let blocksPresent = 0;
|
|
699
|
+
let payloadsRevealed = 0;
|
|
700
|
+
// Full scan, nodes are in import order not slot order (an old block can be imported after newer
|
|
701
|
+
// ones during sync or reorg resolution), so we cannot stop early on an out-of-window slot
|
|
702
|
+
for (const node of this.nodes) {
|
|
703
|
+
// Count each gloas block once via its PENDING variant, pre-gloas nodes are FULL only
|
|
704
|
+
if (node.slot < fromSlot || node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
blocksPresent++;
|
|
708
|
+
if (this.hasPayload(node.blockRoot)) {
|
|
709
|
+
payloadsRevealed++;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return {blocksPresent, payloadsRevealed};
|
|
713
|
+
}
|
|
714
|
+
|
|
675
715
|
/**
|
|
676
716
|
* Update PTC votes for multiple validators attesting to a block
|
|
677
717
|
* Spec: gloas/fork-choice.md#new-notify_ptc_messages
|
|
@@ -1030,7 +1070,7 @@ export class ProtoArray {
|
|
|
1030
1070
|
|
|
1031
1071
|
// update the forkchoice as the invalidation can change the entire forkchoice DAG
|
|
1032
1072
|
this.applyScoreChanges({
|
|
1033
|
-
|
|
1073
|
+
attestationDeltas: Array.from({length: this.nodes.length}, () => 0),
|
|
1034
1074
|
proposerBoost: this.previousProposerBoost,
|
|
1035
1075
|
justifiedEpoch: this.justifiedEpoch,
|
|
1036
1076
|
justifiedRoot: this.justifiedRoot,
|
|
@@ -1541,6 +1581,38 @@ export class ProtoArray {
|
|
|
1541
1581
|
return correctJustified && correctFinalized;
|
|
1542
1582
|
}
|
|
1543
1583
|
|
|
1584
|
+
/** Weights are in EFFECTIVE_BALANCE_INCREMENT units (NOT Gwei); callers scale as needed. */
|
|
1585
|
+
getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
1586
|
+
// Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
|
|
1587
|
+
// checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
|
|
1588
|
+
// that descends from the justified checkpoint block AND is itself viable for head. Iterating
|
|
1589
|
+
// all nodes without the justified-descendant filter would wrongly include FFG-viable leaves
|
|
1590
|
+
// that hang off the finalized checkpoint on a branch not under the current justified checkpoint.
|
|
1591
|
+
const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
|
|
1592
|
+
// Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
|
|
1593
|
+
// tree, identified by (root, payload_status, weight) — emit one entry per variant.
|
|
1594
|
+
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] = [];
|
|
1595
|
+
for (const node of this.nodes) {
|
|
1596
|
+
if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
if (this.justifiedEpoch !== GENESIS_EPOCH) {
|
|
1600
|
+
// Same-root short-circuit: every payload-status variant of the justified block itself is
|
|
1601
|
+
// in the filtered tree, but `isDescendant` from the default (PENDING) variant does not
|
|
1602
|
+
// reach the sibling EMPTY/FULL variants of the same root.
|
|
1603
|
+
const descendsFromJustified =
|
|
1604
|
+
node.blockRoot === this.justifiedRoot ||
|
|
1605
|
+
(justifiedVariant !== undefined &&
|
|
1606
|
+
this.isDescendant(this.justifiedRoot, justifiedVariant, node.blockRoot, node.payloadStatus));
|
|
1607
|
+
if (!descendsFromJustified) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
heads.push({root: node.blockRoot, payloadStatus: node.payloadStatus, weight: node.weight});
|
|
1612
|
+
}
|
|
1613
|
+
return heads;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1544
1616
|
/**
|
|
1545
1617
|
* Return `true` if `node` is equal to or a descendant of the finalized node.
|
|
1546
1618
|
* This function helps improve performance of nodeIsViableForHead a lot by avoiding
|
|
@@ -1898,6 +1970,19 @@ export class ProtoArray {
|
|
|
1898
1970
|
return this.getNodeByIndex(blockIndex);
|
|
1899
1971
|
}
|
|
1900
1972
|
|
|
1973
|
+
/**
|
|
1974
|
+
* Return ProtoNode for the default/canonical variant in a single hash lookup
|
|
1975
|
+
* - Pre-Gloas blocks: the FULL variant
|
|
1976
|
+
* - Gloas blocks: the PENDING variant
|
|
1977
|
+
*/
|
|
1978
|
+
getNodeDefaultStatus(blockRoot: RootHex): ProtoNode | undefined {
|
|
1979
|
+
const nodeIndex = this.getDefaultNodeIndex(blockRoot);
|
|
1980
|
+
if (nodeIndex === undefined) {
|
|
1981
|
+
return undefined;
|
|
1982
|
+
}
|
|
1983
|
+
return this.getNodeByIndex(nodeIndex);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1901
1986
|
/**
|
|
1902
1987
|
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1903
1988
|
*
|
|
@@ -1941,6 +2026,11 @@ export class ProtoArray {
|
|
|
1941
2026
|
* Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
1942
2027
|
* Always returns `false` if either input roots are unknown.
|
|
1943
2028
|
* Still returns `true` if `ancestorRoot` === `descendantRoot` and payload statuses match.
|
|
2029
|
+
*
|
|
2030
|
+
* Gloas payload-status matching: a `PENDING` ancestor matches any payload variant
|
|
2031
|
+
* (PENDING/EMPTY/FULL) of the same block, so this can also return `true` for the same
|
|
2032
|
+
* root when statuses differ (e.g. ancestor `PENDING`, descendant `EMPTY`/`FULL`).
|
|
2033
|
+
* `EMPTY` and `FULL` are mutually exclusive siblings and never match each other.
|
|
1944
2034
|
*/
|
|
1945
2035
|
isDescendant(
|
|
1946
2036
|
ancestorRoot: RootHex,
|
|
@@ -1958,11 +2048,16 @@ export class ProtoArray {
|
|
|
1958
2048
|
}
|
|
1959
2049
|
|
|
1960
2050
|
for (const node of this.iterateAncestorNodes(descendantRoot, descendantPayloadStatus)) {
|
|
1961
|
-
if (node.
|
|
1962
|
-
|
|
2051
|
+
if (node.blockRoot === ancestorNode.blockRoot) {
|
|
2052
|
+
// Gloas is_ancestor: a PENDING ancestor matches any payload variant of the same block.
|
|
2053
|
+
return (
|
|
2054
|
+
node.payloadStatus === ancestorNode.payloadStatus || ancestorNode.payloadStatus === PayloadStatus.PENDING
|
|
2055
|
+
);
|
|
1963
2056
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
2057
|
+
// Ancestors are iterated in decreasing slot, so once we reach the ancestor's slot
|
|
2058
|
+
// without a root match it cannot be in this chain.
|
|
2059
|
+
if (node.slot <= ancestorNode.slot) {
|
|
2060
|
+
return false;
|
|
1966
2061
|
}
|
|
1967
2062
|
}
|
|
1968
2063
|
return false;
|