@lodestar/fork-choice 1.41.0-dev.f2caa915ab → 1.41.0-dev.f7a5f4ddda

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.
Files changed (48) hide show
  1. package/lib/forkChoice/errors.d.ts +9 -1
  2. package/lib/forkChoice/errors.d.ts.map +1 -1
  3. package/lib/forkChoice/errors.js +10 -3
  4. package/lib/forkChoice/errors.js.map +1 -1
  5. package/lib/forkChoice/forkChoice.d.ts +75 -19
  6. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  7. package/lib/forkChoice/forkChoice.js +301 -117
  8. package/lib/forkChoice/forkChoice.js.map +1 -1
  9. package/lib/forkChoice/interface.d.ts +54 -21
  10. package/lib/forkChoice/interface.d.ts.map +1 -1
  11. package/lib/forkChoice/interface.js +6 -3
  12. package/lib/forkChoice/interface.js.map +1 -1
  13. package/lib/forkChoice/safeBlocks.js.map +1 -1
  14. package/lib/forkChoice/store.d.ts +40 -16
  15. package/lib/forkChoice/store.d.ts.map +1 -1
  16. package/lib/forkChoice/store.js +22 -4
  17. package/lib/forkChoice/store.js.map +1 -1
  18. package/lib/index.d.ts +4 -4
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +2 -2
  21. package/lib/index.js.map +1 -1
  22. package/lib/metrics.d.ts.map +1 -1
  23. package/lib/metrics.js.map +1 -1
  24. package/lib/protoArray/computeDeltas.d.ts.map +1 -1
  25. package/lib/protoArray/computeDeltas.js +3 -0
  26. package/lib/protoArray/computeDeltas.js.map +1 -1
  27. package/lib/protoArray/errors.d.ts +15 -2
  28. package/lib/protoArray/errors.d.ts.map +1 -1
  29. package/lib/protoArray/errors.js +7 -2
  30. package/lib/protoArray/errors.js.map +1 -1
  31. package/lib/protoArray/interface.d.ts +20 -2
  32. package/lib/protoArray/interface.d.ts.map +1 -1
  33. package/lib/protoArray/interface.js +19 -1
  34. package/lib/protoArray/interface.js.map +1 -1
  35. package/lib/protoArray/protoArray.d.ts +219 -24
  36. package/lib/protoArray/protoArray.d.ts.map +1 -1
  37. package/lib/protoArray/protoArray.js +748 -133
  38. package/lib/protoArray/protoArray.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/forkChoice/errors.ts +7 -2
  41. package/src/forkChoice/forkChoice.ts +384 -126
  42. package/src/forkChoice/interface.ts +72 -20
  43. package/src/forkChoice/store.ts +52 -20
  44. package/src/index.ts +10 -2
  45. package/src/protoArray/computeDeltas.ts +6 -0
  46. package/src/protoArray/errors.ts +7 -1
  47. package/src/protoArray/interface.ts +36 -3
  48. package/src/protoArray/protoArray.ts +880 -134
