@lodestar/fork-choice 1.47.0-dev.4546ac1229 → 1.47.0-dev.467f8a72f2
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 +39 -1
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +171 -33
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/protoArray/interface.d.ts +3 -1
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +10 -1
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +27 -0
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/forkChoice.ts +205 -38
- package/src/protoArray/interface.ts +8 -1
- package/src/protoArray/protoArray.ts +36 -1
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ChainSafe/lodestar/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "1.47.0-dev.
|
|
14
|
+
"version": "1.47.0-dev.467f8a72f2",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"check-readme": "pnpm exec tsx ../../scripts/check_readme.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@chainsafe/ssz": "^1.6.
|
|
43
|
-
"@lodestar/config": "^1.47.0-dev.
|
|
44
|
-
"@lodestar/params": "^1.47.0-dev.
|
|
45
|
-
"@lodestar/state-transition": "^1.47.0-dev.
|
|
46
|
-
"@lodestar/types": "^1.47.0-dev.
|
|
47
|
-
"@lodestar/utils": "^1.47.0-dev.
|
|
42
|
+
"@chainsafe/ssz": "^1.6.3",
|
|
43
|
+
"@lodestar/config": "^1.47.0-dev.467f8a72f2",
|
|
44
|
+
"@lodestar/params": "^1.47.0-dev.467f8a72f2",
|
|
45
|
+
"@lodestar/state-transition": "^1.47.0-dev.467f8a72f2",
|
|
46
|
+
"@lodestar/types": "^1.47.0-dev.467f8a72f2",
|
|
47
|
+
"@lodestar/utils": "^1.47.0-dev.467f8a72f2"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"ethereum",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"beacon",
|
|
53
53
|
"blockchain"
|
|
54
54
|
],
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "d1ecccd7a8a218fbd449653fcb7c4cbf6b78ff9b"
|
|
56
56
|
}
|
|
@@ -365,6 +365,21 @@ export class ForkChoice implements IForkChoice {
|
|
|
365
365
|
return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
|
|
366
366
|
}
|
|
367
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
|
+
|
|
368
383
|
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
|
|
369
384
|
headBlock,
|
|
370
385
|
parentBlock,
|
|
@@ -375,9 +390,6 @@ export class ForkChoice implements IForkChoice {
|
|
|
375
390
|
return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
|
|
376
391
|
}
|
|
377
392
|
|
|
378
|
-
const currentTimeOk =
|
|
379
|
-
headBlock.slot === currentSlot ||
|
|
380
|
-
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
|
|
381
393
|
if (!currentTimeOk) {
|
|
382
394
|
return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
383
395
|
}
|
|
@@ -462,7 +474,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
462
474
|
* https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg
|
|
463
475
|
* This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()`
|
|
464
476
|
*
|
|
465
|
-
*
|
|
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
|
|
466
479
|
*/
|
|
467
480
|
getProposerHead(
|
|
468
481
|
headBlock: ProtoBlock,
|
|
@@ -493,6 +506,29 @@ export class ForkChoice implements IForkChoice {
|
|
|
493
506
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable};
|
|
494
507
|
}
|
|
495
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
|
+
|
|
496
532
|
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot);
|
|
497
533
|
|
|
498
534
|
if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) {
|
|
@@ -505,14 +541,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
505
541
|
}
|
|
506
542
|
|
|
507
543
|
// No reorg if attempted reorg is more than a single slot
|
|
508
|
-
// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
|
|
509
|
-
const currentTimeOk = headBlock.slot + 1 === slot;
|
|
510
544
|
if (!currentTimeOk) {
|
|
511
545
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
512
546
|
}
|
|
513
547
|
|
|
514
548
|
// No reorg if proposer boost is still in effect
|
|
515
|
-
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
|
|
516
549
|
if (!isProposerBoostWornOff) {
|
|
517
550
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff};
|
|
518
551
|
}
|
|
@@ -591,32 +624,48 @@ export class ForkChoice implements IForkChoice {
|
|
|
591
624
|
computeDeltasMetrics?.newVoteValidators.set(newVoteValidators);
|
|
592
625
|
|
|
593
626
|
this.balances = newBalances;
|
|
594
|
-
/**
|
|
595
|
-
* The structure in line with deltas to propagate boost up the branch
|
|
596
|
-
* starting from the proposerIndex
|
|
597
|
-
*/
|
|
598
|
-
let proposerBoost: {root: RootHex; score: bigint} | null = null;
|
|
599
|
-
if (this.opts?.proposerBoost && this.proposerBoostRoot) {
|
|
600
|
-
const proposerBoostScore =
|
|
601
|
-
this.justifiedProposerBoostScore ??
|
|
602
|
-
getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
603
|
-
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
604
|
-
committeePercent: this.config.PROPOSER_SCORE_BOOST,
|
|
605
|
-
});
|
|
606
|
-
proposerBoost = {root: this.proposerBoostRoot, score: proposerBoostScore};
|
|
607
|
-
this.justifiedProposerBoostScore = proposerBoostScore;
|
|
608
|
-
}
|
|
609
627
|
|
|
610
628
|
const currentSlot = this.fcStore.currentSlot;
|
|
611
|
-
|
|
612
|
-
attestationDeltas,
|
|
613
|
-
proposerBoost,
|
|
629
|
+
const checkpoints = {
|
|
614
630
|
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
615
631
|
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
616
632
|
finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
|
|
617
633
|
finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
|
|
618
634
|
currentSlot,
|
|
619
|
-
}
|
|
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
|
+
}
|
|
620
669
|
|
|
621
670
|
// findHead returns the ProtoNode representing the head
|
|
622
671
|
const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
|
|
@@ -826,6 +875,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
826
875
|
targetRoot: toRootHex(targetRoot),
|
|
827
876
|
stateRoot: toRootHex(block.stateRoot),
|
|
828
877
|
timeliness: isTimely,
|
|
878
|
+
ptcTimeliness: this.isBlockPtcTimely(block, blockDelaySec),
|
|
879
|
+
proposerIndex: block.proposerIndex,
|
|
829
880
|
|
|
830
881
|
justifiedEpoch: stateJustifiedEpoch,
|
|
831
882
|
justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root),
|
|
@@ -1078,11 +1129,20 @@ export class ForkChoice implements IForkChoice {
|
|
|
1078
1129
|
while (this.fcStore.currentSlot < currentSlot) {
|
|
1079
1130
|
const previousSlot = this.fcStore.currentSlot;
|
|
1080
1131
|
// Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
|
|
1081
|
-
this.onTick(previousSlot + 1);
|
|
1132
|
+
const didUpdateCheckpoints = this.onTick(previousSlot + 1);
|
|
1082
1133
|
this.queuedAttestationsPreviousSlot = 0;
|
|
1083
1134
|
// Process any attestations that might now be eligible before running FCR for this slot.
|
|
1084
1135
|
this.processAttestationQueue();
|
|
1085
|
-
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
|
+
|
|
1086
1146
|
this.validatedAttestationDatas = new Set();
|
|
1087
1147
|
}
|
|
1088
1148
|
}
|
|
@@ -1593,7 +1653,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
1593
1653
|
// Only ever called on a block already in fork choice, so a miss is a broken invariant.
|
|
1594
1654
|
const node = this.protoArray.getNodeDefaultStatus(blockRoot);
|
|
1595
1655
|
if (node === undefined) {
|
|
1596
|
-
// 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
|
|
1597
1658
|
throw new ForkChoiceError({code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, root: blockRoot});
|
|
1598
1659
|
}
|
|
1599
1660
|
|
|
@@ -1613,7 +1674,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1613
1674
|
// always 0. Return before fetching the state and walking the block's committees.
|
|
1614
1675
|
if (equivocatingIndices.size > 0) {
|
|
1615
1676
|
const state = this.fcStore.stateGetter({stateRoot: node.stateRoot});
|
|
1616
|
-
// 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.
|
|
1617
1678
|
// A miss is a broken invariant, not a recoverable state.
|
|
1618
1679
|
if (state === null) {
|
|
1619
1680
|
throw new ForkChoiceError({
|
|
@@ -1676,6 +1737,99 @@ export class ForkChoice implements IForkChoice {
|
|
|
1676
1737
|
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1677
1738
|
}
|
|
1678
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
|
+
|
|
1679
1833
|
/**
|
|
1680
1834
|
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
|
|
1681
1835
|
*/
|
|
@@ -1728,23 +1882,31 @@ export class ForkChoice implements IForkChoice {
|
|
|
1728
1882
|
* May need the justified balances of:
|
|
1729
1883
|
* - unrealizedJustified: Already available in `CheckpointWithBalance`
|
|
1730
1884
|
* Since this balances are already available the getter is just `() => balances`, without cache interaction
|
|
1885
|
+
*
|
|
1886
|
+
* @returns Whether either checkpoint was updated.
|
|
1731
1887
|
*/
|
|
1732
1888
|
private updateCheckpoints(
|
|
1733
1889
|
justifiedCheckpoint: CheckpointWithHex,
|
|
1734
1890
|
finalizedCheckpoint: CheckpointWithHex,
|
|
1735
1891
|
getJustifiedBalances: () => JustifiedBalances
|
|
1736
|
-
):
|
|
1892
|
+
): boolean {
|
|
1893
|
+
let updated = false;
|
|
1894
|
+
|
|
1737
1895
|
// Update justified checkpoint.
|
|
1738
1896
|
if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
|
|
1739
1897
|
this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
|
|
1740
1898
|
this.justifiedProposerBoostScore = null;
|
|
1899
|
+
updated = true;
|
|
1741
1900
|
}
|
|
1742
1901
|
|
|
1743
1902
|
// Update finalized checkpoint.
|
|
1744
1903
|
if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
|
|
1745
1904
|
this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
|
|
1746
1905
|
this.justifiedProposerBoostScore = null;
|
|
1906
|
+
updated = true;
|
|
1747
1907
|
}
|
|
1908
|
+
|
|
1909
|
+
return updated;
|
|
1748
1910
|
}
|
|
1749
1911
|
|
|
1750
1912
|
/**
|
|
@@ -2030,8 +2192,10 @@ export class ForkChoice implements IForkChoice {
|
|
|
2030
2192
|
* Equivalent to:
|
|
2031
2193
|
*
|
|
2032
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.
|
|
2033
2197
|
*/
|
|
2034
|
-
private onTick(time: Slot):
|
|
2198
|
+
private onTick(time: Slot): boolean {
|
|
2035
2199
|
const previousSlot = this.fcStore.currentSlot;
|
|
2036
2200
|
|
|
2037
2201
|
if (time > previousSlot + 1) {
|
|
@@ -2052,11 +2216,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2052
2216
|
|
|
2053
2217
|
// Not a new epoch, return.
|
|
2054
2218
|
if (computeSlotsSinceEpochStart(time) !== 0) {
|
|
2055
|
-
return;
|
|
2219
|
+
return false;
|
|
2056
2220
|
}
|
|
2057
2221
|
|
|
2058
|
-
// If a new epoch, pull-up justification and finalization from previous epoch
|
|
2059
|
-
this.updateCheckpoints(
|
|
2222
|
+
// If a new epoch, pull-up justification and finalization from previous epoch.
|
|
2223
|
+
return this.updateCheckpoints(
|
|
2060
2224
|
this.fcStore.unrealizedJustified.checkpoint,
|
|
2061
2225
|
this.fcStore.unrealizedFinalizedCheckpoint,
|
|
2062
2226
|
() => this.fcStore.unrealizedJustified.balances
|
|
@@ -2115,10 +2279,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
2115
2279
|
return {prelimProposerHead};
|
|
2116
2280
|
}
|
|
2117
2281
|
|
|
2118
|
-
|
|
2282
|
+
/** Returns whether it recomputed the head, so the caller can avoid a redundant `updateHead()`. */
|
|
2283
|
+
private runFastConfirmation(): boolean {
|
|
2119
2284
|
const fastConfirmationRule = this.fastConfirmationRule;
|
|
2120
2285
|
const fastConfirmationContext = this.fastConfirmationContext;
|
|
2121
|
-
if (!fastConfirmationRule || !fastConfirmationContext) return;
|
|
2286
|
+
if (!fastConfirmationRule || !fastConfirmationContext) return false;
|
|
2122
2287
|
|
|
2123
2288
|
if (this.fastConfirmationPaused) {
|
|
2124
2289
|
// Keep consumers on a safe, available root while the rule is paused
|
|
@@ -2129,7 +2294,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
2129
2294
|
// Runs outside the timed try/catch below; a throw would escape to the clock listener
|
|
2130
2295
|
this.logger?.debug("Fast confirmation notify failed", {slot: this.fcStore.currentSlot}, err as Error);
|
|
2131
2296
|
}
|
|
2132
|
-
return;
|
|
2297
|
+
return false;
|
|
2133
2298
|
}
|
|
2134
2299
|
|
|
2135
2300
|
withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
|
|
@@ -2152,6 +2317,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
2152
2317
|
);
|
|
2153
2318
|
}
|
|
2154
2319
|
});
|
|
2320
|
+
|
|
2321
|
+
return true;
|
|
2155
2322
|
}
|
|
2156
2323
|
|
|
2157
2324
|
private createFastConfirmationContext(): FastConfirmationContext {
|
|
@@ -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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {BitArray} from "@chainsafe/ssz";
|
|
2
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";
|
|
@@ -2007,6 +2007,41 @@ export class ProtoArray {
|
|
|
2007
2007
|
return this.getNodeByIndex(nodeIndex);
|
|
2008
2008
|
}
|
|
2009
2009
|
|
|
2010
|
+
/**
|
|
2011
|
+
* Return true if a block other than `excludeRoot` at `slot` was proposed by `proposerIndex`.
|
|
2012
|
+
* `should_apply_proposer_boost` only counts PTC-timely blocks (`ptcTimelyOnly`), `is_proposer_equivocation`
|
|
2013
|
+
* counts any known block.
|
|
2014
|
+
*
|
|
2015
|
+
* Iterates unique block roots (via the canonical variant) since `slot`, `proposerIndex` and
|
|
2016
|
+
* `ptcTimeliness` are block-level properties identical across payload-status variants.
|
|
2017
|
+
*/
|
|
2018
|
+
hasEquivocatingBlock(
|
|
2019
|
+
proposerIndex: ValidatorIndex,
|
|
2020
|
+
slot: Slot,
|
|
2021
|
+
excludeRoot: RootHex,
|
|
2022
|
+
ptcTimelyOnly: boolean
|
|
2023
|
+
): boolean {
|
|
2024
|
+
for (const root of this.indices.keys()) {
|
|
2025
|
+
if (root === excludeRoot) {
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
const nodeIndex = this.getDefaultNodeIndex(root);
|
|
2029
|
+
if (nodeIndex === undefined) {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
const node = this.nodes[nodeIndex];
|
|
2033
|
+
if (
|
|
2034
|
+
node !== undefined &&
|
|
2035
|
+
node.slot === slot &&
|
|
2036
|
+
node.proposerIndex === proposerIndex &&
|
|
2037
|
+
(node.ptcTimeliness || !ptcTimelyOnly)
|
|
2038
|
+
) {
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2010
2045
|
/**
|
|
2011
2046
|
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
2012
2047
|
*
|