@lodestar/fork-choice 1.41.0-dev.d41697c085 → 1.41.0-dev.dce096d70c

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