@lodestar/fork-choice 1.51.0-dev.ab56d90f0c → 1.51.0-dev.ab8f3f6381
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/errors.d.ts +6 -1
- package/lib/forkChoice/errors.d.ts.map +1 -1
- package/lib/forkChoice/errors.js +1 -0
- package/lib/forkChoice/errors.js.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.js +19 -24
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +20 -19
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +82 -39
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +12 -4
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/metrics.d.ts +3 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +5 -0
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/interface.d.ts +2 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/interface.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +20 -4
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +115 -51
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +7 -7
- package/src/forkChoice/errors.ts +3 -1
- package/src/forkChoice/fastConfirmation/utils.ts +21 -20
- package/src/forkChoice/forkChoice.ts +89 -41
- package/src/forkChoice/interface.ts +11 -5
- package/src/metrics.ts +5 -0
- package/src/protoArray/interface.ts +2 -0
- package/src/protoArray/protoArray.ts +133 -56
|
@@ -86,8 +86,8 @@ export enum UpdateHeadOpt {
|
|
|
86
86
|
|
|
87
87
|
export type UpdateAndGetHeadOpt =
|
|
88
88
|
| {mode: UpdateHeadOpt.GetCanonicalHead}
|
|
89
|
-
| {mode: UpdateHeadOpt.GetProposerHead;
|
|
90
|
-
| {mode: UpdateHeadOpt.GetPredictedProposerHead;
|
|
89
|
+
| {mode: UpdateHeadOpt.GetProposerHead; msFromSlot: number; slot: Slot}
|
|
90
|
+
| {mode: UpdateHeadOpt.GetPredictedProposerHead; msFromSlot: number; slot: Slot};
|
|
91
91
|
|
|
92
92
|
// the initial vote epoch for all validators
|
|
93
93
|
const INIT_VOTE_SLOT: Slot = 0;
|
|
@@ -315,13 +315,13 @@ export class ForkChoice implements IForkChoice {
|
|
|
315
315
|
const canonicalHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead();
|
|
316
316
|
switch (mode) {
|
|
317
317
|
case UpdateHeadOpt.GetPredictedProposerHead:
|
|
318
|
-
return {head: this.predictProposerHead(canonicalHeadBlock, opt.
|
|
318
|
+
return {head: this.predictProposerHead(canonicalHeadBlock, opt.msFromSlot, opt.slot)};
|
|
319
319
|
case UpdateHeadOpt.GetProposerHead: {
|
|
320
320
|
const {
|
|
321
321
|
proposerHead: head,
|
|
322
322
|
isHeadTimely,
|
|
323
323
|
notReorgedReason,
|
|
324
|
-
} = this.getProposerHead(canonicalHeadBlock, opt.
|
|
324
|
+
} = this.getProposerHead(canonicalHeadBlock, opt.msFromSlot, opt.slot);
|
|
325
325
|
return {head, isHeadTimely, notReorgedReason};
|
|
326
326
|
}
|
|
327
327
|
case UpdateHeadOpt.GetCanonicalHead:
|
|
@@ -338,7 +338,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
338
338
|
// Note when proposer boost reorg is disabled, it always returns false
|
|
339
339
|
shouldOverrideForkChoiceUpdate(
|
|
340
340
|
headBlock: ProtoBlock,
|
|
341
|
-
|
|
341
|
+
msFromSlot: number,
|
|
342
342
|
currentSlot: Slot
|
|
343
343
|
): ShouldOverrideForkChoiceUpdateResult {
|
|
344
344
|
if (headBlock === null) {
|
|
@@ -369,7 +369,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
369
369
|
|
|
370
370
|
const currentTimeOk =
|
|
371
371
|
headBlock.slot === currentSlot ||
|
|
372
|
-
(proposalSlot === currentSlot && this.isProposingOnTime(
|
|
372
|
+
(proposalSlot === currentSlot && this.isProposingOnTime(msFromSlot, currentSlot));
|
|
373
373
|
|
|
374
374
|
// Mirror the proposer equivocation branch of getProposerHead(). The head slot's attestations are
|
|
375
375
|
// still queued at this point so the head is assumed weak, same as for the regular branch below.
|
|
@@ -455,7 +455,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
455
455
|
* By calling this function, we assume we are the proposer of next slot
|
|
456
456
|
*
|
|
457
457
|
*/
|
|
458
|
-
predictProposerHead(headBlock: ProtoBlock,
|
|
458
|
+
predictProposerHead(headBlock: ProtoBlock, msFromSlot: number, currentSlot: Slot): ProtoBlock {
|
|
459
459
|
const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
|
|
460
460
|
// Skip re-org attempt if proposer boost (reorg) are disabled
|
|
461
461
|
if (!proposerBoost || !proposerBoostReorg) {
|
|
@@ -468,7 +468,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
const blockRoot = headBlock.blockRoot;
|
|
471
|
-
const result = this.shouldOverrideForkChoiceUpdate(headBlock,
|
|
471
|
+
const result = this.shouldOverrideForkChoiceUpdate(headBlock, msFromSlot, currentSlot);
|
|
472
472
|
|
|
473
473
|
if (result.shouldOverrideFcu) {
|
|
474
474
|
this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
|
|
@@ -499,7 +499,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
499
499
|
*/
|
|
500
500
|
getProposerHead(
|
|
501
501
|
headBlock: ProtoBlock,
|
|
502
|
-
|
|
502
|
+
msFromSlot: number,
|
|
503
503
|
slot: Slot
|
|
504
504
|
): {proposerHead: ProtoBlock; isHeadTimely: boolean; notReorgedReason?: NotReorgedReason} {
|
|
505
505
|
const isHeadTimely = headBlock.timeliness;
|
|
@@ -556,7 +556,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
556
556
|
}
|
|
557
557
|
|
|
558
558
|
// Only re-org if we are proposing on-time
|
|
559
|
-
if (!this.isProposingOnTime(
|
|
559
|
+
if (!this.isProposingOnTime(msFromSlot, slot)) {
|
|
560
560
|
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.NotProposingOnTime};
|
|
561
561
|
}
|
|
562
562
|
|
|
@@ -693,7 +693,20 @@ export class ForkChoice implements IForkChoice {
|
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
// findHead returns the ProtoNode representing the head
|
|
696
|
-
|
|
696
|
+
let head: ProtoNode;
|
|
697
|
+
try {
|
|
698
|
+
head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
// A poisoned tree never recovers, shut the node down instead of failing every head computation
|
|
701
|
+
if (
|
|
702
|
+
e instanceof ProtoArrayError &&
|
|
703
|
+
(e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE ||
|
|
704
|
+
e.type.code === ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS)
|
|
705
|
+
) {
|
|
706
|
+
this.irrecoverableError = e;
|
|
707
|
+
}
|
|
708
|
+
throw e;
|
|
709
|
+
}
|
|
697
710
|
|
|
698
711
|
this.head = head;
|
|
699
712
|
return this.head;
|
|
@@ -753,12 +766,15 @@ export class ForkChoice implements IForkChoice {
|
|
|
753
766
|
*
|
|
754
767
|
* `justifiedBalances` balances of justified state which is updated synchronously.
|
|
755
768
|
* This ensures that the forkchoice is never out of sync.
|
|
769
|
+
*
|
|
770
|
+
* A block whose parent payload chain was invalidated is rejected. `executionStatus` is ignored for a gloas
|
|
771
|
+
* block: it has no payload of its own and inherits the status of the parent variant it builds on.
|
|
756
772
|
*/
|
|
757
773
|
onBlock(
|
|
758
774
|
block: BeaconBlock,
|
|
759
775
|
state: IBeaconStateView,
|
|
760
|
-
|
|
761
|
-
|
|
776
|
+
receiveDelayMs: number,
|
|
777
|
+
importDelayMs: number,
|
|
762
778
|
currentSlot: Slot,
|
|
763
779
|
executionStatus: BlockExecutionStatus,
|
|
764
780
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
@@ -781,6 +797,19 @@ export class ForkChoice implements IForkChoice {
|
|
|
781
797
|
});
|
|
782
798
|
}
|
|
783
799
|
|
|
800
|
+
// An execution engine verdict can invalidate the parent while this block is being verified. Inserting the
|
|
801
|
+
// block would put a viable node below an invalidated one, or poison the tree if this block is VALID
|
|
802
|
+
if (parentBlock.executionStatus === ExecutionStatus.Invalid) {
|
|
803
|
+
throw new ForkChoiceError({
|
|
804
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
805
|
+
err: {
|
|
806
|
+
code: InvalidBlockCode.PARENT_EXECUTION_INVALID,
|
|
807
|
+
root: parentRootHex,
|
|
808
|
+
hash: parentHashHex,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
784
813
|
// Blocks cannot be in the future. If they are, their consideration must be delayed until
|
|
785
814
|
// the are in the past.
|
|
786
815
|
//
|
|
@@ -830,7 +859,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
830
859
|
// Decide whether this block should receive proposer boost, and whether the block is timely.
|
|
831
860
|
// The store field `this.proposerBoostRoot` and `updateCheckpoints()` are mutated only after
|
|
832
861
|
// `protoArray.onBlock()` succeeds
|
|
833
|
-
const isTimely = this.isBlockReceivedTimely(block,
|
|
862
|
+
const isTimely = this.isBlockReceivedTimely(block, receiveDelayMs);
|
|
834
863
|
const isProposerBoostBlock =
|
|
835
864
|
this.opts?.proposerBoost === true &&
|
|
836
865
|
isTimely &&
|
|
@@ -849,14 +878,17 @@ export class ForkChoice implements IForkChoice {
|
|
|
849
878
|
const blockEpoch = computeEpochAtSlot(slot);
|
|
850
879
|
|
|
851
880
|
// same logic to compute_pulled_up_tip in the spec, making it inline because of reusing variables
|
|
852
|
-
// If the parent checkpoints are already at the same epoch as the
|
|
853
|
-
// it's impossible for the unrealized checkpoints to differ from the parent's.
|
|
854
|
-
// holds true because:
|
|
881
|
+
// If the block has no slashings and the parent checkpoints are already at the same epoch as the
|
|
882
|
+
// block being imported, it's impossible for the unrealized checkpoints to differ from the parent's.
|
|
883
|
+
// This holds true because:
|
|
855
884
|
//
|
|
856
885
|
// 1. A child block cannot have lower FFG checkpoints than its parent.
|
|
857
886
|
// 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`.
|
|
858
887
|
// 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`.
|
|
859
888
|
//
|
|
889
|
+
// Slashings are excluded because they can reduce the unslashed target balance of the child state
|
|
890
|
+
// and therefore lower its unrealized checkpoints.
|
|
891
|
+
//
|
|
860
892
|
// This is an optimization. It should reduce the amount of times we run
|
|
861
893
|
// `process_justification_and_finalization` by approximately 1/3rd when the chain is
|
|
862
894
|
// performing optimally.
|
|
@@ -864,6 +896,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
864
896
|
let unrealizedFinalizedCheckpoint: CheckpointWithHex;
|
|
865
897
|
if (this.opts?.computeUnrealized) {
|
|
866
898
|
if (
|
|
899
|
+
block.body.proposerSlashings.length === 0 &&
|
|
900
|
+
block.body.attesterSlashings.length === 0 &&
|
|
867
901
|
parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
|
|
868
902
|
parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch
|
|
869
903
|
) {
|
|
@@ -901,8 +935,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
901
935
|
targetRoot: toRootHex(targetRoot),
|
|
902
936
|
stateRoot: toRootHex(block.stateRoot),
|
|
903
937
|
timeliness: isTimely,
|
|
904
|
-
ptcTimeliness: this.isBlockPtcTimely(block,
|
|
905
|
-
importedTimely: this.isBlockImportedTimely(block,
|
|
938
|
+
ptcTimeliness: this.isBlockPtcTimely(block, receiveDelayMs),
|
|
939
|
+
importedTimely: this.isBlockImportedTimely(block, importDelayMs),
|
|
906
940
|
proposerIndex: block.proposerIndex,
|
|
907
941
|
|
|
908
942
|
justifiedEpoch: stateJustifiedEpoch,
|
|
@@ -937,7 +971,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
937
971
|
executionPayloadBlockHash: parentBlockHashFromBid,
|
|
938
972
|
executionPayloadNumber: parentMeta.number,
|
|
939
973
|
executionPayloadGasLimit: parentMeta.gasLimit,
|
|
940
|
-
|
|
974
|
+
// The parent's default (PENDING) variant does not reflect a SYNCING parent payload, inheriting
|
|
975
|
+
// from the variant matching the bid's parent block hash keeps that payload from being marked VALID
|
|
976
|
+
executionStatus: this.getPostMergeExecStatus(parentBlock.executionStatus),
|
|
941
977
|
dataAvailabilityStatus,
|
|
942
978
|
};
|
|
943
979
|
})()
|
|
@@ -998,14 +1034,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
998
1034
|
*
|
|
999
1035
|
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
|
|
1000
1036
|
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* too expensive to call again.
|
|
1004
|
-
*
|
|
1005
|
-
* ## Notes:
|
|
1006
|
-
*
|
|
1007
|
-
* The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
|
|
1008
|
-
* will not be run here.
|
|
1037
|
+
* Does not run `is_valid_indexed_attestation`. The caller must ensure all of its
|
|
1038
|
+
* validity requirements except index ordering: `attestingIndices` may be unsorted.
|
|
1009
1039
|
*/
|
|
1010
1040
|
onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void {
|
|
1011
1041
|
// Ignore any attestations to the zero hash.
|
|
@@ -1144,6 +1174,19 @@ export class ForkChoice implements IForkChoice {
|
|
|
1144
1174
|
executionStatus: PayloadExecutionStatus,
|
|
1145
1175
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
1146
1176
|
): void {
|
|
1177
|
+
// Same guard as onBlock: the PENDING variant carries the status of the payload chain this block builds on
|
|
1178
|
+
const pendingBlock = this.getBlockHexDefaultStatus(blockRoot);
|
|
1179
|
+
if (pendingBlock?.executionStatus === ExecutionStatus.Invalid) {
|
|
1180
|
+
throw new ForkChoiceError({
|
|
1181
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
1182
|
+
err: {
|
|
1183
|
+
code: InvalidBlockCode.PARENT_EXECUTION_INVALID,
|
|
1184
|
+
root: pendingBlock.parentRoot,
|
|
1185
|
+
hash: pendingBlock.executionPayloadBlockHash,
|
|
1186
|
+
},
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1147
1190
|
this.protoArray.onExecutionPayload(
|
|
1148
1191
|
blockRoot,
|
|
1149
1192
|
this.fcStore.currentSlot,
|
|
@@ -1432,7 +1475,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1432
1475
|
*
|
|
1433
1476
|
* `ancestors` is the raw walk and includes the previous finalized block as its last element —
|
|
1434
1477
|
* callers that don't want the boundary should slice it off themselves.
|
|
1435
|
-
*
|
|
1478
|
+
* For Gloas parent blocks, `ancestors` contains the selected EMPTY or FULL variant.
|
|
1479
|
+
* The other variant, if present in the traversed range, goes into `nonAncestors` with the same root.
|
|
1480
|
+
* Exclude roots present in `ancestors` before pruning by block root.
|
|
1436
1481
|
*/
|
|
1437
1482
|
getAllAncestorAndNonAncestorBlocks(
|
|
1438
1483
|
blockRoot: RootHex,
|
|
@@ -1540,7 +1585,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1540
1585
|
|
|
1541
1586
|
/** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
|
|
1542
1587
|
getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] {
|
|
1543
|
-
return this.protoArray.nodes.filter(
|
|
1588
|
+
return this.protoArray.nodes.filter(
|
|
1589
|
+
(node) => node.parentRoot === parentRoot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)
|
|
1590
|
+
);
|
|
1544
1591
|
}
|
|
1545
1592
|
|
|
1546
1593
|
/** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
|
|
@@ -1549,7 +1596,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
1549
1596
|
const blocksAtSlot: ProtoBlock[] = [];
|
|
1550
1597
|
for (let i = 0, len = nodes.length; i < len; i++) {
|
|
1551
1598
|
const node = nodes[i];
|
|
1552
|
-
if (node.slot === slot) {
|
|
1599
|
+
if (node.slot === slot && (!isGloasBlock(node) || node.payloadStatus === PayloadStatus.PENDING)) {
|
|
1553
1600
|
blocksAtSlot.push(node);
|
|
1554
1601
|
}
|
|
1555
1602
|
}
|
|
@@ -1586,13 +1633,14 @@ export class ForkChoice implements IForkChoice {
|
|
|
1586
1633
|
* justified's status as well as validate the terminal conditions if terminal block
|
|
1587
1634
|
* becomes valid
|
|
1588
1635
|
*/
|
|
1589
|
-
validateLatestHash(execResponse: LVHExecResponse):
|
|
1636
|
+
validateLatestHash(execResponse: LVHExecResponse, opts?: {strict?: boolean}): boolean {
|
|
1590
1637
|
try {
|
|
1591
|
-
this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot);
|
|
1638
|
+
return this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot, opts);
|
|
1592
1639
|
} catch (e) {
|
|
1593
1640
|
if (e instanceof ProtoArrayError && e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE) {
|
|
1594
1641
|
this.irrecoverableError = e;
|
|
1595
1642
|
}
|
|
1643
|
+
return false;
|
|
1596
1644
|
}
|
|
1597
1645
|
}
|
|
1598
1646
|
|
|
@@ -1775,9 +1823,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1775
1823
|
* Return true if the block is timely for the current slot.
|
|
1776
1824
|
* Child class can overwrite this for testing purpose.
|
|
1777
1825
|
*/
|
|
1778
|
-
protected isBlockReceivedTimely(block: BeaconBlock,
|
|
1826
|
+
protected isBlockReceivedTimely(block: BeaconBlock, blockDelayMs: number): boolean {
|
|
1779
1827
|
const fork = this.config.getForkName(block.slot);
|
|
1780
|
-
const isBeforeLateBlockCutoff =
|
|
1828
|
+
const isBeforeLateBlockCutoff = blockDelayMs < this.config.getAttestationDueMs(fork);
|
|
1781
1829
|
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1782
1830
|
}
|
|
1783
1831
|
|
|
@@ -1791,12 +1839,12 @@ export class ForkChoice implements IForkChoice {
|
|
|
1791
1839
|
*
|
|
1792
1840
|
* Child class can overwrite this for testing purpose.
|
|
1793
1841
|
*/
|
|
1794
|
-
protected isBlockPtcTimely(block: BeaconBlock,
|
|
1842
|
+
protected isBlockPtcTimely(block: BeaconBlock, blockDelayMs: number): boolean {
|
|
1795
1843
|
const ptcThresholdMs = this.config.getSlotComponentDurationMs(
|
|
1796
1844
|
this.config.getForkName(block.slot),
|
|
1797
1845
|
this.config.PAYLOAD_ATTESTATION_DUE_BPS
|
|
1798
1846
|
);
|
|
1799
|
-
return this.fcStore.currentSlot === block.slot &&
|
|
1847
|
+
return this.fcStore.currentSlot === block.slot && blockDelayMs < ptcThresholdMs;
|
|
1800
1848
|
}
|
|
1801
1849
|
|
|
1802
1850
|
/**
|
|
@@ -1804,9 +1852,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
1804
1852
|
* block was imported in a timely manner for its own slot.
|
|
1805
1853
|
* This is not part of the spec, we use this to determine late canonical blocks.
|
|
1806
1854
|
*/
|
|
1807
|
-
protected isBlockImportedTimely(block: BeaconBlock,
|
|
1855
|
+
protected isBlockImportedTimely(block: BeaconBlock, importDelayMs: number): boolean {
|
|
1808
1856
|
const fork = this.config.getForkName(block.slot);
|
|
1809
|
-
const isBeforeLateBlockCutoff =
|
|
1857
|
+
const isBeforeLateBlockCutoff = importDelayMs < this.config.getAttestationDueMs(fork);
|
|
1810
1858
|
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1811
1859
|
}
|
|
1812
1860
|
|
|
@@ -1891,10 +1939,10 @@ export class ForkChoice implements IForkChoice {
|
|
|
1891
1939
|
/**
|
|
1892
1940
|
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
|
|
1893
1941
|
*/
|
|
1894
|
-
private isProposingOnTime(
|
|
1942
|
+
private isProposingOnTime(msFromSlot: number, slot: Slot): boolean {
|
|
1895
1943
|
const fork = this.config.getForkName(slot);
|
|
1896
1944
|
const proposerReorgCutoff = this.config.getProposerReorgCutoffMs(fork);
|
|
1897
|
-
return
|
|
1945
|
+
return msFromSlot <= proposerReorgCutoff;
|
|
1898
1946
|
}
|
|
1899
1947
|
|
|
1900
1948
|
private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge {
|
|
@@ -123,7 +123,7 @@ export interface IForkChoice {
|
|
|
123
123
|
*/
|
|
124
124
|
shouldOverrideForkChoiceUpdate(
|
|
125
125
|
headBlock: ProtoBlock,
|
|
126
|
-
|
|
126
|
+
msFromSlot: number,
|
|
127
127
|
currentSlot: Slot
|
|
128
128
|
): ShouldOverrideForkChoiceUpdateResult;
|
|
129
129
|
/**
|
|
@@ -155,12 +155,15 @@ export interface IForkChoice {
|
|
|
155
155
|
* ## Notes:
|
|
156
156
|
*
|
|
157
157
|
* The supplied block **must** pass the `state_transition` function as it will not be run here.
|
|
158
|
+
*
|
|
159
|
+
* Throws `InvalidBlockCode.PARENT_EXECUTION_INVALID` if the parent payload chain was invalidated.
|
|
160
|
+
* `executionStatus` is ignored for a gloas block, it inherits the status of the parent variant it builds on.
|
|
158
161
|
*/
|
|
159
162
|
onBlock(
|
|
160
163
|
block: BeaconBlock,
|
|
161
164
|
state: IBeaconStateView,
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
receiveDelayMs: number,
|
|
166
|
+
importDelayMs: number,
|
|
164
167
|
currentSlot: Slot,
|
|
165
168
|
executionStatus: BlockExecutionStatus,
|
|
166
169
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
@@ -350,14 +353,17 @@ export interface IForkChoice {
|
|
|
350
353
|
* `CheckpointWithHex` / finalized root without a specific payload-status variant in mind.
|
|
351
354
|
*/
|
|
352
355
|
forwardIterateDescendantsDefaultStatus(blockRoot: RootHex): IterableIterator<ProtoBlock>;
|
|
356
|
+
/** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
|
|
353
357
|
getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[];
|
|
358
|
+
/** One summary per block, its default variant (FULL pre-Gloas, PENDING for Gloas) */
|
|
354
359
|
getBlockSummariesAtSlot(slot: Slot): ProtoBlock[];
|
|
355
360
|
/** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
|
|
356
361
|
getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult;
|
|
357
362
|
/**
|
|
358
|
-
*
|
|
363
|
+
* Apply an execution engine verdict with a latest valid hash, see `ProtoArray.validateLatestHash`.
|
|
364
|
+
* Returns whether any execution status changed. A consensus failure is recorded in `irrecoverableError`.
|
|
359
365
|
*/
|
|
360
|
-
validateLatestHash(execResponse: LVHExecResponse):
|
|
366
|
+
validateLatestHash(execResponse: LVHExecResponse, opts?: {strict?: boolean}): boolean;
|
|
361
367
|
|
|
362
368
|
/**
|
|
363
369
|
* A dependent root is the block root of the last block before the state transition that decided a specific shuffling
|
package/src/metrics.ts
CHANGED
|
@@ -32,6 +32,11 @@ export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
|
|
|
32
32
|
name: "beacon_fork_choice_changed_head_total",
|
|
33
33
|
help: "Count of occasions fork choice has found a new head",
|
|
34
34
|
}),
|
|
35
|
+
executionInvalidations: register.gauge<{source: string}>({
|
|
36
|
+
name: "beacon_fork_choice_execution_invalidations_total",
|
|
37
|
+
help: "Count of execution engine INVALID verdicts applied to fork choice, by verdict source",
|
|
38
|
+
labelNames: ["source"],
|
|
39
|
+
}),
|
|
35
40
|
reorg: register.gauge({
|
|
36
41
|
name: "beacon_fork_choice_reorg_total",
|
|
37
42
|
help: "Count of occasions fork choice has switched to a different chain",
|
|
@@ -55,6 +55,8 @@ export function isGloasBlock(block: ProtoBlock): block is ProtoBlock & {parentBl
|
|
|
55
55
|
export type LVHValidResponse = {
|
|
56
56
|
executionStatus: ExecutionStatus.Valid;
|
|
57
57
|
latestValidExecHash: RootHex;
|
|
58
|
+
/** Resolves the exact node to validate when several nodes carry the same payload hash (gloas variants) */
|
|
59
|
+
blockRoot?: RootHex;
|
|
58
60
|
};
|
|
59
61
|
export type LVHInvalidResponse = {
|
|
60
62
|
executionStatus: ExecutionStatus.Invalid;
|
|
@@ -1019,12 +1019,23 @@ export class ProtoArray {
|
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
1021
|
/**
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1022
|
+
* Apply an execution engine verdict with a latest valid hash (LVH) to the tree. VALID flips Syncing nodes up to the
|
|
1023
|
+
* LVH node. INVALID invalidates the node identified by `invalidateFromParentBlockRoot/Hash` and its ancestors up to
|
|
1024
|
+
* the LVH node (exclusive), then every descendant. Returns whether any status changed.
|
|
1025
|
+
*
|
|
1026
|
+
* A null or unknown LVH means the engine only vouches for the queried payload being invalid: the queried node and
|
|
1027
|
+
* the nodes that inherit that payload (gloas PENDING and EMPTY variants) are invalidated, nothing above them.
|
|
1028
|
+
*
|
|
1029
|
+
* `strict` keeps the consensus failure semantics for verdicts about a payload the engine just processed: a VALID
|
|
1030
|
+
* or PreMerge node on the invalidated chain poisons the tree. Verdicts from forkchoiceUpdated or about an ancestor
|
|
1031
|
+
* chain are applied with `strict: false`: a node the engine already validated is never invalidated and the justified
|
|
1032
|
+
* node is never crossed. If the walk to the LVH meets either, the LVH is treated as unknown.
|
|
1025
1033
|
*/
|
|
1026
|
-
|
|
1027
|
-
|
|
1034
|
+
validateLatestHash(
|
|
1035
|
+
execResponse: LVHExecResponse,
|
|
1036
|
+
currentSlot: Slot,
|
|
1037
|
+
{strict = true}: {strict?: boolean} = {}
|
|
1038
|
+
): boolean {
|
|
1028
1039
|
// Look reverse because its highly likely node with latestValidExecHash is towards the
|
|
1029
1040
|
// the leaves of the forkchoice
|
|
1030
1041
|
//
|
|
@@ -1035,71 +1046,128 @@ export class ProtoArray {
|
|
|
1035
1046
|
// https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167
|
|
1036
1047
|
|
|
1037
1048
|
if (execResponse.executionStatus === ExecutionStatus.Valid) {
|
|
1038
|
-
const {latestValidExecHash} = execResponse;
|
|
1049
|
+
const {latestValidExecHash, blockRoot} = execResponse;
|
|
1039
1050
|
// We use -1 for denoting not found
|
|
1040
1051
|
let latestValidHashIndex = -1;
|
|
1041
1052
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1053
|
+
if (blockRoot !== undefined) {
|
|
1054
|
+
latestValidHashIndex = this.getNodeIndexByRootAndBlockHash(blockRoot, latestValidExecHash) ?? -1;
|
|
1055
|
+
} else {
|
|
1056
|
+
for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
|
|
1057
|
+
if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
|
|
1058
|
+
latestValidHashIndex = nodeIndex;
|
|
1059
|
+
// We found the block corresponding to latestValidHashIndex, exit the loop
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1047
1062
|
}
|
|
1048
1063
|
}
|
|
1049
1064
|
|
|
1050
1065
|
// We are trying to be as forgiving as possible here because ideally latestValidHashIndex
|
|
1051
1066
|
// should be found in the forkchoice
|
|
1052
|
-
if (latestValidHashIndex
|
|
1053
|
-
|
|
1067
|
+
if (latestValidHashIndex < 0) {
|
|
1068
|
+
return false;
|
|
1054
1069
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
invalidateFromParentBlockHash
|
|
1073
|
-
);
|
|
1074
|
-
if (invalidateFromParentIndex === undefined) {
|
|
1075
|
-
throw Error(
|
|
1076
|
-
`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
|
|
1077
|
-
);
|
|
1070
|
+
const {executionStatus} = this.nodes[latestValidHashIndex];
|
|
1071
|
+
if (executionStatus === ExecutionStatus.Valid || executionStatus === ExecutionStatus.PreMerge) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
if (!strict) {
|
|
1075
|
+
// The walk up flips every Syncing ancestor, a lenient verdict must not contradict an invalidated one
|
|
1076
|
+
let nodeIndex: number | undefined = latestValidHashIndex;
|
|
1077
|
+
while (nodeIndex !== undefined) {
|
|
1078
|
+
const node: ProtoNode = this.nodes[nodeIndex];
|
|
1079
|
+
if (node.executionStatus === ExecutionStatus.Valid || node.executionStatus === ExecutionStatus.PreMerge) {
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
if (node.executionStatus === ExecutionStatus.Invalid) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
nodeIndex = node.parent;
|
|
1086
|
+
}
|
|
1078
1087
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1088
|
+
this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// In case of invalidation, ideally:
|
|
1093
|
+
// i) Find the invalid payload
|
|
1094
|
+
// ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
|
|
1095
|
+
// iii) Obtain a chain [Last_known_valid_node, ...., LVH]
|
|
1096
|
+
//
|
|
1097
|
+
// Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
|
|
1098
|
+
// gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
|
|
1099
|
+
// as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
|
|
1100
|
+
// So we currently ignore this chain and hope eventually it gets resolved
|
|
1101
|
+
//
|
|
1102
|
+
// Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
|
|
1103
|
+
// if its in fcU.
|
|
1104
|
+
//
|
|
1105
|
+
const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
|
|
1106
|
+
const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
|
|
1107
|
+
invalidateFromParentBlockRoot,
|
|
1108
|
+
invalidateFromParentBlockHash
|
|
1109
|
+
);
|
|
1110
|
+
if (invalidateFromParentIndex === undefined) {
|
|
1111
|
+
throw Error(
|
|
1112
|
+
`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (!strict && this.nodes[invalidateFromParentIndex].executionStatus !== ExecutionStatus.Syncing) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
let latestValidHashIndex =
|
|
1120
|
+
latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
|
|
1121
|
+
if (latestValidHashIndex === null) {
|
|
1122
|
+
if (strict) {
|
|
1123
|
+
// The strict from node is the parent of the rejected payload and may be valid, never guess here
|
|
1098
1124
|
throw Error(`Unable to find latestValidExecHash=${latestValidExecHash} in the forkchoice`);
|
|
1099
1125
|
}
|
|
1126
|
+
latestValidHashIndex = this.getUnknownLVHIndex(invalidateFromParentIndex);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (!strict) {
|
|
1130
|
+
// Never invalidate the justified block, the EMPTY variant sharing its chain, or anything at or below it in
|
|
1131
|
+
// the tree. The justified block's own payload (FULL variant) is not part of the checkpoint
|
|
1132
|
+
const justifiedIndex = this.getDefaultNodeIndex(this.justifiedRoot) ?? -1;
|
|
1133
|
+
let nodeIndex: number | undefined = invalidateFromParentIndex;
|
|
1134
|
+
while (nodeIndex !== undefined && nodeIndex > latestValidHashIndex) {
|
|
1135
|
+
const node: ProtoNode = this.nodes[nodeIndex];
|
|
1136
|
+
if (
|
|
1137
|
+
node.executionStatus !== ExecutionStatus.Syncing ||
|
|
1138
|
+
nodeIndex <= justifiedIndex ||
|
|
1139
|
+
(node.blockRoot === this.justifiedRoot && node.payloadStatus === PayloadStatus.EMPTY)
|
|
1140
|
+
) {
|
|
1141
|
+
// Conflicting LVH: fall back to the unknown LVH bound, but never cross this node
|
|
1142
|
+
latestValidHashIndex = Math.max(nodeIndex, this.getUnknownLVHIndex(invalidateFromParentIndex));
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
nodeIndex = node.parent;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1100
1148
|
|
|
1101
|
-
|
|
1149
|
+
if (invalidateFromParentIndex <= latestValidHashIndex) {
|
|
1150
|
+
return false;
|
|
1102
1151
|
}
|
|
1152
|
+
|
|
1153
|
+
this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Upper bound (exclusive) for invalidating from `fromIndex` when the engine gave no usable LVH: the parent of the
|
|
1159
|
+
* last ancestor that still inherits the queried payload. Pre-gloas this is the parent of `fromIndex`. For a gloas
|
|
1160
|
+
* EMPTY or PENDING node it reaches up to the FULL node that owns the payload. -1 when the chain reaches the root.
|
|
1161
|
+
*/
|
|
1162
|
+
private getUnknownLVHIndex(fromIndex: number): number {
|
|
1163
|
+
const payloadBlockHash = this.nodes[fromIndex].executionPayloadBlockHash;
|
|
1164
|
+
let nodeIndex = fromIndex;
|
|
1165
|
+
let parentIndex = this.nodes[nodeIndex].parent;
|
|
1166
|
+
while (parentIndex !== undefined && this.nodes[parentIndex].executionPayloadBlockHash === payloadBlockHash) {
|
|
1167
|
+
nodeIndex = parentIndex;
|
|
1168
|
+
parentIndex = this.nodes[nodeIndex].parent;
|
|
1169
|
+
}
|
|
1170
|
+
return parentIndex ?? -1;
|
|
1103
1171
|
}
|
|
1104
1172
|
|
|
1105
1173
|
private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
|
|
@@ -1295,6 +1363,15 @@ export class ProtoArray {
|
|
|
1295
1363
|
|
|
1296
1364
|
// For Gloas: check if from previous slot
|
|
1297
1365
|
if (node.slot + 1 !== currentSlot) {
|
|
1366
|
+
// During the block's own slot a payload known to not satisfy the inclusion list
|
|
1367
|
+
// constraints ranks below its EMPTY variant, as it will from the next slot on
|
|
1368
|
+
if (
|
|
1369
|
+
node.slot === currentSlot &&
|
|
1370
|
+
node.payloadStatus === PayloadStatus.FULL &&
|
|
1371
|
+
this.payloadInclusionListSatisfaction.get(node.blockRoot) === false
|
|
1372
|
+
) {
|
|
1373
|
+
return PayloadStatus.PENDING;
|
|
1374
|
+
}
|
|
1298
1375
|
return node.payloadStatus;
|
|
1299
1376
|
}
|
|
1300
1377
|
|