@lodestar/fork-choice 1.42.0-dev.c3829113f4 → 1.42.0-dev.c58e92512e

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,
@@ -973,7 +983,8 @@ export class ForkChoice implements IForkChoice {
973
983
  blockRoot: RootHex,
974
984
  executionPayloadBlockHash: RootHex,
975
985
  executionPayloadNumber: number,
976
- executionPayloadStateRoot: RootHex
986
+ executionPayloadStateRoot: RootHex,
987
+ executionStatus: PayloadExecutionStatus
977
988
  ): void {
978
989
  this.protoArray.onExecutionPayload(
979
990
  blockRoot,
@@ -981,7 +992,8 @@ export class ForkChoice implements IForkChoice {
981
992
  executionPayloadBlockHash,
982
993
  executionPayloadNumber,
983
994
  executionPayloadStateRoot,
984
- this.proposerBoostRoot
995
+ this.proposerBoostRoot,
996
+ executionStatus
985
997
  );
986
998
  }
987
999
 
@@ -1039,19 +1051,34 @@ export class ForkChoice implements IForkChoice {
1039
1051
  }
1040
1052
 
1041
1053
  /**
1042
- * 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.
1043
1055
  */
1044
1056
  hasBlockUnsafe(blockRoot: Root): boolean {
1045
1057
  return this.hasBlockHexUnsafe(toRootHex(blockRoot));
1046
1058
  }
1047
1059
 
1048
1060
  /**
1049
- * 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.
1050
1062
  */
1051
1063
  hasBlockHexUnsafe(blockRoot: RootHex): boolean {
1052
1064
  return this.protoArray.hasBlock(blockRoot);
1053
1065
  }
1054
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
+
1055
1082
  /**
1056
1083
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
1057
1084
  */
@@ -1421,7 +1448,7 @@ export class ForkChoice implements IForkChoice {
1421
1448
  return secFromSlot * 1000 <= proposerReorgCutoff;
1422
1449
  }
1423
1450
 
1424
- private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
1451
+ private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge {
1425
1452
  if (executionStatus !== ExecutionStatus.PreMerge)
1426
1453
  throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);
1427
1454
  return executionStatus;
@@ -1436,7 +1463,7 @@ export class ForkChoice implements IForkChoice {
1436
1463
  }
1437
1464
 
1438
1465
  private getPreGloasExecStatus(
1439
- executionStatus: MaybeValidExecutionStatus
1466
+ executionStatus: BlockExecutionStatus
1440
1467
  ): ExecutionStatus.Valid | ExecutionStatus.Syncing {
1441
1468
  if (executionStatus === ExecutionStatus.PreMerge || executionStatus === ExecutionStatus.PayloadSeparated)
1442
1469
  throw Error(
@@ -1445,7 +1472,7 @@ export class ForkChoice implements IForkChoice {
1445
1472
  return executionStatus;
1446
1473
  }
1447
1474
 
1448
- private getPostGloasExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PayloadSeparated {
1475
+ private getPostGloasExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PayloadSeparated {
1449
1476
  if (executionStatus !== ExecutionStatus.PayloadSeparated)
1450
1477
  throw Error(
1451
1478
  `Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}`
@@ -1669,6 +1696,20 @@ export class ForkChoice implements IForkChoice {
1669
1696
  });
1670
1697
  }
1671
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
+
1672
1713
  this.validatedAttestationDatas.add(attDataRoot);
1673
1714
  }
1674
1715
 
@@ -1851,24 +1892,31 @@ export function getCommitteeFraction(
1851
1892
  * Pre-Gloas: always FULL (payload embedded in block)
1852
1893
  * Gloas: determined by state.execution_payload_availability
1853
1894
  *
1895
+ * @param config - The chain fork config to determine fork at checkpoint slot
1854
1896
  * @param state - The state to check execution_payload_availability
1855
1897
  * @param checkpointEpoch - The epoch of the checkpoint
1856
1898
  */
1857
- export function getCheckpointPayloadStatus(state: CachedBeaconStateAllForks, checkpointEpoch: number): PayloadStatus {
1899
+ export function getCheckpointPayloadStatus(
1900
+ config: ChainForkConfig,
1901
+ state: IBeaconStateView,
1902
+ checkpointEpoch: number
1903
+ ): PayloadStatus {
1858
1904
  // Compute checkpoint slot first to determine the correct fork
1859
1905
  const checkpointSlot = computeStartSlotAtEpoch(checkpointEpoch);
1860
- const fork = state.config.getForkSeq(checkpointSlot);
1906
+ const fork = config.getForkSeq(checkpointSlot);
1861
1907
 
1862
1908
  // Pre-Gloas: always FULL
1863
1909
  if (fork < ForkSeq.gloas) {
1864
1910
  return PayloadStatus.FULL;
1865
1911
  }
1912
+ if (!isStatePostGloas(state)) {
1913
+ throw new Error(`Expected gloas+ state for checkpoint payload status, got fork=${state.forkName}`);
1914
+ }
1866
1915
 
1867
1916
  // For Gloas, check state.execution_payload_availability
1868
1917
  // - For non-skipped slots at checkpoint: returns false (EMPTY) since payload hasn't arrived yet
1869
1918
  // - For skipped slots at checkpoint: returns the actual availability status from state
1870
- const gloasState = state as CachedBeaconStateGloas;
1871
- const payloadAvailable = gloasState.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT);
1919
+ const payloadAvailable = state.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT);
1872
1920
 
1873
1921
  return payloadAvailable ? PayloadStatus.FULL : PayloadStatus.EMPTY;
1874
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
  | {
@@ -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,
@@ -541,7 +542,8 @@ export class ProtoArray {
541
542
  executionPayloadBlockHash: RootHex,
542
543
  executionPayloadNumber: number,
543
544
  executionPayloadStateRoot: RootHex,
544
- proposerBoostRoot: RootHex | null
545
+ proposerBoostRoot: RootHex | null,
546
+ executionStatus: PayloadExecutionStatus
545
547
  ): void {
546
548
  // First check if block exists
547
549
  const variants = this.indices.get(blockRoot);
@@ -591,7 +593,8 @@ export class ProtoArray {
591
593
  weight: 0,
592
594
  bestChild: undefined,
593
595
  bestDescendant: undefined,
594
- executionStatus: ExecutionStatus.Valid,
596
+ // TODO GLOAS: handle optimistic sync
597
+ executionStatus,
595
598
  executionPayloadBlockHash,
596
599
  executionPayloadNumber,
597
600
  stateRoot: executionPayloadStateRoot,
@@ -650,9 +653,7 @@ export class ProtoArray {
650
653
  }
651
654
 
652
655
  // 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) {
656
+ if (!this.hasPayload(blockRoot)) {
656
657
  return false;
657
658
  }
658
659
 
@@ -1059,10 +1060,8 @@ export class ProtoArray {
1059
1060
  });
1060
1061
  }
1061
1062
 
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;
1063
+ // For Gloas, PENDING variant (index 0) is always the smallest since it's inserted first
1064
+ const finalizedIndex = Array.isArray(variants) ? variants[PayloadStatus.PENDING] : variants;
1066
1065
 
1067
1066
  if (finalizedIndex < this.pruneThreshold) {
1068
1067
  // Pruning at small numbers incurs more cost than benefit
@@ -1672,6 +1671,16 @@ export class ProtoArray {
1672
1671
  return this.getDefaultNodeIndex(blockRoot) !== undefined;
1673
1672
  }
1674
1673
 
1674
+ /**
1675
+ * Check if a FULL payload variant (execution payload envelope) exists for this block root.
1676
+ * Returns true once the SignedExecutionPayloadEnvelope for this block has been received and processed.
1677
+ */
1678
+ hasPayload(blockRoot: RootHex): boolean {
1679
+ // we should also make sure this blockRoot is a gloas block, however we only call this function
1680
+ // starting from GLOAS_FORK_EPOCH, so we can assume the blockRoot is from gloas block
1681
+ return this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL) !== undefined;
1682
+ }
1683
+
1675
1684
  /**
1676
1685
  * Return ProtoNode for blockRoot with explicit payload status
1677
1686
  *