@@ -1,7 +1,8 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
+ import {ForkSeq, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
3
3
  import {
4
4
  CachedBeaconStateAllForks,
5
+ CachedBeaconStateGloas,
5
6
  DataAvailabilityStatus,
6
7
  EffectiveBalanceIncrements,
7
8
  ZERO_HASH,
@@ -37,9 +38,11 @@ import {
37
38
  LVHExecResponse,
38
39
  MaybeValidExecutionStatus,
39
40
  NULL_VOTE_INDEX,
41
+ PayloadStatus,
40
42
  ProtoBlock,
41
43
  ProtoNode,
42
44
  VoteIndex,
45
+ isGloasBlock,
43
46
  } from "../protoArray/interface.js";
44
47
  import {ProtoArray} from "../protoArray/protoArray.js";
45
48
  import {ForkChoiceError, ForkChoiceErrorCode, InvalidAttestationCode, InvalidBlockCode} from "./errors.js";
@@ -51,7 +54,7 @@ import {
51
54
  NotReorgedReason,
52
55
  ShouldOverrideForkChoiceUpdateResult,
53
56
  } from "./interface.js";
54
- import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js";
57
+ import {CheckpointWithPayloadStatus, IForkChoiceStore, JustifiedBalances, toCheckpointWithPayload} from "./store.js";
55
58
 
56
59
  export type ForkChoiceOpts = {
57
60
  proposerBoost?: boolean;
@@ -71,7 +74,7 @@ export type UpdateAndGetHeadOpt =
71
74
  | {mode: UpdateHeadOpt.GetPredictedProposerHead; secFromSlot: number; slot: Slot};
72
75
 
73
76
  // the initial vote epoch for all validators
74
- const INIT_VOTE_EPOCH: Epoch = 0;
77
+ const INIT_VOTE_SLOT: Slot = 0;
75
78
 
76
79
  /**
77
80
  * Provides an implementation of "Ethereum Consensus -- Beacon Chain Fork Choice":
@@ -94,18 +97,28 @@ export class ForkChoice implements IForkChoice {
94
97
  irrecoverableError?: Error;
95
98
  /**
96
99
  * Votes currently tracked in the protoArray. Instead of tracking a VoteTracker of currentIndex, nextIndex and epoch,
97
- * we decompose the struct and track them in 3 separate arrays for performance reason.
100
+ * we decompose the struct and track them in separate arrays for performance reason.
101
+ *
102
+ * For Gloas (ePBS), LatestMessage tracks slot instead of epoch and includes payload_present flag.
103
+ * Spec: gloas/fork-choice.md#modified-latestmessage
104
+ *
105
+ * IMPORTANT: voteCurrentIndices and voteNextIndices point to the EXACT variant node index.
106
+ * The payload status is encoded in the node index itself (different variants have different indices).
107
+ * For example, if a validator votes for the EMPTY variant, voteNextIndices[i] points to that specific EMPTY node.
98
108
  */
99
109
  private readonly voteCurrentIndices: VoteIndex[];
100
110
  private readonly voteNextIndices: VoteIndex[];
101
- private readonly voteNextEpochs: Epoch[];
111
+ private readonly voteNextSlots: Slot[];
102
112
 
103
113
  /**
104
114
  * Attestations that arrived at the current slot and must be queued for later processing.
105
115
  * NOT currently tracked in the protoArray
116
+ *
117
+ * Modified for Gloas to track PayloadStatus per validator.
118
+ * Maps: Slot -> BlockRoot -> ValidatorIndex -> PayloadStatus
106
119
  */
107
- private readonly queuedAttestations: MapDef<Slot, MapDef<RootHex, Set<ValidatorIndex>>> = new MapDef(
108
- () => new MapDef(() => new Set())
120
+ private readonly queuedAttestations: MapDef<Slot, MapDef<RootHex, Map<ValidatorIndex, PayloadStatus>>> = new MapDef(
121
+ () => new MapDef(() => new Map())
109
122
  );
110
123
 
111
124
  /**
@@ -150,13 +163,14 @@ export class ForkChoice implements IForkChoice {
150
163
  this.voteCurrentIndices = new Array(validatorCount).fill(NULL_VOTE_INDEX);
151
164
  this.voteNextIndices = new Array(validatorCount).fill(NULL_VOTE_INDEX);
152
165
  // when compute deltas, we ignore epoch if voteNextIndex is NULL_VOTE_INDEX anyway
153
- this.voteNextEpochs = new Array(validatorCount).fill(INIT_VOTE_EPOCH);
166
+
167
+ this.voteNextSlots = new Array(validatorCount).fill(0);
154
168
 
155
169
  this.head = this.updateHead();
156
170
  this.balances = this.fcStore.justified.balances;
157
171
 
158
172
  metrics?.forkChoice.votes.addCollect(() => {
159
- metrics.forkChoice.votes.set(this.voteNextEpochs.length);
173
+ metrics.forkChoice.votes.set(this.voteNextSlots.length);
160
174
  metrics.forkChoice.queuedAttestations.set(this.queuedAttestationsPreviousSlot);
161
175
  metrics.forkChoice.validatedAttestationDatas.set(this.validatedAttestationDatas.size);
162
176
  metrics.forkChoice.balancesLength.set(this.balances.length);
@@ -177,7 +191,7 @@ export class ForkChoice implements IForkChoice {
177
191
  *
178
192
  * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
179
193
  */
180
- getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex {
194
+ getAncestor(blockRoot: RootHex, ancestorSlot: Slot): ProtoNode {
181
195
  return this.protoArray.getAncestor(blockRoot, ancestorSlot);
182
196
  }
183
197
 
@@ -237,11 +251,10 @@ export class ForkChoice implements IForkChoice {
237
251
  // Return false otherwise.
238
252
  // Note when proposer boost reorg is disabled, it always returns false
239
253
  shouldOverrideForkChoiceUpdate(
240
- blockRoot: RootHex,
254
+ headBlock: ProtoBlock,
241
255
  secFromSlot: number,
242
256
  currentSlot: Slot
243
257
  ): ShouldOverrideForkChoiceUpdateResult {
244
- const headBlock = this.getBlockHex(blockRoot);
245
258
  if (headBlock === null) {
246
259
  // should not happen because this block just got imported. Fall back to no-reorg.
247
260
  return {shouldOverrideFcu: false, reason: NotReorgedReason.HeadBlockNotAvailable};
@@ -257,7 +270,10 @@ export class ForkChoice implements IForkChoice {
257
270
  return {shouldOverrideFcu: false, reason: NotReorgedReason.ProposerBoostReorgDisabled};
258
271
  }
259
272
 
260
- const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
273
+ const parentBlock = this.protoArray.getBlock(
274
+ headBlock.parentRoot,
275
+ this.protoArray.getParentPayloadStatus(headBlock)
276
+ );
261
277
  const proposalSlot = headBlock.slot + 1;
262
278
 
263
279
  // No reorg if parentBlock isn't available
@@ -282,7 +298,10 @@ export class ForkChoice implements IForkChoice {
282
298
  return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
283
299
  }
284
300
 
285
- this.logger?.verbose("Block is weak. Should override forkchoice update", {blockRoot, slot: currentSlot});
301
+ this.logger?.verbose("Block is weak. Should override forkchoice update", {
302
+ blockRoot: headBlock.blockRoot,
303
+ slot: currentSlot,
304
+ });
286
305
  return {shouldOverrideFcu: true, parentBlock};
287
306
  }
288
307
 
@@ -316,7 +335,7 @@ export class ForkChoice implements IForkChoice {
316
335
  }
317
336
 
318
337
  const blockRoot = headBlock.blockRoot;
319
- const result = this.shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot);
338
+ const result = this.shouldOverrideForkChoiceUpdate(headBlock, secFromSlot, currentSlot);
320
339
 
321
340
  if (result.shouldOverrideFcu) {
322
341
  this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
@@ -363,7 +382,10 @@ export class ForkChoice implements IForkChoice {
363
382
  return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostReorgDisabled};
364
383
  }
365
384
 
366
- const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
385
+ const parentBlock = this.protoArray.getBlock(
386
+ headBlock.parentRoot,
387
+ this.protoArray.getParentPayloadStatus(headBlock)
388
+ );
367
389
 
368
390
  // No reorg if parentBlock isn't available
369
391
  if (parentBlock === undefined) {
@@ -400,7 +422,7 @@ export class ForkChoice implements IForkChoice {
400
422
  slotsPerEpoch: SLOTS_PER_EPOCH,
401
423
  committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
402
424
  });
403
- const headNode = this.protoArray.getNode(headBlock.blockRoot);
425
+ const headNode = this.protoArray.getNode(headBlock.blockRoot, headBlock.payloadStatus);
404
426
  // If headNode is unavailable, give up reorg
405
427
  if (headNode === undefined || headNode.weight >= reorgThreshold) {
406
428
  return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
@@ -412,7 +434,7 @@ export class ForkChoice implements IForkChoice {
412
434
  slotsPerEpoch: SLOTS_PER_EPOCH,
413
435
  committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
414
436
  });
415
- const parentNode = this.protoArray.getNode(parentBlock.blockRoot);
437
+ const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentBlock.payloadStatus);
416
438
  // If parentNode is unavailable, give up reorg
417
439
  if (parentNode === undefined || parentNode.weight <= parentThreshold) {
418
440
  return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
@@ -509,23 +531,10 @@ export class ForkChoice implements IForkChoice {
509
531
  currentSlot,
510
532
  });
511
533
 
512
- const headRoot = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
513
- const headIndex = this.protoArray.indices.get(headRoot);
514
- if (headIndex === undefined) {
515
- throw new ForkChoiceError({
516
- code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
517
- root: headRoot,
518
- });
519
- }
520
- const headNode = this.protoArray.nodes[headIndex];
521
- if (headNode === undefined) {
522
- throw new ForkChoiceError({
523
- code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
524
- root: headRoot,
525
- });
526
- }
534
+ // findHead returns the ProtoNode representing the head
535
+ const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
527
536
 
528
- this.head = headNode;
537
+ this.head = head;
529
538
  return this.head;
530
539
  }
531
540
 
@@ -548,11 +557,11 @@ export class ForkChoice implements IForkChoice {
548
557
  return this.protoArray.nodes;
549
558
  }
550
559
 
551
- getFinalizedCheckpoint(): CheckpointWithHex {
560
+ getFinalizedCheckpoint(): CheckpointWithPayloadStatus {
552
561
  return this.fcStore.finalizedCheckpoint;
553
562
  }
554
563
 
555
- getJustifiedCheckpoint(): CheckpointWithHex {
564
+ getJustifiedCheckpoint(): CheckpointWithPayloadStatus {
556
565
  return this.fcStore.justified.checkpoint;
557
566
  }
558
567
 
@@ -585,14 +594,18 @@ export class ForkChoice implements IForkChoice {
585
594
  ): ProtoBlock {
586
595
  const {parentRoot, slot} = block;
587
596
  const parentRootHex = toRootHex(parentRoot);
588
- // Parent block must be known
589
- const parentBlock = this.protoArray.getBlock(parentRootHex);
597
+ // Parent block must be known because state_transition would have failed otherwise.
598
+ const parentHashHex = isGloasBeaconBlock(block)
599
+ ? toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
600
+ : null;
601
+ const parentBlock = this.protoArray.getParent(parentRootHex, parentHashHex);
590
602
  if (!parentBlock) {
591
603
  throw new ForkChoiceError({
592
604
  code: ForkChoiceErrorCode.INVALID_BLOCK,
593
605
  err: {
594
606
  code: InvalidBlockCode.UNKNOWN_PARENT,
595
607
  root: parentRootHex,
608
+ hash: parentHashHex,
596
609
  },
597
610
  });
598
611
  }
@@ -627,15 +640,18 @@ export class ForkChoice implements IForkChoice {
627
640
  }
628
641
 
629
642
  // Check block is a descendant of the finalized block at the checkpoint finalized slot.
630
- const blockAncestorRoot = this.getAncestor(parentRootHex, finalizedSlot);
631
- const finalizedRoot = this.fcStore.finalizedCheckpoint.rootHex;
632
- if (blockAncestorRoot !== finalizedRoot) {
643
+ const blockAncestorNode = this.getAncestor(parentRootHex, finalizedSlot);
644
+ const fcStoreFinalized = this.fcStore.finalizedCheckpoint;
645
+ if (
646
+ blockAncestorNode.blockRoot !== fcStoreFinalized.rootHex ||
647
+ blockAncestorNode.payloadStatus !== fcStoreFinalized.payloadStatus
648
+ ) {
633
649
  throw new ForkChoiceError({
634
650
  code: ForkChoiceErrorCode.INVALID_BLOCK,
635
651
  err: {
636
652
  code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT,
637
- finalizedRoot,
638
- blockAncestor: blockAncestorRoot,
653
+ finalizedRoot: fcStoreFinalized.rootHex,
654
+ blockAncestor: blockAncestorNode.blockRoot,
639
655
  },
640
656
  });
641
657
  }
@@ -655,10 +671,15 @@ export class ForkChoice implements IForkChoice {
655
671
  this.proposerBoostRoot = blockRootHex;
656
672
  }
657
673
 
658
- const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint);
659
- const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint);
674
+ // Get justified checkpoint with payload status for Gloas
675
+ const justifiedPayloadStatus = getCheckpointPayloadStatus(state, state.currentJustifiedCheckpoint.epoch);
676
+ const justifiedCheckpoint = toCheckpointWithPayload(state.currentJustifiedCheckpoint, justifiedPayloadStatus);
660
677
  const stateJustifiedEpoch = justifiedCheckpoint.epoch;
661
678
 
679
+ // Get finalized checkpoint with payload status for Gloas
680
+ const finalizedPayloadStatus = getCheckpointPayloadStatus(state, state.finalizedCheckpoint.epoch);
681
+ const finalizedCheckpoint = toCheckpointWithPayload(state.finalizedCheckpoint, finalizedPayloadStatus);
682
+
662
683
  // Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if
663
684
  // the justified checkpoint changes
664
685
  this.updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, () =>
@@ -679,29 +700,57 @@ export class ForkChoice implements IForkChoice {
679
700
  // This is an optimization. It should reduce the amount of times we run
680
701
  // `process_justification_and_finalization` by approximately 1/3rd when the chain is
681
702
  // performing optimally.
682
- let unrealizedJustifiedCheckpoint: CheckpointWithHex;
683
- let unrealizedFinalizedCheckpoint: CheckpointWithHex;
703
+ let unrealizedJustifiedCheckpoint: CheckpointWithPayloadStatus;
704
+ let unrealizedFinalizedCheckpoint: CheckpointWithPayloadStatus;
684
705
  if (this.opts?.computeUnrealized) {
685
706
  if (
686
707
  parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
687
708
  parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch
688
709
  ) {
689
710
  // reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet
711
+ // Get payload status for unrealized justified checkpoint
712
+ const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(
713
+ state,
714
+ parentBlock.unrealizedJustifiedEpoch
715
+ );
690
716
  unrealizedJustifiedCheckpoint = {
691
717
  epoch: parentBlock.unrealizedJustifiedEpoch,
692
718
  root: fromHex(parentBlock.unrealizedJustifiedRoot),
693
719
  rootHex: parentBlock.unrealizedJustifiedRoot,
720
+ payloadStatus: unrealizedJustifiedPayloadStatus,
694
721
  };
722
+ // Get payload status for unrealized finalized checkpoint
723
+ const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(
724
+ state,
725
+ parentBlock.unrealizedFinalizedEpoch
726
+ );
695
727
  unrealizedFinalizedCheckpoint = {
696
728
  epoch: parentBlock.unrealizedFinalizedEpoch,
697
729
  root: fromHex(parentBlock.unrealizedFinalizedRoot),
698
730
  rootHex: parentBlock.unrealizedFinalizedRoot,
731
+ payloadStatus: unrealizedFinalizedPayloadStatus,
699
732
  };
700
733
  } else {
701
734
  // compute new, happens 2/3 first blocks of epoch as monitored in mainnet
702
735
  const unrealized = computeUnrealizedCheckpoints(state);
703
- unrealizedJustifiedCheckpoint = toCheckpointWithHex(unrealized.justifiedCheckpoint);
704
- unrealizedFinalizedCheckpoint = toCheckpointWithHex(unrealized.finalizedCheckpoint);
736
+ // Get payload status for unrealized justified checkpoint
737
+ const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(
738
+ state,
739
+ unrealized.justifiedCheckpoint.epoch
740
+ );
741
+ unrealizedJustifiedCheckpoint = toCheckpointWithPayload(
742
+ unrealized.justifiedCheckpoint,
743
+ unrealizedJustifiedPayloadStatus
744
+ );
745
+ // Get payload status for unrealized finalized checkpoint
746
+ const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(
747
+ state,
748
+ unrealized.finalizedCheckpoint.epoch
749
+ );
750
+ unrealizedFinalizedCheckpoint = toCheckpointWithPayload(
751
+ unrealized.finalizedCheckpoint,
752
+ unrealizedFinalizedPayloadStatus
753
+ );
705
754
  }
706
755
  } else {
707
756
  unrealizedJustifiedCheckpoint = justifiedCheckpoint;
@@ -743,32 +792,56 @@ export class ForkChoice implements IForkChoice {
743
792
  unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch,
744
793
  unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
745
794
 
746
- // TODO GLOAS: Need to update this when we are merging nc/epbs-fc. Need to define `getPostGloasExecStatus`
747
- // to make sure execution status of post-gloas blocks is ExecutionStatus.PayloadSeparated
748
- ...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
749
- ? {
750
- executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
751
- executionPayloadNumber: block.body.executionPayload.blockNumber,
752
- executionStatus: this.getPreGloasExecStatus(executionStatus),
753
- dataAvailabilityStatus,
754
- }
755
- : {
756
- executionPayloadBlockHash: null,
757
- executionStatus: this.getPreMergeExecStatus(executionStatus),
758
- dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
759
- }),
760
795
  ...(isGloasBeaconBlock(block)
761
796
  ? {
762
- builderIndex: block.body.signedExecutionPayloadBid.message.builderIndex,
763
- blockHashHex: toRootHex(block.body.signedExecutionPayloadBid.message.blockHash),
797
+ executionPayloadBlockHash: toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash), // post-gloas, we don't know payload hash until we import execution payload. Set to parent payload hash for now
798
+ executionPayloadNumber: (() => {
799
+ // Determine parent's execution payload number based on which variant the block extends
800
+ const parentBlockHashFromBid = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash);
801
+
802
+ // If parent is pre-merge, return 0
803
+ if (parentBlock.executionPayloadBlockHash === null) {
804
+ return 0;
805
+ }
806
+
807
+ // If parent is pre-Gloas, it only has FULL variant
808
+ if (parentBlock.parentBlockHash === null) {
809
+ return parentBlock.executionPayloadNumber;
810
+ }
811
+
812
+ // Parent is Gloas: get the variant that matches the parentBlockHash from bid
813
+ const parentVariant = this.getBlockHexAndBlockHash(parentRootHex, parentBlockHashFromBid);
814
+ if (parentVariant && parentVariant.executionPayloadBlockHash !== null) {
815
+ return parentVariant.executionPayloadNumber;
816
+ }
817
+ // Fallback to parent block's number (we know it's post-merge from check above)
818
+ return parentBlock.executionPayloadNumber;
819
+ })(),
820
+ executionStatus: this.getPostGloasExecStatus(executionStatus),
821
+ dataAvailabilityStatus,
764
822
  }
765
- : {
766
- builderIndex: undefined,
767
- blockHashHex: undefined,
768
- }),
823
+ : isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
824
+ ? {
825
+ executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
826
+ executionPayloadNumber: block.body.executionPayload.blockNumber,
827
+ executionStatus: this.getPreGloasExecStatus(executionStatus),
828
+ dataAvailabilityStatus,
829
+ }
830
+ : {
831
+ executionPayloadBlockHash: null,
832
+ executionStatus: this.getPreMergeExecStatus(executionStatus),
833
+ dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
834
+ }),
835
+
836
+ payloadStatus: isGloasBeaconBlock(block) ? PayloadStatus.PENDING : PayloadStatus.FULL,
837
+ builderIndex: isGloasBeaconBlock(block) ? block.body.signedExecutionPayloadBid.message.builderIndex : null,
838
+ blockHashFromBid: isGloasBeaconBlock(block)
839
+ ? toRootHex(block.body.signedExecutionPayloadBid.message.blockHash)
840
+ : null,
841
+ parentBlockHash: parentHashHex,
769
842
  };
770
843
 
771
- this.protoArray.onBlock(protoBlock, currentSlot);
844
+ this.protoArray.onBlock(protoBlock, currentSlot, this.proposerBoostRoot);
772
845
 
773
846
  return protoBlock;
774
847
  }
@@ -815,10 +888,45 @@ export class ForkChoice implements IForkChoice {
815
888
 
816
889
  this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
817
890
 
891
+ // Pre-gloas: payload is always present
892
+ // Post-gloas:
893
+ // - always add weight to PENDING
894
+ // - if message.slot > block.slot, it also add weights to FULL or EMPTY
895
+ let payloadStatus: PayloadStatus;
896
+
897
+ // We need to retrieve block to check if it's Gloas and to compare slot
898
+ // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-is_supporting_vote
899
+ const block = this.getBlockHexDefaultStatus(blockRootHex);
900
+
901
+ if (block && isGloasBlock(block)) {
902
+ // Post-Gloas block: determine FULL/EMPTY/PENDING based on slot and committee index
903
+ // If slot > block.slot, we can determine FULL or EMPTY. Else always PENDING
904
+ if (slot > block.slot) {
905
+ if (attestationData.index === 1) {
906
+ payloadStatus = PayloadStatus.FULL;
907
+ } else if (attestationData.index === 0) {
908
+ payloadStatus = PayloadStatus.EMPTY;
909
+ } else {
910
+ throw new ForkChoiceError({
911
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
912
+ err: {
913
+ code: InvalidAttestationCode.INVALID_DATA_INDEX,
914
+ index: attestationData.index,
915
+ },
916
+ });
917
+ }
918
+ } else {
919
+ payloadStatus = PayloadStatus.PENDING;
920
+ }
921
+ } else {
922
+ // Pre-Gloas block or block not found: always FULL
923
+ payloadStatus = PayloadStatus.FULL;
924
+ }
925
+
818
926
  if (slot < this.fcStore.currentSlot) {
819
927
  for (const validatorIndex of attestation.attestingIndices) {
820
928
  if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
821
- this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
929
+ this.addLatestMessage(validatorIndex, slot, blockRootHex, payloadStatus);
822
930
  }
823
931
  }
824
932
  } else {
@@ -829,10 +937,10 @@ export class ForkChoice implements IForkChoice {
829
937
  // Delay consideration in the fork choice until their slot is in the past.
830
938
  // ```
831
939
  const byRoot = this.queuedAttestations.getOrDefault(slot);
832
- const validatorIndices = byRoot.getOrDefault(blockRootHex);
940
+ const validatorVotes = byRoot.getOrDefault(blockRootHex);
833
941
  for (const validatorIndex of attestation.attestingIndices) {
834
942
  if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
835
- validatorIndices.add(validatorIndex);
943
+ validatorVotes.set(validatorIndex, payloadStatus);
836
944
  }
837
945
  }
838
946
  }
@@ -851,6 +959,36 @@ export class ForkChoice implements IForkChoice {
851
959
  }
852
960
  }
853
961
 
962
+ /**
963
+ * Process a PTC (Payload Timeliness Committee) message
964
+ * Updates the PTC votes for multiple validators attesting to a block
965
+ * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
966
+ */
967
+ notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void {
968
+ this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent);
969
+ }
970
+
971
+ /**
972
+ * Notify fork choice that an execution payload has arrived (Gloas fork)
973
+ * Creates the FULL variant of a Gloas block when the payload becomes available
974
+ * Spec: gloas/fork-choice.md#new-on_execution_payload
975
+ */
976
+ onExecutionPayload(
977
+ blockRoot: RootHex,
978
+ executionPayloadBlockHash: RootHex,
979
+ executionPayloadNumber: number,
980
+ executionPayloadStateRoot: RootHex
981
+ ): void {
982
+ this.protoArray.onExecutionPayload(
983
+ blockRoot,
984
+ this.fcStore.currentSlot,
985
+ executionPayloadBlockHash,
986
+ executionPayloadNumber,
987
+ executionPayloadStateRoot,
988
+ this.proposerBoostRoot
989
+ );
990
+ }
991
+
854
992
  /**
855
993
  * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
856
994
  * This should only be called once per slot because:
@@ -882,15 +1020,21 @@ export class ForkChoice implements IForkChoice {
882
1020
  return this.hasBlockHex(toRootHex(blockRoot));
883
1021
  }
884
1022
  /** Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. */
885
- getBlock(blockRoot: Root): ProtoBlock | null {
886
- return this.getBlockHex(toRootHex(blockRoot));
1023
+ getBlock(blockRoot: Root, payloadStatus: PayloadStatus): ProtoBlock | null {
1024
+ return this.getBlockHex(toRootHex(blockRoot), payloadStatus);
1025
+ }
1026
+
1027
+ getBlockDefaultStatus(blockRoot: Root): ProtoBlock | null {
1028
+ return this.getBlockHexDefaultStatus(toRootHex(blockRoot));
887
1029
  }
888
1030
 
889
1031
  /**
890
1032
  * Returns `true` if the block is known **and** a descendant of the finalized root.
1033
+ * Uses default variant (PENDING for Gloas, FULL for pre-Gloas).
891
1034
  */
892
1035
  hasBlockHex(blockRoot: RootHex): boolean {
893
- const node = this.protoArray.getNode(blockRoot);
1036
+ const defaultStatus = this.protoArray.getDefaultVariant(blockRoot);
1037
+ const node = defaultStatus !== undefined ? this.protoArray.getNode(blockRoot, defaultStatus) : undefined;
894
1038
  if (node === undefined) {
895
1039
  return false;
896
1040
  }
@@ -915,8 +1059,8 @@ export class ForkChoice implements IForkChoice {
915
1059
  /**
916
1060
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
917
1061
  */
918
- getBlockHex(blockRoot: RootHex): ProtoBlock | null {
919
- const node = this.protoArray.getNode(blockRoot);
1062
+ getBlockHex(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock | null {
1063
+ const node = this.protoArray.getNode(blockRoot, payloadStatus);
920
1064
  if (!node) {
921
1065
  return null;
922
1066
  }
@@ -930,23 +1074,49 @@ export class ForkChoice implements IForkChoice {
930
1074
  };
931
1075
  }
932
1076
 
1077
+ /**
1078
+ * Returns a `ProtoBlock` with the default variant for the given block root
1079
+ * - Pre-Gloas blocks: returns FULL variant (only variant)
1080
+ * - Gloas blocks: returns PENDING variant
1081
+ *
1082
+ * Use this when you need the canonical block reference regardless of payload status.
1083
+ * For searching by execution payload hash and variant-specific info, use `getBlockHexAndBlockHash` instead.
1084
+ */
1085
+ getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null {
1086
+ const defaultStatus = this.protoArray.getDefaultVariant(blockRoot);
1087
+ if (defaultStatus === undefined) {
1088
+ return null;
1089
+ }
1090
+
1091
+ return this.getBlockHex(blockRoot, defaultStatus);
1092
+ }
1093
+
1094
+ /**
1095
+ * Returns EMPTY or FULL `ProtoBlock` that has matching block root and block hash
1096
+ */
1097
+ getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
1098
+ return this.protoArray.getBlockHexAndBlockHash(blockRoot, blockHash);
1099
+ }
1100
+
933
1101
  getJustifiedBlock(): ProtoBlock {
934
- const block = this.getBlockHex(this.fcStore.justified.checkpoint.rootHex);
1102
+ const {rootHex, payloadStatus} = this.fcStore.justified.checkpoint;
1103
+ const block = this.getBlockHex(rootHex, payloadStatus);
935
1104
  if (!block) {
936
1105
  throw new ForkChoiceError({
937
1106
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
938
- root: this.fcStore.justified.checkpoint.rootHex,
1107
+ root: rootHex,
939
1108
  });
940
1109
  }
941
1110
  return block;
942
1111
  }
943
1112
 
944
1113
  getFinalizedBlock(): ProtoBlock {
945
- const block = this.getBlockHex(this.fcStore.finalizedCheckpoint.rootHex);
1114
+ const {rootHex, payloadStatus} = this.fcStore.finalizedCheckpoint;
1115
+ const block = this.getBlockHex(rootHex, payloadStatus);
946
1116
  if (!block) {
947
1117
  throw new ForkChoiceError({
948
1118
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
949
- root: this.fcStore.finalizedCheckpoint.rootHex,
1119
+ root: rootHex,
950
1120
  });
951
1121
  }
952
1122
  return block;
@@ -963,8 +1133,13 @@ export class ForkChoice implements IForkChoice {
963
1133
  * Always returns `false` if either input roots are unknown.
964
1134
  * Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
965
1135
  */
966
- isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
967
- return this.protoArray.isDescendant(ancestorRoot, descendantRoot);
1136
+ isDescendant(
1137
+ ancestorRoot: RootHex,
1138
+ ancestorPayloadStatus: PayloadStatus,
1139
+ descendantRoot: RootHex,
1140
+ descendantPayloadStatus: PayloadStatus
1141
+ ): boolean {
1142
+ return this.protoArray.isDescendant(ancestorRoot, ancestorPayloadStatus, descendantRoot, descendantPayloadStatus);
968
1143
  }
969
1144
 
970
1145
  /**
@@ -973,7 +1148,7 @@ export class ForkChoice implements IForkChoice {
973
1148
  prune(finalizedRoot: RootHex): ProtoBlock[] {
974
1149
  const prunedNodes = this.protoArray.maybePrune(finalizedRoot);
975
1150
  const prunedCount = prunedNodes.length;
976
- for (let i = 0; i < this.voteNextEpochs.length; i++) {
1151
+ for (let i = 0; i < this.voteNextSlots.length; i++) {
977
1152
  const currentIndex = this.voteCurrentIndices[i];
978
1153
 
979
1154
  if (currentIndex !== NULL_VOTE_INDEX) {
@@ -1007,16 +1182,16 @@ export class ForkChoice implements IForkChoice {
1007
1182
  * Iterates backwards through block summaries, starting from a block root.
1008
1183
  * Return only the non-finalized blocks.
1009
1184
  */
1010
- iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<ProtoBlock> {
1011
- return this.protoArray.iterateAncestorNodes(blockRoot);
1185
+ iterateAncestorBlocks(blockRoot: RootHex, payloadStatus: PayloadStatus): IterableIterator<ProtoBlock> {
1186
+ return this.protoArray.iterateAncestorNodes(blockRoot, payloadStatus);
1012
1187
  }
1013
1188
 
1014
1189
  /**
1015
1190
  * Returns all blocks backwards starting from a block root.
1016
1191
  * Return only the non-finalized blocks.
1017
1192
  */
1018
- getAllAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
1019
- const blocks = this.protoArray.getAllAncestorNodes(blockRoot);
1193
+ getAllAncestorBlocks(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock[] {
1194
+ const blocks = this.protoArray.getAllAncestorNodes(blockRoot, payloadStatus);
1020
1195
  // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
1021
1196
  return blocks.slice(0, blocks.length - 1);
1022
1197
  }
@@ -1024,15 +1199,18 @@ export class ForkChoice implements IForkChoice {
1024
1199
  /**
1025
1200
  * The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
1026
1201
  */
1027
- getAllNonAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
1028
- return this.protoArray.getAllNonAncestorNodes(blockRoot);
1202
+ getAllNonAncestorBlocks(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock[] {
1203
+ return this.protoArray.getAllNonAncestorNodes(blockRoot, payloadStatus);
1029
1204
  }
1030
1205
 
1031
1206
  /**
1032
1207
  * Returns both ancestor and non-ancestor blocks in a single traversal.
1033
1208
  */
1034
- getAllAncestorAndNonAncestorBlocks(blockRoot: RootHex): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]} {
1035
- const {ancestors, nonAncestors} = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot);
1209
+ getAllAncestorAndNonAncestorBlocks(
1210
+ blockRoot: RootHex,
1211
+ payloadStatus: PayloadStatus
1212
+ ): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]} {
1213
+ const {ancestors, nonAncestors} = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot, payloadStatus);
1036
1214
 
1037
1215
  return {
1038
1216
  // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
@@ -1041,6 +1219,21 @@ export class ForkChoice implements IForkChoice {
1041
1219
  };
1042
1220
  }
1043
1221
 
1222
+ getCanonicalBlockByRoot(blockRoot: Root): ProtoBlock | null {
1223
+ const blockRootHex = toRootHex(blockRoot);
1224
+ if (blockRootHex === this.head.blockRoot) {
1225
+ return this.head;
1226
+ }
1227
+
1228
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
1229
+ if (block.blockRoot === blockRootHex) {
1230
+ return block;
1231
+ }
1232
+ }
1233
+
1234
+ return null;
1235
+ }
1236
+
1044
1237
  getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null {
1045
1238
  if (slot > this.head.slot) {
1046
1239
  return null;
@@ -1050,7 +1243,7 @@ export class ForkChoice implements IForkChoice {
1050
1243
  return this.head;
1051
1244
  }
1052
1245
 
1053
- for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
1246
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
1054
1247
  if (block.slot === slot) {
1055
1248
  return block;
1056
1249
  }
@@ -1063,7 +1256,7 @@ export class ForkChoice implements IForkChoice {
1063
1256
  return this.head;
1064
1257
  }
1065
1258
 
1066
- for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
1259
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
1067
1260
  if (slot >= block.slot) {
1068
1261
  return block;
1069
1262
  }
@@ -1076,10 +1269,9 @@ export class ForkChoice implements IForkChoice {
1076
1269
  return this.protoArray.nodes;
1077
1270
  }
1078
1271
 
1079
- *forwardIterateDescendants(blockRoot: RootHex): IterableIterator<ProtoBlock> {
1272
+ *forwardIterateDescendants(blockRoot: RootHex, payloadStatus: PayloadStatus): IterableIterator<ProtoBlock> {
1080
1273
  const rootsInChain = new Set([blockRoot]);
1081
-
1082
- const blockIndex = this.protoArray.indices.get(blockRoot);
1274
+ const blockIndex = this.protoArray.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
1083
1275
  if (blockIndex === undefined) {
1084
1276
  throw new ForkChoiceError({
1085
1277
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
@@ -1116,8 +1308,8 @@ export class ForkChoice implements IForkChoice {
1116
1308
 
1117
1309
  /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
1118
1310
  getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult {
1119
- const prevNode = this.protoArray.getNode(prevBlock.blockRoot);
1120
- const newNode = this.protoArray.getNode(newBlock.blockRoot);
1311
+ const prevNode = this.protoArray.getNode(prevBlock.blockRoot, prevBlock.payloadStatus);
1312
+ const newNode = this.protoArray.getNode(newBlock.blockRoot, newBlock.payloadStatus);
1121
1313
  if (!prevNode || !newNode) {
1122
1314
  return {code: AncestorStatus.BlockUnknown};
1123
1315
  }
@@ -1198,12 +1390,17 @@ export class ForkChoice implements IForkChoice {
1198
1390
  return block.parentRoot;
1199
1391
  }
1200
1392
 
1201
- block =
1202
- block.blockRoot === block.targetRoot
1203
- ? // For the first slot of the epoch, a block is it's own target
1204
- this.protoArray.getBlockReadonly(block.parentRoot)
1205
- : // else we can navigate much faster jumping to the target block
1206
- this.protoArray.getBlockReadonly(block.targetRoot);
1393
+ // For the first slot of the epoch, a block is it's own target
1394
+ const nextRoot = block.blockRoot === block.targetRoot ? block.parentRoot : block.targetRoot;
1395
+ // Use default variant (PENDING for Gloas, FULL for pre-Gloas)
1396
+ // For Gloas: we search for PENDING blocks because dependent root is determined by the block itself,
1397
+ // not the payload. In state-transition, block parentage is independent of payload status,
1398
+ // so linking by PENDING block in fork-choice is correct.
1399
+ const defaultStatus = this.protoArray.getDefaultVariant(nextRoot);
1400
+ if (defaultStatus === undefined) {
1401
+ throw Error(`No block for root ${nextRoot}`);
1402
+ }
1403
+ block = this.protoArray.getBlockReadonly(nextRoot, defaultStatus);
1207
1404
  }
1208
1405
 
1209
1406
  throw Error(`Not found dependent root for block slot ${block.slot}, epoch difference ${epochDifference}`);
@@ -1252,6 +1449,14 @@ export class ForkChoice implements IForkChoice {
1252
1449
  return executionStatus;
1253
1450
  }
1254
1451
 
1452
+ private getPostGloasExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PayloadSeparated {
1453
+ if (executionStatus !== ExecutionStatus.PayloadSeparated)
1454
+ throw Error(
1455
+ `Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}`
1456
+ );
1457
+ return executionStatus;
1458
+ }
1459
+
1255
1460
  /**
1256
1461
  * Why `getJustifiedBalances` getter?
1257
1462
  * - updateCheckpoints() is called in both on_block and on_tick.
@@ -1269,12 +1474,12 @@ export class ForkChoice implements IForkChoice {
1269
1474
  *
1270
1475
  * **`on_tick`**
1271
1476
  * May need the justified balances of:
1272
- * - unrealizedJustified: Already available in `CheckpointHexWithBalance`
1477
+ * - unrealizedJustified: Already available in `CheckpointWithPayloadAndBalance`
1273
1478
  * Since this balances are already available the getter is just `() => balances`, without cache interaction
1274
1479
  */
1275
1480
  private updateCheckpoints(
1276
- justifiedCheckpoint: CheckpointWithHex,
1277
- finalizedCheckpoint: CheckpointWithHex,
1481
+ justifiedCheckpoint: CheckpointWithPayloadStatus,
1482
+ finalizedCheckpoint: CheckpointWithPayloadStatus,
1278
1483
  getJustifiedBalances: () => JustifiedBalances
1279
1484
  ): void {
1280
1485
  // Update justified checkpoint.
@@ -1294,8 +1499,8 @@ export class ForkChoice implements IForkChoice {
1294
1499
  * Update unrealized checkpoints in store if necessary
1295
1500
  */
1296
1501
  private updateUnrealizedCheckpoints(
1297
- unrealizedJustifiedCheckpoint: CheckpointWithHex,
1298
- unrealizedFinalizedCheckpoint: CheckpointWithHex,
1502
+ unrealizedJustifiedCheckpoint: CheckpointWithPayloadStatus,
1503
+ unrealizedFinalizedCheckpoint: CheckpointWithPayloadStatus,
1299
1504
  getJustifiedBalances: () => JustifiedBalances
1300
1505
  ): void {
1301
1506
  if (unrealizedJustifiedCheckpoint.epoch > this.fcStore.unrealizedJustified.checkpoint.epoch) {
@@ -1414,7 +1619,9 @@ export class ForkChoice implements IForkChoice {
1414
1619
  //
1415
1620
  // Attestations must be for a known block. If the block is unknown, we simply drop the
1416
1621
  // attestation and do not delay consideration for later.
1417
- const block = this.protoArray.getBlock(beaconBlockRootHex);
1622
+ // We don't care which variant it is, just need to find the block
1623
+ const defaultStatus = this.protoArray.getDefaultVariant(beaconBlockRootHex);
1624
+ const block = defaultStatus !== undefined ? this.protoArray.getBlock(beaconBlockRootHex, defaultStatus) : undefined;
1418
1625
  if (!block) {
1419
1626
  throw new ForkChoiceError({
1420
1627
  code: ForkChoiceErrorCode.INVALID_ATTESTATION,
@@ -1455,33 +1662,57 @@ export class ForkChoice implements IForkChoice {
1455
1662
  });
1456
1663
  }
1457
1664
 
1665
+ // For Gloas blocks, attestation index must be 0 or 1
1666
+ if (isGloasBlock(block) && attestationData.index !== 0 && attestationData.index !== 1) {
1667
+ throw new ForkChoiceError({
1668
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1669
+ err: {
1670
+ code: InvalidAttestationCode.INVALID_DATA_INDEX,
1671
+ index: attestationData.index,
1672
+ },
1673
+ });
1674
+ }
1675
+
1458
1676
  this.validatedAttestationDatas.add(attDataRoot);
1459
1677
  }
1460
1678
 
1461
1679
  /**
1462
1680
  * Add a validator's latest message to the tracked votes.
1463
1681
  * Always sync voteCurrentIndices and voteNextIndices so that it'll not throw in computeDeltas()
1682
+ *
1683
+ * Modified for Gloas to accept slot and payloadPresent.
1684
+ * Spec: gloas/fork-choice.md#modified-update_latest_messages
1685
+ *
1686
+ * For backward compatibility with Fulu (pre-Gloas):
1687
+ * - Accepts both epoch-derived and slot parameters
1688
+ * - payloadPresent defaults to true for Fulu (payloads embedded in blocks)
1464
1689
  */
1465
- private addLatestMessage(validatorIndex: ValidatorIndex, nextEpoch: Epoch, nextRoot: RootHex): void {
1690
+ private addLatestMessage(
1691
+ validatorIndex: ValidatorIndex,
1692
+ nextSlot: Slot,
1693
+ nextRoot: RootHex,
1694
+ nextPayloadStatus: PayloadStatus
1695
+ ): void {
1466
1696
  // should not happen, attestation is validated before this step
1467
- const nextIndex = this.protoArray.indices.get(nextRoot);
1697
+ // Get the node index for the voted block
1698
+ const nextIndex = this.protoArray.getNodeIndexByRootAndStatus(nextRoot, nextPayloadStatus);
1468
1699
  if (nextIndex === undefined) {
1469
- throw new Error(`Could not find proto index for nextRoot ${nextRoot}`);
1700
+ throw new Error(`Could not find proto index for nextRoot ${nextRoot} with payloadStatus ${nextPayloadStatus}`);
1470
1701
  }
1471
1702
 
1472
1703
  // ensure there is no undefined entries in Votes arrays
1473
- if (this.voteNextEpochs.length < validatorIndex + 1) {
1474
- for (let i = this.voteNextEpochs.length; i < validatorIndex + 1; i++) {
1475
- this.voteNextEpochs[i] = INIT_VOTE_EPOCH;
1704
+ if (this.voteNextSlots.length < validatorIndex + 1) {
1705
+ for (let i = this.voteNextSlots.length; i < validatorIndex + 1; i++) {
1706
+ this.voteNextSlots[i] = INIT_VOTE_SLOT;
1476
1707
  this.voteCurrentIndices[i] = this.voteNextIndices[i] = NULL_VOTE_INDEX;
1477
1708
  }
1478
1709
  }
1479
1710
 
1480
- const existingNextEpoch = this.voteNextEpochs[validatorIndex];
1481
- if (existingNextEpoch === INIT_VOTE_EPOCH || nextEpoch > existingNextEpoch) {
1711
+ const existingNextSlot = this.voteNextSlots[validatorIndex];
1712
+ if (existingNextSlot === INIT_VOTE_SLOT || computeEpochAtSlot(nextSlot) > computeEpochAtSlot(existingNextSlot)) {
1482
1713
  // nextIndex is transfered to currentIndex in computeDeltas()
1483
1714
  this.voteNextIndices[validatorIndex] = nextIndex;
1484
- this.voteNextEpochs[validatorIndex] = nextEpoch;
1715
+ this.voteNextSlots[validatorIndex] = nextSlot;
1485
1716
  }
1486
1717
  // else its an old vote, don't count it
1487
1718
  }
@@ -1493,18 +1724,17 @@ export class ForkChoice implements IForkChoice {
1493
1724
  private processAttestationQueue(): void {
1494
1725
  const currentSlot = this.fcStore.currentSlot;
1495
1726
  for (const [slot, byRoot] of this.queuedAttestations.entries()) {
1496
- const targetEpoch = computeEpochAtSlot(slot);
1497
1727
  if (slot < currentSlot) {
1498
1728
  this.queuedAttestations.delete(slot);
1499
- for (const [blockRoot, validatorIndices] of byRoot.entries()) {
1729
+ for (const [blockRoot, validatorVotes] of byRoot.entries()) {
1500
1730
  const blockRootHex = blockRoot;
1501
- for (const validatorIndex of validatorIndices) {
1731
+ for (const [validatorIndex, payloadStatus] of validatorVotes.entries()) {
1502
1732
  // equivocatingIndices was checked in onAttestation
1503
- this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
1733
+ this.addLatestMessage(validatorIndex, slot, blockRootHex, payloadStatus);
1504
1734
  }
1505
1735
 
1506
1736
  if (slot === currentSlot - 1) {
1507
- this.queuedAttestationsPreviousSlot += validatorIndices.size;
1737
+ this.queuedAttestationsPreviousSlot += validatorVotes.size;
1508
1738
  }
1509
1739
  }
1510
1740
  } else {
@@ -1618,3 +1848,31 @@ export function getCommitteeFraction(
1618
1848
  const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
1619
1849
  return Math.floor((committeeWeight * config.committeePercent) / 100);
1620
1850
  }
1851
+
1852
+ /**
1853
+ * Get the payload status for a checkpoint.
1854
+ *
1855
+ * Pre-Gloas: always FULL (payload embedded in block)
1856
+ * Gloas: determined by state.execution_payload_availability
1857
+ *
1858
+ * @param state - The state to check execution_payload_availability
1859
+ * @param checkpointEpoch - The epoch of the checkpoint
1860
+ */
1861
+ export function getCheckpointPayloadStatus(state: CachedBeaconStateAllForks, checkpointEpoch: number): PayloadStatus {
1862
+ // Compute checkpoint slot first to determine the correct fork
1863
+ const checkpointSlot = computeStartSlotAtEpoch(checkpointEpoch);
1864
+ const fork = state.config.getForkSeq(checkpointSlot);
1865
+
1866
+ // Pre-Gloas: always FULL
1867
+ if (fork < ForkSeq.gloas) {
1868
+ return PayloadStatus.FULL;
1869
+ }
1870
+
1871
+ // For Gloas, check state.execution_payload_availability
1872
+ // - For non-skipped slots at checkpoint: returns false (EMPTY) since payload hasn't arrived yet
1873
+ // - For skipped slots at checkpoint: returns the actual availability status from state
1874
+ const gloasState = state as CachedBeaconStateGloas;
1875
+ const payloadAvailable = gloasState.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT);
1876
+
1877
+ return payloadAvailable ? PayloadStatus.FULL : PayloadStatus.EMPTY;
1878
+ }