@lodestar/beacon-node 1.45.0-dev.e10fb5e991 → 1.45.0-dev.e6c77a6488

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 (100) hide show
  1. package/lib/api/impl/beacon/index.d.ts +1 -1
  2. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  3. package/lib/api/impl/beacon/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +6 -42
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/beacon/state/index.d.ts +1 -1
  8. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  9. package/lib/api/impl/beacon/state/index.js +48 -5
  10. package/lib/api/impl/beacon/state/index.js.map +1 -1
  11. package/lib/api/impl/beacon/state/utils.d.ts +13 -3
  12. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  13. package/lib/api/impl/beacon/state/utils.js +48 -4
  14. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  15. package/lib/api/impl/debug/index.d.ts +1 -1
  16. package/lib/api/impl/debug/index.d.ts.map +1 -1
  17. package/lib/api/impl/debug/index.js +2 -2
  18. package/lib/api/impl/debug/index.js.map +1 -1
  19. package/lib/api/impl/lodestar/index.js +2 -2
  20. package/lib/api/impl/lodestar/index.js.map +1 -1
  21. package/lib/api/impl/proof/index.d.ts +1 -1
  22. package/lib/api/impl/proof/index.d.ts.map +1 -1
  23. package/lib/api/impl/proof/index.js +2 -2
  24. package/lib/api/impl/proof/index.js.map +1 -1
  25. package/lib/api/impl/utils.d.ts +23 -0
  26. package/lib/api/impl/utils.d.ts.map +1 -1
  27. package/lib/api/impl/utils.js +44 -1
  28. package/lib/api/impl/utils.js.map +1 -1
  29. package/lib/api/impl/validator/index.d.ts +0 -14
  30. package/lib/api/impl/validator/index.d.ts.map +1 -1
  31. package/lib/api/impl/validator/index.js +53 -58
  32. package/lib/api/impl/validator/index.js.map +1 -1
  33. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  34. package/lib/chain/blocks/importBlock.js +1 -4
  35. package/lib/chain/blocks/importBlock.js.map +1 -1
  36. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  37. package/lib/chain/produceBlock/produceBlockBody.js +4 -12
  38. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  39. package/lib/chain/validation/executionPayloadBid.js +11 -10
  40. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  41. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  42. package/lib/chain/validation/proposerPreferences.js +3 -2
  43. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  44. package/lib/network/core/metrics.d.ts +65 -0
  45. package/lib/network/core/metrics.d.ts.map +1 -1
  46. package/lib/network/core/metrics.js +94 -0
  47. package/lib/network/core/metrics.js.map +1 -1
  48. package/lib/network/discv5/utils.d.ts.map +1 -1
  49. package/lib/network/discv5/utils.js +12 -3
  50. package/lib/network/discv5/utils.js.map +1 -1
  51. package/lib/network/gossip/encoding.d.ts.map +1 -1
  52. package/lib/network/gossip/encoding.js +7 -0
  53. package/lib/network/gossip/encoding.js.map +1 -1
  54. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  55. package/lib/network/gossip/gossipsub.js +2 -1
  56. package/lib/network/gossip/gossipsub.js.map +1 -1
  57. package/lib/network/gossip/topic.d.ts +765 -3
  58. package/lib/network/gossip/topic.d.ts.map +1 -1
  59. package/lib/network/gossip/topic.js +23 -3
  60. package/lib/network/gossip/topic.js.map +1 -1
  61. package/lib/network/peers/peerManager.d.ts.map +1 -1
  62. package/lib/network/peers/peerManager.js +10 -0
  63. package/lib/network/peers/peerManager.js.map +1 -1
  64. package/lib/network/peers/score/score.d.ts +3 -1
  65. package/lib/network/peers/score/score.d.ts.map +1 -1
  66. package/lib/network/peers/score/score.js +6 -1
  67. package/lib/network/peers/score/score.js.map +1 -1
  68. package/lib/network/peers/score/store.d.ts.map +1 -1
  69. package/lib/network/peers/score/store.js +6 -1
  70. package/lib/network/peers/score/store.js.map +1 -1
  71. package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
  72. package/lib/network/peers/utils/prioritizePeers.js +14 -0
  73. package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
  74. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  75. package/lib/network/processor/gossipHandlers.js +6 -2
  76. package/lib/network/processor/gossipHandlers.js.map +1 -1
  77. package/package.json +18 -18
  78. package/src/api/impl/beacon/index.ts +1 -1
  79. package/src/api/impl/beacon/pool/index.ts +5 -52
  80. package/src/api/impl/beacon/state/index.ts +55 -3
  81. package/src/api/impl/beacon/state/utils.ts +68 -4
  82. package/src/api/impl/debug/index.ts +3 -2
  83. package/src/api/impl/lodestar/index.ts +2 -2
  84. package/src/api/impl/proof/index.ts +2 -2
  85. package/src/api/impl/utils.ts +51 -1
  86. package/src/api/impl/validator/index.ts +61 -63
  87. package/src/chain/blocks/importBlock.ts +1 -6
  88. package/src/chain/produceBlock/produceBlockBody.ts +9 -17
  89. package/src/chain/validation/executionPayloadBid.ts +14 -9
  90. package/src/chain/validation/proposerPreferences.ts +3 -2
  91. package/src/network/core/metrics.ts +94 -0
  92. package/src/network/discv5/utils.ts +11 -3
  93. package/src/network/gossip/encoding.ts +7 -0
  94. package/src/network/gossip/gossipsub.ts +2 -1
  95. package/src/network/gossip/topic.ts +28 -4
  96. package/src/network/peers/peerManager.ts +11 -0
  97. package/src/network/peers/score/score.ts +8 -1
  98. package/src/network/peers/score/store.ts +8 -1
  99. package/src/network/peers/utils/prioritizePeers.ts +15 -0
  100. package/src/network/processor/gossipHandlers.ts +6 -1
