@lodestar/fork-choice 1.41.0-dev.afd446235e → 1.41.0-dev.b90dff673d

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 +76 -20
  6. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  7. package/lib/forkChoice/forkChoice.js +304 -118
  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 +33 -3
  32. package/lib/protoArray/interface.d.ts.map +1 -1
  33. package/lib/protoArray/interface.js +31 -1
  34. package/lib/protoArray/interface.js.map +1 -1
  35. package/lib/protoArray/protoArray.d.ts +225 -24
  36. package/lib/protoArray/protoArray.d.ts.map +1 -1
  37. package/lib/protoArray/protoArray.js +763 -134
  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 +387 -127
  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 +48 -3
  48. package/src/protoArray/protoArray.ts +896 -135
@@ -1,22 +1,23 @@
1
- import { SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT } from "@lodestar/params";
1
+ import { ForkSeq, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT } from "@lodestar/params";
2
2
  import { DataAvailabilityStatus, ZERO_HASH, computeEpochAtSlot, computeSlotsSinceEpochStart, computeStartSlotAtEpoch, getAttesterSlashableIndices, isExecutionBlockBodyType, isExecutionEnabled, isExecutionStateType, } from "@lodestar/state-transition";
3
3
  import { computeUnrealizedCheckpoints } from "@lodestar/state-transition/epoch";
4
4
  import { isGloasBeaconBlock, ssz, } from "@lodestar/types";
5
5
  import { MapDef, fromHex, toRootHex } from "@lodestar/utils";
6
6
  import { computeDeltas } from "../protoArray/computeDeltas.js";
7
7
  import { ProtoArrayError, ProtoArrayErrorCode } from "../protoArray/errors.js";
8
- import { ExecutionStatus, HEX_ZERO_HASH, NULL_VOTE_INDEX, } from "../protoArray/interface.js";
8
+ import { ExecutionStatus, HEX_ZERO_HASH, NULL_VOTE_INDEX, PayloadStatus, isGloasBlock, } from "../protoArray/interface.js";
9
9
  import { ForkChoiceError, ForkChoiceErrorCode, InvalidAttestationCode, InvalidBlockCode } from "./errors.js";
10
10
  import { AncestorStatus, NotReorgedReason, } from "./interface.js";
11
- import { toCheckpointWithHex } from "./store.js";
12
- export var UpdateHeadOpt;
11
+ import { toCheckpointWithPayload } from "./store.js";
12
+ export { UpdateHeadOpt };
13
+ var UpdateHeadOpt;
13
14
  (function (UpdateHeadOpt) {
14
15
  UpdateHeadOpt["GetCanonicalHead"] = "getCanonicalHead";
15
16
  UpdateHeadOpt["GetProposerHead"] = "getProposerHead";
16
17
  UpdateHeadOpt["GetPredictedProposerHead"] = "getPredictedProposerHead";
17
18
  })(UpdateHeadOpt || (UpdateHeadOpt = {}));
18
19
  // the initial vote epoch for all validators
19
- const INIT_VOTE_EPOCH = 0;
20
+ const INIT_VOTE_SLOT = 0;
20
21
  /**
21
22
  * Provides an implementation of "Ethereum Consensus -- Beacon Chain Fork Choice":
22
23
  *
@@ -44,16 +45,26 @@ export class ForkChoice {
44
45
  irrecoverableError;
45
46
  /**
46
47
  * Votes currently tracked in the protoArray. Instead of tracking a VoteTracker of currentIndex, nextIndex and epoch,
47
- * we decompose the struct and track them in 3 separate arrays for performance reason.
48
+ * we decompose the struct and track them in separate arrays for performance reason.
49
+ *
50
+ * For Gloas (ePBS), LatestMessage tracks slot instead of epoch and includes payload_present flag.
51
+ * Spec: gloas/fork-choice.md#modified-latestmessage
52
+ *
53
+ * IMPORTANT: voteCurrentIndices and voteNextIndices point to the EXACT variant node index.
54
+ * The payload status is encoded in the node index itself (different variants have different indices).
55
+ * For example, if a validator votes for the EMPTY variant, voteNextIndices[i] points to that specific EMPTY node.
48
56
  */
49
57
  voteCurrentIndices;
50
58
  voteNextIndices;
51
- voteNextEpochs;
59
+ voteNextSlots;
52
60
  /**
53
61
  * Attestations that arrived at the current slot and must be queued for later processing.
54
62
  * NOT currently tracked in the protoArray
63
+ *
64
+ * Modified for Gloas to track PayloadStatus per validator.
65
+ * Maps: Slot -> BlockRoot -> ValidatorIndex -> PayloadStatus
55
66
  */
