@lodestar/fork-choice 1.42.0-dev.4411584fd8 → 1.42.0-dev.47afaa6bb7

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.
@@ -1,20 +1,18 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkSeq, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
3
3
  import {
4
- CachedBeaconStateAllForks,
5
- CachedBeaconStateGloas,
6
4
  DataAvailabilityStatus,
7
5
  EffectiveBalanceIncrements,
6
+ IBeaconStateView,
8
7
  ZERO_HASH,
9
8
  computeEpochAtSlot,
10
9
  computeSlotsSinceEpochStart,
11
10
  computeStartSlotAtEpoch,
12
11
  getAttesterSlashableIndices,
13
12
  isExecutionBlockBodyType,
14
- isExecutionEnabled,
15
- isExecutionStateType,
13
+ isStatePostBellatrix,
14
+ isStatePostGloas,
16
15
  } from "@lodestar/state-transition";
17
- import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
18
16
  import {
19
17
  AttesterSlashing,
20
18
  BeaconBlock,
@@ -33,11 +31,12 @@ import {ForkChoiceMetrics} from "../metrics.js";
33
31
  import {computeDeltas} from "../protoArray/computeDeltas.js";
34
32
  import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";
35
33
  import {
34
+ BlockExecutionStatus,
36
35
  ExecutionStatus,
37
36
  HEX_ZERO_HASH,
38
37
  LVHExecResponse,
39
- MaybeValidExecutionStatus,
40
38
  NULL_VOTE_INDEX,
39
+ PayloadExecutionStatus,
41
40
  PayloadStatus,
42
41
  ProtoBlock,
43
42
  ProtoNode,
@@ -586,10 +585,10 @@ export class ForkChoice implements IForkChoice {
586
585
  */
587
586
  onBlock(
588
587
  block: BeaconBlock,
589
- state: CachedBeaconStateAllForks,
588
+ state: IBeaconStateView,
590
589
  blockDelaySec: number,
591
590
  currentSlot: Slot,
592
- executionStatus: MaybeValidExecutionStatus,
591
+ executionStatus: BlockExecutionStatus,
593
592
  dataAvailabilityStatus: DataAvailabilityStatus
594
593
  ): ProtoBlock {
595
594
  const {parentRoot, slot} = block;
@@ -672,12 +671,16 @@ export class ForkChoice implements IForkChoice {
672
671
  }
673
672
 
674
673
  // Get justified checkpoint with payload status for Gloas
675
- const justifiedPayloadStatus = getCheckpointPayloadStatus(state, state.currentJustifiedCheckpoint.epoch);
674
+ const justifiedPayloadStatus = getCheckpointPayloadStatus(
675
+ this.config,
676
+ state,
677
+ state.currentJustifiedCheckpoint.epoch
678
+ );
676
679
  const justifiedCheckpoint = toCheckpointWithPayload(state.currentJustifiedCheckpoint, justifiedPayloadStatus);
677
680
  const stateJustifiedEpoch = justifiedCheckpoint.epoch;
678
681
 
679
682
  // Get finalized checkpoint with payload status for Gloas
680
- const finalizedPayloadStatus = getCheckpointPayloadStatus(state, state.finalizedCheckpoint.epoch);
683
+ const finalizedPayloadStatus = getCheckpointPayloadStatus(this.config, state, state.finalizedCheckpoint.epoch);
681
684
  const finalizedCheckpoint = toCheckpointWithPayload(state.finalizedCheckpoint, finalizedPayloadStatus);
682
685
 
683
686
  // Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if
@@ -710,6 +713,7 @@ export class ForkChoice implements IForkChoice {
710
713
  // reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet
711
714
  // Get payload status for unrealized justified checkpoint
712
715
  const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(
716
+ this.config,
713
717
  state,
714
718
  parentBlock.unrealizedJustifiedEpoch
715
719
  );
@@ -721,6 +725,7 @@ export class ForkChoice implements IForkChoice {
721
725
  };
722
726
  // Get payload status for unrealized finalized checkpoint
723
727
  const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(
728
+ this.config,
724
729
  state,
725
730
  parentBlock.unrealizedFinalizedEpoch
726
731
  );
@@ -732,9 +737,10 @@ export class ForkChoice implements IForkChoice {
732
737
  };
733
738
  } else {
734
739
  // compute new, happens 2/3 first blocks of epoch as monitored in mainnet
735
- const unrealized = computeUnrealizedCheckpoints(state);
740
+ const unrealized = state.computeUnrealizedCheckpoints();
736
741
  // Get payload status for unrealized justified checkpoint
737
742
  const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus(
743
+ this.config,
738
744
  state,
739
745
  unrealized.justifiedCheckpoint.epoch
740
746
  );
@@ -744,6 +750,7 @@ export class ForkChoice implements IForkChoice {
744
750
  );
745
751
  // Get payload status for unrealized finalized checkpoint
746
752
  const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus(
753
+ this.config,
747
754
  state,
748
755
  unrealized.finalizedCheckpoint.epoch
749
756
  );
@@ -771,7 +778,7 @@ export class ForkChoice implements IForkChoice {
771
778
  }
772
779
 
773
780
  const targetSlot = computeStartSlotAtEpoch(blockEpoch);
774
- const targetRoot = slot === targetSlot ? blockRoot : state.blockRoots.get(targetSlot % SLOTS_PER_HISTORICAL_ROOT);
781
+ const targetRoot = slot === targetSlot ? blockRoot : state.getBlockRootAtSlot(targetSlot);
775
782
 
776
783
  // This does not apply a vote to the block, it just makes fork choice aware of the block so
777
784
  // it can still be identified as the head even if it doesn't have any votes.
@@ -820,7 +827,10 @@ export class ForkChoice implements IForkChoice {
820
827
  executionStatus: this.getPostGloasExecStatus(executionStatus),
821
828
  dataAvailabilityStatus,
822
829
  }
823
- : isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
830
+ : isExecutionBlockBodyType(block.body) &&
831
+ isStatePostBellatrix(state) &&
832
+ state.isExecutionStateType &&
833
+ state.isExecutionEnabled(block)
824
834
  ? {
825
835
  executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
826
836
  executionPayloadNumber: block.body.executionPayload.blockNumber,
@@ -834,10 +844,6 @@ export class ForkChoice implements IForkChoice {
834
844
  }),
835
845
 
836
846
  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
847
  parentBlockHash: parentHashHex,
842
848
  };
843
849
 
@@ -977,7 +983,8 @@ export class ForkChoice implements IForkChoice {
977
983
  blockRoot: RootHex,
978
984
  executionPayloadBlockHash: RootHex,
979
985
  executionPayloadNumber: number,
980
- executionPayloadStateRoot: RootHex
986
+ executionPayloadStateRoot: RootHex,
987
+ executionStatus: PayloadExecutionStatus
981
988
  ): void {
982
989
  this.protoArray.onExecutionPayload(
983
990
  blockRoot,
@@ -985,7 +992,8 @@ export class ForkChoice implements IForkChoice {
985
992
  executionPayloadBlockHash,
986
993
  executionPayloadNumber,
987
994
  executionPayloadStateRoot,
988
- this.proposerBoostRoot
995
+ this.proposerBoostRoot,
996
+ executionStatus
989
997
  );
990
998
  }
991
999
 
@@ -1043,19 +1051,34 @@ export class ForkChoice implements IForkChoice {
1043
1051
  }
1044
1052
 
1045
1053
  /**
1046
- * Same to hasBlock but without checking if the block is a descendant of the finalized root.
1054
+ * Same as hasBlock but without checking if the block is a descendant of the finalized root.
1047
1055
  */
1048
1056
  hasBlockUnsafe(blockRoot: Root): boolean {
1049
1057
  return this.hasBlockHexUnsafe(toRootHex(blockRoot));
1050
1058
  }
1051
1059
 
1052
1060
  /**
1053
- * Same to hasBlockHex but without checking if the block is a descendant of the finalized root.
1061
+ * Same as hasBlockHex but without checking if the block is a descendant of the finalized root.
1054
1062
  */
1055
1063
  hasBlockHexUnsafe(blockRoot: RootHex): boolean {
1056
1064
  return this.protoArray.hasBlock(blockRoot);
1057
1065
  }
1058
1066
 
1067
+ /**
1068
+ * Returns true if the FULL payload variant (execution payload envelope) exists for this block root,
1069
+ * without checking if the block is a descendant of the finalized root.
1070
+ */
1071
+ hasPayloadUnsafe(blockRoot: Root): boolean {
1072
+ return this.hasPayloadHexUnsafe(toRootHex(blockRoot));
1073
+ }
1074
+
1075
+ /**
1076
+ * Same as hasPayloadUnsafe but accepts a hex-encoded block root.
1077
+ */
1078
+ hasPayloadHexUnsafe(blockRoot: RootHex): boolean {
1079
+ return this.protoArray.hasPayload(blockRoot);
1080
+ }
1081
+
1059
1082
  /**
1060
1083
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
1061
1084
  */
@@ -1425,7 +1448,7 @@ export class ForkChoice implements IForkChoice {
1425
1448
  return secFromSlot * 1000 <= proposerReorgCutoff;
1426
1449
  }
1427
1450
 
1428
- private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
1451
+ private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge {
1429
1452
  if (executionStatus !== ExecutionStatus.PreMerge)
1430
1453
  throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);
1431
1454
  return executionStatus;
@@ -1440,7 +1463,7 @@ export class ForkChoice implements IForkChoice {
1440
1463
  }
1441
1464
 
1442
1465
  private getPreGloasExecStatus(
1443
- executionStatus: MaybeValidExecutionStatus
1466
+ executionStatus: BlockExecutionStatus
1444
1467
  ): ExecutionStatus.Valid | ExecutionStatus.Syncing {
1445
1468
  if (executionStatus === ExecutionStatus.PreMerge || executionStatus === ExecutionStatus.PayloadSeparated)
1446
1469
  throw Error(
@@ -1449,7 +1472,7 @@ export class ForkChoice implements IForkChoice {
1449
1472
  return executionStatus;
1450
1473
  }
1451
1474
 
1452
- private getPostGloasExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PayloadSeparated {
1475
+ private getPostGloasExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PayloadSeparated {
1453
1476
  if (executionStatus !== ExecutionStatus.PayloadSeparated)
1454
1477
  throw Error(
1455
1478
  `Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}`
@@ -1673,6 +1696,20 @@ export class ForkChoice implements IForkChoice {
1673
1696
  });
1674
1697
  }
1675
1698
 
1699
+ // If attesting for a full node, the payload must be known
1700
+ if (isGloasBlock(block) && attestationData.index === 1) {
1701
+ const fullNodeIndex = this.protoArray.getNodeIndexByRootAndStatus(beaconBlockRootHex, PayloadStatus.FULL);
1702
+ if (fullNodeIndex === undefined) {
1703
+ throw new ForkChoiceError({
1704
+ code: ForkChoiceErrorCode.INVALID_ATTESTATION,
1705
+ err: {
1706
+ code: InvalidAttestationCode.UNKNOWN_PAYLOAD_STATUS,
1707
+ beaconBlockRoot: beaconBlockRootHex,
1708
+ },
1709
+ });
1710
+ }
1711
+ }
1712
+
1676
1713
  this.validatedAttestationDatas.add(attDataRoot);
1677
1714
  }
1678
1715
 
@@ -1855,24 +1892,31 @@ export function getCommitteeFraction(
1855
1892
  * Pre-Gloas: always FULL (payload embedded in block)
1856
1893
  * Gloas: determined by state.execution_payload_availability
1857
1894
  *
1895
+ * @param config - The chain fork config to determine fork at checkpoint slot
1858
1896
  * @param state - The state to check execution_payload_availability
1859
1897
  * @param checkpointEpoch - The epoch of the checkpoint
1860
1898
  */
1861
- export function getCheckpointPayloadStatus(state: CachedBeaconStateAllForks, checkpointEpoch: number): PayloadStatus {
1899
+ export function getCheckpointPayloadStatus(
1900
+ config: ChainForkConfig,
1901
+ state: IBeaconStateView,
1902
+ checkpointEpoch: number
1903
+ ): PayloadStatus {
1862
1904
  // Compute checkpoint slot first to determine the correct fork
1863
1905
  const checkpointSlot = computeStartSlotAtEpoch(checkpointEpoch);
1864
- const fork = state.config.getForkSeq(checkpointSlot);
1906
+ const fork = config.getForkSeq(checkpointSlot);
1865
1907
 
1866
1908
  // Pre-Gloas: always FULL
1867
1909
  if (fork < ForkSeq.gloas) {
1868
1910
  return PayloadStatus.FULL;
1869
1911
  }
1912
+ if (!isStatePostGloas(state)) {
1913
+ throw new Error(`Expected gloas+ state for checkpoint payload status, got fork=${state.forkName}`);
1914
+ }
1870
1915
 
1871
1916
  // For Gloas, check state.execution_payload_availability
1872
1917
  // - For non-skipped slots at checkpoint: returns false (EMPTY) since payload hasn't arrived yet
1873
1918
  // - 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);
1919
+ const payloadAvailable = state.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT);
1876
1920
 
1877
1921
  return payloadAvailable ? PayloadStatus.FULL : PayloadStatus.EMPTY;
1878
1922
  }
@@ -1,12 +1,9 @@
1
- import {
2
- CachedBeaconStateAllForks,
3
- DataAvailabilityStatus,
4
- EffectiveBalanceIncrements,
5
- } from "@lodestar/state-transition";
1
+ import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
6
2
  import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types";
7
3
  import {
4
+ BlockExecutionStatus,
8
5
  LVHExecResponse,
9
- MaybeValidExecutionStatus,
6
+ PayloadExecutionStatus,
10
7
  PayloadStatus,
11
8
  ProtoBlock,
12
9
  ProtoNode,
@@ -144,10 +141,10 @@ export interface IForkChoice {
144
141
  */
145
142
  onBlock(
146
143
  block: BeaconBlock,
147
- state: CachedBeaconStateAllForks,
144
+ state: IBeaconStateView,
148
145
  blockDelaySec: number,
149
146
  currentSlot: Slot,
150
- executionStatus: MaybeValidExecutionStatus,
147
+ executionStatus: BlockExecutionStatus,
151
148
  dataAvailabilityStatus: DataAvailabilityStatus
152
149
  ): ProtoBlock;
153
150
  /**
@@ -207,7 +204,8 @@ export interface IForkChoice {
207
204
  blockRoot: RootHex,
208
205
  executionPayloadBlockHash: RootHex,
209
206
  executionPayloadNumber: number,
210
- executionPayloadStateRoot: RootHex
207
+ executionPayloadStateRoot: RootHex,
208
+ executionStatus: PayloadExecutionStatus
211
209
  ): void;
212
210
  /**
213
211
  * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
@@ -228,6 +226,12 @@ export interface IForkChoice {
228
226
  */
229
227
  hasBlockUnsafe(blockRoot: Root): boolean;
230
228
  hasBlockHexUnsafe(blockRoot: RootHex): boolean;
229
+ /**
230
+ * Returns true if the FULL payload variant (execution payload envelope) exists for this block root,
231
+ * without checking if the block is a descendant of the finalized root.
232
+ */
233
+ hasPayloadUnsafe(blockRoot: Root): boolean;
234
+ hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
231
235
  getSlotsPresent(windowStart: number): number;
232
236
  /**
233
237
  * Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
@@ -1,4 +1,4 @@
1
- import {CachedBeaconStateAllForks, EffectiveBalanceIncrements} from "@lodestar/state-transition";
1
+ import {EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
2
2
  import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
3
3
  import {toRootHex} from "@lodestar/utils";
4
4
  import {PayloadStatus} from "../protoArray/interface.js";
@@ -30,7 +30,7 @@ export type JustifiedBalances = EffectiveBalanceIncrements;
30
30
  */
31
31
  export type JustifiedBalancesGetter = (
32
32
  checkpoint: CheckpointWithPayloadStatus,
33
- blockState: CachedBeaconStateAllForks
33
+ blockState: IBeaconStateView
34
34
  ) => JustifiedBalances;
35
35
 
36
36
  /**
package/src/index.ts CHANGED
@@ -31,10 +31,11 @@ export {
31
31
  } from "./forkChoice/store.js";
32
32
  export {type ForkChoiceMetrics, getForkChoiceMetrics} from "./metrics.js";
33
33
  export type {
34
+ BlockExecutionStatus,
34
35
  BlockExtraMeta,
35
36
  LVHInvalidResponse,
36
37
  LVHValidResponse,
37
- MaybeValidExecutionStatus,
38
+ PayloadExecutionStatus,
38
39
  ProtoBlock,
39
40
  ProtoNode,
40
41
  } from "./protoArray/interface.js";
@@ -64,7 +64,18 @@ export type LVHInvalidResponse = {
64
64
  };
65
65
  export type LVHExecResponse = LVHValidResponse | LVHInvalidResponse;
66
66
 
67
- export type MaybeValidExecutionStatus = Exclude<ExecutionStatus, ExecutionStatus.Invalid>;
67
+ /**
68
+ * Any execution status that is not definitively invalid.
69
+ * Pre-Gloas: Valid | Syncing | PreMerge
70
+ * Post-Gloas: execution status must be PayloadSeparated (beacon block imported before its payload arrives via SignedExecutionPayloadEnvelope)
71
+ */
72
+ export type BlockExecutionStatus = Exclude<ExecutionStatus, ExecutionStatus.Invalid>;
73
+
74
+ /**
75
+ * Execution status for a block whose execution payload is present and has been submitted to the EL.
76
+ * Used post-Gloas when transitioning a PayloadSeparated block to FULL via onExecutionPayload().
77
+ */
78
+ export type PayloadExecutionStatus = ExecutionStatus.Valid | ExecutionStatus.Syncing;
68
79
 
69
80
  export type BlockExtraMeta =
70
81
  | {
@@ -127,12 +138,6 @@ export type ProtoBlock = BlockExtraMeta & {
127
138
  /** Payload status for this node (Gloas fork). Always FULL in pre-gloas */
128
139
  payloadStatus: PayloadStatus;
129
140
 
130
- // GLOAS: The followings are from bids. They are null in pre-gloas
131
- // Used for execution payload gossip validation
132
- builderIndex: number | null;
133
- // Used for execution payload gossip validation. Not to be confused with executionPayloadBlockHash
134
- blockHashFromBid: RootHex | null;
135
-
136
141
  // Used to determine if this block extends EMPTY or FULL parent variant
137
142
  // Spec: gloas/fork-choice.md#new-get_parent_payload_status
138
143
  parentBlockHash: RootHex | null;
@@ -9,6 +9,7 @@ import {
9
9
  ExecutionStatus,
10
10
  HEX_ZERO_HASH,
11
11
  LVHExecResponse,
12
+ PayloadExecutionStatus,
12
13
  PayloadStatus,
13
14
  ProtoBlock,
14
15
  ProtoNode,
@@ -107,6 +108,14 @@ export class ProtoArray {
107
108
  currentSlot,
108
109
  null
109
110
  );
111
+
112
+ // Anchor block PTC votes must be all-true per spec get_forkchoice_store:
113
+ // payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))}
114
+ // Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/fork-choice.md#modified-get_forkchoice_store
115
+ if (protoArray.ptcVotes.has(block.blockRoot)) {
116
+ protoArray.ptcVotes.set(block.blockRoot, BitArray.fromBoolArray(Array.from({length: PTC_SIZE}, () => true)));
117
+ }
118
+
110
119
  return protoArray;
111
120
  }
112
121
 
@@ -541,7 +550,8 @@ export class ProtoArray {
541
550
  executionPayloadBlockHash: RootHex,
542
551
  executionPayloadNumber: number,
543
552
  executionPayloadStateRoot: RootHex,
544
- proposerBoostRoot: RootHex | null
553
+ proposerBoostRoot: RootHex | null,
554
+ executionStatus: PayloadExecutionStatus
545
555
  ): void {
546
556
  // First check if block exists
547
557
  const variants = this.indices.get(blockRoot);
@@ -591,7 +601,8 @@ export class ProtoArray {
591
601
  weight: 0,
592
602
  bestChild: undefined,
593
603
  bestDescendant: undefined,
594
- executionStatus: ExecutionStatus.Valid,
604
+ // TODO GLOAS: handle optimistic sync
605
+ executionStatus,
595
606
  executionPayloadBlockHash,
596
607
  executionPayloadNumber,
597
608
  stateRoot: executionPayloadStateRoot,
@@ -650,9 +661,7 @@ export class ProtoArray {
650
661
  }
651
662
 
652
663
  // If payload is not locally available, it's not timely
653
- // In our implementation, payload is locally available if proto array has FULL variant of the block
654
- const fullNodeIndex = this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL);
655
- if (fullNodeIndex === undefined) {
664
+ if (!this.hasPayload(blockRoot)) {
656
665
  return false;
657
666
  }
658
667
 
@@ -1059,10 +1068,8 @@ export class ProtoArray {
1059
1068
  });
1060
1069
  }
1061
1070
 
1062
- // Find the minimum index among all variants to ensure we don't prune too much
1063
- const finalizedIndex = Array.isArray(variants)
1064
- ? Math.min(...variants.filter((idx) => idx !== undefined))
1065
- : variants;
1071
+ // For Gloas, PENDING variant (index 0) is always the smallest since it's inserted first
1072
+ const finalizedIndex = Array.isArray(variants) ? variants[PayloadStatus.PENDING] : variants;
1066
1073
 
1067
1074
  if (finalizedIndex < this.pruneThreshold) {
1068
1075
  // Pruning at small numbers incurs more cost than benefit
@@ -1672,6 +1679,16 @@ export class ProtoArray {
1672
1679
  return this.getDefaultNodeIndex(blockRoot) !== undefined;
1673
1680
  }
1674
1681
 
1682
+ /**
1683
+ * Check if a FULL payload variant (execution payload envelope) exists for this block root.
1684
+ * Returns true once the SignedExecutionPayloadEnvelope for this block has been received and processed.
1685
+ */
1686
+ hasPayload(blockRoot: RootHex): boolean {
1687
+ // we should also make sure this blockRoot is a gloas block, however we only call this function
1688
+ // starting from GLOAS_FORK_EPOCH, so we can assume the blockRoot is from gloas block
1689
+ return this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL) !== undefined;
1690
+ }
1691
+
1675
1692
  /**
1676
1693
  * Return ProtoNode for blockRoot with explicit payload status
1677
1694
  *