@@ -1,11 +1,22 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice";
3
3
  import {GENESIS_SLOT} from "@lodestar/params";
4
- import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition";
5
- import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types";
6
- import {fromHex} from "@lodestar/utils";
4
+ import {IBeaconStateView, IBeaconStateViewGloas, PubkeyCache} from "@lodestar/state-transition";
5
+ import {
6
+ BLSPubkey,
7
+ BuilderIndex,
8
+ Epoch,
9
+ RootHex,
10
+ Slot,
11
+ ValidatorIndex,
12
+ getValidatorStatus,
13
+ phase0,
14
+ } from "@lodestar/types";
15
+ import {byteArrayEquals, fromHex} from "@lodestar/utils";
7
16
  import {IBeaconChain} from "../../../../chain/index.js";
17
+ import {IBeaconSync} from "../../../../sync/index.js";
8
18
  import {ApiError, ValidationError} from "../../errors.js";
19
+ import {notWhileSyncing} from "../../utils.js";
9
20
 
10
21
  export function resolveStateId(
11
22
  forkChoice: IForkChoice,
@@ -42,8 +53,17 @@ export function resolveStateId(
42
53
 
43
54
  export async function getStateResponseWithRegen(
44
55
  chain: IBeaconChain,
56
+ sync: IBeaconSync,
45
57
  inStateId: routes.beacon.StateId
46
58
  ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean}> {
59
+ // "head", "finalized" and "justified" resolve to already-available cached states, and "genesis" to a
60
+ // historical DB read - none trigger the forward regen that can walk back past the block-root window
61
+ // (SLOTS_PER_HISTORICAL_ROOT) and wedge a far-behind node. Keep serving those (node observability,
62
+ // dashboards, validator client checks) even while syncing; guard only the regen-capable lookups.
63
+ if (inStateId !== "head" && inStateId !== "finalized" && inStateId !== "justified" && inStateId !== "genesis") {
64
+ notWhileSyncing(chain, sync.state);
65
+ }
66
+
47
67
  const stateId = resolveStateId(chain.forkChoice, inStateId);
48
68
 
49
69
  const res =
@@ -78,6 +98,50 @@ export function toValidatorResponse(
78
98
  };
79
99
  }
80
100
 
101
+ type StateBuilderIndexResponse =
102
+ | {valid: true; builderIndex: BuilderIndex}
103
+ | {valid: false; code: number; reason: string};
104
+
105
+ export function getStateBuilderIndex(
106
+ id: routes.beacon.BuilderId | BLSPubkey,
107
+ state: IBeaconStateViewGloas
108
+ ): StateBuilderIndexResponse {
109
+ if (typeof id === "string") {
110
+ // mutate `id` and fallthrough to below
111
+ if (id.startsWith("0x")) {
112
+ try {
113
+ id = fromHex(id);
114
+ } catch (_e) {
115
+ return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"};
116
+ }
117
+ } else {
118
+ id = Number(id);
119
+ }
120
+ }
121
+
122
+ if (typeof id === "number") {
123
+ const builderIndex = id;
124
+ // builder is invalid or added later than given stateId
125
+ if (!Number.isSafeInteger(builderIndex) || builderIndex < 0) {
126
+ return {valid: false, code: 400, reason: "Invalid builder index"};
127
+ }
128
+ if (builderIndex >= state.getBuildersLength()) {
129
+ return {valid: false, code: 404, reason: "Builder index from future state"};
130
+ }
131
+ return {valid: true, builderIndex};
132
+ }
133
+
134
+ // typeof id === Uint8Array
135
+ // There is no builder pubkey cache, linear scan over the registry
136
+ const buildersLength = state.getBuildersLength();
137
+ for (let builderIndex = 0; builderIndex < buildersLength; builderIndex++) {
138
+ if (byteArrayEquals(state.getBuilder(builderIndex).pubkey, id)) {
139
+ return {valid: true, builderIndex};
140
+ }
141
+ }
142
+ return {valid: false, code: 404, reason: "Builder pubkey not found in state"};
143
+ }
144
+
81
145
  export function filterStateValidatorsByStatus(
82
146
  statuses: string[],
83
147
  state: IBeaconStateView,
@@ -122,7 +186,7 @@ export function getStateValidatorIndex(
122
186
  if (typeof id === "number") {
123
187
  const validatorIndex = id;
124
188
  // validator is invalid or added later than given stateId
125
- if (!Number.isSafeInteger(validatorIndex)) {
189
+ if (!Number.isSafeInteger(validatorIndex) || validatorIndex < 0) {
126
190
  return {valid: false, code: 400, reason: "Invalid validator index"};
127
191
  }
128
192
  if (validatorIndex >= state.validatorCount) {
@@ -39,7 +39,8 @@ function toPayloadStatusName(status: PayloadStatus): "pending" | "empty" | "full
39
39
  export function getDebugApi({
40
40
  chain,
41
41
  config,
42
- }: Pick<ApiModules, "chain" | "config" | "db">): ApplicationMethods<routes.debug.Endpoints> {
42
+ sync,
43
+ }: Pick<ApiModules, "chain" | "config" | "db" | "sync">): ApplicationMethods<routes.debug.Endpoints> {
43
44
  return {
44
45
  async getDebugChainHeadsV2() {
45
46
  const heads = chain.forkChoice.getHeads();
@@ -132,7 +133,7 @@ export function getDebugApi({
132
133
  },
133
134
 
134
135
  async getStateV2({stateId}, context) {
135
- const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId);
136
+ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, sync, stateId);
136
137
  let slot: number, data: Uint8Array | BeaconState;
137
138
  if (state instanceof Uint8Array) {
138
139
  slot = getStateSlotFromBytes(state);
@@ -217,7 +217,7 @@ export function getLodestarApi({
217
217
  },
218
218
 
219
219
  async getHistoricalSummaries({stateId}) {
220
- const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId);
220
+ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, sync, stateId);
221
221
 
222
222
  const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
223
223
 
@@ -332,7 +332,7 @@ export function getLodestarApi({
332
332
 
333
333
  for (const [epoch, attestationsPerEpoch] of attestations) {
334
334
  const slot = computeStartSlotAtEpoch(epoch);
335
- const {state} = await getStateResponseWithRegen(chain, slot);
335
+ const {state} = await getStateResponseWithRegen(chain, sync, slot);
336
336
  const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
337
337
  const shuffling = stateView.getShufflingAtEpoch(epoch);
338
338
  for (const attestation of attestationsPerEpoch) {
@@ -8,7 +8,7 @@ import {ApiModules} from "../types.js";
8
8
 
9
9
  export function getProofApi(
10
10
  opts: ApiOptions,
11
- {chain, config}: Pick<ApiModules, "chain" | "config" | "db">
11
+ {chain, config, sync}: Pick<ApiModules, "chain" | "config" | "db" | "sync">
12
12
  ): ApplicationMethods<routes.proof.Endpoints> {
13
13
  // It's currently possible to request gigantic proofs (eg: a proof of the entire beacon state)
14
14
  // We want some some sort of resistance against this DoS vector.
@@ -21,7 +21,7 @@ export function getProofApi(
21
21
  throw new Error("Requested proof is too large.");
22
22
  }
23
23
 
24
- const res = await getStateResponseWithRegen(chain, stateId);
24
+ const res = await getStateResponseWithRegen(chain, sync, stateId);
25
25
 
26
26
  const state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state;
27
27
 
@@ -1,4 +1,54 @@
1
- import {ApiError} from "./errors.js";
1
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import type {IBeaconChain} from "../../chain/index.js";
3
+ import {SyncState} from "../../sync/index.js";
4
+ import {ApiError, NodeIsSyncing} from "./errors.js";
5
+
6
+ /**
7
+ * If the node is within this many epochs from the head, we declare it to be synced regardless of
8
+ * the network sync state.
9
+ *
10
+ * This helps prevent attacks where nodes can convince us that we're syncing some non-existent
11
+ * finalized head.
12
+ *
13
+ * TODO: Lighthouse uses 8 for the attack described above. However, 8 kills Lodestar since validators
14
+ * can trigger regen to fast-forward head state 8 epochs to be immediately invalidated as sync sets
15
+ * a new head. Then the checkpoint state cache grows unbounded with very different states (because
16
+ * they are 8 epochs apart) and causes an OOM. Research a proper solution once regen and the state
17
+ * caches are better.
18
+ */
19
+ export const SYNC_TOLERANCE_EPOCHS = 1;
20
+
21
+ /**
22
+ * Reject any request while the node is syncing. Used by endpoints that must not serve while the
23
+ * node is behind — validator duties, and beacon state lookups whose regen could otherwise walk
24
+ * back past the block-root window (`SLOTS_PER_HISTORICAL_ROOT`) and wedge the node. Throws
25
+ * {@link NodeIsSyncing} (503).
26
+ */
27
+ export function notWhileSyncing(chain: IBeaconChain, syncState: SyncState): void {
28
+ // Consider node synced before or close to genesis
29
+ if (chain.clock.currentSlot < SLOTS_PER_EPOCH) {
30
+ return;
31
+ }
32
+
33
+ switch (syncState) {
34
+ case SyncState.SyncingFinalized:
35
+ case SyncState.SyncingHead: {
36
+ const currentSlot = chain.clock.currentSlot;
37
+ const headSlot = chain.forkChoice.getHead().slot;
38
+ if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) {
39
+ throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`);
40
+ }
41
+
42
+ return;
43
+ }
44
+
45
+ case SyncState.Synced:
46
+ return;
47
+
48
+ case SyncState.Stalled:
49
+ throw new NodeIsSyncing("waiting for peers");
50
+ }
51
+ }
2
52
 
3
53
  /**
4
54
  * Ensures that the array contains unique values, and throws an ApiError
@@ -65,6 +65,8 @@ import {
65
65
  AttestationError,
66
66
  AttestationErrorCode,
67
67
  GossipAction,
68
+ ProposerPreferencesError,
69
+ ProposerPreferencesErrorCode,
68
70
  SyncCommitteeError,
69
71
  SyncCommitteeErrorCode,
70
72
  } from "../../../chain/errors/index.js";
@@ -74,12 +76,12 @@ import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/prod
74
76
  import {RegenCaller} from "../../../chain/regen/index.js";
75
77
  import {CheckpointHex} from "../../../chain/stateCache/types.js";
76
78
  import {validateApiAggregateAndProof} from "../../../chain/validation/index.js";
79
+ import {validateGossipProposerPreferences} from "../../../chain/validation/proposerPreferences.js";
77
80
  import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js";
78
81
  import {ZERO_HASH} from "../../../constants/index.js";
79
82
  import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js";
80
83
  import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js";
81
84
  import {CommitteeSubscription} from "../../../network/subnets/index.js";
82
- import {SyncState} from "../../../sync/index.js";
83
85
  import {callInNextEventLoop} from "../../../util/eventLoop.js";
84
86
  import {isOptimisticBlock} from "../../../util/forkChoice.js";
85
87
  import {getBlockGraffiti, toGraffitiBytes} from "../../../util/graffiti.js";
@@ -88,23 +90,9 @@ import {ApiOptions} from "../../options.js";
88
90
  import {getStateResponseWithRegen} from "../beacon/state/utils.js";
89
91
  import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js";
90
92
  import {ApiModules} from "../types.js";
93
+ import {notWhileSyncing} from "../utils.js";
91
94
  import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js";
92
95
 
93
- /**
94
- * If the node is within this many epochs from the head, we declare it to be synced regardless of
95
- * the network sync state.
96
- *
97
- * This helps prevent attacks where nodes can convince us that we're syncing some non-existent
98
- * finalized head.
99
- *
100
- * TODO: Lighthouse uses 8 for the attack described above. However, 8 kills Lodestar since validators
101
- * can trigger regen to fast-forward head state 8 epochs to be immediately invalidated as sync sets
102
- * a new head. Then the checkpoint state cache grows unbounded with very different states (because
103
- * they are 8 epochs apart) and causes an OOM. Research a proper solution once regen and the state
104
- * caches are better.
105
- */
106
- export const SYNC_TOLERANCE_EPOCHS = 1;
107
-
108
96
  /**
109
97
  * Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
110
98
  * Post this time, race execution and builder to pick whatever resolves first.
@@ -338,36 +326,6 @@ export function getValidatorApi(
338
326
  return null;
339
327
  }
340
328
 
341
- /**
342
- * Reject any request while the node is syncing
343
- */
344
- function notWhileSyncing(): void {
345
- // Consider node synced before or close to genesis
346
- if (chain.clock.currentSlot < SLOTS_PER_EPOCH) {
347
- return;
348
- }
349
-
350
- const syncState = sync.state;
351
- switch (syncState) {
352
- case SyncState.SyncingFinalized:
353
- case SyncState.SyncingHead: {
354
- const currentSlot = chain.clock.currentSlot;
355
- const headSlot = chain.forkChoice.getHead().slot;
356
- if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) {
357
- throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`);
358
- }
359
-
360
- return;
361
- }
362
-
363
- case SyncState.Synced:
364
- return;
365
-
366
- case SyncState.Stalled:
367
- throw new NodeIsSyncing("waiting for peers");
368
- }
369
- }
370
-
371
329
  /**
372
330
  * Post merge, the CL and EL could be out of step in the sync, and could result in
373
331
  * Syncing status of the chain head. To be precise:
@@ -570,7 +528,7 @@ export function getValidatorApi(
570
528
  builderBoostFactor?: bigint,
571
529
  {feeRecipient, builderSelection, strictFeeRecipientCheck}: routes.validator.ExtraProduceBlockOpts = {}
572
530
  ): Promise<ProduceBlindedBlockOrBlockContentsRes> {
573
- notWhileSyncing();
531
+ notWhileSyncing(chain, sync.state);
574
532
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
575
533
 
576
534
  const parentBlock = chain.getProposerHead(slot);
@@ -914,7 +872,7 @@ export function getValidatorApi(
914
872
  throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
915
873
  }
916
874
 
917
- notWhileSyncing();
875
+ notWhileSyncing(chain, sync.state);
918
876
  await waitForSlot(slot);
919
877
 
920
878
  const parentBlock = chain.getProposerHead(slot);
@@ -1032,7 +990,7 @@ export function getValidatorApi(
1032
990
  },
1033
991
 
1034
992
  async produceAttestationData({committeeIndex, slot}) {
1035
- notWhileSyncing();
993
+ notWhileSyncing(chain, sync.state);
1036
994
 
1037
995
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
1038
996
 
@@ -1113,13 +1071,13 @@ export function getValidatorApi(
1113
1071
  throw new ApiError(400, `producePayloadAttestationData is not supported before Gloas fork=${fork}`);
1114
1072
  }
1115
1073
 
1116
- notWhileSyncing();
1074
+ notWhileSyncing(chain, sync.state);
1117
1075
  await waitForSlot(slot);
1118
1076
 
1119
1077
  const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
1120
1078
  if (!block) {
1121
- // No block is seen at slot. Return 404 so vc can skip casting payload attestation.
1122
- throw new ApiError(404, `No canonical block found at slot=${slot}`);
1079
+ // No canonical block is seen at slot. Return 204 so vc can skip casting payload attestation.
1080
+ return {data: undefined, meta: {version: fork}, status: 204};
1123
1081
  }
1124
1082
 
1125
1083
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
@@ -1198,7 +1156,7 @@ export function getValidatorApi(
1198
1156
  },
1199
1157
 
1200
1158
  async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
1201
- notWhileSyncing();
1159
+ notWhileSyncing(chain, sync.state);
1202
1160
 
1203
1161
  const currentEpoch = currentEpochWithDisparity();
1204
1162
  const nextEpoch = currentEpoch + 1;
@@ -1245,7 +1203,7 @@ export function getValidatorApi(
1245
1203
  // requested epoch is within that range, we can use the head state at current epoch
1246
1204
  state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties);
1247
1205
  } else {
1248
- const res = await getStateResponseWithRegen(chain, startSlot);
1206
+ const res = await getStateResponseWithRegen(chain, sync, startSlot);
1249
1207
 
1250
1208
  state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state;
1251
1209
 
@@ -1339,7 +1297,7 @@ export function getValidatorApi(
1339
1297
  },
1340
1298
 
1341
1299
  async getAttesterDuties({epoch, indices}) {
1342
- notWhileSyncing();
1300
+ notWhileSyncing(chain, sync.state);
1343
1301
 
1344
1302
  if (indices.length === 0) {
1345
1303
  throw new ApiError(400, "No validator to get attester duties");
@@ -1399,7 +1357,7 @@ export function getValidatorApi(
1399
1357
  },
1400
1358
 
1401
1359
  async getPtcDuties({epoch, indices}) {
1402
- notWhileSyncing();
1360
+ notWhileSyncing(chain, sync.state);
1403
1361
 
1404
1362
  if (indices.length === 0) {
1405
1363
  throw new ApiError(400, "No validator to get PTC duties");
@@ -1461,7 +1419,7 @@ export function getValidatorApi(
1461
1419
  * @param validatorIndices an array of the validator indices for which to obtain the duties.
1462
1420
  */
1463
1421
  async getSyncCommitteeDuties({epoch, indices}) {
1464
- notWhileSyncing();
1422
+ notWhileSyncing(chain, sync.state);
1465
1423
 
1466
1424
  if (indices.length === 0) {
1467
1425
  throw new ApiError(400, "No validator to get attester duties");
@@ -1508,7 +1466,7 @@ export function getValidatorApi(
1508
1466
  },
1509
1467
 
1510
1468
  async getAggregatedAttestationV2({attestationDataRoot, slot, committeeIndex}) {
1511
- notWhileSyncing();
1469
+ notWhileSyncing(chain, sync.state);
1512
1470
 
1513
1471
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
1514
1472
 
@@ -1531,7 +1489,7 @@ export function getValidatorApi(
1531
1489
  },
1532
1490
 
1533
1491
  async publishAggregateAndProofsV2({signedAggregateAndProofs}) {
1534
- notWhileSyncing();
1492
+ notWhileSyncing(chain, sync.state);
1535
1493
 
1536
1494
  const seenTimestampSec = Date.now() / 1000;
1537
1495
  const failures: FailureList = [];
@@ -1592,7 +1550,7 @@ export function getValidatorApi(
1592
1550
  * https://github.com/ethereum/beacon-APIs/pull/137
1593
1551
  */
1594
1552
  async publishContributionAndProofs({contributionAndProofs}) {
1595
- notWhileSyncing();
1553
+ notWhileSyncing(chain, sync.state);
1596
1554
 
1597
1555
  const failures: FailureList = [];
1598
1556
 
@@ -1641,7 +1599,7 @@ export function getValidatorApi(
1641
1599
  },
1642
1600
 
1643
1601
  async prepareBeaconCommitteeSubnet({subscriptions}) {
1644
- notWhileSyncing();
1602
+ notWhileSyncing(chain, sync.state);
1645
1603
 
1646
1604
  await network.prepareBeaconCommitteeSubnets(
1647
1605
  subscriptions.map(({validatorIndex, slot, isAggregator, committeesAtSlot, committeeIndex}) => ({
@@ -1674,7 +1632,7 @@ export function getValidatorApi(
1674
1632
  * https://github.com/ethereum/beacon-APIs/pull/136
1675
1633
  */
1676
1634
  async prepareSyncCommitteeSubnets({subscriptions}) {
1677
- notWhileSyncing();
1635
+ notWhileSyncing(chain, sync.state);
1678
1636
 
1679
1637
  // A `validatorIndex` can be in multiple subnets, so compute the CommitteeSubscription with double for loop
1680
1638
  const subs: CommitteeSubscription[] = [];
@@ -1771,6 +1729,46 @@ export function getValidatorApi(
1771
1729
  });
1772
1730
  },
1773
1731
 
1732
+ async submitProposerPreferences({signedProposerPreferences}) {
1733
+ const failures: FailureList = [];
1734
+
1735
+ await Promise.all(
1736
+ signedProposerPreferences.map(async (signed, i) => {
1737
+ try {
1738
+ await validateGossipProposerPreferences(chain, signed);
1739
+
1740
+ chain.proposerPreferencesPool.add(signed);
1741
+ await network.publishProposerPreferences(signed);
1742
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1743
+ version: config.getForkName(signed.message.proposalSlot),
1744
+ data: signed,
1745
+ });
1746
+ } catch (e) {
1747
+ const logCtx = {
1748
+ slot: signed.message.proposalSlot,
1749
+ validatorIndex: signed.message.validatorIndex,
1750
+ dependentRoot: toRootHex(signed.message.dependentRoot),
1751
+ };
1752
+
1753
+ if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
1754
+ logger.debug("Ignoring known signed proposer preferences", logCtx);
1755
+ return;
1756
+ }
1757
+
1758
+ failures.push({index: i, message: (e as Error).message});
1759
+ logger.verbose(`Error on submitProposerPreferences [${i}]`, logCtx, e as Error);
1760
+ if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
1761
+ chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
1762
+ }
1763
+ }
1764
+ })
1765
+ );
1766
+
1767
+ if (failures.length > 0) {
1768
+ throw new IndexedError("Error processing signed proposer preferences", failures);
1769
+ }
1770
+ },
1771
+
1774
1772
  async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) {
1775
1773
  const fork = config.getForkName(slot);
1776
1774
 
@@ -1778,7 +1776,7 @@ export function getValidatorApi(
1778
1776
  throw new ApiError(400, `getExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
1779
1777
  }
1780
1778
 
1781
- notWhileSyncing();
1779
+ notWhileSyncing(chain, sync.state);
1782
1780
  await waitForSlot(slot);
1783
1781
 
1784
1782
  const blockRootHex = toRootHex(beaconBlockRoot);
@@ -124,18 +124,13 @@ export async function importBlock(
124
124
  executionStatus = parentBlock.executionStatus;
125
125
  }
126
126
 
127
- // getBeaconProposerOrNull will return null if head state is more than one epoch away
128
- // from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer
129
- const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot);
130
-
131
127
  const blockSummary = this.forkChoice.onBlock(
132
128
  block.message,
133
129
  postState,
134
130
  blockDelaySec,
135
131
  currentSlot,
136
132
  executionStatus,
137
- dataAvailabilityStatus,
138
- expectedProposerIndex
133
+ dataAvailabilityStatus
139
134
  );
140
135
 
141
136
  // This adds the state necessary to process the next block
@@ -43,6 +43,7 @@ import {
43
43
  ValidatorIndex,
44
44
  Wei,
45
45
  altair,
46
+ bellatrix,
46
47
  capella,
47
48
  deneb,
48
49
  electra,
@@ -830,28 +831,19 @@ export function getPayloadAttributesForSSE(
830
831
  feeRecipient,
831
832
  });
832
833
 
833
- let parentBlockNumber: number;
834
- if (isForkPostGloas(fork)) {
835
- const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(
836
- toRootHex(parentBlockRoot),
837
- toRootHex(parentBlockHash)
838
- );
839
- if (parentBlock?.executionPayloadBlockHash == null) {
840
- throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`);
841
- }
842
- parentBlockNumber = parentBlock.executionPayloadNumber;
843
- } else {
844
- parentBlockNumber = prepareState.payloadBlockNumber;
845
- }
846
-
847
- const ssePayloadAttributes: SSEPayloadAttributes = {
834
+ const ssePayloadAttributes = {
848
835
  proposerIndex: prepareState.getBeaconProposer(prepareSlot),
849
836
  proposalSlot: prepareSlot,
850
- parentBlockNumber,
851
837
  parentBlockRoot,
852
838
  parentBlockHash,
853
839
  payloadAttributes,
854
- };
840
+ } as SSEPayloadAttributes;
841
+
842
+ if (!isForkPostGloas(fork)) {
843
+ // Removed in Gloas, builders can get the block number from the EL via the block hash if required
844
+ (ssePayloadAttributes as bellatrix.SSEPayloadAttributes).parentBlockNumber = prepareState.payloadBlockNumber;
845
+ }
846
+
855
847
  return ssePayloadAttributes;
856
848
  }
857
849
 
@@ -37,9 +37,11 @@ async function validateExecutionPayloadBid(
37
37
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
38
38
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
39
39
 
40
- // [IGNORE] `bid.slot` is the current slot or the next slot.
41
- const currentSlot = chain.clock.currentSlot;
42
- if (bid.slot !== currentSlot && bid.slot !== currentSlot + 1) {
40
+ // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
41
+ if (
42
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
43
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
44
+ ) {
43
45
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
44
46
  code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
45
47
  builderIndex: bid.builderIndex,
@@ -122,17 +124,20 @@ async function validateExecutionPayloadBid(
122
124
  throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
123
125
  }
124
126
 
125
- // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
126
- // `is_active_builder(state, bid.builder_index)` returns `True`.
127
- let builder: gloas.Builder;
128
- try {
129
- builder = state.getBuilder(bid.builderIndex);
130
- } catch {
127
+ // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
128
+ // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
129
+ // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
130
+ // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
131
+ if (bid.builderIndex >= state.getBuildersLength()) {
131
132
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
132
133
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
133
134
  builderIndex: bid.builderIndex,
134
135
  });
135
136
  }
137
+
138
+ // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
139
+ // `is_active_builder(state, bid.builder_index)` returns `True`.
140
+ const builder = state.getBuilder(bid.builderIndex);
136
141
  if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
137
142
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
138
143
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
@@ -32,8 +32,9 @@ export async function validateGossipProposerPreferences(
32
32
  });
33
33
  }
34
34
 
35
- // [IGNORE] `preferences.proposal_slot` has not already passed.
36
- const currentSlot = chain.clock.currentSlot;
35
+ // [IGNORE] `preferences.proposal_slot` has not already passed, i.e. `proposal_slot > current_slot`,
36
+ // allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
37
+ const currentSlot = chain.clock.currentSlotWithGossipDisparity;
37
38
  if (proposalSlot <= currentSlot) {
38
39
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
39
40
  code: ProposerPreferencesErrorCode.PROPOSAL_SLOT_PASSED,