@lodestar/fork-choice 1.47.0-dev.1759dccc9c → 1.47.0-dev.1d09a4c954
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 +33 -14
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +139 -44
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +1 -0
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js +1 -0
- 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 +14 -4
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +52 -19
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/forkChoice.ts +172 -53
- package/src/forkChoice/interface.ts +1 -0
- 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 +58 -24
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EFFECTIVE_BALANCE_INCREMENT,
|
|
4
|
+
MIN_SEED_LOOKAHEAD,
|
|
5
|
+
SLOTS_PER_EPOCH,
|
|
6
|
+
isForkPostFulu,
|
|
7
|
+
isForkPostGloas,
|
|
8
|
+
} from "@lodestar/params";
|
|
3
9
|
import {
|
|
4
10
|
DataAvailabilityStatus,
|
|
5
11
|
EffectiveBalanceIncrements,
|
|
@@ -68,6 +74,8 @@ export type ForkChoiceOpts = {
|
|
|
68
74
|
fastConfirmation?: boolean;
|
|
69
75
|
};
|
|
70
76
|
|
|
77
|
+
const EFFECTIVE_BALANCE_INCREMENT_BIGINT = BigInt(EFFECTIVE_BALANCE_INCREMENT);
|
|
78
|
+
|
|
71
79
|
export enum UpdateHeadOpt {
|
|
72
80
|
GetCanonicalHead = "getCanonicalHead", // Skip getProposerHead
|
|
73
81
|
GetProposerHead = "getProposerHead", // With getProposerHead
|
|
@@ -147,7 +155,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
147
155
|
/** Boost the entire branch with this proposer root as the leaf */
|
|
148
156
|
private proposerBoostRoot: RootHex | null = null;
|
|
149
157
|
/** Score to use in proposer boost, evaluated lazily from justified balances */
|
|
150
|
-
private justifiedProposerBoostScore:
|
|
158
|
+
private justifiedProposerBoostScore: bigint | null = null;
|
|
151
159
|
/** The current effective balances */
|
|
152
160
|
private balances: EffectiveBalanceIncrements;
|
|
153
161
|
/** Optional fast confirmation rule implementation */
|
|
@@ -583,32 +591,48 @@ export class ForkChoice implements IForkChoice {
|
|
|
583
591
|
computeDeltasMetrics?.newVoteValidators.set(newVoteValidators);
|
|
584
592
|
|
|
585
593
|
this.balances = newBalances;
|
|
586
|
-
/**
|
|
587
|
-
* The structure in line with deltas to propagate boost up the branch
|
|
588
|
-
* starting from the proposerIndex
|
|
589
|
-
*/
|
|
590
|
-
let proposerBoost: {root: RootHex; score: number} | null = null;
|
|
591
|
-
if (this.opts?.proposerBoost && this.proposerBoostRoot) {
|
|
592
|
-
const proposerBoostScore =
|
|
593
|
-
this.justifiedProposerBoostScore ??
|
|
594
|
-
getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
595
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
596
|
-
committeePercent: this.config.PROPOSER_SCORE_BOOST,
|
|
597
|
-
});
|
|
598
|
-
proposerBoost = {root: this.proposerBoostRoot, score: proposerBoostScore};
|
|
599
|
-
this.justifiedProposerBoostScore = proposerBoostScore;
|
|
600
|
-
}
|
|
601
594
|
|
|
602
595
|
const currentSlot = this.fcStore.currentSlot;
|
|
603
|
-
|
|
604
|
-
attestationDeltas,
|
|
605
|
-
proposerBoost,
|
|
596
|
+
const checkpoints = {
|
|
606
597
|
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
607
598
|
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
608
599
|
finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
|
|
609
600
|
finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
|
|
610
601
|
currentSlot,
|
|
611
|
-
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const boostedBlock =
|
|
605
|
+
this.opts?.proposerBoost && this.proposerBoostRoot ? this.getBlockHexDefaultStatus(this.proposerBoostRoot) : null;
|
|
606
|
+
|
|
607
|
+
if (boostedBlock && isGloasBlock(boostedBlock)) {
|
|
608
|
+
// should_apply_proposer_boost judges the parent against the attestations known to the store,
|
|
609
|
+
// via is_head_weak (which also assumes equivocators' votes were already discounted before
|
|
610
|
+
// their balance is added back). Apply the attestation deltas first so the decision reads
|
|
611
|
+
// post-delta scores, then apply the boost in a second pass.
|
|
612
|
+
// TODO GLOAS: applyScoreChanges() updates weights and best child/descendant in one call;
|
|
613
|
+
// splitting them would let the two passes update weights and recompute best child/descendant
|
|
614
|
+
// once at the end.
|
|
615
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
|
|
616
|
+
this.protoArray.applyScoreChanges({attestationDeltas, proposerBoost: null, ...checkpoints});
|
|
617
|
+
const proposerBoost = this.shouldApplyProposerBoost() ? this.getProposerBoost() : null;
|
|
618
|
+
// The first pass already rolled back the previous boost and left a coherent tree, so a
|
|
619
|
+
// withheld boost needs no second pass
|
|
620
|
+
if (proposerBoost !== null) {
|
|
621
|
+
this.protoArray.applyScoreChanges({
|
|
622
|
+
attestationDeltas: new Array<number>(this.protoArray.nodes.length).fill(0),
|
|
623
|
+
proposerBoost,
|
|
624
|
+
...checkpoints,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
// Pre-gloas the boost is unconditional, so attestation deltas and boost deltas can propagate
|
|
629
|
+
// up the branch in a single pass
|
|
630
|
+
this.protoArray.applyScoreChanges({
|
|
631
|
+
attestationDeltas,
|
|
632
|
+
proposerBoost: boostedBlock ? this.getProposerBoost() : null,
|
|
633
|
+
...checkpoints,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
612
636
|
|
|
613
637
|
// findHead returns the ProtoNode representing the head
|
|
614
638
|
const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
|
|
@@ -635,22 +659,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
635
659
|
return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
|
|
636
660
|
}
|
|
637
661
|
|
|
638
|
-
/**
|
|
639
|
-
|
|
640
|
-
* For compliance test use only
|
|
641
|
-
*/
|
|
642
|
-
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
662
|
+
/** Returns exact Gwei weights for the compliance test. */
|
|
663
|
+
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
|
|
643
664
|
return this.protoArray.getViableHeads(this.fcStore.currentSlot);
|
|
644
665
|
}
|
|
645
666
|
|
|
646
|
-
/**
|
|
647
|
-
* The cached justified total active balance, in EFFECTIVE_BALANCE_INCREMENT units.
|
|
648
|
-
* For compliance test use only
|
|
649
|
-
*/
|
|
650
|
-
getJustifiedTotalActiveBalanceByIncrement(): number {
|
|
651
|
-
return this.fcStore.justified.totalBalance;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
667
|
/** This is for the debug API only */
|
|
655
668
|
getAllNodes(): ProtoNode[] {
|
|
656
669
|
return this.protoArray.nodes;
|
|
@@ -829,6 +842,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
829
842
|
targetRoot: toRootHex(targetRoot),
|
|
830
843
|
stateRoot: toRootHex(block.stateRoot),
|
|
831
844
|
timeliness: isTimely,
|
|
845
|
+
ptcTimeliness: this.isBlockPtcTimely(block, blockDelaySec),
|
|
846
|
+
proposerIndex: block.proposerIndex,
|
|
832
847
|
|
|
833
848
|
justifiedEpoch: stateJustifiedEpoch,
|
|
834
849
|
justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root),
|
|
@@ -1081,11 +1096,20 @@ export class ForkChoice implements IForkChoice {
|
|
|
1081
1096
|
while (this.fcStore.currentSlot < currentSlot) {
|
|
1082
1097
|
const previousSlot = this.fcStore.currentSlot;
|
|
1083
1098
|
// Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
|
|
1084
|
-
this.onTick(previousSlot + 1);
|
|
1099
|
+
const didUpdateCheckpoints = this.onTick(previousSlot + 1);
|
|
1085
1100
|
this.queuedAttestationsPreviousSlot = 0;
|
|
1086
1101
|
// Process any attestations that might now be eligible before running FCR for this slot.
|
|
1087
1102
|
this.processAttestationQueue();
|
|
1088
|
-
this.runFastConfirmation();
|
|
1103
|
+
const didRecomputeHead = this.runFastConfirmation();
|
|
1104
|
+
|
|
1105
|
+
// An epoch-boundary checkpoint pull-up can move the head's dependent root and stale the cached
|
|
1106
|
+
// head before block 0 of the new epoch is imported, making isProposerBoostSameDependentRoot()
|
|
1107
|
+
// wrong for that block. Recompute the head so it reflects the new checkpoint and the queued
|
|
1108
|
+
// votes — unless fast confirmation already did, to avoid a redundant head calculation.
|
|
1109
|
+
if (didUpdateCheckpoints && !didRecomputeHead) {
|
|
1110
|
+
this.updateHead();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1089
1113
|
this.validatedAttestationDatas = new Set();
|
|
1090
1114
|
}
|
|
1091
1115
|
}
|
|
@@ -1596,7 +1620,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
1596
1620
|
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
|
|
1597
1621
|
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
|
|
1598
1622
|
if (node === undefined) {
|
|
1599
|
-
// this is called for head
|
|
1623
|
+
// this is called for head or the boosted block's parent, both of which should always be
|
|
1624
|
+
// in forkchoice, otherwise we have a serious error
|
|
1600
1625
|
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
|
|
1601
1626
|
}
|
|
1602
1627
|
|
|
@@ -1616,7 +1641,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1616
1641
|
// always 0. Return before fetching the state and walking the block's committees.
|
|
1617
1642
|
if (equivocatingIndices.size > 0) {
|
|
1618
1643
|
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
|
|
1619
|
-
// Only ever called on the head, so the state is always cached.
|
|
1644
|
+
// Only ever called on the head or the boosted block's parent, so the state is always cached.
|
|
1620
1645
|
// A miss is a broken invariant, not a recoverable state.
|
|
1621
1646
|
if (state === null) {
|
|
1622
1647
|
throw new ForkChoiceError({
|
|
@@ -1636,7 +1661,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1636
1661
|
// the justified state) - fall back to the head state's effective balance
|
|
1637
1662
|
balance = state.effectiveBalanceIncrements[validatorIndex];
|
|
1638
1663
|
}
|
|
1639
|
-
headWeight += balance;
|
|
1664
|
+
headWeight += BigInt(balance) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
|
|
1640
1665
|
}
|
|
1641
1666
|
}
|
|
1642
1667
|
}
|
|
@@ -1666,7 +1691,6 @@ export class ForkChoice implements IForkChoice {
|
|
|
1666
1691
|
|
|
1667
1692
|
// pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
|
|
1668
1693
|
const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;
|
|
1669
|
-
|
|
1670
1694
|
return parentWeight > parentThreshold;
|
|
1671
1695
|
}
|
|
1672
1696
|
|
|
@@ -1680,6 +1704,81 @@ export class ForkChoice implements IForkChoice {
|
|
|
1680
1704
|
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1681
1705
|
}
|
|
1682
1706
|
|
|
1707
|
+
/**
|
|
1708
|
+
* Return true if the block arrived before the PTC (payload-timeliness committee) deadline,
|
|
1709
|
+
* ie. block_timeliness[PTC_TIMELINESS_INDEX]. should_apply_proposer_boost uses this as the
|
|
1710
|
+
* definition of an "early" proposer equivocation: only a sibling seen by the PTC deadline
|
|
1711
|
+
* proves the proposer equivocated soon enough that honest nodes could have withheld the boost.
|
|
1712
|
+
*
|
|
1713
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-record_block_timeliness
|
|
1714
|
+
*
|
|
1715
|
+
* Child class can overwrite this for testing purpose.
|
|
1716
|
+
*/
|
|
1717
|
+
protected isBlockPtcTimely(block: BeaconBlock, blockDelaySec: number): boolean {
|
|
1718
|
+
const ptcThresholdMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
|
|
1719
|
+
return this.fcStore.currentSlot === block.slot && blockDelaySec * 1000 < ptcThresholdMs;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Determine whether proposer boost should be applied to `proposerBoostRoot`.
|
|
1724
|
+
*
|
|
1725
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
|
|
1726
|
+
*
|
|
1727
|
+
* Pre-gloas blocks always receive the boost (unconditional, backward compatible). For gloas
|
|
1728
|
+
* blocks the boost is withheld when the parent is a weak block from the previous slot and the
|
|
1729
|
+
* proposer of that parent equivocated (published another PTC-timely block at the same slot).
|
|
1730
|
+
*/
|
|
1731
|
+
private shouldApplyProposerBoost(): boolean {
|
|
1732
|
+
if (!this.proposerBoostRoot) {
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const boostedBlock = this.getBlockHexDefaultStatus(this.proposerBoostRoot);
|
|
1737
|
+
// Pre-gloas blocks always get boost
|
|
1738
|
+
if (!boostedBlock || !isGloasBlock(boostedBlock)) {
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const parentBlock = this.getBlockHexDefaultStatus(boostedBlock.parentRoot);
|
|
1743
|
+
if (!parentBlock) {
|
|
1744
|
+
return true;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Apply proposer boost if parent is not from the previous slot
|
|
1748
|
+
if (parentBlock.slot + 1 < boostedBlock.slot) {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Apply proposer boost if parent is not weak
|
|
1753
|
+
if (!this.isHeadWeak(parentBlock.blockRoot)) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Parent is weak and from the previous slot: apply boost only if there are no early
|
|
1758
|
+
// equivocations, ie. no other PTC-timely block at the parent's slot from the same proposer.
|
|
1759
|
+
return !this.protoArray.hasEquivocatingBlock(parentBlock.proposerIndex, parentBlock.slot, parentBlock.blockRoot);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* The proposer boost to apply to `proposerBoostRoot`, propagated up the branch by
|
|
1764
|
+
* applyScoreChanges() together with the deltas.
|
|
1765
|
+
*/
|
|
1766
|
+
private getProposerBoost(): {root: RootHex; score: bigint} | null {
|
|
1767
|
+
if (!this.proposerBoostRoot) {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const proposerBoostScore =
|
|
1772
|
+
this.justifiedProposerBoostScore ??
|
|
1773
|
+
getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1774
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1775
|
+
committeePercent: this.config.PROPOSER_SCORE_BOOST,
|
|
1776
|
+
});
|
|
1777
|
+
this.justifiedProposerBoostScore = proposerBoostScore;
|
|
1778
|
+
|
|
1779
|
+
return {root: this.proposerBoostRoot, score: proposerBoostScore};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1683
1782
|
/**
|
|
1684
1783
|
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
|
|
1685
1784
|
*/
|
|
@@ -1732,23 +1831,31 @@ export class ForkChoice implements IForkChoice {
|
|
|
1732
1831
|
* May need the justified balances of:
|
|
1733
1832
|
* - unrealizedJustified: Already available in `CheckpointWithBalance`
|
|
1734
1833
|
* Since this balances are already available the getter is just `() => balances`, without cache interaction
|
|
1834
|
+
*
|
|
1835
|
+
* @returns Whether either checkpoint was updated.
|
|
1735
1836
|
*/
|
|
1736
1837
|
private updateCheckpoints(
|
|
1737
1838
|
justifiedCheckpoint: CheckpointWithHex,
|
|
1738
1839
|
finalizedCheckpoint: CheckpointWithHex,
|
|
1739
1840
|
getJustifiedBalances: () => JustifiedBalances
|
|
1740
|
-
):
|
|
1841
|
+
): boolean {
|
|
1842
|
+
let updated = false;
|
|
1843
|
+
|
|
1741
1844
|
// Update justified checkpoint.
|
|
1742
1845
|
if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
|
|
1743
1846
|
this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
|
|
1744
1847
|
this.justifiedProposerBoostScore = null;
|
|
1848
|
+
updated = true;
|
|
1745
1849
|
}
|
|
1746
1850
|
|
|
1747
1851
|
// Update finalized checkpoint.
|
|
1748
1852
|
if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
|
|
1749
1853
|
this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
|
|
1750
1854
|
this.justifiedProposerBoostScore = null;
|
|
1855
|
+
updated = true;
|
|
1751
1856
|
}
|
|
1857
|
+
|
|
1858
|
+
return updated;
|
|
1752
1859
|
}
|
|
1753
1860
|
|
|
1754
1861
|
/**
|
|
@@ -2034,8 +2141,10 @@ export class ForkChoice implements IForkChoice {
|
|
|
2034
2141
|
* Equivalent to:
|
|
2035
2142
|
*
|
|
2036
2143
|
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_tick
|
|
2144
|
+
*
|
|
2145
|
+
* @returns Whether an epoch-boundary checkpoint was updated.
|
|
2037
2146
|
*/
|
|
2038
|
-
private onTick(time: Slot):
|
|
2147
|
+
private onTick(time: Slot): boolean {
|
|
2039
2148
|
const previousSlot = this.fcStore.currentSlot;
|
|
2040
2149
|
|
|
2041
2150
|
if (time > previousSlot + 1) {
|
|
@@ -2056,11 +2165,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2056
2165
|
|
|
2057
2166
|
// Not a new epoch, return.
|
|
2058
2167
|
if (computeSlotsSinceEpochStart(time) !== 0) {
|
|
2059
|
-
return;
|
|
2168
|
+
return false;
|
|
2060
2169
|
}
|
|
2061
2170
|
|
|
2062
|
-
// If a new epoch, pull-up justification and finalization from previous epoch
|
|
2063
|
-
this.updateCheckpoints(
|
|
2171
|
+
// If a new epoch, pull-up justification and finalization from previous epoch.
|
|
2172
|
+
return this.updateCheckpoints(
|
|
2064
2173
|
this.fcStore.unrealizedJustified.checkpoint,
|
|
2065
2174
|
this.fcStore.unrealizedFinalizedCheckpoint,
|
|
2066
2175
|
() => this.fcStore.unrealizedJustified.balances
|
|
@@ -2086,6 +2195,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2086
2195
|
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
|
|
2087
2196
|
}
|
|
2088
2197
|
|
|
2198
|
+
const isShufflingStable = isForkPostFulu(this.config.getForkName(slot)) || slot % SLOTS_PER_EPOCH !== 0;
|
|
2199
|
+
if (!isShufflingStable) {
|
|
2200
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotShufflingStable};
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2089
2203
|
// No reorg if headBlock and parentBlock are not ffg competitive
|
|
2090
2204
|
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_ffg_competitive
|
|
2091
2205
|
const {unrealizedJustifiedEpoch: headBlockCpEpoch, unrealizedJustifiedRoot: headBlockCpRoot} = headBlock;
|
|
@@ -2114,10 +2228,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2114
2228
|
return {prelimProposerHead};
|
|
2115
2229
|
}
|
|
2116
2230
|
|
|
2117
|
-
|
|
2231
|
+
/** Returns whether it recomputed the head, so the caller can avoid a redundant `updateHead()`. */
|
|
2232
|
+
private runFastConfirmation(): boolean {
|
|
2118
2233
|
const fastConfirmationRule = this.fastConfirmationRule;
|
|
2119
2234
|
const fastConfirmationContext = this.fastConfirmationContext;
|
|
2120
|
-
if (!fastConfirmationRule || !fastConfirmationContext) return;
|
|
2235
|
+
if (!fastConfirmationRule || !fastConfirmationContext) return false;
|
|
2121
2236
|
|
|
2122
2237
|
if (this.fastConfirmationPaused) {
|
|
2123
2238
|
// Keep consumers on a safe, available root while the rule is paused
|
|
@@ -2128,7 +2243,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
2128
2243
|
// Runs outside the timed try/catch below; a throw would escape to the clock listener
|
|
2129
2244
|
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
2130
2245
|
}
|
|
2131
|
-
return;
|
|
2246
|
+
return false;
|
|
2132
2247
|
}
|
|
2133
2248
|
|
|
2134
2249
|
withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
|
|
@@ -2151,6 +2266,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
2151
2266
|
);
|
|
2152
2267
|
}
|
|
2153
2268
|
});
|
|
2269
|
+
|
|
2270
|
+
return true;
|
|
2154
2271
|
}
|
|
2155
2272
|
|
|
2156
2273
|
private createFastConfirmationContext(): FastConfirmationContext {
|
|
@@ -2202,12 +2319,14 @@ export class ForkChoice implements IForkChoice {
|
|
|
2202
2319
|
}
|
|
2203
2320
|
}
|
|
2204
2321
|
|
|
2205
|
-
//
|
|
2322
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
|
|
2206
2323
|
// Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
|
|
2207
|
-
|
|
2324
|
+
function getCommitteeFraction(
|
|
2208
2325
|
justifiedTotalActiveBalanceByIncrement: number,
|
|
2209
2326
|
config: {slotsPerEpoch: number; committeePercent: number}
|
|
2210
|
-
):
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2327
|
+
): bigint {
|
|
2328
|
+
const committeeWeightGwei =
|
|
2329
|
+
(BigInt(justifiedTotalActiveBalanceByIncrement) * EFFECTIVE_BALANCE_INCREMENT_BIGINT) /
|
|
2330
|
+
BigInt(config.slotsPerEpoch);
|
|
2331
|
+
return (committeeWeightGwei * BigInt(config.committeePercent)) / 100n;
|
|
2213
2332
|
}
|
|
@@ -54,6 +54,7 @@ export enum NotReorgedReason {
|
|
|
54
54
|
HeadBlockIsTimely = "headBlockIsTimely",
|
|
55
55
|
ParentBlockNotAvailable = "parentBlockNotAvailable",
|
|
56
56
|
ProposerBoostReorgDisabled = "proposerBoostReorgDisabled",
|
|
57
|
+
NotShufflingStable = "notShufflingStable",
|
|
57
58
|
NotFFGCompetitive = "notFFGCompetitive",
|
|
58
59
|
ChainLongUnfinality = "chainLongUnfinality",
|
|
59
60
|
ParentBlockDistanceMoreThanOneSlot = "parentBlockDistanceMoreThanOneSlot",
|
|
@@ -13,7 +13,7 @@ import {IForkChoice} from "./interface.js";
|
|
|
13
13
|
* payload may not yet be confirmed canonical, so we report the parent EL block which has
|
|
14
14
|
* been (the bid commits to extending it).
|
|
15
15
|
*
|
|
16
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
16
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/bellatrix/fast-confirmation.md#new-get_safe_execution_block_hash
|
|
17
17
|
*/
|
|
18
18
|
export function getSafeExecutionBlockHash(forkChoice: IForkChoice, logger?: Pick<Logger, LogLevel.debug>): RootHex {
|
|
19
19
|
const confirmedRoot = forkChoice.getConfirmedRoot();
|
|
@@ -36,7 +36,7 @@ export function getSafeExecutionBlockHash(forkChoice: IForkChoice, logger?: Pick
|
|
|
36
36
|
* Get execution payload hash to report as `finalizedBlockHash` in `engine_forkchoiceUpdated`.
|
|
37
37
|
* Mirrors `getSafeExecutionBlockHash`: post-Gloas returns the bid `parent_block_hash`.
|
|
38
38
|
*
|
|
39
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
39
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#notify_forkchoice_updated
|
|
40
40
|
*/
|
|
41
41
|
export function getFinalizedExecutionBlockHash(forkChoice: IForkChoice): RootHex {
|
|
42
42
|
return getExecutionBlockHash(forkChoice.getFinalizedBlock());
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
type IFastConfirmationStore,
|
|
18
18
|
getFastConfirmationMetrics,
|
|
19
19
|
} from "./forkChoice/fastConfirmation/fastConfirmationRule.js";
|
|
20
|
-
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt
|
|
20
|
+
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
|
|
21
21
|
export {
|
|
22
22
|
type AncestorResult,
|
|
23
23
|
AncestorStatus,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {DataAvailabilityStatus} from "@lodestar/state-transition";
|
|
2
|
-
import {Epoch, RootHex, Slot, UintNum64} from "@lodestar/types";
|
|
2
|
+
import {Epoch, RootHex, Slot, UintNum64, ValidatorIndex} from "@lodestar/types";
|
|
3
3
|
|
|
4
4
|
// RootHex is a root as a hex string
|
|
5
5
|
// Used for lightweight and easy comparison
|
|
@@ -144,6 +144,13 @@ export type ProtoBlock = BlockExtraMeta & {
|
|
|
144
144
|
// Indicate whether block arrives in a timely manner ie. before the 4 second mark
|
|
145
145
|
timeliness: boolean;
|
|
146
146
|
|
|
147
|
+
// Indicate whether block arrives before the PTC deadline
|
|
148
|
+
// Spec: gloas/fork-choice.md#modified-record_block_timeliness (block_timeliness[PTC_TIMELINESS_INDEX])
|
|
149
|
+
ptcTimeliness: boolean;
|
|
150
|
+
|
|
151
|
+
// The index of the block proposer. Used by should_apply_proposer_boost to detect proposer equivocations
|
|
152
|
+
proposerIndex: ValidatorIndex;
|
|
153
|
+
|
|
147
154
|
/** Payload status for this node (Gloas fork). Always FULL in pre-gloas */
|
|
148
155
|
payloadStatus: PayloadStatus;
|
|
149
156
|
|
|
@@ -160,13 +167,13 @@ export type ProtoBlock = BlockExtraMeta & {
|
|
|
160
167
|
*/
|
|
161
168
|
export type ProtoNode = ProtoBlock & {
|
|
162
169
|
parent?: number;
|
|
163
|
-
/** Total weight, ie. attestationScore plus the proposer boost credited to this node */
|
|
164
|
-
weight:
|
|
170
|
+
/** Total weight in Gwei, ie. attestationScore plus the proposer boost credited to this node */
|
|
171
|
+
weight: bigint;
|
|
165
172
|
/**
|
|
166
|
-
* Weight from attester votes only, excluding proposer boost.
|
|
173
|
+
* Weight in Gwei from attester votes only, excluding proposer boost.
|
|
167
174
|
* Spec: get_attestation_score
|
|
168
175
|
*/
|
|
169
|
-
attestationScore:
|
|
176
|
+
attestationScore: bigint;
|
|
170
177
|
bestChild?: number;
|
|
171
178
|
bestDescendant?: number;
|
|
172
179
|
};
|
|
@@ -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,
|
|
@@ -1496,13 +1500,13 @@ export class ProtoArray {
|
|
|
1496
1500
|
childNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1497
1501
|
childNode.slot + 1 !== currentSlot
|
|
1498
1502
|
? childNode.weight
|
|
1499
|
-
:
|
|
1503
|
+
: 0n;
|
|
1500
1504
|
const bestChildEffectiveWeight =
|
|
1501
1505
|
!isGloasBlock(bestChildNode) ||
|
|
1502
1506
|
bestChildNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1503
1507
|
bestChildNode.slot + 1 !== currentSlot
|
|
1504
1508
|
? bestChildNode.weight
|
|
1505
|
-
:
|
|
1509
|
+
: 0n;
|
|
1506
1510
|
|
|
1507
1511
|
if (childEffectiveWeight !== bestChildEffectiveWeight) {
|
|
1508
1512
|
// Different effective weights, choose the winner by weight
|
|
@@ -1597,8 +1601,8 @@ export class ProtoArray {
|
|
|
1597
1601
|
return correctJustified && correctFinalized;
|
|
1598
1602
|
}
|
|
1599
1603
|
|
|
1600
|
-
/**
|
|
1601
|
-
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}[] {
|
|
1602
1606
|
// Mirror the spec's `get_filtered_block_tree`, which is rooted at the store's justified
|
|
1603
1607
|
// checkpoint: a viable head is a leaf (no viable descendant, i.e. `bestChild === undefined`)
|
|
1604
1608
|
// that descends from the justified checkpoint block AND is itself viable for head. Iterating
|
|
@@ -1607,7 +1611,7 @@ export class ProtoArray {
|
|
|
1607
1611
|
const justifiedVariant = this.getDefaultVariant(this.justifiedRoot);
|
|
1608
1612
|
// Gloas payload-status variants of one blockRoot are distinct nodes in the spec's filtered
|
|
1609
1613
|
// tree, identified by (root, payload_status, weight) — emit one entry per variant.
|
|
1610
|
-
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight:
|
|
1614
|
+
const heads: {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] = [];
|
|
1611
1615
|
for (const node of this.nodes) {
|
|
1612
1616
|
if (node.bestChild !== undefined || !this.nodeIsViableForHead(node, currentSlot)) {
|
|
1613
1617
|
continue;
|
|
@@ -1624,7 +1628,11 @@ export class ProtoArray {
|
|
|
1624
1628
|
continue;
|
|
1625
1629
|
}
|
|
1626
1630
|
}
|
|
1627
|
-
heads.push({
|
|
1631
|
+
heads.push({
|
|
1632
|
+
root: node.blockRoot,
|
|
1633
|
+
payloadStatus: node.payloadStatus,
|
|
1634
|
+
weight: node.weight,
|
|
1635
|
+
});
|
|
1628
1636
|
}
|
|
1629
1637
|
return heads;
|
|
1630
1638
|
}
|
|
@@ -1999,6 +2007,32 @@ export class ProtoArray {
|
|
|
1999
2007
|
return this.getNodeByIndex(nodeIndex);
|
|
2000
2008
|
}
|
|
2001
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
|
+
|
|
2002
2036
|
/**
|
|
2003
2037
|
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
2004
2038
|
*
|