@lodestar/fork-choice 1.47.0-dev.2a04194b90 → 1.47.0-dev.2fba076c28
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/forkChoice.d.ts +36 -17
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +140 -51
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +5 -8
- 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/forkChoice/safeBlocks.d.ts +2 -2
- package/lib/forkChoice/safeBlocks.js +2 -2
- 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/protoArray/interface.d.ts +7 -5
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +18 -11
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +76 -41
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/forkChoice.ts +172 -60
- package/src/forkChoice/interface.ts +3 -6
- package/src/forkChoice/safeBlocks.ts +2 -2
- package/src/index.ts +1 -1
- package/src/protoArray/interface.ts +12 -5
- package/src/protoArray/protoArray.ts +92 -46
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {BitArray} from "@chainsafe/ssz";
|
|
2
|
-
import {GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
|
|
2
|
+
import {EFFECTIVE_BALANCE_INCREMENT, GENESIS_EPOCH, GENESIS_SLOT, PTC_SIZE} from "@lodestar/params";
|
|
3
3
|
import {DataAvailabilityStatus, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
4
|
-
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
4
|
+
import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
|
|
5
5
|
import {bitCount, toRootHex} from "@lodestar/utils";
|
|
6
6
|
import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
|
|
7
7
|
import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
|
|
@@ -31,7 +31,8 @@ const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
|
31
31
|
* Proposer boost deltas, back-propagated to the boosted node's ancestors in applyScoreChanges().
|
|
32
32
|
* Reuse the array to avoid memory reallocation and gc, as computeDeltas does for attestation deltas.
|
|
33
33
|
*/
|
|
34
|
-
const boostDeltas = new Array<
|
|
34
|
+
const boostDeltas = new Array<bigint>();
|
|
35
|
+
const EFFECTIVE_BALANCE_INCREMENT_BIGINT = BigInt(EFFECTIVE_BALANCE_INCREMENT);
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* popcount(attended AND NOT yes) — explicit False-vote count.
|
|
@@ -52,7 +53,7 @@ export function countNoVotes(attended: BitArray, yes: BitArray): number {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
55
|
-
type ProposerBoost = {root: RootHex; score:
|
|
56
|
+
type ProposerBoost = {root: RootHex; score: bigint};
|
|
56
57
|
|
|
57
58
|
const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
|
|
58
59
|
|
|
@@ -379,7 +380,7 @@ export class ProtoArray {
|
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
boostDeltas.length = this.nodes.length;
|
|
382
|
-
boostDeltas.fill(
|
|
383
|
+
boostDeltas.fill(0n);
|
|
383
384
|
|
|
384
385
|
if (
|
|
385
386
|
justifiedEpoch !== this.justifiedEpoch ||
|
|
@@ -416,17 +417,20 @@ export class ProtoArray {
|
|
|
416
417
|
// the boost neutral with respect to EMPTY vs FULL selection.
|
|
417
418
|
const isBoostVariant = isGloasBlock(node) ? node.payloadStatus === PayloadStatus.PENDING : true; // pre-Gloas has only FULL, always boost
|
|
418
419
|
const currentBoost =
|
|
419
|
-
proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score :
|
|
420
|
+
proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0n;
|
|
420
421
|
const previousBoost =
|
|
421
422
|
this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot && isBoostVariant
|
|
422
423
|
? this.previousProposerBoost.score
|
|
423
|
-
:
|
|
424
|
+
: 0n;
|
|
424
425
|
|
|
425
426
|
// If this node's execution status has been marked invalid, then the weight of the node
|
|
426
427
|
// needs to be taken out of consideration after which the node weight will become 0
|
|
427
428
|
// for subsequent iterations of applyScoreChanges
|
|
428
429
|
const isInvalid = node.executionStatus === ExecutionStatus.Invalid;
|
|
429
|
-
const attestationDelta = isInvalid
|
|
430
|
+
const attestationDelta = isInvalid
|
|
431
|
+
? -Number(node.attestationScore / EFFECTIVE_BALANCE_INCREMENT_BIGINT)
|
|
432
|
+
: attestationDeltas[nodeIndex];
|
|
433
|
+
const attestationDeltaGwei = BigInt(attestationDelta) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
|
|
430
434
|
const boostDelta = isInvalid
|
|
431
435
|
? // old boost = weight - attestationScore
|
|
432
436
|
-(node.weight - node.attestationScore)
|
|
@@ -434,8 +438,8 @@ export class ProtoArray {
|
|
|
434
438
|
|
|
435
439
|
// Apply the deltas to the node. Their sum is the node's total delta, so weight is unaffected
|
|
436
440
|
// by tracking the two scores apart.
|
|
437
|
-
node.attestationScore +=
|
|
438
|
-
node.weight +=
|
|
441
|
+
node.attestationScore += attestationDeltaGwei;
|
|
442
|
+
node.weight += attestationDeltaGwei + boostDelta;
|
|
439
443
|
|
|
440
444
|
// Update the parent deltas (if any)
|
|
441
445
|
const parentIndex = node.parent;
|
|
@@ -527,8 +531,8 @@ export class ProtoArray {
|
|
|
527
531
|
...block,
|
|
528
532
|
parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
|
|
529
533
|
payloadStatus: PayloadStatus.PENDING,
|
|
530
|
-
weight:
|
|
531
|
-
attestationScore:
|
|
534
|
+
weight: 0n,
|
|
535
|
+
attestationScore: 0n,
|
|
532
536
|
bestChild: undefined,
|
|
533
537
|
bestDescendant: undefined,
|
|
534
538
|
};
|
|
@@ -541,8 +545,8 @@ export class ProtoArray {
|
|
|
541
545
|
...block,
|
|
542
546
|
parent: pendingIndex, // Points to own PENDING
|
|
543
547
|
payloadStatus: PayloadStatus.EMPTY,
|
|
544
|
-
weight:
|
|
545
|
-
attestationScore:
|
|
548
|
+
weight: 0n,
|
|
549
|
+
attestationScore: 0n,
|
|
546
550
|
bestChild: undefined,
|
|
547
551
|
bestDescendant: undefined,
|
|
548
552
|
};
|
|
@@ -577,8 +581,8 @@ export class ProtoArray {
|
|
|
577
581
|
...block,
|
|
578
582
|
parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
|
|
579
583
|
payloadStatus: PayloadStatus.FULL,
|
|
580
|
-
weight:
|
|
581
|
-
attestationScore:
|
|
584
|
+
weight: 0n,
|
|
585
|
+
attestationScore: 0n,
|
|
582
586
|
bestChild: undefined,
|
|
583
587
|
bestDescendant: undefined,
|
|
584
588
|
};
|
|
@@ -663,8 +667,8 @@ export class ProtoArray {
|
|
|
663
667
|
...pendingNode,
|
|
664
668
|
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
665
669
|
payloadStatus: PayloadStatus.FULL,
|
|
666
|
-
weight:
|
|
667
|
-
attestationScore:
|
|
670
|
+
weight: 0n,
|
|
671
|
+
attestationScore: 0n,
|
|
668
672
|
bestChild: undefined,
|
|
669
673
|
bestDescendant: undefined,
|
|
670
674
|
executionStatus,
|
|
@@ -690,30 +694,42 @@ export class ProtoArray {
|
|
|
690
694
|
this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
|
|
691
695
|
}
|
|
692
696
|
|
|
693
|
-
/**
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
697
|
+
/** Count blocks selected as FULL or EMPTY by the supplied head chain in the inclusive slot range. */
|
|
698
|
+
getCanonicalPayloadCounts(
|
|
699
|
+
fromSlot: Slot,
|
|
700
|
+
toSlot: Slot,
|
|
701
|
+
headRoot: RootHex,
|
|
702
|
+
headPayloadStatus: PayloadStatus
|
|
703
|
+
): {full: number; empty: number} {
|
|
704
|
+
let full = 0;
|
|
705
|
+
let empty = 0;
|
|
706
|
+
|
|
707
|
+
// Walk the canonical chain newest-first from the head, following ancestors via `getParentNodeIndex`.
|
|
708
|
+
// Ancestors are strictly slot-descending, so we stop as soon as a node falls below `fromSlot`
|
|
709
|
+
// instead of materializing the whole chain back to the anchor as `getAllAncestorNodes` does.
|
|
710
|
+
// This keeps the scan O(window) rather than O(chain-to-anchor), relevant under prolonged non-finality.
|
|
711
|
+
const headIndex = this.getNodeIndexByRootAndStatus(headRoot, headPayloadStatus);
|
|
712
|
+
let node = headIndex !== undefined ? this.nodes[headIndex] : undefined;
|
|
713
|
+
|
|
714
|
+
while (node !== undefined && node.slot >= fromSlot) {
|
|
715
|
+
if (
|
|
716
|
+
node.slot !== GENESIS_SLOT &&
|
|
717
|
+
node.slot <= toSlot &&
|
|
718
|
+
isGloasBlock(node) &&
|
|
719
|
+
node.payloadStatus !== PayloadStatus.PENDING
|
|
720
|
+
) {
|
|
721
|
+
if (node.payloadStatus === PayloadStatus.FULL) {
|
|
722
|
+
full++;
|
|
723
|
+
} else {
|
|
724
|
+
empty++;
|
|
725
|
+
}
|
|
714
726
|
}
|
|
727
|
+
|
|
728
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
729
|
+
node = parentIndex === undefined ? undefined : this.nodes[parentIndex];
|
|
715
730
|
}
|
|
716
|
-
|
|
731
|
+
|
|
732
|
+
return {full, empty};
|
|
717
733
|
}
|
|
718
734
|
|
|
719
735
|
/**
|
|
@@ -1484,13 +1500,13 @@ export class ProtoArray {
|
|
|
1484
1500
|
childNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1485
1501
|
childNode.slot + 1 !== currentSlot
|
|
1486
1502
|
? childNode.weight
|
|
1487
|
-
:
|
|
1503
|
+
: 0n;
|
|
1488
1504
|
const bestChildEffectiveWeight =
|
|
1489
1505
|
!isGloasBlock(bestChildNode) ||
|
|
1490
1506
|
bestChildNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1491
1507
|
bestChildNode.slot + 1 !== currentSlot
|
|
1492
1508
|
? bestChildNode.weight
|
|
1493
|
-
:
|
|
1509
|
+
: 0n;
|
|
1494
1510
|
|
|
1495
1511
|
if (childEffectiveWeight !== bestChildEffectiveWeight) {
|
|
1496
1512
|
// Different effective weights, choose the winner by weight
|
|
@@ -1585,8 +1601,8 @@ export class ProtoArray {
|
|
|
1585
1601
|
return correctJustified && correctFinalized;
|
|
1586
1602
|
}
|
|
1587
1603
|
|
|
1588
|
-
/**
|
|
1589
|
-
getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight:
|
|
1604
|
+
/** Returns exact Gwei weights for the compliance test boundary. */
|
|
1605
|
+
getViableHeads(currentSlot: Slot): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
|
|
1590
1606
|
// Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
|
|
1591
1607
|
// checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
|
|
1592
1608
|
// that descends from the justified checkpoint block AND is itself viable for head. Iterating
|
|
@@ -1595,7 +1611,7 @@ export class ProtoArray {
|
|
|
1595
1611
|
const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
|
|
1596
1612
|
// Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
|
|
1597
1613
|
// tree, identified by (root, payload_status, weight) — emit one entry per variant.
|
|
1598
|
-
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight:
|
|
1614
|
+
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] = [];
|
|
1599
1615
|
for (const node of this.nodes) {
|
|
1600
1616
|
if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
|
|
1601
1617
|
continue;
|
|
@@ -1612,7 +1628,11 @@ export class ProtoArray {
|
|
|
1612
1628
|
continue;
|
|
1613
1629
|
}
|
|
1614
1630
|
}
|
|
1615
|
-
heads.push({
|
|
1631
|
+
heads.push({
|
|
1632
|
+
root: node.blockRoot,
|
|
1633
|
+
payloadStatus: node.payloadStatus,
|
|
1634
|
+
weight: node.weight,
|
|
1635
|
+
});
|
|
1616
1636
|
}
|
|
1617
1637
|
return heads;
|
|
1618
1638
|
}
|
|
@@ -1987,6 +2007,32 @@ export class ProtoArray {
|
|
|
1987
2007
|
return this.getNodeByIndex(nodeIndex);
|
|
1988
2008
|
}
|
|
1989
2009
|
|
|
2010
|
+
/**
|
|
2011
|
+
* Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex` and
|
|
2012
|
+
* is PTC-timely. Used by `should_apply_proposer_boost` to detect proposer equivocations.
|
|
2013
|
+
*
|
|
2014
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
|
|
2015
|
+
*
|
|
2016
|
+
* Iterates unique block roots (via the canonical variant) since `slot`, `proposerIndex` and
|
|
2017
|
+
* `ptcTimeliness` are block-level properties identical across payload-status variants.
|
|
2018
|
+
*/
|
|
2019
|
+
hasEquivocatingBlock(proposerIndex: ValidatorIndex, slot: Slot, excludeRoot: RootHex): boolean {
|
|
2020
|
+
for (const root of this.indices.keys()) {
|
|
2021
|
+
if (root === excludeRoot) {
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
const nodeIndex = this.getDefaultNodeIndex(root);
|
|
2025
|
+
if (nodeIndex === undefined) {
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
const node = this.nodes[nodeIndex];
|
|
2029
|
+
if (node !== undefined && node.slot === slot && node.proposerIndex === proposerIndex && node.ptcTimeliness) {
|
|
2030
|
+
return true;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1990
2036
|
/**
|
|
1991
2037
|
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1992
2038
|
*
|