56
- queuedAttestations = new MapDef(() => new MapDef(() => new Set()));
67
+ queuedAttestations = new MapDef(() => new MapDef(() => new Map()));
57
68
  /**
58
69
  * It's inconsistent to count number of queued attestations at different intervals of slot.
59
70
  * Instead of that, we count number of queued attestations at the previous slot.
@@ -93,11 +104,11 @@ export class ForkChoice {
93
104
  this.voteCurrentIndices = new Array(validatorCount).fill(NULL_VOTE_INDEX);
94
105
  this.voteNextIndices = new Array(validatorCount).fill(NULL_VOTE_INDEX);
95
106
  // when compute deltas, we ignore epoch if voteNextIndex is NULL_VOTE_INDEX anyway
96
- this.voteNextEpochs = new Array(validatorCount).fill(INIT_VOTE_EPOCH);
107
+ this.voteNextSlots = new Array(validatorCount).fill(0);
97
108
  this.head = this.updateHead();
98
109
  this.balances = this.fcStore.justified.balances;
99
110
  metrics?.forkChoice.votes.addCollect(() => {
100
- metrics.forkChoice.votes.set(this.voteNextEpochs.length);
111
+ metrics.forkChoice.votes.set(this.voteNextSlots.length);
101
112
  metrics.forkChoice.queuedAttestations.set(this.queuedAttestationsPreviousSlot);
102
113
  metrics.forkChoice.validatedAttestationDatas.set(this.validatedAttestationDatas.size);
103
114
  metrics.forkChoice.balancesLength.set(this.balances.length);
@@ -163,8 +174,7 @@ export class ForkChoice {
163
174
  // Return true if the given block passes all criteria to be re-orged out
164
175
  // Return false otherwise.
165
176
  // Note when proposer boost reorg is disabled, it always returns false
166
- shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot) {
167
- const headBlock = this.getBlockHex(blockRoot);
177
+ shouldOverrideForkChoiceUpdate(headBlock, secFromSlot, currentSlot) {
168
178
  if (headBlock === null) {
169
179
  // should not happen because this block just got imported. Fall back to no-reorg.
170
180
  return { shouldOverrideFcu: false, reason: NotReorgedReason.HeadBlockNotAvailable };
@@ -179,7 +189,7 @@ export class ForkChoice {
179
189
  });
180
190
  return { shouldOverrideFcu: false, reason: NotReorgedReason.ProposerBoostReorgDisabled };
181
191
  }
182
- const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
192
+ const parentBlock = this.protoArray.getBlock(headBlock.parentRoot, this.protoArray.getParentPayloadStatus(headBlock));
183
193
  const proposalSlot = headBlock.slot + 1;
184
194
  // No reorg if parentBlock isn't available
185
195
  if (parentBlock === undefined) {
@@ -194,7 +204,10 @@ export class ForkChoice {
194
204
  if (!currentTimeOk) {
195
205
  return { shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot };
196
206
  }
197
- this.logger?.verbose("Block is weak. Should override forkchoice update", { blockRoot, slot: currentSlot });
207
+ this.logger?.verbose("Block is weak. Should override forkchoice update", {
208
+ blockRoot: headBlock.blockRoot,
209
+ slot: currentSlot,
210
+ });
198
211
  return { shouldOverrideFcu: true, parentBlock };
199
212
  }
200
213
  /**
@@ -225,7 +238,7 @@ export class ForkChoice {
225
238
  return headBlock;
226
239
  }
227
240
  const blockRoot = headBlock.blockRoot;
228
- const result = this.shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot);
241
+ const result = this.shouldOverrideForkChoiceUpdate(headBlock, secFromSlot, currentSlot);
229
242
  if (result.shouldOverrideFcu) {
230
243
  this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
231
244
  slot: currentSlot,
@@ -262,7 +275,7 @@ export class ForkChoice {
262
275
  });
263
276
  return { proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostReorgDisabled };
264
277
  }
265
- const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
278
+ const parentBlock = this.protoArray.getBlock(headBlock.parentRoot, this.protoArray.getParentPayloadStatus(headBlock));
266
279
  // No reorg if parentBlock isn't available
267
280
  if (parentBlock === undefined) {
268
281
  return { proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable };
@@ -292,7 +305,7 @@ export class ForkChoice {
292
305
  slotsPerEpoch: SLOTS_PER_EPOCH,
293
306
  committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
294
307
  });
295
- const headNode = this.protoArray.getNode(headBlock.blockRoot);
308
+ const headNode = this.protoArray.getNode(headBlock.blockRoot, headBlock.payloadStatus);
296
309
  // If headNode is unavailable, give up reorg
297
310
  if (headNode === undefined || headNode.weight >= reorgThreshold) {
298
311
  return { proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak };
@@ -303,7 +316,7 @@ export class ForkChoice {
303
316
  slotsPerEpoch: SLOTS_PER_EPOCH,
304
317
  committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
305
318
  });
306
- const parentNode = this.protoArray.getNode(parentBlock.blockRoot);
319
+ const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentBlock.payloadStatus);
307
320
  // If parentNode is unavailable, give up reorg
308
321
  if (parentNode === undefined || parentNode.weight <= parentThreshold) {
309
322
  return { proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong };
@@ -375,22 +388,9 @@ export class ForkChoice {
375
388
  finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
376
389
  currentSlot,
377
390
  });
378
- const headRoot = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
379
- const headIndex = this.protoArray.indices.get(headRoot);
380
- if (headIndex === undefined) {
381
- throw new ForkChoiceError({
382
- code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
383
- root: headRoot,
384
- });
385
- }
386
- const headNode = this.protoArray.nodes[headIndex];
387
- if (headNode === undefined) {
388
- throw new ForkChoiceError({
389
- code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
390
- root: headRoot,
391
- });
392
- }
393
- this.head = headNode;
391
+ // findHead returns the ProtoNode representing the head
392
+ const head = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
393
+ this.head = head;
394
394
  return this.head;
395
395
  }
396
396
  /**
@@ -437,14 +437,18 @@ export class ForkChoice {
437
437
  onBlock(block, state, blockDelaySec, currentSlot, executionStatus, dataAvailabilityStatus) {
438
438
  const { parentRoot, slot } = block;
439
439
  const parentRootHex = toRootHex(parentRoot);
440
- // Parent block must be known
441
- const parentBlock = this.protoArray.getBlock(parentRootHex);
440
+ // Parent block must be known because state_transition would have failed otherwise.
441
+ const parentHashHex = isGloasBeaconBlock(block)
442
+ ? toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
443
+ : null;
444
+ const parentBlock = this.protoArray.getParent(parentRootHex, parentHashHex);
442
445
  if (!parentBlock) {
443
446
  throw new ForkChoiceError({
444
447
  code: ForkChoiceErrorCode.INVALID_BLOCK,
445
448
  err: {
446
449
  code: InvalidBlockCode.UNKNOWN_PARENT,
447
450
  root: parentRootHex,
451
+ hash: parentHashHex,
448
452
  },
449
453
  });
450
454
  }
@@ -476,15 +480,16 @@ export class ForkChoice {
476
480
  });
477
481
  }
478
482
  // Check block is a descendant of the finalized block at the checkpoint finalized slot.
479
- const blockAncestorRoot = this.getAncestor(parentRootHex, finalizedSlot);
480
- const finalizedRoot = this.fcStore.finalizedCheckpoint.rootHex;
481
- if (blockAncestorRoot !== finalizedRoot) {
483
+ const blockAncestorNode = this.getAncestor(parentRootHex, finalizedSlot);
484
+ const fcStoreFinalized = this.fcStore.finalizedCheckpoint;
485
+ if (blockAncestorNode.blockRoot !== fcStoreFinalized.rootHex ||
486
+ blockAncestorNode.payloadStatus !== fcStoreFinalized.payloadStatus) {
482
487
  throw new ForkChoiceError({
483
488
  code: ForkChoiceErrorCode.INVALID_BLOCK,
484
489
  err: {
485
490
  code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT,
486
- finalizedRoot,
487
- blockAncestor: blockAncestorRoot,
491
+ finalizedRoot: fcStoreFinalized.rootHex,
492
+ blockAncestor: blockAncestorNode.blockRoot,
488
493
  },
489
494
  });
490
495
  }
@@ -499,9 +504,13 @@ export class ForkChoice {
499
504
  this.proposerBoostRoot === null) {
500
505
  this.proposerBoostRoot = blockRootHex;
501
506
  }
502
- const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint);
503
- const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint);
507
+ // Get justified checkpoint with payload status for Gloas
508
+ const justifiedPayloadStatus = getCheckpointPayloadStatus(state, state.currentJustifiedCheckpoint.epoch);
509
+ const justifiedCheckpoint = toCheckpointWithPayload(state.currentJustifiedCheckpoint, justifiedPayloadStatus);
504
510
  const stateJustifiedEpoch = justifiedCheckpoint.epoch;
511
+ // Get finalized checkpoint with payload status for Gloas
512
+ const finalizedPayloadStatus = getCheckpointPayloadStatus(state, state.finalizedCheckpoint.epoch);
513
+ const finalizedCheckpoint = toCheckpointWithPayload(state.finalizedCheckpoint, finalizedPayloadStatus);
505
514
  // Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if
506
515
  // the justified checkpoint changes
507
516
  this.updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, () => this.fcStore.justifiedBalancesGetter(justifiedCheckpoint, state));
@@ -524,22 +533,32 @@ export class ForkChoice {
524
533
  if (parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
525
534
  parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch) {
526
535
  // reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet
536
+ // Get payload status for unrealized justified checkpoint
537
+ const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(state, parentBlock.unrealizedJustifiedEpoch);
527
538
  unrealizedJustifiedCheckpoint = {
528
539
  epoch: parentBlock.unrealizedJustifiedEpoch,
529
540
  root: fromHex(parentBlock.unrealizedJustifiedRoot),
530
541
  rootHex: parentBlock.unrealizedJustifiedRoot,
542
+ payloadStatus: unrealizedJustifiedPayloadStatus,
531
543
  };
544
+ // Get payload status for unrealized finalized checkpoint
545
+ const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(state, parentBlock.unrealizedFinalizedEpoch);
532
546
  unrealizedFinalizedCheckpoint = {
533
547
  epoch: parentBlock.unrealizedFinalizedEpoch,
534
548
  root: fromHex(parentBlock.unrealizedFinalizedRoot),
535
549
  rootHex: parentBlock.unrealizedFinalizedRoot,
550
+ payloadStatus: unrealizedFinalizedPayloadStatus,
536
551
  };
537
552
  }
538
553
  else {
539
554
  // compute new, happens 2/3 first blocks of epoch as monitored in mainnet
540
555
  const unrealized = computeUnrealizedCheckpoints(state);
541
- unrealizedJustifiedCheckpoint = toCheckpointWithHex(unrealized.justifiedCheckpoint);
542
- unrealizedFinalizedCheckpoint = toCheckpointWithHex(unrealized.finalizedCheckpoint);
556
+ // Get payload status for unrealized justified checkpoint
557
+ const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(state, unrealized.justifiedCheckpoint.epoch);
558
+ unrealizedJustifiedCheckpoint = toCheckpointWithPayload(unrealized.justifiedCheckpoint, unrealizedJustifiedPayloadStatus);
559
+ // Get payload status for unrealized finalized checkpoint
560
+ const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(state, unrealized.finalizedCheckpoint.epoch);
561
+ unrealizedFinalizedCheckpoint = toCheckpointWithPayload(unrealized.finalizedCheckpoint, unrealizedFinalizedPayloadStatus);
543
562
  }
544
563
  }
545
564
  else {
@@ -572,29 +591,51 @@ export class ForkChoice {
572
591
  unrealizedJustifiedRoot: unrealizedJustifiedCheckpoint.rootHex,
573
592
  unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch,
574
593
  unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
575
- ...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
576
- ? {
577
- executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
578
- executionPayloadNumber: block.body.executionPayload.blockNumber,
579
- executionStatus: this.getPostMergeExecStatus(executionStatus),
580
- dataAvailabilityStatus,
581
- }
582
- : {
583
- executionPayloadBlockHash: null,
584
- executionStatus: this.getPreMergeExecStatus(executionStatus),
585
- dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
586
- }),
587
594
  ...(isGloasBeaconBlock(block)
588
595
  ? {
589
- builderIndex: block.body.signedExecutionPayloadBid.message.builderIndex,
590
- blockHashHex: toRootHex(block.body.signedExecutionPayloadBid.message.blockHash),
596
+ 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
597
+ executionPayloadNumber: (() => {
598
+ // Determine parent's execution payload number based on which variant the block extends
599
+ const parentBlockHashFromBid = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash);
600
+ // If parent is pre-merge, return 0
601
+ if (parentBlock.executionPayloadBlockHash === null) {
602
+ return 0;
603
+ }
604
+ // If parent is pre-Gloas, it only has FULL variant
605
+ if (parentBlock.parentBlockHash === null) {
606
+ return parentBlock.executionPayloadNumber;
607
+ }
608
+ // Parent is Gloas: get the variant that matches the parentBlockHash from bid
609
+ const parentVariant = this.getBlockHexAndBlockHash(parentRootHex, parentBlockHashFromBid);
610
+ if (parentVariant && parentVariant.executionPayloadBlockHash !== null) {
611
+ return parentVariant.executionPayloadNumber;
612
+ }
613
+ // Fallback to parent block's number (we know it's post-merge from check above)
614
+ return parentBlock.executionPayloadNumber;
615
+ })(),
616
+ executionStatus: this.getPostGloasExecStatus(executionStatus),
617
+ dataAvailabilityStatus,
591
618
  }
592
- : {
593
- builderIndex: undefined,
594
- blockHashHex: undefined,
595
- }),
619
+ : isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
620
+ ? {
621
+ executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
622
+ executionPayloadNumber: block.body.executionPayload.blockNumber,
623
+ executionStatus: this.getPreGloasExecStatus(executionStatus),
624
+ dataAvailabilityStatus,
625
+ }
626
+ : {
627
+ executionPayloadBlockHash: null,
628
+ executionStatus: this.getPreMergeExecStatus(executionStatus),
629
+ dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
630
+ }),
631
+ payloadStatus: isGloasBeaconBlock(block) ? PayloadStatus.PENDING : PayloadStatus.FULL,
632
+ builderIndex: isGloasBeaconBlock(block) ? block.body.signedExecutionPayloadBid.message.builderIndex : null,
633
+ blockHashFromBid: isGloasBeaconBlock(block)
634
+ ? toRootHex(block.body.signedExecutionPayloadBid.message.blockHash)
635
+ : null,
636
+ parentBlockHash: parentHashHex,
596
637
  };
597
- this.protoArray.onBlock(protoBlock, currentSlot);
638
+ this.protoArray.onBlock(protoBlock, currentSlot, this.proposerBoostRoot);
598
639
  return protoBlock;
599
640
  }
600
641
  /**
@@ -637,10 +678,46 @@ export class ForkChoice {
637
678
  return;
638
679
  }
639
680
  this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
681
+ // Pre-gloas: payload is always present
682
+ // Post-gloas:
683
+ // - always add weight to PENDING
684
+ // - if message.slot > block.slot, it also add weights to FULL or EMPTY
685
+ let payloadStatus;
686
+ // We need to retrieve block to check if it's Gloas and to compare slot
687
+ // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-is_supporting_vote
688
+ const block = this.getBlockHexDefaultStatus(blockRootHex);
689
+ if (block && isGloasBlock(block)) {
690
+ // Post-Gloas block: determine FULL/EMPTY/PENDING based on slot and committee index
691
+ // If slot > block.slot, we can determine FULL or EMPTY. Else always PENDING
692
+ if (slot > block.slot) {
693
+ if (attestationData.index === 1) {
694
+ payloadStatus = PayloadStatus.FULL;
695
+ }
696
+ else if (attestationData.index === 0) {
697
+ payloadStatus = PayloadStatus.EMPTY;
698
+ }
699
+ else {
700
+ throw new ForkChoiceError({
701
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
702
+ err: {
703
+ code: InvalidAttestationCode.INVALID_DATA_INDEX,
704
+ index: attestationData.index,
705
+ },
706
+ });
707
+ }
708
+ }
709
+ else {
710
+ payloadStatus = PayloadStatus.PENDING;
711
+ }
712
+ }
713
+ else {
714
+ // Pre-Gloas block or block not found: always FULL
715
+ payloadStatus = PayloadStatus.FULL;
716
+ }
640
717
  if (slot < this.fcStore.currentSlot) {
641
718
  for (const validatorIndex of attestation.attestingIndices) {
642
719
  if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
643
- this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
720
+ this.addLatestMessage(validatorIndex, slot, blockRootHex, payloadStatus);
644
721
  }
645
722
  }
646
723
  }
@@ -652,10 +729,10 @@ export class ForkChoice {
652
729
  // Delay consideration in the fork choice until their slot is in the past.
653
730
  // ```
654
731
  const byRoot = this.queuedAttestations.getOrDefault(slot);
655
- const validatorIndices = byRoot.getOrDefault(blockRootHex);
732
+ const validatorVotes = byRoot.getOrDefault(blockRootHex);
656
733
  for (const validatorIndex of attestation.attestingIndices) {
657
734
  if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
658
- validatorIndices.add(validatorIndex);
735
+ validatorVotes.set(validatorIndex, payloadStatus);
659
736
  }
660
737
  }
661
738
  }
@@ -672,6 +749,22 @@ export class ForkChoice {
672
749
  this.fcStore.equivocatingIndices.add(validatorIndex);
673
750
  }
674
751
  }
752
+ /**
753
+ * Process a PTC (Payload Timeliness Committee) message
754
+ * Updates the PTC votes for multiple validators attesting to a block
755
+ * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
756
+ */
757
+ notifyPtcMessages(blockRoot, ptcIndices, payloadPresent) {
758
+ this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent);
759
+ }
760
+ /**
761
+ * Notify fork choice that an execution payload has arrived (Gloas fork)
762
+ * Creates the FULL variant of a Gloas block when the payload becomes available
763
+ * Spec: gloas/fork-choice.md#new-on_execution_payload
764
+ */
765
+ onExecutionPayload(blockRoot, executionPayloadBlockHash, executionPayloadNumber, executionPayloadStateRoot) {
766
+ this.protoArray.onExecutionPayload(blockRoot, this.fcStore.currentSlot, executionPayloadBlockHash, executionPayloadNumber, executionPayloadStateRoot, this.proposerBoostRoot);
767
+ }
675
768
  /**
676
769
  * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
677
770
  * This should only be called once per slot because:
@@ -701,14 +794,19 @@ export class ForkChoice {
701
794
  return this.hasBlockHex(toRootHex(blockRoot));
702
795
  }
703
796
  /** Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. */
