@lodestar/fork-choice 1.44.0-dev.6ef8199cfa → 1.44.0-dev.b506aab66d

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)
68
91
  *
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
92
+ * Bit i = PTC member i voted payloadPresent=true (timeliness YES vote)
71
93
  */
72
94
  private ptcVotes = new Map<RootHex, BitArray>();
95
+ /**
96
+ * Blob data availability votes per block.
97
+ * Spec: gloas/fork-choice.md#modified-store (payload_data_availability_vote)
98
+ *
99
+ * Bit i = PTC member i voted blobDataAvailable=true (DA YES vote)
100
+ */
101
+ private daVotes = 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)
553
+ // Initialize PTC vote bitvectors for this block.
554
+ // Spec: gloas/fork-choice.md#modified-on_block
519
555
  this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
556
+ this.ptcAttested.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
557
+ this.daVotes.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 = {
@@ -558,6 +596,7 @@ export class ProtoArray {
558
596
  currentSlot: Slot,
559
597
  executionPayloadBlockHash: RootHex,
560
598
  executionPayloadNumber: number,
599
+ executionPayloadGasLimit: number,
561
600
  proposerBoostRoot: RootHex | null,
562
601
  executionStatus: PayloadExecutionStatus,
563
602
  dataAvailabilityStatus: DataAvailabilityStatus
@@ -613,6 +652,7 @@ export class ProtoArray {
613
652
  executionStatus,
614
653
  executionPayloadBlockHash,
615
654
  executionPayloadNumber,
655
+ executionPayloadGasLimit,
616
656
  dataAvailabilityStatus,
617
657
  };
618
658
 
@@ -634,15 +674,18 @@ export class ProtoArray {
634
674
 
635
675
  /**
636
676
  * Update PTC votes for multiple validators attesting to a block
637
- * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
638
- *
639
- * @param blockRoot - The beacon block root being attested
640
- * @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1)
641
- * @param payloadPresent - Whether the validators attest the payload is present
677
+ * Spec: gloas/fork-choice.md#new-notify_ptc_messages
642
678
  */
643
- notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void {
679
+ notifyPtcMessages(
680
+ blockRoot: RootHex,
681
+ ptcIndices: number[],
682
+ payloadPresent: boolean,
683
+ blobDataAvailable: boolean
684
+ ): void {
644
685
  const votes = this.ptcVotes.get(blockRoot);
645
- if (votes === undefined) {
686
+ const attended = this.ptcAttested.get(blockRoot);
687
+ const daVotes = this.daVotes.get(blockRoot);
688
+ if (votes === undefined || attended === undefined || daVotes === undefined) {
646
689
  // Block not found or not a Gloas block, ignore
647
690
  return;
648
691
  }
@@ -651,8 +694,9 @@ export class ProtoArray {
651
694
  if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
652
695
  throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
653
696
  }
654
-
655
697
  votes.set(ptcIndex, payloadPresent);
698
+ daVotes.set(ptcIndex, blobDataAvailable);
699
+ attended.set(ptcIndex, true);
656
700
  }
657
701
  }
658
702
 
@@ -667,31 +711,61 @@ export class ProtoArray {
667
711
  }
668
712
 
669
713
  /**
670
- * Check if execution payload for a block is timely
671
- * Spec: gloas/fork-choice.md#new-is_payload_timely
672
- *
673
- * Returns true if:
674
- * 1. Block has PTC votes tracked
675
- * 2. Payload is locally available (FULL variant exists in proto array)
676
- * 3. More than PAYLOAD_TIMELY_THRESHOLD (>50% of PTC) members voted payload_present=true
677
- *
678
- * @param blockRoot - The beacon block root to check
714
+ * Spec: payload_timeliness(store, root, timely=True)
679
715
  */
680
716
  isPayloadTimely(blockRoot: RootHex): boolean {
681
717
  const votes = this.ptcVotes.get(blockRoot);
682
- if (votes === undefined) {
683
- // Block not found or not a Gloas block
684
- return false;
685
- }
718
+ if (votes === undefined) return false;
719
+ if (!this.hasPayload(blockRoot)) return false;
720
+ return bitCount(votes.uint8Array) > PAYLOAD_TIMELY_THRESHOLD;
721
+ }
686
722
 
687
- // If payload is not locally available, it's not timely
688
- if (!this.hasPayload(blockRoot)) {
689
- return false;
690
- }
723
+ /**
724
+ * Spec: payload_timeliness(store, root, timely=False)
725
+ */
726
+ isPayloadNotTimely(blockRoot: RootHex): boolean {
727
+ const votes = this.ptcVotes.get(blockRoot);
728
+ const attended = this.ptcAttested.get(blockRoot);
729
+ if (votes === undefined || attended === undefined) return false;
730
+ // Spec: not verified locally → returns `not False = True`
731
+ if (!this.hasPayload(blockRoot)) return true;
732
+ return countNoVotes(attended, votes) > PAYLOAD_TIMELY_THRESHOLD;
733
+ }
734
+
735
+ /**
736
+ * Spec: payload_data_availability(store, root, available=True)
737
+ */
738
+ isPayloadDataAvailable(blockRoot: RootHex): boolean {
739
+ const daVotes = this.daVotes.get(blockRoot);
740
+ if (daVotes === undefined) return false;
741
+ if (!this.hasPayload(blockRoot)) return false;
742
+ return bitCount(daVotes.uint8Array) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
743
+ }
691
744
 
692
- // Count votes for payload_present=true
693
- const yesVotes = bitCount(votes.uint8Array);
694
- return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
745
+ /**
746
+ * Spec: payload_data_availability(store, root, available=False)
747
+ */
748
+ isPayloadDataNotAvailable(blockRoot: RootHex): boolean {
749
+ const daVotes = this.daVotes.get(blockRoot);
750
+ const attended = this.ptcAttested.get(blockRoot);
751
+ if (daVotes === undefined || attended === undefined) return false;
752
+ // Spec: not verified locally → returns `not False = True`
753
+ if (!this.hasPayload(blockRoot)) return true;
754
+ return countNoVotes(attended, daVotes) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
755
+ }
756
+
757
+ /**
758
+ * Spec: should_build_on_full(store, head)
759
+ *
760
+ * The proposer is forced to build on the EMPTY variant (effectively reorging)
761
+ * when the PTC majority voted that the blob data is not available.
762
+ */
763
+ shouldBuildOnFull(head: ProtoBlock): boolean {
764
+ if (head.payloadStatus === PayloadStatus.PENDING) {
765
+ throw new Error("shouldBuildOnFull called with PENDING head");
766
+ }
767
+ if (head.payloadStatus === PayloadStatus.EMPTY) return false;
768
+ return !this.isPayloadDataNotAvailable(head.blockRoot);
695
769
  }
696
770
 
697
771
  /**
@@ -1132,6 +1206,8 @@ export class ProtoArray {
1132
1206
  // Prune PTC votes for this block to prevent memory leak
1133
1207
  // Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
1134
1208
  this.ptcVotes.delete(root);
1209
+ this.ptcAttested.delete(root);
1210
+ this.daVotes.delete(root);
1135
1211
  }
1136
1212
 
1137
1213
  // Store nodes prior to finalization