@lodestar/fork-choice 1.47.0-dev.ec596194e2 → 1.47.0-dev.f0f26cd2b6
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 +41 -15
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +183 -55
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +1 -1
- 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 +13 -4
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +54 -19
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/forkChoice.ts +228 -65
- package/src/forkChoice/interface.ts +1 -1
- 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 +67 -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 */
|
|
@@ -357,6 +365,21 @@ export class ForkChoice implements IForkChoice {
|
|
|
357
365
|
return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
|
|
358
366
|
}
|
|
359
367
|
|
|
368
|
+
const currentTimeOk =
|
|
369
|
+
headBlock.slot === currentSlot ||
|
|
370
|
+
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
|
|
371
|
+
|
|
372
|
+
// Mirror the proposer equivocation branch of getProposerHead(). The head slot's attestations are
|
|
373
|
+
// still queued at this point so the head is assumed weak, same as for the regular branch below.
|
|
374
|
+
if (currentTimeOk && this.isProposerEquivocation(headBlock)) {
|
|
375
|
+
this.logger?.verbose("Head proposer equivocated. Should override forkchoice update", {
|
|
376
|
+
blockRoot: headBlock.blockRoot,
|
|
377
|
+
slot: currentSlot,
|
|
378
|
+
proposerIndex: headBlock.proposerIndex,
|
|
379
|
+
});
|
|
380
|
+
return {shouldOverrideFcu: true, parentBlock};
|
|
381
|
+
}
|
|
382
|
+
|
|
360
383
|
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
|
|
361
384
|
headBlock,
|
|
362
385
|
parentBlock,
|
|
@@ -367,9 +390,6 @@ export class ForkChoice implements IForkChoice {
|
|
|
367
390
|
return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
|
|
368
391
|
}
|
|
369
392
|
|
|
370
|
-
const currentTimeOk =
|
|
371
|
-
headBlock.slot === currentSlot ||
|
|
372
|
-
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
|
|
373
393
|
if (!currentTimeOk) {
|
|
374
394
|
return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
375
395
|
}
|
|
@@ -454,7 +474,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
454
474
|
* https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg
|
|
455
475
|
* This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()`
|
|
456
476
|
*
|
|
457
|
-
*
|
|
477
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#get_proposer_head
|
|
478
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-get_proposer_head
|
|
458
479
|
*/
|
|
459
480
|
getProposerHead(
|
|
460
481
|
headBlock: ProtoBlock,
|
|
@@ -485,6 +506,29 @@ export class ForkChoice implements IForkChoice {
|
|
|
485
506
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable};
|
|
486
507
|
}
|
|
487
508
|
|
|
509
|
+
// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
|
|
510
|
+
const currentTimeOk = headBlock.slot + 1 === slot;
|
|
511
|
+
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
|
|
512
|
+
|
|
513
|
+
// Re-org more aggressively if there is a proposer equivocation in the previous slot, skipping the
|
|
514
|
+
// regular reorg conditions. Any known equivocation counts here, timely or not.
|
|
515
|
+
if (
|
|
516
|
+
currentTimeOk &&
|
|
517
|
+
isProposerBoostWornOff &&
|
|
518
|
+
this.isProposerEquivocation(headBlock) &&
|
|
519
|
+
this.isHeadWeak(headBlock.blockRoot)
|
|
520
|
+
) {
|
|
521
|
+
this.logger?.verbose("Performing single-slot reorg to remove weak head of equivocating proposer", {
|
|
522
|
+
slot,
|
|
523
|
+
proposerHead: parentBlock.blockRoot,
|
|
524
|
+
weakHead: headBlock.blockRoot,
|
|
525
|
+
proposerIndex: headBlock.proposerIndex,
|
|
526
|
+
});
|
|
527
|
+
proposerHead = parentBlock;
|
|
528
|
+
|
|
529
|
+
return {proposerHead, isHeadTimely};
|
|
530
|
+
}
|
|
531
|
+
|
|
488
532
|
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot);
|
|
489
533
|
|
|
490
534
|
if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) {
|
|
@@ -497,14 +541,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
497
541
|
}
|
|
498
542
|
|
|
499
543
|
// No reorg if attempted reorg is more than a single slot
|
|
500
|
-
// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
|
|
501
|
-
const currentTimeOk = headBlock.slot + 1 === slot;
|
|
502
544
|
if (!currentTimeOk) {
|
|
503
545
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
504
546
|
}
|
|
505
547
|
|
|
506
548
|
// No reorg if proposer boost is still in effect
|
|
507
|
-
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
|
|
508
549
|
if (!isProposerBoostWornOff) {
|
|
509
550
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff};
|
|
510
551
|
}
|
|
@@ -583,32 +624,48 @@ export class ForkChoice implements IForkChoice {
|
|
|
583
624
|
computeDeltasMetrics?.newVoteValidators.set(newVoteValidators);
|
|
584
625
|
|
|
585
626
|
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
627
|
|
|
602
628
|
const currentSlot = this.fcStore.currentSlot;
|
|
603
|
-
|
|
604
|
-
attestationDeltas,
|
|
605
|
-
proposerBoost,
|
|
629
|
+
const checkpoints = {
|
|
606
630
|
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
607
631
|
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
608
632
|
finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
|
|
609
633
|
finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
|
|
610
634
|
currentSlot,
|
|
611
|
-
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const boostedBlock =
|
|
638
|
+
this.opts?.proposerBoost && this.proposerBoostRoot ? this.getBlockHexDefaultStatus(this.proposerBoostRoot) : null;
|
|
639
|
+
|
|
640
|
+
if (boostedBlock && isGloasBlock(boostedBlock)) {
|
|
641
|
+
// should_apply_proposer_boost judges the parent against the attestations known to the store,
|
|
642
|
+
// via is_head_weak (which also assumes equivocators' votes were already discounted before
|
|
643
|
+
// their balance is added back). Apply the attestation deltas first so the decision reads
|
|
644
|
+
// post-delta scores, then apply the boost in a second pass.
|
|
645
|
+
// TODO GLOAS: applyScoreChanges() updates weights and best child/descendant in one call;
|
|
646
|
+
// splitting them would let the two passes update weights and recompute best child/descendant
|
|
647
|
+
// once at the end.
|
|
648
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
|
|
649
|
+
this.protoArray.applyScoreChanges({attestationDeltas, proposerBoost: null, ...checkpoints});
|
|
650
|
+
const proposerBoost = this.shouldApplyProposerBoost() ? this.getProposerBoost() : null;
|
|
651
|
+
// The first pass already rolled back the previous boost and left a coherent tree, so a
|
|
652
|
+
// withheld boost needs no second pass
|
|
653
|
+
if (proposerBoost !== null) {
|
|
654
|
+
this.protoArray.applyScoreChanges({
|
|
655
|
+
attestationDeltas: new Array<number>(this.protoArray.nodes.length).fill(0),
|
|
656
|
+
proposerBoost,
|
|
657
|
+
...checkpoints,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
// Pre-gloas the boost is unconditional, so attestation deltas and boost deltas can propagate
|
|
662
|
+
// up the branch in a single pass
|
|
663
|
+
this.protoArray.applyScoreChanges({
|
|
664
|
+
attestationDeltas,
|
|
665
|
+
proposerBoost: boostedBlock ? this.getProposerBoost() : null,
|
|
666
|
+
...checkpoints,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
612
669
|
|
|
613
670
|
// findHead returns the ProtoNode representing the head
|
|
614
671
|
const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
|
|
@@ -635,22 +692,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
635
692
|
return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
|
|
636
693
|
}
|
|
637
694
|
|
|
638
|
-
/**
|
|
639
|
-
|
|
640
|
-
* For compliance test use only
|
|
641
|
-
*/
|
|
642
|
-
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: number}[] {
|
|
695
|
+
/** Returns exact Gwei weights for the compliance test. */
|
|
696
|
+
getViableHeads(): {root: RootHex; payloadStatus: PayloadStatus; weight: bigint}[] {
|
|
643
697
|
return this.protoArray.getViableHeads(this.fcStore.currentSlot);
|
|
644
698
|
}
|
|
645
699
|
|
|
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
700
|
/** This is for the debug API only */
|
|
655
701
|
getAllNodes(): ProtoNode[] {
|
|
656
702
|
return this.protoArray.nodes;
|
|
@@ -829,6 +875,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
829
875
|
targetRoot: toRootHex(targetRoot),
|
|
830
876
|
stateRoot: toRootHex(block.stateRoot),
|
|
831
877
|
timeliness: isTimely,
|
|
878
|
+
ptcTimeliness: this.isBlockPtcTimely(block, blockDelaySec),
|
|
879
|
+
proposerIndex: block.proposerIndex,
|
|
832
880
|
|
|
833
881
|
justifiedEpoch: stateJustifiedEpoch,
|
|
834
882
|
justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root),
|
|
@@ -1081,11 +1129,20 @@ export class ForkChoice implements IForkChoice {
|
|
|
1081
1129
|
while (this.fcStore.currentSlot < currentSlot) {
|
|
1082
1130
|
const previousSlot = this.fcStore.currentSlot;
|
|
1083
1131
|
// 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);
|
|
1132
|
+
const didUpdateCheckpoints = this.onTick(previousSlot + 1);
|
|
1085
1133
|
this.queuedAttestationsPreviousSlot = 0;
|
|
1086
1134
|
// Process any attestations that might now be eligible before running FCR for this slot.
|
|
1087
1135
|
this.processAttestationQueue();
|
|
1088
|
-
this.runFastConfirmation();
|
|
1136
|
+
const didRecomputeHead = this.runFastConfirmation();
|
|
1137
|
+
|
|
1138
|
+
// An epoch-boundary checkpoint pull-up can move the head's dependent root and stale the cached
|
|
1139
|
+
// head before block 0 of the new epoch is imported, making isProposerBoostSameDependentRoot()
|
|
1140
|
+
// wrong for that block. Recompute the head so it reflects the new checkpoint and the queued
|
|
1141
|
+
// votes — unless fast confirmation already did, to avoid a redundant head calculation.
|
|
1142
|
+
if (didUpdateCheckpoints && !didRecomputeHead) {
|
|
1143
|
+
this.updateHead();
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1089
1146
|
this.validatedAttestationDatas = new Set();
|
|
1090
1147
|
}
|
|
1091
1148
|
}
|
|
@@ -1596,7 +1653,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
1596
1653
|
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
|
|
1597
1654
|
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
|
|
1598
1655
|
if (node === undefined) {
|
|
1599
|
-
// this is called for head
|
|
1656
|
+
// this is called for head or the boosted block's parent, both of which should always be
|
|
1657
|
+
// in forkchoice, otherwise we have a serious error
|
|
1600
1658
|
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
|
|
1601
1659
|
}
|
|
1602
1660
|
|
|
@@ -1616,7 +1674,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1616
1674
|
// always 0. Return before fetching the state and walking the block's committees.
|
|
1617
1675
|
if (equivocatingIndices.size > 0) {
|
|
1618
1676
|
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
|
|
1619
|
-
// Only ever called on the head, so the state is always cached.
|
|
1677
|
+
// Only ever called on the head or the boosted block's parent, so the state is always cached.
|
|
1620
1678
|
// A miss is a broken invariant, not a recoverable state.
|
|
1621
1679
|
if (state === null) {
|
|
1622
1680
|
throw new ForkChoiceError({
|
|
@@ -1636,7 +1694,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1636
1694
|
// the justified state) - fall back to the head state's effective balance
|
|
1637
1695
|
balance = state.effectiveBalanceIncrements[validatorIndex];
|
|
1638
1696
|
}
|
|
1639
|
-
headWeight += balance;
|
|
1697
|
+
headWeight += BigInt(balance) * EFFECTIVE_BALANCE_INCREMENT_BIGINT;
|
|
1640
1698
|
}
|
|
1641
1699
|
}
|
|
1642
1700
|
}
|
|
@@ -1666,7 +1724,6 @@ export class ForkChoice implements IForkChoice {
|
|
|
1666
1724
|
|
|
1667
1725
|
// pre-gloas uses get_weight() (boost-inclusive), gloas uses get_attestation_score() (boost-excluded)
|
|
1668
1726
|
const parentWeight = isForkPostGloas(this.config.getForkName(node.slot)) ? node.attestationScore : node.weight;
|
|
1669
|
-
|
|
1670
1727
|
return parentWeight > parentThreshold;
|
|
1671
1728
|
}
|
|
1672
1729
|
|
|
@@ -1680,6 +1737,99 @@ export class ForkChoice implements IForkChoice {
|
|
|
1680
1737
|
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1681
1738
|
}
|
|
1682
1739
|
|
|
1740
|
+
/**
|
|
1741
|
+
* Return true if the block arrived before the PTC (payload-timeliness committee) deadline,
|
|
1742
|
+
* ie. block_timeliness[PTC_TIMELINESS_INDEX]. should_apply_proposer_boost uses this as the
|
|
1743
|
+
* definition of an "early" proposer equivocation: only a sibling seen by the PTC deadline
|
|
1744
|
+
* proves the proposer equivocated soon enough that honest nodes could have withheld the boost.
|
|
1745
|
+
*
|
|
1746
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#modified-record_block_timeliness
|
|
1747
|
+
*
|
|
1748
|
+
* Child class can overwrite this for testing purpose.
|
|
1749
|
+
*/
|
|
1750
|
+
protected isBlockPtcTimely(block: BeaconBlock, blockDelaySec: number): boolean {
|
|
1751
|
+
const ptcThresholdMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
|
|
1752
|
+
return this.fcStore.currentSlot === block.slot && blockDelaySec * 1000 < ptcThresholdMs;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Determine whether proposer boost should be applied to `proposerBoostRoot`.
|
|
1757
|
+
*
|
|
1758
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
|
|
1759
|
+
*
|
|
1760
|
+
* Pre-gloas blocks always receive the boost (unconditional, backward compatible). For gloas
|
|
1761
|
+
* blocks the boost is withheld when the parent is a weak block from the previous slot and the
|
|
1762
|
+
* proposer of that parent equivocated (published another PTC-timely block at the same slot).
|
|
1763
|
+
*/
|
|
1764
|
+
private shouldApplyProposerBoost(): boolean {
|
|
1765
|
+
if (!this.proposerBoostRoot) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const boostedBlock = this.getBlockHexDefaultStatus(this.proposerBoostRoot);
|
|
1770
|
+
// Pre-gloas blocks always get boost
|
|
1771
|
+
if (!boostedBlock || !isGloasBlock(boostedBlock)) {
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const parentBlock = this.getBlockHexDefaultStatus(boostedBlock.parentRoot);
|
|
1776
|
+
if (!parentBlock) {
|
|
1777
|
+
return true;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Apply proposer boost if parent is not from the previous slot
|
|
1781
|
+
if (parentBlock.slot + 1 < boostedBlock.slot) {
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Apply proposer boost if parent is not weak
|
|
1786
|
+
if (!this.isHeadWeak(parentBlock.blockRoot)) {
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Parent is weak and from the previous slot: apply boost only if there are no early
|
|
1791
|
+
// equivocations, ie. no other PTC-timely block at the parent's slot from the same proposer.
|
|
1792
|
+
return !this.protoArray.hasEquivocatingBlock(
|
|
1793
|
+
parentBlock.proposerIndex,
|
|
1794
|
+
parentBlock.slot,
|
|
1795
|
+
parentBlock.blockRoot,
|
|
1796
|
+
// Only a sibling seen before the PTC deadline counts. A late released one might not have been
|
|
1797
|
+
// seen by the proposer, so it cannot be expected to reorg it and is not denied the boost for it.
|
|
1798
|
+
true
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Return true if another block at the same slot from the same proposer is known to fork choice.
|
|
1804
|
+
*
|
|
1805
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/phase0/fork-choice.md#is_proposer_equivocation
|
|
1806
|
+
*/
|
|
1807
|
+
private isProposerEquivocation(block: ProtoBlock): boolean {
|
|
1808
|
+
// Any known sibling counts, timely or not. Timeliness only matters for withholding the boost from
|
|
1809
|
+
// the next proposer, the reorg itself is safe to attempt whenever the equivocation is visible.
|
|
1810
|
+
return this.protoArray.hasEquivocatingBlock(block.proposerIndex, block.slot, block.blockRoot, false);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* The proposer boost to apply to `proposerBoostRoot`, propagated up the branch by
|
|
1815
|
+
* applyScoreChanges() together with the deltas.
|
|
1816
|
+
*/
|
|
1817
|
+
private getProposerBoost(): {root: RootHex; score: bigint} | null {
|
|
1818
|
+
if (!this.proposerBoostRoot) {
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const proposerBoostScore =
|
|
1823
|
+
this.justifiedProposerBoostScore ??
|
|
1824
|
+
getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
1825
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
1826
|
+
committeePercent: this.config.PROPOSER_SCORE_BOOST,
|
|
1827
|
+
});
|
|
1828
|
+
this.justifiedProposerBoostScore = proposerBoostScore;
|
|
1829
|
+
|
|
1830
|
+
return {root: this.proposerBoostRoot, score: proposerBoostScore};
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1683
1833
|
/**
|
|
1684
1834
|
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
|
|
1685
1835
|
*/
|
|
@@ -1732,23 +1882,31 @@ export class ForkChoice implements IForkChoice {
|
|
|
1732
1882
|
* May need the justified balances of:
|
|
1733
1883
|
* - unrealizedJustified: Already available in `CheckpointWithBalance`
|
|
1734
1884
|
* Since this balances are already available the getter is just `() => balances`, without cache interaction
|
|
1885
|
+
*
|
|
1886
|
+
* @returns Whether either checkpoint was updated.
|
|
1735
1887
|
*/
|
|
1736
1888
|
private updateCheckpoints(
|
|
1737
1889
|
justifiedCheckpoint: CheckpointWithHex,
|
|
1738
1890
|
finalizedCheckpoint: CheckpointWithHex,
|
|
1739
1891
|
getJustifiedBalances: () => JustifiedBalances
|
|
1740
|
-
):
|
|
1892
|
+
): boolean {
|
|
1893
|
+
let updated = false;
|
|
1894
|
+
|
|
1741
1895
|
// Update justified checkpoint.
|
|
1742
1896
|
if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
|
|
1743
1897
|
this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
|
|
1744
1898
|
this.justifiedProposerBoostScore = null;
|
|
1899
|
+
updated = true;
|
|
1745
1900
|
}
|
|
1746
1901
|
|
|
1747
1902
|
// Update finalized checkpoint.
|
|
1748
1903
|
if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
|
|
1749
1904
|
this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
|
|
1750
1905
|
this.justifiedProposerBoostScore = null;
|
|
1906
|
+
updated = true;
|
|
1751
1907
|
}
|
|
1908
|
+
|
|
1909
|
+
return updated;
|
|
1752
1910
|
}
|
|
1753
1911
|
|
|
1754
1912
|
/**
|
|
@@ -2034,8 +2192,10 @@ export class ForkChoice implements IForkChoice {
|
|
|
2034
2192
|
* Equivalent to:
|
|
2035
2193
|
*
|
|
2036
2194
|
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_tick
|
|
2195
|
+
*
|
|
2196
|
+
* @returns Whether an epoch-boundary checkpoint was updated.
|
|
2037
2197
|
*/
|
|
2038
|
-
private onTick(time: Slot):
|
|
2198
|
+
private onTick(time: Slot): boolean {
|
|
2039
2199
|
const previousSlot = this.fcStore.currentSlot;
|
|
2040
2200
|
|
|
2041
2201
|
if (time > previousSlot + 1) {
|
|
@@ -2056,11 +2216,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2056
2216
|
|
|
2057
2217
|
// Not a new epoch, return.
|
|
2058
2218
|
if (computeSlotsSinceEpochStart(time) !== 0) {
|
|
2059
|
-
return;
|
|
2219
|
+
return false;
|
|
2060
2220
|
}
|
|
2061
2221
|
|
|
2062
|
-
// If a new epoch, pull-up justification and finalization from previous epoch
|
|
2063
|
-
this.updateCheckpoints(
|
|
2222
|
+
// If a new epoch, pull-up justification and finalization from previous epoch.
|
|
2223
|
+
return this.updateCheckpoints(
|
|
2064
2224
|
this.fcStore.unrealizedJustified.checkpoint,
|
|
2065
2225
|
this.fcStore.unrealizedFinalizedCheckpoint,
|
|
2066
2226
|
() => this.fcStore.unrealizedJustified.balances
|
|
@@ -2086,11 +2246,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
2086
2246
|
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
|
|
2087
2247
|
}
|
|
2088
2248
|
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
if (isAtEpochBoundary) {
|
|
2093
|
-
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.AtEpochBoundary};
|
|
2249
|
+
const isShufflingStable = isForkPostFulu(this.config.getForkName(slot)) || slot % SLOTS_PER_EPOCH !== 0;
|
|
2250
|
+
if (!isShufflingStable) {
|
|
2251
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotShufflingStable};
|
|
2094
2252
|
}
|
|
2095
2253
|
|
|
2096
2254
|
// No reorg if headBlock and parentBlock are not ffg competitive
|
|
@@ -2121,10 +2279,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2121
2279
|
return {prelimProposerHead};
|
|
2122
2280
|
}
|
|
2123
2281
|
|
|
2124
|
-
|
|
2282
|
+
/** Returns whether it recomputed the head, so the caller can avoid a redundant `updateHead()`. */
|
|
2283
|
+
private runFastConfirmation(): boolean {
|
|
2125
2284
|
const fastConfirmationRule = this.fastConfirmationRule;
|
|
2126
2285
|
const fastConfirmationContext = this.fastConfirmationContext;
|
|
2127
|
-
if (!fastConfirmationRule || !fastConfirmationContext) return;
|
|
2286
|
+
if (!fastConfirmationRule || !fastConfirmationContext) return false;
|
|
2128
2287
|
|
|
2129
2288
|
if (this.fastConfirmationPaused) {
|
|
2130
2289
|
// Keep consumers on a safe, available root while the rule is paused
|
|
@@ -2135,7 +2294,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
2135
2294
|
// Runs outside the timed try/catch below; a throw would escape to the clock listener
|
|
2136
2295
|
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
2137
2296
|
}
|
|
2138
|
-
return;
|
|
2297
|
+
return false;
|
|
2139
2298
|
}
|
|
2140
2299
|
|
|
2141
2300
|
withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
|
|
@@ -2158,6 +2317,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
2158
2317
|
);
|
|
2159
2318
|
}
|
|
2160
2319
|
});
|
|
2320
|
+
|
|
2321
|
+
return true;
|
|
2161
2322
|
}
|
|
2162
2323
|
|
|
2163
2324
|
private createFastConfirmationContext(): FastConfirmationContext {
|
|
@@ -2209,12 +2370,14 @@ export class ForkChoice implements IForkChoice {
|
|
|
2209
2370
|
}
|
|
2210
2371
|
}
|
|
2211
2372
|
|
|
2212
|
-
//
|
|
2373
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
|
|
2213
2374
|
// Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
|
|
2214
|
-
|
|
2375
|
+
function getCommitteeFraction(
|
|
2215
2376
|
justifiedTotalActiveBalanceByIncrement: number,
|
|
2216
2377
|
config: {slotsPerEpoch: number; committeePercent: number}
|
|
2217
|
-
):
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2378
|
+
): bigint {
|
|
2379
|
+
const committeeWeightGwei =
|
|
2380
|
+
(BigInt(justifiedTotalActiveBalanceByIncrement) * EFFECTIVE_BALANCE_INCREMENT_BIGINT) /
|
|
2381
|
+
BigInt(config.slotsPerEpoch);
|
|
2382
|
+
return (committeeWeightGwei * BigInt(config.committeePercent)) / 100n;
|
|
2220
2383
|
}
|
|
@@ -54,7 +54,7 @@ export enum NotReorgedReason {
|
|
|
54
54
|
HeadBlockIsTimely = "headBlockIsTimely",
|
|
55
55
|
ParentBlockNotAvailable = "parentBlockNotAvailable",
|
|
56
56
|
ProposerBoostReorgDisabled = "proposerBoostReorgDisabled",
|
|
57
|
-
|
|
57
|
+
NotShufflingStable = "notShufflingStable",
|
|
58
58
|
NotFFGCompetitive = "notFFGCompetitive",
|
|
59
59
|
ChainLongUnfinality = "chainLongUnfinality",
|
|
60
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
|
};
|