704
- getBlock(blockRoot) {
705
- return this.getBlockHex(toRootHex(blockRoot));
797
+ getBlock(blockRoot, payloadStatus) {
798
+ return this.getBlockHex(toRootHex(blockRoot), payloadStatus);
799
+ }
800
+ getBlockDefaultStatus(blockRoot) {
801
+ return this.getBlockHexDefaultStatus(toRootHex(blockRoot));
706
802
  }
707
803
  /**
708
804
  * Returns `true` if the block is known **and** a descendant of the finalized root.
805
+ * Uses default variant (PENDING for Gloas, FULL for pre-Gloas).
709
806
  */
710
807
  hasBlockHex(blockRoot) {
711
- const node = this.protoArray.getNode(blockRoot);
808
+ const defaultStatus = this.protoArray.getDefaultVariant(blockRoot);
809
+ const node = defaultStatus !== undefined ? this.protoArray.getNode(blockRoot, defaultStatus) : undefined;
712
810
  if (node === undefined) {
713
811
  return false;
714
812
  }
@@ -729,8 +827,8 @@ export class ForkChoice {
729
827
  /**
730
828
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
731
829
  */
732
- getBlockHex(blockRoot) {
733
- const node = this.protoArray.getNode(blockRoot);
830
+ getBlockHex(blockRoot, payloadStatus) {
831
+ const node = this.protoArray.getNode(blockRoot, payloadStatus);
734
832
  if (!node) {
735
833
  return null;
736
834
  }
@@ -741,22 +839,45 @@ export class ForkChoice {
741
839
  ...node,
742
840
  };
743
841
  }
842
+ /**
843
+ * Returns a `ProtoBlock` with the default variant for the given block root
844
+ * - Pre-Gloas blocks: returns FULL variant (only variant)
845
+ * - Gloas blocks: returns PENDING variant
846
+ *
847
+ * Use this when you need the canonical block reference regardless of payload status.
848
+ * For searching by execution payload hash and variant-specific info, use `getBlockHexAndBlockHash` instead.
849
+ */
850
+ getBlockHexDefaultStatus(blockRoot) {
851
+ const defaultStatus = this.protoArray.getDefaultVariant(blockRoot);
852
+ if (defaultStatus === undefined) {
853
+ return null;
854
+ }
855
+ return this.getBlockHex(blockRoot, defaultStatus);
856
+ }
857
+ /**
858
+ * Returns EMPTY or FULL `ProtoBlock` that has matching block root and block hash
859
+ */
860
+ getBlockHexAndBlockHash(blockRoot, blockHash) {
861
+ return this.protoArray.getBlockHexAndBlockHash(blockRoot, blockHash);
862
+ }
744
863
  getJustifiedBlock() {
745
- const block = this.getBlockHex(this.fcStore.justified.checkpoint.rootHex);
864
+ const { rootHex, payloadStatus } = this.fcStore.justified.checkpoint;
865
+ const block = this.getBlockHex(rootHex, payloadStatus);
746
866
  if (!block) {
747
867
  throw new ForkChoiceError({
748
868
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
749
- root: this.fcStore.justified.checkpoint.rootHex,
869
+ root: rootHex,
750
870
  });
751
871
  }
752
872
  return block;
753
873
  }
754
874
  getFinalizedBlock() {
755
- const block = this.getBlockHex(this.fcStore.finalizedCheckpoint.rootHex);
875
+ const { rootHex, payloadStatus } = this.fcStore.finalizedCheckpoint;
876
+ const block = this.getBlockHex(rootHex, payloadStatus);
756
877
  if (!block) {
757
878
  throw new ForkChoiceError({
758
879
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
759
- root: this.fcStore.finalizedCheckpoint.rootHex,
880
+ root: rootHex,
760
881
  });
761
882
  }
762
883
  return block;
@@ -771,8 +892,8 @@ export class ForkChoice {
771
892
  * Always returns `false` if either input roots are unknown.
772
893
  * Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
773
894
  */
774
- isDescendant(ancestorRoot, descendantRoot) {
775
- return this.protoArray.isDescendant(ancestorRoot, descendantRoot);
895
+ isDescendant(ancestorRoot, ancestorPayloadStatus, descendantRoot, descendantPayloadStatus) {
896
+ return this.protoArray.isDescendant(ancestorRoot, ancestorPayloadStatus, descendantRoot, descendantPayloadStatus);
776
897
  }
777
898
  /**
778
899
  * All indices in votes are relative to proto array so always keep it up to date
@@ -780,7 +901,7 @@ export class ForkChoice {
780
901
  prune(finalizedRoot) {
781
902
  const prunedNodes = this.protoArray.maybePrune(finalizedRoot);
782
903
  const prunedCount = prunedNodes.length;
783
- for (let i = 0; i < this.voteNextEpochs.length; i++) {
904
+ for (let i = 0; i < this.voteNextSlots.length; i++) {
784
905
  const currentIndex = this.voteCurrentIndices[i];
785
906
  if (currentIndex !== NULL_VOTE_INDEX) {
786
907
  if (currentIndex >= prunedCount) {
@@ -811,35 +932,47 @@ export class ForkChoice {
811
932
  * Iterates backwards through block summaries, starting from a block root.
812
933
  * Return only the non-finalized blocks.
813
934
  */
814
- iterateAncestorBlocks(blockRoot) {
815
- return this.protoArray.iterateAncestorNodes(blockRoot);
935
+ iterateAncestorBlocks(blockRoot, payloadStatus) {
936
+ return this.protoArray.iterateAncestorNodes(blockRoot, payloadStatus);
816
937
  }
817
938
  /**
818
939
  * Returns all blocks backwards starting from a block root.
819
940
  * Return only the non-finalized blocks.
820
941
  */
821
- getAllAncestorBlocks(blockRoot) {
822
- const blocks = this.protoArray.getAllAncestorNodes(blockRoot);
942
+ getAllAncestorBlocks(blockRoot, payloadStatus) {
943
+ const blocks = this.protoArray.getAllAncestorNodes(blockRoot, payloadStatus);
823
944
  // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
824
945
  return blocks.slice(0, blocks.length - 1);
825
946
  }
826
947
  /**
827
948
  * The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
828
949
  */
829
- getAllNonAncestorBlocks(blockRoot) {
830
- return this.protoArray.getAllNonAncestorNodes(blockRoot);
950
+ getAllNonAncestorBlocks(blockRoot, payloadStatus) {
951
+ return this.protoArray.getAllNonAncestorNodes(blockRoot, payloadStatus);
831
952
  }
832
953
  /**
833
954
  * Returns both ancestor and non-ancestor blocks in a single traversal.
834
955
  */
835
- getAllAncestorAndNonAncestorBlocks(blockRoot) {
836
- const { ancestors, nonAncestors } = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot);
956
+ getAllAncestorAndNonAncestorBlocks(blockRoot, payloadStatus) {
957
+ const { ancestors, nonAncestors } = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot, payloadStatus);
837
958
  return {
838
959
  // the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
839
960
  ancestors: ancestors.slice(0, ancestors.length - 1),
840
961
  nonAncestors,
841
962
  };
842
963
  }
964
+ getCanonicalBlockByRoot(blockRoot) {
965
+ const blockRootHex = toRootHex(blockRoot);
966
+ if (blockRootHex === this.head.blockRoot) {
967
+ return this.head;
968
+ }
969
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
970
+ if (block.blockRoot === blockRootHex) {
971
+ return block;
972
+ }
973
+ }
974
+ return null;
975
+ }
843
976
  getCanonicalBlockAtSlot(slot) {
844
977
  if (slot > this.head.slot) {
845
978
  return null;
@@ -847,7 +980,7 @@ export class ForkChoice {
847
980
  if (slot === this.head.slot) {
848
981
  return this.head;
849
982
  }
850
- for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
983
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
851
984
  if (block.slot === slot) {
852
985
  return block;
853
986
  }
@@ -858,7 +991,7 @@ export class ForkChoice {
858
991
  if (slot >= this.head.slot) {
859
992
  return this.head;
860
993
  }
861
- for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
994
+ for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot, this.head.payloadStatus)) {
862
995
  if (slot >= block.slot) {
863
996
  return block;
864
997
  }
@@ -869,9 +1002,9 @@ export class ForkChoice {
869
1002
  forwarditerateAncestorBlocks() {
870
1003
  return this.protoArray.nodes;
871
1004
  }
872
- *forwardIterateDescendants(blockRoot) {
1005
+ *forwardIterateDescendants(blockRoot, payloadStatus) {
873
1006
  const rootsInChain = new Set([blockRoot]);
874
- const blockIndex = this.protoArray.indices.get(blockRoot);
1007
+ const blockIndex = this.protoArray.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
875
1008
  if (blockIndex === undefined) {
876
1009
  throw new ForkChoiceError({
877
1010
  code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
@@ -904,8 +1037,8 @@ export class ForkChoice {
904
1037
  }
905
1038
  /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
906
1039
  getCommonAncestorDepth(prevBlock, newBlock) {
907
- const prevNode = this.protoArray.getNode(prevBlock.blockRoot);
908
- const newNode = this.protoArray.getNode(newBlock.blockRoot);
1040
+ const prevNode = this.protoArray.getNode(prevBlock.blockRoot, prevBlock.payloadStatus);
1041
+ const newNode = this.protoArray.getNode(newBlock.blockRoot, newBlock.payloadStatus);
909
1042
  if (!prevNode || !newNode) {
910
1043
  return { code: AncestorStatus.BlockUnknown };
911
1044
  }
@@ -977,12 +1110,17 @@ export class ForkChoice {
977
1110
  if (block.slot === beforeSlot) {
978
1111
  return block.parentRoot;
979
1112
  }
980
- block =
981
- block.blockRoot === block.targetRoot
982
- ? // For the first slot of the epoch, a block is it's own target
983
- this.protoArray.getBlockReadonly(block.parentRoot)
984
- : // else we can navigate much faster jumping to the target block
985
- this.protoArray.getBlockReadonly(block.targetRoot);
1113
+ // For the first slot of the epoch, a block is it's own target
1114
+ const nextRoot = block.blockRoot === block.targetRoot ? block.parentRoot : block.targetRoot;
1115
+ // Use default variant (PENDING for Gloas, FULL for pre-Gloas)
1116
+ // For Gloas: we search for PENDING blocks because dependent root is determined by the block itself,
1117
+ // not the payload. In state-transition, block parentage is independent of payload status,
1118
+ // so linking by PENDING block in fork-choice is correct.
1119
+ const defaultStatus = this.protoArray.getDefaultVariant(nextRoot);
1120
+ if (defaultStatus === undefined) {
1121
+ throw Error(`No block for root ${nextRoot}`);
1122
+ }
1123
+ block = this.protoArray.getBlockReadonly(nextRoot, defaultStatus);
986
1124
  }
987
1125
  throw Error(`Not found dependent root for block slot ${block.slot}, epoch difference ${epochDifference}`);
988
1126
  }
@@ -1013,9 +1151,14 @@ export class ForkChoice {
1013
1151
  throw Error(`Invalid pre-merge data status: expected: ${DataAvailabilityStatus.PreData}, got ${dataAvailabilityStatus}`);
1014
1152
  return dataAvailabilityStatus;
1015
1153
  }
1016
- getPostMergeExecStatus(executionStatus) {
1017
- if (executionStatus === ExecutionStatus.PreMerge)
1018
- throw Error(`Invalid post-merge execution status: expected: ${ExecutionStatus.Syncing} or ${ExecutionStatus.Valid} , got ${executionStatus}`);
1154
+ getPreGloasExecStatus(executionStatus) {
1155
+ if (executionStatus === ExecutionStatus.PreMerge || executionStatus === ExecutionStatus.PayloadSeparated)
1156
+ throw Error(`Invalid post-merge execution status: expected: ${ExecutionStatus.Syncing} or ${ExecutionStatus.Valid}, got ${executionStatus}`);
1157
+ return executionStatus;
1158
+ }
1159
+ getPostGloasExecStatus(executionStatus) {
1160
+ if (executionStatus !== ExecutionStatus.PayloadSeparated)
1161
+ throw Error(`Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}`);
1019
1162
  return executionStatus;
1020
1163
  }
1021
1164
  /**
@@ -1035,7 +1178,7 @@ export class ForkChoice {
1035
1178
  *
1036
1179
  * **`on_tick`**
1037
1180
  * May need the justified balances of:
1038
- * - unrealizedJustified: Already available in `CheckpointHexWithBalance`
1181
+ * - unrealizedJustified: Already available in `CheckpointWithPayloadAndBalance`
1039
1182
  * Since this balances are already available the getter is just `() => balances`, without cache interaction
1040
1183
  */
1041
1184
  updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, getJustifiedBalances) {
@@ -1150,7 +1293,9 @@ export class ForkChoice {
1150
1293
  //
1151
1294
  // Attestations must be for a known block. If the block is unknown, we simply drop the
1152
1295
  // attestation and do not delay consideration for later.
1153
- const block = this.protoArray.getBlock(beaconBlockRootHex);
1296
+ // We don't care which variant it is, just need to find the block
1297
+ const defaultStatus = this.protoArray.getDefaultVariant(beaconBlockRootHex);
1298
+ const block = defaultStatus !== undefined ? this.protoArray.getBlock(beaconBlockRootHex, defaultStatus) : undefined;
1154
1299
  if (!block) {
1155
1300
  throw new ForkChoiceError({
1156
1301
  code: ForkChoiceErrorCode.INVALID_ATTESTATION,
@@ -1187,30 +1332,48 @@ export class ForkChoice {
1187
1332
  },
1188
1333
  });
1189
1334
  }
1335
+ // For Gloas blocks, attestation index must be 0 or 1
1336
+ if (isGloasBlock(block) && attestationData.index !== 0 && attestationData.index !== 1) {
1337
+ throw new ForkChoiceError({
1338
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1339
+ err: {
1340
+ code: InvalidAttestationCode.INVALID_DATA_INDEX,
1341
+ index: attestationData.index,
1342
+ },
1343
+ });
1344
+ }
1190
1345
  this.validatedAttestationDatas.add(attDataRoot);
1191
1346
  }
1192
1347
  /**
1193
1348
  * Add a validator's latest message to the tracked votes.
1194
1349
  * Always sync voteCurrentIndices and voteNextIndices so that it'll not throw in computeDeltas()
1350
+ *
1351
+ * Modified for Gloas to accept slot and payloadPresent.
1352
+ * Spec: gloas/fork-choice.md#modified-update_latest_messages
1353
+ *
1354
+ * For backward compatibility with Fulu (pre-Gloas):
1355
+ * - Accepts both epoch-derived and slot parameters
1356
+ * - payloadPresent defaults to true for Fulu (payloads embedded in blocks)
1195
1357
  */
1196
- addLatestMessage(validatorIndex, nextEpoch, nextRoot) {
1358
+ addLatestMessage(validatorIndex, nextSlot, nextRoot, nextPayloadStatus) {
1197
1359
  // should not happen, attestation is validated before this step
1198
- const nextIndex = this.protoArray.indices.get(nextRoot);
1360
+ // Get the node index for the voted block
1361
+ const nextIndex = this.protoArray.getNodeIndexByRootAndStatus(nextRoot, nextPayloadStatus);
1199
1362
  if (nextIndex === undefined) {
1200
- throw new Error(`Could not find proto index for nextRoot ${nextRoot}`);
1363
+ throw new Error(`Could not find proto index for nextRoot ${nextRoot} with payloadStatus ${nextPayloadStatus}`);
1201
1364
  }
1202
1365
  // ensure there is no undefined entries in Votes arrays
1203
- if (this.voteNextEpochs.length < validatorIndex + 1) {
1204
- for (let i = this.voteNextEpochs.length; i < validatorIndex + 1; i++) {
1205
- this.voteNextEpochs[i] = INIT_VOTE_EPOCH;
1366
+ if (this.voteNextSlots.length < validatorIndex + 1) {
1367
+ for (let i = this.voteNextSlots.length; i < validatorIndex + 1; i++) {
1368
+ this.voteNextSlots[i] = INIT_VOTE_SLOT;
1206
1369
  this.voteCurrentIndices[i] = this.voteNextIndices[i] = NULL_VOTE_INDEX;
1207
1370
  }
1208
1371
  }
1209
- const existingNextEpoch = this.voteNextEpochs[validatorIndex];
1210
- if (existingNextEpoch === INIT_VOTE_EPOCH || nextEpoch > existingNextEpoch) {
1372
+ const existingNextSlot = this.voteNextSlots[validatorIndex];
1373
+ if (existingNextSlot === INIT_VOTE_SLOT || computeEpochAtSlot(nextSlot) > computeEpochAtSlot(existingNextSlot)) {
1211
1374
  // nextIndex is transfered to currentIndex in computeDeltas()
1212
1375
  this.voteNextIndices[validatorIndex] = nextIndex;
1213
- this.voteNextEpochs[validatorIndex] = nextEpoch;
1376
+ this.voteNextSlots[validatorIndex] = nextSlot;
1214
1377
  }
1215
1378
  // else its an old vote, don't count it
1216
1379
  }
@@ -1221,17 +1384,16 @@ export class ForkChoice {
1221
1384
  processAttestationQueue() {
1222
1385
  const currentSlot = this.fcStore.currentSlot;
1223
1386
  for (const [slot, byRoot] of this.queuedAttestations.entries()) {
1224
- const targetEpoch = computeEpochAtSlot(slot);
1225
1387
  if (slot < currentSlot) {
1226
1388
  this.queuedAttestations.delete(slot);
1227
- for (const [blockRoot, validatorIndices] of byRoot.entries()) {
1389
+ for (const [blockRoot, validatorVotes] of byRoot.entries()) {
1228
1390
  const blockRootHex = blockRoot;
1229
- for (const validatorIndex of validatorIndices) {
1391
+ for (const [validatorIndex, payloadStatus] of validatorVotes.entries()) {
1230
1392
  // equivocatingIndices was checked in onAttestation
1231
- this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
1393
+ this.addLatestMessage(validatorIndex, slot, blockRootHex, payloadStatus);
1232
1394
  }
1233
1395
  if (slot === currentSlot - 1) {
1234
- this.queuedAttestationsPreviousSlot += validatorIndices.size;
1396
+ this.queuedAttestationsPreviousSlot += validatorVotes.size;
1235
1397
  }
1236
1398
  }
1237
1399
  }
@@ -1322,4 +1484,28 @@ export function getCommitteeFraction(justifiedTotalActiveBalanceByIncrement, con
1322
1484
  const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
1323
1485
  return Math.floor((committeeWeight * config.committeePercent) / 100);
1324
1486
  }
1487
+ /**
1488
+ * Get the payload status for a checkpoint.
1489
+ *
1490
+ * Pre-Gloas: always FULL (payload embedded in block)
1491
+ * Gloas: determined by state.execution_payload_availability
1492
+ *
1493
+ * @param state - The state to check execution_payload_availability
1494
+ * @param checkpointEpoch - The epoch of the checkpoint
1495
+ */
1496
+ export function getCheckpointPayloadStatus(state, checkpointEpoch) {
1497
+ // Compute checkpoint slot first to determine the correct fork
1498
+ const checkpointSlot = computeStartSlotAtEpoch(checkpointEpoch);
1499
+ const fork = state.config.getForkSeq(checkpointSlot);
1500
+ // Pre-Gloas: always FULL
1501
+ if (fork < ForkSeq.gloas) {
1502
+ return PayloadStatus.FULL;
1503
+ }
1504
+ // For Gloas, check state.execution_payload_availability
1505
+ // - For non-skipped slots at checkpoint: returns false (EMPTY) since payload hasn't arrived yet
1506
+ // - For skipped slots at checkpoint: returns the actual availability status from state
1507
+ const gloasState = state;
1508
+ const payloadAvailable = gloasState.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT);
1509
+ return payloadAvailable ? PayloadStatus.FULL : PayloadStatus.EMPTY;
1510
+ }
1325
1511
  //# sourceMappingURL=forkChoice.js.map