@lodestar/fork-choice 1.44.0-dev.985999b30c → 1.44.0-dev.a879adb124
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/fastConfirmation/data.d.ts +4 -0
- package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/data.js +31 -0
- package/lib/forkChoice/fastConfirmation/data.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts +17 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js +129 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/index.d.ts +4 -0
- package/lib/forkChoice/fastConfirmation/index.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/index.js +4 -0
- package/lib/forkChoice/fastConfirmation/index.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts +21 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.js +42 -0
- package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/rules.d.ts +9 -0
- package/lib/forkChoice/fastConfirmation/rules.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/rules.js +91 -0
- package/lib/forkChoice/fastConfirmation/rules.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts +101 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/types.js +12 -0
- package/lib/forkChoice/fastConfirmation/types.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/utils.d.ts +47 -0
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/utils.js +681 -0
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -0
- package/lib/forkChoice/forkChoice.d.ts +23 -3
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +116 -9
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +19 -7
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/forkChoice/safeBlocks.d.ts +2 -6
- package/lib/forkChoice/safeBlocks.d.ts.map +1 -1
- package/lib/forkChoice/safeBlocks.js +15 -7
- package/lib/forkChoice/safeBlocks.js.map +1 -1
- package/lib/forkChoice/store.d.ts +13 -2
- package/lib/forkChoice/store.d.ts.map +1 -1
- package/lib/forkChoice/store.js +29 -1
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +12 -1
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +2 -0
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +67 -20
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +170 -38
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +7 -7
- package/src/forkChoice/fastConfirmation/data.ts +43 -0
- package/src/forkChoice/fastConfirmation/fastConfirmationRule.ts +159 -0
- package/src/forkChoice/fastConfirmation/index.ts +3 -0
- package/src/forkChoice/fastConfirmation/metrics.ts +44 -0
- package/src/forkChoice/fastConfirmation/rules.ts +124 -0
- package/src/forkChoice/fastConfirmation/types.ts +111 -0
- package/src/forkChoice/fastConfirmation/utils.ts +968 -0
- package/src/forkChoice/forkChoice.ts +150 -10
- package/src/forkChoice/interface.ts +36 -7
- package/src/forkChoice/safeBlocks.ts +15 -7
- package/src/forkChoice/store.ts +34 -1
- package/src/index.ts +11 -0
- package/src/metrics.ts +3 -1
- package/src/protoArray/protoArray.ts +184 -41
|
@@ -21,6 +21,29 @@ import {
|
|
|
21
21
|
* Spec: gloas/fork-choice.md (PAYLOAD_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
22
22
|
*/
|
|
23
23
|
const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
24
|
+
/**
|
|
25
|
+
* Threshold for blob data availability via PTC vote
|
|
26
|
+
* Spec: gloas/fork-choice.md (DATA_AVAILABILITY_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
27
|
+
*/
|
|
28
|
+
const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* popcount(attended AND NOT yes) — explicit False-vote count.
|
|
32
|
+
* Excludes PTC members who didn't attest (the None state).
|
|
33
|
+
*/
|
|
34
|
+
export function countNoVotes(attended: BitArray, yes: BitArray): number {
|
|
35
|
+
const a = attended.uint8Array;
|
|
36
|
+
const y = yes.uint8Array;
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++) {
|
|
39
|
+
let byte = a[i] & ~y[i] & 0xff;
|
|
40
|
+
while (byte) {
|
|
41
|
+
byte &= byte - 1;
|
|
42
|
+
count++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
24
47
|
|
|
25
48
|
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
26
49
|
type ProposerBoost = {root: RootHex; score: number};
|
|
@@ -64,12 +87,25 @@ export class ProtoArray {
|
|
|
64
87
|
/**
|
|
65
88
|
* PTC (Payload Timeliness Committee) votes per block as bitvectors
|
|
66
89
|
* Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal)
|
|
67
|
-
* Spec: gloas/fork-choice.md#modified-store (
|
|
90
|
+
* Spec: gloas/fork-choice.md#modified-store (payload_timeliness_vote)
|
|
91
|
+
*
|
|
92
|
+
* Bit i = PTC member i voted payloadPresent=true (timeliness YES vote)
|
|
93
|
+
*/
|
|
94
|
+
private payloadTimelinessVotes = new Map<RootHex, BitArray>();
|
|
95
|
+
/**
|
|
96
|
+
* Blob data availability votes per block.
|
|
97
|
+
* Spec: gloas/fork-choice.md#modified-store (payload_data_availability_vote)
|
|
68
98
|
*
|
|
69
|
-
* Bit i
|
|
70
|
-
* Used by is_payload_timely() to determine if payload is timely
|
|
99
|
+
* Bit i = PTC member i voted blobDataAvailable=true (DA YES vote)
|
|
71
100
|
*/
|
|
72
|
-
private
|
|
101
|
+
private payloadDataAvailabilityVotes = new Map<RootHex, BitArray>();
|
|
102
|
+
/**
|
|
103
|
+
* Tracks which PTC members have attested at all (any payload_status).
|
|
104
|
+
* Without this, we cannot tell "didn't vote" (None) from "voted false" —
|
|
105
|
+
* a distinction required by payload_timeliness/payload_data_availability
|
|
106
|
+
* when called with the negative parameter value.
|
|
107
|
+
*/
|
|
108
|
+
private ptcAttested = new Map<RootHex, BitArray>();
|
|
73
109
|
|
|
74
110
|
constructor({
|
|
75
111
|
pruneThreshold,
|
|
@@ -514,9 +550,11 @@ export class ProtoArray {
|
|
|
514
550
|
// Update bestChild for PENDING → EMPTY edge
|
|
515
551
|
this.maybeUpdateBestChildAndDescendant(pendingIndex, emptyIndex, currentSlot, proposerBoostRoot);
|
|
516
552
|
|
|
517
|
-
// Initialize PTC
|
|
518
|
-
// Spec: gloas/fork-choice.md#modified-on_block
|
|
519
|
-
this.
|
|
553
|
+
// Initialize PTC vote bitvectors for this block.
|
|
554
|
+
// Spec: gloas/fork-choice.md#modified-on_block
|
|
555
|
+
this.payloadTimelinessVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
556
|
+
this.ptcAttested.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
557
|
+
this.payloadDataAvailabilityVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
520
558
|
} else {
|
|
521
559
|
// Pre-Gloas: Only create FULL node (payload embedded in block)
|
|
522
560
|
const node: ProtoNode = {
|
|
@@ -636,30 +674,42 @@ export class ProtoArray {
|
|
|
636
674
|
|
|
637
675
|
/**
|
|
638
676
|
* Update PTC votes for multiple validators attesting to a block
|
|
639
|
-
* Spec: gloas/fork-choice.md#new-
|
|
640
|
-
*
|
|
641
|
-
* @param blockRoot - The beacon block root being attested
|
|
642
|
-
* @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1)
|
|
643
|
-
* @param payloadPresent - Whether the validators attest the payload is present
|
|
677
|
+
* Spec: gloas/fork-choice.md#new-notify_ptc_messages
|
|
644
678
|
*/
|
|
645
|
-
notifyPtcMessages(
|
|
646
|
-
|
|
647
|
-
|
|
679
|
+
notifyPtcMessages(
|
|
680
|
+
blockRoot: RootHex,
|
|
681
|
+
slot: Slot,
|
|
682
|
+
ptcIndices: number[],
|
|
683
|
+
payloadPresent: boolean,
|
|
684
|
+
blobDataAvailable: boolean
|
|
685
|
+
): void {
|
|
686
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
687
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
688
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
689
|
+
if (votes === undefined || attended === undefined || daVotes === undefined) {
|
|
648
690
|
// Block not found or not a Gloas block, ignore
|
|
649
691
|
return;
|
|
650
692
|
}
|
|
651
693
|
|
|
694
|
+
// PTC votes can only change the vote for their assigned beacon block, return early otherwise
|
|
695
|
+
const nodeIndex = this.getDefaultNodeIndex(blockRoot);
|
|
696
|
+
const node = nodeIndex !== undefined ? this.getNodeByIndex(nodeIndex) : undefined;
|
|
697
|
+
if (node === undefined || node.slot !== slot) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
652
701
|
for (const ptcIndex of ptcIndices) {
|
|
653
702
|
if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
|
|
654
703
|
throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
|
|
655
704
|
}
|
|
656
|
-
|
|
657
705
|
votes.set(ptcIndex, payloadPresent);
|
|
706
|
+
daVotes.set(ptcIndex, blobDataAvailable);
|
|
707
|
+
attended.set(ptcIndex, true);
|
|
658
708
|
}
|
|
659
709
|
}
|
|
660
710
|
|
|
661
711
|
getPTCVotes(blockRootHex: RootHex): BitArray | null {
|
|
662
|
-
const votes = this.
|
|
712
|
+
const votes = this.payloadTimelinessVotes.get(blockRootHex);
|
|
663
713
|
if (votes === undefined) {
|
|
664
714
|
// Block not found or not a Gloas block
|
|
665
715
|
return null;
|
|
@@ -669,31 +719,122 @@ export class ProtoArray {
|
|
|
669
719
|
}
|
|
670
720
|
|
|
671
721
|
/**
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* Returns true if:
|
|
676
|
-
* 1. Block has PTC votes tracked
|
|
677
|
-
* 2. Payload is locally available (FULL variant exists in proto array)
|
|
678
|
-
* 3. More than PAYLOAD_TIMELY_THRESHOLD (>50% of PTC) members voted payload_present=true
|
|
679
|
-
*
|
|
680
|
-
* @param blockRoot - The beacon block root to check
|
|
722
|
+
* Raw PTC vote tallies for a block root, for the debug fork choice endpoint.
|
|
723
|
+
* Returns `null` for pre-Gloas (or pruned) roots, which have no vote maps.
|
|
681
724
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
725
|
+
getPTCVoteCounts(blockRootHex: RootHex): {
|
|
726
|
+
attesterCount: number;
|
|
727
|
+
payloadPresentCount: number;
|
|
728
|
+
dataAvailableCount: number;
|
|
729
|
+
} | null {
|
|
730
|
+
const attended = this.ptcAttested.get(blockRootHex);
|
|
731
|
+
const timelinessVotes = this.payloadTimelinessVotes.get(blockRootHex);
|
|
732
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRootHex);
|
|
733
|
+
// The three maps share a lifecycle (set together in onBlock, deleted together on prune)
|
|
734
|
+
if (attended === undefined || timelinessVotes === undefined || daVotes === undefined) {
|
|
735
|
+
return null;
|
|
687
736
|
}
|
|
737
|
+
return {
|
|
738
|
+
attesterCount: bitCount(attended.uint8Array),
|
|
739
|
+
payloadPresentCount: bitCount(timelinessVotes.uint8Array),
|
|
740
|
+
dataAvailableCount: bitCount(daVotes.uint8Array),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
688
743
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
744
|
+
getPreviousProposerBoostRoot(): RootHex {
|
|
745
|
+
return this.previousProposerBoost?.root ?? HEX_ZERO_HASH;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Timeliness votes per PTC position, `null` where the member has not attested.
|
|
750
|
+
* Returns `null` if the block is unknown or not a Gloas block.
|
|
751
|
+
*/
|
|
752
|
+
getPayloadTimelinessVotes(blockRootHex: RootHex): (boolean | null)[] | null {
|
|
753
|
+
return this.toAttendanceAwareVotes(this.payloadTimelinessVotes.get(blockRootHex), blockRootHex);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Data-availability votes per PTC position, `null` where the member has not attested.
|
|
758
|
+
* Returns `null` if the block is unknown or not a Gloas block.
|
|
759
|
+
*/
|
|
760
|
+
getPayloadDataAvailabilityVotes(blockRootHex: RootHex): (boolean | null)[] | null {
|
|
761
|
+
return this.toAttendanceAwareVotes(this.payloadDataAvailabilityVotes.get(blockRootHex), blockRootHex);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private toAttendanceAwareVotes(votes: BitArray | undefined, blockRootHex: RootHex): (boolean | null)[] | null {
|
|
765
|
+
const attended = this.ptcAttested.get(blockRootHex);
|
|
766
|
+
if (votes === undefined || attended === undefined) {
|
|
767
|
+
return null;
|
|
692
768
|
}
|
|
693
769
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
770
|
+
return Array.from({length: PTC_SIZE}, (_, i) => (attended.get(i) ? votes.get(i) : null));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Spec: payload_timeliness(store, root, timely=True)
|
|
775
|
+
*/
|
|
776
|
+
isPayloadTimely(blockRoot: RootHex): boolean {
|
|
777
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
778
|
+
if (votes === undefined) return false;
|
|
779
|
+
if (!this.hasPayload(blockRoot)) return false;
|
|
780
|
+
return bitCount(votes.uint8Array) > PAYLOAD_TIMELY_THRESHOLD;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Spec: payload_timeliness(store, root, timely=False)
|
|
785
|
+
*/
|
|
786
|
+
isPayloadNotTimely(blockRoot: RootHex): boolean {
|
|
787
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
788
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
789
|
+
if (votes === undefined || attended === undefined) return false;
|
|
790
|
+
// Spec: not verified locally → returns `not False = True`
|
|
791
|
+
if (!this.hasPayload(blockRoot)) return true;
|
|
792
|
+
return countNoVotes(attended, votes) > PAYLOAD_TIMELY_THRESHOLD;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Spec: payload_data_availability(store, root, available=True)
|
|
797
|
+
*/
|
|
798
|
+
isPayloadDataAvailable(blockRoot: RootHex): boolean {
|
|
799
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
800
|
+
if (daVotes === undefined) return false;
|
|
801
|
+
if (!this.hasPayload(blockRoot)) return false;
|
|
802
|
+
return bitCount(daVotes.uint8Array) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Spec: payload_data_availability(store, root, available=False)
|
|
807
|
+
*/
|
|
808
|
+
isPayloadDataNotAvailable(blockRoot: RootHex): boolean {
|
|
809
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
810
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
811
|
+
if (daVotes === undefined || attended === undefined) return false;
|
|
812
|
+
// Spec: not verified locally → returns `not False = True`
|
|
813
|
+
if (!this.hasPayload(blockRoot)) return true;
|
|
814
|
+
return countNoVotes(attended, daVotes) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Spec: should_build_on_full(store, head)
|
|
819
|
+
*
|
|
820
|
+
* The proposer is forced to build on the EMPTY variant (effectively reorging)
|
|
821
|
+
* when the PTC majority voted that the blob data is not available or that the
|
|
822
|
+
* payload was not timely.
|
|
823
|
+
*/
|
|
824
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
|
|
825
|
+
if (head.payloadStatus === PayloadStatus.PENDING) {
|
|
826
|
+
throw new Error("shouldBuildOnFull called with PENDING head");
|
|
827
|
+
}
|
|
828
|
+
if (head.payloadStatus === PayloadStatus.EMPTY) return false;
|
|
829
|
+
|
|
830
|
+
// The PTC data availability and timeliness views are only consulted for a head from the
|
|
831
|
+
// previous slot. For an earlier head the empty/full variant has already been resolved by
|
|
832
|
+
// weight in getHead.
|
|
833
|
+
if (head.slot + 1 !== slot) return true;
|
|
834
|
+
|
|
835
|
+
if (this.isPayloadDataNotAvailable(head.blockRoot)) return false;
|
|
836
|
+
|
|
837
|
+
return !this.isPayloadNotTimely(head.blockRoot);
|
|
697
838
|
}
|
|
698
839
|
|
|
699
840
|
/**
|
|
@@ -711,7 +852,7 @@ export class ProtoArray {
|
|
|
711
852
|
* Spec: gloas/fork-choice.md#new-should_extend_payload
|
|
712
853
|
*
|
|
713
854
|
* Returns true if payload is verified (FULL variant exists) AND:
|
|
714
|
-
* 1. Payload is timely, OR
|
|
855
|
+
* 1. Payload is timely AND data is available, OR
|
|
715
856
|
* 2. No proposer boost root (empty/zero hash), OR
|
|
716
857
|
* 3. Proposer boost root's parent is not this block, OR
|
|
717
858
|
* 4. Proposer boost root extends FULL parent
|
|
@@ -724,8 +865,8 @@ export class ProtoArray {
|
|
|
724
865
|
return false;
|
|
725
866
|
}
|
|
726
867
|
|
|
727
|
-
// Condition 1: Payload is timely
|
|
728
|
-
if (this.isPayloadTimely(blockRoot)) {
|
|
868
|
+
// Condition 1: Payload is timely AND data is available
|
|
869
|
+
if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) {
|
|
729
870
|
return true;
|
|
730
871
|
}
|
|
731
872
|
|
|
@@ -1133,7 +1274,9 @@ export class ProtoArray {
|
|
|
1133
1274
|
this.indices.delete(root);
|
|
1134
1275
|
// Prune PTC votes for this block to prevent memory leak
|
|
1135
1276
|
// Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
|
|
1136
|
-
this.
|
|
1277
|
+
this.payloadTimelinessVotes.delete(root);
|
|
1278
|
+
this.ptcAttested.delete(root);
|
|
1279
|
+
this.payloadDataAvailabilityVotes.delete(root);
|
|
1137
1280
|
}
|
|
1138
1281
|
|
|
1139
1282
|
// Store nodes prior to finalization
|