@lodestar/fork-choice 1.43.0-dev.aef3645690 → 1.43.0-dev.b05ea98d04
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 +27 -20
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +64 -77
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +27 -8
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/store.d.ts +16 -40
- package/lib/forkChoice/store.d.ts.map +1 -1
- package/lib/forkChoice/store.js +4 -22
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +3 -3
- 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 -8
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/interface.js +3 -4
- package/lib/protoArray/interface.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +10 -2
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +81 -45
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/forkChoice.ts +78 -126
- package/src/forkChoice/interface.ts +28 -9
- package/src/forkChoice/store.ts +20 -52
- package/src/index.ts +3 -9
- package/src/protoArray/interface.ts +8 -7
- package/src/protoArray/protoArray.ts +94 -48
|
@@ -24,16 +24,15 @@ export type VoteIndex = number;
|
|
|
24
24
|
* - Syncing: EL is syncing, payload validity unknown (optimistic sync)
|
|
25
25
|
* - PreMerge: Block is from before The Merge, no execution payload exists
|
|
26
26
|
* - Invalid: Execution payload was invalidated by the EL (post-import status)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* For gloas blocks the PENDING/EMPTY variants inherit `executionStatus` from the parent's chain
|
|
29
|
+
* (Valid/Syncing/PreMerge); the FULL variant carries the EL response for this block's own payload.
|
|
30
30
|
*/
|
|
31
31
|
export enum ExecutionStatus {
|
|
32
32
|
Valid = "Valid",
|
|
33
33
|
Syncing = "Syncing",
|
|
34
34
|
PreMerge = "PreMerge",
|
|
35
35
|
Invalid = "Invalid",
|
|
36
|
-
PayloadSeparated = "PayloadSeparated",
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
@@ -61,19 +60,21 @@ export type LVHInvalidResponse = {
|
|
|
61
60
|
executionStatus: ExecutionStatus.Invalid;
|
|
62
61
|
latestValidExecHash: RootHex | null;
|
|
63
62
|
invalidateFromParentBlockRoot: RootHex;
|
|
63
|
+
// EL block hash from invalid block's bid (gloas) or payload's parentHash (pre-gloas).
|
|
64
|
+
// Disambiguates which variant of the parent (FULL vs EMPTY) to invalidate from.
|
|
65
|
+
invalidateFromParentBlockHash: RootHex;
|
|
64
66
|
};
|
|
65
67
|
export type LVHExecResponse = LVHValidResponse | LVHInvalidResponse;
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Any execution status that is not definitively invalid.
|
|
69
|
-
*
|
|
70
|
-
* Post-Gloas: execution status must be PayloadSeparated (beacon block imported before its payload arrives via SignedExecutionPayloadEnvelope)
|
|
71
|
+
* Valid | Syncing | PreMerge
|
|
71
72
|
*/
|
|
72
73
|
export type BlockExecutionStatus = Exclude<ExecutionStatus, ExecutionStatus.Invalid>;
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
76
|
* Execution status for a block whose execution payload is present and has been submitted to the EL.
|
|
76
|
-
* Used post-Gloas when transitioning a
|
|
77
|
+
* Used post-Gloas when transitioning a PENDING block to FULL via onExecutionPayload().
|
|
77
78
|
*/
|
|
78
79
|
export type PayloadExecutionStatus = ExecutionStatus.Valid | ExecutionStatus.Syncing;
|
|
79
80
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {BitArray} from "@chainsafe/ssz";
|
|
2
2
|
import {GENESIS_EPOCH, PTC_SIZE} from "@lodestar/params";
|
|
3
|
-
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
3
|
+
import {DataAvailabilityStatus, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
4
4
|
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
5
5
|
import {bitCount, toRootHex} from "@lodestar/utils";
|
|
6
6
|
import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
|
|
@@ -109,13 +109,6 @@ export class ProtoArray {
|
|
|
109
109
|
null
|
|
110
110
|
);
|
|
111
111
|
|
|
112
|
-
// Anchor block PTC votes must be all-true per spec get_forkchoice_store:
|
|
113
|
-
// payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))}
|
|
114
|
-
// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/fork-choice.md#modified-get_forkchoice_store
|
|
115
|
-
if (protoArray.ptcVotes.has(block.blockRoot)) {
|
|
116
|
-
protoArray.ptcVotes.set(block.blockRoot, BitArray.fromBoolArray(Array.from({length: PTC_SIZE}, () => true)));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
112
|
return protoArray;
|
|
120
113
|
}
|
|
121
114
|
|
|
@@ -221,6 +214,11 @@ export class ProtoArray {
|
|
|
221
214
|
return PayloadStatus.FULL;
|
|
222
215
|
}
|
|
223
216
|
|
|
217
|
+
// Genesis block has no parent in the proto array
|
|
218
|
+
if (block.parentRoot === HEX_ZERO_HASH) {
|
|
219
|
+
return PayloadStatus.FULL;
|
|
220
|
+
}
|
|
221
|
+
|
|
224
222
|
const parentBlock = this.getBlockHexAndBlockHash(block.parentRoot, parentBlockHash);
|
|
225
223
|
if (parentBlock == null) {
|
|
226
224
|
throw new ProtoArrayError({
|
|
@@ -259,38 +257,43 @@ export class ProtoArray {
|
|
|
259
257
|
}
|
|
260
258
|
|
|
261
259
|
/**
|
|
262
|
-
* Returns an EMPTY or FULL
|
|
260
|
+
* Returns the node index of an EMPTY or FULL variant matching block root and block hash.
|
|
261
|
+
* Pre-gloas: checks the single variant. Post-gloas: prefers FULL, falls back to EMPTY.
|
|
263
262
|
*/
|
|
264
|
-
|
|
263
|
+
getNodeIndexByRootAndBlockHash(blockRoot: RootHex, blockHash: RootHex): number | undefined {
|
|
265
264
|
const variantIndices = this.indices.get(blockRoot);
|
|
266
265
|
if (variantIndices === undefined) {
|
|
267
|
-
return
|
|
266
|
+
return undefined;
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
// Pre-Gloas
|
|
271
270
|
if (!Array.isArray(variantIndices)) {
|
|
272
|
-
|
|
273
|
-
return node.executionPayloadBlockHash === blockHash ? node : null;
|
|
271
|
+
return this.nodes[variantIndices].executionPayloadBlockHash === blockHash ? variantIndices : undefined;
|
|
274
272
|
}
|
|
275
273
|
|
|
276
|
-
// Post-Gloas,
|
|
274
|
+
// Post-Gloas, prefer FULL then EMPTY
|
|
277
275
|
const fullNodeIndex = variantIndices[PayloadStatus.FULL];
|
|
278
|
-
if (fullNodeIndex !== undefined) {
|
|
279
|
-
|
|
280
|
-
if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
|
|
281
|
-
return fullNode;
|
|
282
|
-
}
|
|
276
|
+
if (fullNodeIndex !== undefined && this.nodes[fullNodeIndex].executionPayloadBlockHash === blockHash) {
|
|
277
|
+
return fullNodeIndex;
|
|
283
278
|
}
|
|
284
279
|
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
return
|
|
280
|
+
const emptyNodeIndex = variantIndices[PayloadStatus.EMPTY];
|
|
281
|
+
if (this.nodes[emptyNodeIndex].executionPayloadBlockHash === blockHash) {
|
|
282
|
+
return emptyNodeIndex;
|
|
288
283
|
}
|
|
289
284
|
|
|
290
285
|
// PENDING is the same to EMPTY so not likely we can return it
|
|
291
286
|
// also it's only specific for fork-choice
|
|
292
287
|
|
|
293
|
-
return
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
|
|
293
|
+
*/
|
|
294
|
+
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
|
|
295
|
+
const idx = this.getNodeIndexByRootAndBlockHash(blockRoot, blockHash);
|
|
296
|
+
return idx !== undefined ? this.nodes[idx] : null;
|
|
294
297
|
}
|
|
295
298
|
|
|
296
299
|
/**
|
|
@@ -362,9 +365,15 @@ export class ProtoArray {
|
|
|
362
365
|
continue;
|
|
363
366
|
}
|
|
364
367
|
|
|
365
|
-
|
|
368
|
+
// For Gloas blocks, PENDING/EMPTY/FULL all share the same blockRoot.
|
|
369
|
+
// Only apply proposer boost to PENDING (for Gloas) or FULL (for pre-Gloas) — to avoid
|
|
370
|
+
// double-counting the boost across variants during delta back-propagation, and to keep
|
|
371
|
+
// the boost neutral with respect to EMPTY vs FULL selection.
|
|
372
|
+
const isBoostVariant = isGloasBlock(node) ? node.payloadStatus === PayloadStatus.PENDING : true; // pre-Gloas has only FULL, always boost
|
|
373
|
+
const currentBoost =
|
|
374
|
+
proposerBoost && proposerBoost.root === node.blockRoot && isBoostVariant ? proposerBoost.score : 0;
|
|
366
375
|
const previousBoost =
|
|
367
|
-
this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot
|
|
376
|
+
this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot && isBoostVariant
|
|
368
377
|
? this.previousProposerBoost.score
|
|
369
378
|
: 0;
|
|
370
379
|
|
|
@@ -549,9 +558,9 @@ export class ProtoArray {
|
|
|
549
558
|
currentSlot: Slot,
|
|
550
559
|
executionPayloadBlockHash: RootHex,
|
|
551
560
|
executionPayloadNumber: number,
|
|
552
|
-
executionPayloadStateRoot: RootHex,
|
|
553
561
|
proposerBoostRoot: RootHex | null,
|
|
554
|
-
executionStatus: PayloadExecutionStatus
|
|
562
|
+
executionStatus: PayloadExecutionStatus,
|
|
563
|
+
dataAvailabilityStatus: DataAvailabilityStatus
|
|
555
564
|
): void {
|
|
556
565
|
// First check if block exists
|
|
557
566
|
const variants = this.indices.get(blockRoot);
|
|
@@ -593,7 +602,7 @@ export class ProtoArray {
|
|
|
593
602
|
});
|
|
594
603
|
}
|
|
595
604
|
|
|
596
|
-
// Create FULL variant as a child of PENDING (sibling to EMPTY)
|
|
605
|
+
// Create FULL variant as a child of PENDING (sibling to EMPTY).
|
|
597
606
|
const fullNode: ProtoNode = {
|
|
598
607
|
...pendingNode,
|
|
599
608
|
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
@@ -601,11 +610,10 @@ export class ProtoArray {
|
|
|
601
610
|
weight: 0,
|
|
602
611
|
bestChild: undefined,
|
|
603
612
|
bestDescendant: undefined,
|
|
604
|
-
// TODO GLOAS: handle optimistic sync
|
|
605
613
|
executionStatus,
|
|
606
614
|
executionPayloadBlockHash,
|
|
607
615
|
executionPayloadNumber,
|
|
608
|
-
|
|
616
|
+
dataAvailabilityStatus,
|
|
609
617
|
};
|
|
610
618
|
|
|
611
619
|
const fullIndex = this.nodes.length;
|
|
@@ -614,6 +622,12 @@ export class ProtoArray {
|
|
|
614
622
|
// Add FULL variant to the indices array
|
|
615
623
|
variants[PayloadStatus.FULL] = fullIndex;
|
|
616
624
|
|
|
625
|
+
if (executionStatus === ExecutionStatus.Valid) {
|
|
626
|
+
// Walk up from FULL's parent (its own PENDING). FULL is already Valid; the loop breaks
|
|
627
|
+
// immediately if we start at FULL. Same pattern as pre-gloas onBlock at line ~533.
|
|
628
|
+
this.propagateValidExecutionStatusByIndex(pendingIndex);
|
|
629
|
+
}
|
|
630
|
+
|
|
617
631
|
// Update bestChild for PENDING node (may now prefer FULL over EMPTY)
|
|
618
632
|
this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
|
|
619
633
|
}
|
|
@@ -642,6 +656,16 @@ export class ProtoArray {
|
|
|
642
656
|
}
|
|
643
657
|
}
|
|
644
658
|
|
|
659
|
+
getPTCVotes(blockRootHex: RootHex): BitArray | null {
|
|
660
|
+
const votes = this.ptcVotes.get(blockRootHex);
|
|
661
|
+
if (votes === undefined) {
|
|
662
|
+
// Block not found or not a Gloas block
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return votes;
|
|
667
|
+
}
|
|
668
|
+
|
|
645
669
|
/**
|
|
646
670
|
* Check if execution payload for a block is timely
|
|
647
671
|
* Spec: gloas/fork-choice.md#new-is_payload_timely
|
|
@@ -684,7 +708,7 @@ export class ProtoArray {
|
|
|
684
708
|
* Determine if we should extend the payload (prefer FULL over EMPTY)
|
|
685
709
|
* Spec: gloas/fork-choice.md#new-should_extend_payload
|
|
686
710
|
*
|
|
687
|
-
* Returns true if:
|
|
711
|
+
* Returns true if payload is verified (FULL variant exists) AND:
|
|
688
712
|
* 1. Payload is timely, OR
|
|
689
713
|
* 2. No proposer boost root (empty/zero hash), OR
|
|
690
714
|
* 3. Proposer boost root's parent is not this block, OR
|
|
@@ -694,6 +718,10 @@ export class ProtoArray {
|
|
|
694
718
|
* @param proposerBoostRoot - Current proposer boost root (from ForkChoice)
|
|
695
719
|
*/
|
|
696
720
|
shouldExtendPayload(blockRoot: RootHex, proposerBoostRoot: RootHex | null): boolean {
|
|
721
|
+
if (!this.hasPayload(blockRoot)) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
|
|
697
725
|
// Condition 1: Payload is timely
|
|
698
726
|
if (this.isPayloadTimely(blockRoot)) {
|
|
699
727
|
return true;
|
|
@@ -774,11 +802,15 @@ export class ProtoArray {
|
|
|
774
802
|
// Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
|
|
775
803
|
// if its in fcU.
|
|
776
804
|
//
|
|
777
|
-
const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
|
|
778
|
-
|
|
779
|
-
|
|
805
|
+
const {invalidateFromParentBlockRoot, invalidateFromParentBlockHash, latestValidExecHash} = execResponse;
|
|
806
|
+
const invalidateFromParentIndex = this.getNodeIndexByRootAndBlockHash(
|
|
807
|
+
invalidateFromParentBlockRoot,
|
|
808
|
+
invalidateFromParentBlockHash
|
|
809
|
+
);
|
|
780
810
|
if (invalidateFromParentIndex === undefined) {
|
|
781
|
-
throw Error(
|
|
811
|
+
throw Error(
|
|
812
|
+
`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} invalidateFromParentBlockHash=${invalidateFromParentBlockHash} in forkChoice`
|
|
813
|
+
);
|
|
782
814
|
}
|
|
783
815
|
const latestValidHashIndex =
|
|
784
816
|
latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
|
|
@@ -814,12 +846,6 @@ export class ProtoArray {
|
|
|
814
846
|
if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
|
|
815
847
|
break;
|
|
816
848
|
}
|
|
817
|
-
// If PayloadSeparated, that means the node is either PENDING or EMPTY, there could be
|
|
818
|
-
// some ancestor still has syncing status.
|
|
819
|
-
if (node.executionStatus === ExecutionStatus.PayloadSeparated) {
|
|
820
|
-
nodeIndex = node.parent;
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
849
|
this.validateNodeByIndex(nodeIndex);
|
|
824
850
|
nodeIndex = node.parent;
|
|
825
851
|
}
|
|
@@ -917,6 +943,13 @@ export class ProtoArray {
|
|
|
917
943
|
invalidNode.executionStatus = ExecutionStatus.Invalid;
|
|
918
944
|
invalidNode.bestChild = undefined;
|
|
919
945
|
invalidNode.bestDescendant = undefined;
|
|
946
|
+
// Gloas: PENDING and sibling EMPTY share chain status, flip together
|
|
947
|
+
if (invalidNode.payloadStatus === PayloadStatus.PENDING) {
|
|
948
|
+
const variants = this.indices.get(invalidNode.blockRoot);
|
|
949
|
+
if (Array.isArray(variants)) {
|
|
950
|
+
this.invalidateNodeByIndex(variants[PayloadStatus.EMPTY]);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
920
953
|
|
|
921
954
|
return invalidNode;
|
|
922
955
|
}
|
|
@@ -938,6 +971,13 @@ export class ProtoArray {
|
|
|
938
971
|
if (validNode.executionStatus === ExecutionStatus.Syncing) {
|
|
939
972
|
validNode.executionStatus = ExecutionStatus.Valid;
|
|
940
973
|
}
|
|
974
|
+
// Gloas: PENDING and sibling EMPTY share chain status, flip together
|
|
975
|
+
if (validNode.payloadStatus === PayloadStatus.PENDING) {
|
|
976
|
+
const variants = this.indices.get(validNode.blockRoot);
|
|
977
|
+
if (Array.isArray(variants)) {
|
|
978
|
+
this.validateNodeByIndex(variants[PayloadStatus.EMPTY]);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
941
981
|
return validNode;
|
|
942
982
|
}
|
|
943
983
|
|
|
@@ -1486,9 +1526,16 @@ export class ProtoArray {
|
|
|
1486
1526
|
*/
|
|
1487
1527
|
private getParentNodeIndex(node: ProtoNode): number | undefined {
|
|
1488
1528
|
if (isGloasBlock(node)) {
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1529
|
+
// Traversal may reach the finalized ProtoBlock, should not throw error in that case
|
|
1530
|
+
try {
|
|
1531
|
+
const parentPayloadStatus = this.getParentPayloadStatus(node);
|
|
1532
|
+
return this.getNodeIndexByRootAndStatus(node.parentRoot, parentPayloadStatus);
|
|
1533
|
+
} catch (e) {
|
|
1534
|
+
if (e instanceof ProtoArrayError && e.type.code === ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK) {
|
|
1535
|
+
return undefined;
|
|
1536
|
+
}
|
|
1537
|
+
throw e;
|
|
1538
|
+
}
|
|
1492
1539
|
}
|
|
1493
1540
|
// Simple parent traversal for pre-Gloas blocks (includes fork transition)
|
|
1494
1541
|
return node.parent;
|
|
@@ -1642,10 +1689,9 @@ export class ProtoArray {
|
|
|
1642
1689
|
const ancestors: ProtoNode[] = [];
|
|
1643
1690
|
const nonAncestors: ProtoNode[] = [];
|
|
1644
1691
|
|
|
1645
|
-
//
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}
|
|
1692
|
+
// caller of this method may pass default status
|
|
1693
|
+
// this is the only node that we accept PENDING
|
|
1694
|
+
ancestors.push(node);
|
|
1649
1695
|
|
|
1650
1696
|
let nodeIndex = startIndex;
|
|
1651
1697
|
while (node.parent !== undefined) {
|