@lodestar/fork-choice 1.44.0-dev.985999b30c → 1.44.0-dev.a9a9fe2e63

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.
@@ -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 (line 148)
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 is set if PTC member i voted payload_present=true
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 ptcVotes = new Map<RootHex, BitArray>();
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 votes for this block (all false initially)
518
- // Spec: gloas/fork-choice.md#modified-on_block (line 645)
519
- this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
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-on_payload_attestation_message
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(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void {
646
- const votes = this.ptcVotes.get(blockRoot);
647
- if (votes === undefined) {
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.ptcVotes.get(blockRootHex);
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,66 @@ export class ProtoArray {
669
719
  }
670
720
 
671
721
  /**
672
- * Check if execution payload for a block is timely
673
- * Spec: gloas/fork-choice.md#new-is_payload_timely
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
+ * Spec: payload_timeliness(store, root, timely=True)
681
723
  */
682
724
  isPayloadTimely(blockRoot: RootHex): boolean {
683
- const votes = this.ptcVotes.get(blockRoot);
684
- if (votes === undefined) {
685
- // Block not found or not a Gloas block
686
- return false;
687
- }
725
+ const votes = this.payloadTimelinessVotes.get(blockRoot);
726
+ if (votes === undefined) return false;
727
+ if (!this.hasPayload(blockRoot)) return false;
728
+ return bitCount(votes.uint8Array) > PAYLOAD_TIMELY_THRESHOLD;
729
+ }
688
730
 
689
- // If payload is not locally available, it's not timely
690
- if (!this.hasPayload(blockRoot)) {
691
- return false;
731
+ /**
732
+ * Spec: payload_timeliness(store, root, timely=False)
733
+ */
734
+ isPayloadNotTimely(blockRoot: RootHex): boolean {
735
+ const votes = this.payloadTimelinessVotes.get(blockRoot);
736
+ const attended = this.ptcAttested.get(blockRoot);
737
+ if (votes === undefined || attended === undefined) return false;
738
+ // Spec: not verified locally → returns `not False = True`
739
+ if (!this.hasPayload(blockRoot)) return true;
740
+ return countNoVotes(attended, votes) > PAYLOAD_TIMELY_THRESHOLD;
741
+ }
742
+
743
+ /**
744
+ * Spec: payload_data_availability(store, root, available=True)
745
+ */
746
+ isPayloadDataAvailable(blockRoot: RootHex): boolean {
747
+ const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
748
+ if (daVotes === undefined) return false;
749
+ if (!this.hasPayload(blockRoot)) return false;
750
+ return bitCount(daVotes.uint8Array) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
751
+ }
752
+
753
+ /**
754
+ * Spec: payload_data_availability(store, root, available=False)
755
+ */
756
+ isPayloadDataNotAvailable(blockRoot: RootHex): boolean {
757
+ const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
758
+ const attended = this.ptcAttested.get(blockRoot);
759
+ if (daVotes === undefined || attended === undefined) return false;
760
+ // Spec: not verified locally → returns `not False = True`
761
+ if (!this.hasPayload(blockRoot)) return true;
762
+ return countNoVotes(attended, daVotes) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
763
+ }
764
+
765
+ /**
766
+ * Spec: should_build_on_full(store, head)
767
+ *
768
+ * The proposer is forced to build on the EMPTY variant (effectively reorging)
769
+ * when the PTC majority voted that the blob data is not available.
770
+ */
771
+ shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
772
+ if (head.payloadStatus === PayloadStatus.PENDING) {
773
+ throw new Error("shouldBuildOnFull called with PENDING head");
692
774
  }
775
+ if (head.payloadStatus === PayloadStatus.EMPTY) return false;
776
+
777
+ // The PTC data availability view is only consulted for a head from the previous slot.
778
+ // For an earlier head the empty/full variant has already been resolved by weight in getHead.
779
+ if (head.slot + 1 !== slot) return true;
693
780
 
694
- // Count votes for payload_present=true
695
- const yesVotes = bitCount(votes.uint8Array);
696
- return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
781
+ return !this.isPayloadDataNotAvailable(head.blockRoot);
697
782
  }
698
783
 
699
784
  /**
@@ -711,7 +796,7 @@ export class ProtoArray {
711
796
  * Spec: gloas/fork-choice.md#new-should_extend_payload
712
797
  *
713
798
  * Returns true if payload is verified (FULL variant exists) AND:
714
- * 1. Payload is timely, OR
799
+ * 1. Payload is timely AND data is available, OR
715
800
  * 2. No proposer boost root (empty/zero hash), OR
716
801
  * 3. Proposer boost root's parent is not this block, OR
717
802
  * 4. Proposer boost root extends FULL parent
@@ -724,8 +809,8 @@ export class ProtoArray {
724
809
  return false;
725
810
  }
726
811
 
727
- // Condition 1: Payload is timely
728
- if (this.isPayloadTimely(blockRoot)) {
812
+ // Condition 1: Payload is timely AND data is available
813
+ if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) {
729
814
  return true;
730
815
  }
731
816
 
@@ -1133,7 +1218,9 @@ export class ProtoArray {
1133
1218
  this.indices.delete(root);
1134
1219
  // Prune PTC votes for this block to prevent memory leak
1135
1220
  // Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
1136
- this.ptcVotes.delete(root);
1221
+ this.payloadTimelinessVotes.delete(root);
1222
+ this.ptcAttested.delete(root);
1223
+ this.payloadDataAvailabilityVotes.delete(root);
1137
1224
  }
1138
1225
 
1139
1226
  // Store nodes prior to finalization