@lodestar/beacon-node 1.48.0-dev.b62c97bb9f → 1.48.0-dev.b71ab45af3

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 (54) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +39 -39
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  5. package/lib/chain/blocks/importBlock.js +13 -0
  6. package/lib/chain/blocks/importBlock.js.map +1 -1
  7. package/lib/chain/chain.d.ts +0 -5
  8. package/lib/chain/chain.d.ts.map +1 -1
  9. package/lib/chain/chain.js +38 -67
  10. package/lib/chain/chain.js.map +1 -1
  11. package/lib/chain/emitter.d.ts +1 -1
  12. package/lib/chain/emitter.js +1 -1
  13. package/lib/chain/errors/blockError.d.ts +5 -3
  14. package/lib/chain/errors/blockError.d.ts.map +1 -1
  15. package/lib/chain/errors/blockError.js +2 -2
  16. package/lib/chain/errors/blockError.js.map +1 -1
  17. package/lib/chain/errors/executionPayloadEnvelope.d.ts +11 -0
  18. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/errors/executionPayloadEnvelope.js +2 -0
  20. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/prepareNextSlot.js +13 -13
  22. package/lib/chain/prepareNextSlot.js.map +1 -1
  23. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  24. package/lib/chain/produceBlock/produceBlockBody.js +64 -46
  25. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  26. package/lib/chain/validation/block.d.ts.map +1 -1
  27. package/lib/chain/validation/block.js +50 -9
  28. package/lib/chain/validation/block.js.map +1 -1
  29. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  30. package/lib/chain/validation/executionPayloadEnvelope.js +29 -0
  31. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  32. package/lib/chain/validatorMonitor.d.ts +1 -7
  33. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  34. package/lib/chain/validatorMonitor.js +5 -22
  35. package/lib/chain/validatorMonitor.js.map +1 -1
  36. package/lib/execution/engine/mock.d.ts.map +1 -1
  37. package/lib/execution/engine/mock.js +1 -4
  38. package/lib/execution/engine/mock.js.map +1 -1
  39. package/lib/network/reqresp/rateLimit.js +1 -2
  40. package/lib/network/reqresp/rateLimit.js.map +1 -1
  41. package/package.json +15 -15
  42. package/src/api/impl/validator/index.ts +43 -44
  43. package/src/chain/blocks/importBlock.ts +13 -0
  44. package/src/chain/chain.ts +46 -74
  45. package/src/chain/emitter.ts +1 -1
  46. package/src/chain/errors/blockError.ts +3 -3
  47. package/src/chain/errors/executionPayloadEnvelope.ts +9 -0
  48. package/src/chain/prepareNextSlot.ts +16 -16
  49. package/src/chain/produceBlock/produceBlockBody.ts +71 -50
  50. package/src/chain/validation/block.ts +66 -9
  51. package/src/chain/validation/executionPayloadEnvelope.ts +37 -0
  52. package/src/chain/validatorMonitor.ts +7 -24
  53. package/src/execution/engine/mock.ts +1 -4
  54. package/src/network/reqresp/rateLimit.ts +1 -2
@@ -1271,65 +1271,59 @@ export class BeaconChain implements IBeaconChain {
1271
1271
  try {
1272
1272
  const prevHead = this.forkChoice.getHead();
1273
1273
  const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
1274
- this.emitHeadEvents(prevHead, head);
1275
- return head;
1276
- } catch (e) {
1277
- this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1278
- throw e;
1279
- } finally {
1280
- timer?.();
1281
- }
1282
- }
1283
1274
 
1284
- /**
1285
- * Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
1286
- * can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
1287
- */
1288
- private emitHeadEvents(prevHead: ProtoBlock, head: ProtoBlock): void {
1289
- const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1275
+ const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1290
1276
 
1291
- if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1292
- return;
1293
- }
1277
+ if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1278
+ return head;
1279
+ }
1294
1280
 
1295
- try {
1296
- const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1297
- const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1298
- const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1299
- const executionOptimistic = isOptimisticBlock(head);
1300
-
1301
- if (headRootChanged) {
1302
- this.emitter.emit(routes.events.EventType.head, {
1303
- block: head.blockRoot,
1304
- epochTransition,
1305
- slot: head.slot,
1306
- state: head.stateRoot,
1307
- previousDutyDependentRoot,
1308
- currentDutyDependentRoot,
1309
- executionOptimistic,
1281
+ try {
1282
+ const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1283
+ const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1284
+ const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1285
+ const executionOptimistic = isOptimisticBlock(head);
1286
+
1287
+ if (headRootChanged) {
1288
+ this.emitter.emit(routes.events.EventType.head, {
1289
+ block: head.blockRoot,
1290
+ epochTransition,
1291
+ slot: head.slot,
1292
+ state: head.stateRoot,
1293
+ previousDutyDependentRoot,
1294
+ currentDutyDependentRoot,
1295
+ executionOptimistic,
1296
+ });
1297
+ }
1298
+
1299
+ this.emitter.emit(routes.events.EventType.headV2, {
1300
+ version: this.config.getForkName(head.slot),
1301
+ data: {
1302
+ slot: head.slot,
1303
+ block: head.blockRoot,
1304
+ state: head.stateRoot,
1305
+ payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1306
+ epochTransition,
1307
+ currentEpochDependentRoot: previousDutyDependentRoot,
1308
+ nextEpochDependentRoot: currentDutyDependentRoot,
1309
+ executionOptimistic,
1310
+ },
1310
1311
  });
1312
+ } catch (e) {
1313
+ // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1314
+ this.logger.debug(
1315
+ "Error emitting head/head_v2 event",
1316
+ {slot: head.slot, root: head.blockRoot, headRootChanged},
1317
+ e as Error
1318
+ );
1311
1319
  }
1312
1320
 
1313
- this.emitter.emit(routes.events.EventType.headV2, {
1314
- version: this.config.getForkName(head.slot),
1315
- data: {
1316
- slot: head.slot,
1317
- block: head.blockRoot,
1318
- state: head.stateRoot,
1319
- payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1320
- epochTransition,
1321
- currentEpochDependentRoot: previousDutyDependentRoot,
1322
- nextEpochDependentRoot: currentDutyDependentRoot,
1323
- executionOptimistic,
1324
- },
1325
- });
1321
+ return head;
1326
1322
  } catch (e) {
1327
- // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1328
- this.logger.debug(
1329
- "Error emitting head/head_v2 event",
1330
- {slot: head.slot, root: head.blockRoot, headRootChanged},
1331
- e as Error
1332
- );
1323
+ this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1324
+ throw e;
1325
+ } finally {
1326
+ timer?.();
1333
1327
  }
1334
1328
  }
1335
1329
 
@@ -1618,10 +1612,7 @@ export class BeaconChain implements IBeaconChain {
1618
1612
  this.processShutdownCallback(this.forkChoice.irrecoverableError);
1619
1613
  }
1620
1614
 
1621
- // updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
1622
- const prevHead = this.forkChoice.getHead();
1623
1615
  this.forkChoice.updateTime(slot);
1624
- this.emitHeadEvents(prevHead, this.forkChoice.getHead());
1625
1616
  this.metrics?.clockSlot.set(slot);
1626
1617
 
1627
1618
  this.attestationPool.prune(slot);
@@ -1679,25 +1670,6 @@ export class BeaconChain implements IBeaconChain {
1679
1670
 
1680
1671
  private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
1681
1672
  this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex});
1682
- this.metrics?.finalizedEpoch.set(cp.epoch);
1683
- const finalizedBlock = this.forkChoice.getBlockHexDefaultStatus(cp.rootHex);
1684
- if (finalizedBlock) {
1685
- // The callback runs synchronously inside fork choice checkpoint updates, defer writing to subscribers
1686
- callInNextEventLoop(() => {
1687
- this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
1688
- block: cp.rootHex,
1689
- epoch: cp.epoch,
1690
- state: finalizedBlock.stateRoot,
1691
- executionOptimistic: isOptimisticBlock(finalizedBlock),
1692
- });
1693
- });
1694
- } else {
1695
- this.logger.debug("Finalized block not found in fork choice, skip finalized_checkpoint event", {
1696
- epoch: cp.epoch,
1697
- root: cp.rootHex,
1698
- });
1699
- }
1700
-
1701
1673
  const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
1702
1674
  this.seenBlockProposers.prune(finalizedSlot);
1703
1675
 
@@ -33,7 +33,7 @@ export enum ChainEvent {
33
33
  /**
34
34
  * This event signals that the fork choice store has been updated.
35
35
  *
36
- * This event is guaranteed to be triggered whenever the fork choice finalized checkpoint is updated. This is either in response to a newly processed block or a new clock tick.
36
+ * This event is guaranteed to be triggered whenever the fork choice justified checkpoint is updated. This is in response to a newly processed block.
37
37
  */
38
38
  forkChoiceFinalized = "forkChoice:finalized",
39
39
  /**
@@ -78,8 +78,8 @@ export enum BlockErrorCode {
78
78
  TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
79
79
  /** Bid parent block root does not match block parent root */
80
80
  BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
81
- /** A block contains deposits where none are allowed */
82
- NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
81
+ /** A block body operation or parent execution request list exceeds its per-block limit */
82
+ TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS",
83
83
  /** The parent block's execution payload has been verified as invalid */
84
84
  PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
85
85
  /**
@@ -153,7 +153,7 @@ export type BlockErrorType =
153
153
  | {code: BlockErrorCode.DATA_UNAVAILABLE}
154
154
  | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
155
155
  | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
156
- | {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: number}
156
+ | {code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number}
157
157
  | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
158
158
  | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}
159
159
  | {code: BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS; parentBlockHash: RootHex; expectedBlockHash: RootHex};
@@ -12,6 +12,8 @@ export enum ExecutionPayloadEnvelopeErrorCode {
12
12
  BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
13
13
  BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
14
14
  EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
15
+ EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED",
16
+ WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED",
15
17
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
16
18
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
17
19
  }
@@ -42,6 +44,13 @@ export type ExecutionPayloadEnvelopeErrorType =
42
44
  envelopeRequestsRoot: RootHex;
43
45
  bidRequestsRoot: RootHex;
44
46
  }
47
+ | {
48
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED;
49
+ name: string;
50
+ count: number;
51
+ limit: number;
52
+ }
53
+ | {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number}
45
54
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
46
55
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
47
56
 
@@ -243,22 +243,6 @@ export class PrepareNextSlotScheduler {
243
243
  parentBlockHash = preparedState.latestExecutionPayloadHeader.blockHash;
244
244
  }
245
245
 
246
- // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
247
- // every slot. Without the flag, only emit the event if we are proposing in the next slot.
248
- if (
249
- (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
250
- this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
251
- ) {
252
- const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
253
- prepareState: stateAfterParentPayload,
254
- prepareSlot,
255
- parentBlockRoot: fromHex(updatedHead.blockRoot),
256
- parentBlockHash,
257
- feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
258
- });
259
- this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
260
- }
261
-
262
246
  if (feeRecipient) {
263
247
  const preparationTime =
264
248
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
@@ -289,6 +273,22 @@ export class PrepareNextSlotScheduler {
289
273
  }
290
274
 
291
275
  this.computeStateHashTreeRoot(preparedState, isEpochTransition);
276
+
277
+ // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
278
+ // every slot. Without the flag, only emit the event if we are proposing in the next slot.
279
+ if (
280
+ (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
281
+ this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
282
+ ) {
283
+ const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
284
+ prepareState: stateAfterParentPayload,
285
+ prepareSlot,
286
+ parentBlockRoot: fromHex(updatedHead.blockRoot),
287
+ parentBlockHash,
288
+ feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
289
+ });
290
+ this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
291
+ }
292
292
  } else {
293
293
  // Pre-bellatrix only reaches here at an epoch transition to precompute the next epoch state
294
294
  const preparedState = await this.chain.regen.getBlockSlotState(
@@ -528,18 +528,23 @@ export async function produceBlockBody<T extends BlockType>(
528
528
  }
529
529
  }
530
530
 
531
- const {blobKzgCommitments, executionRequests} = builderRes;
532
- if (blobKzgCommitments === undefined) {
533
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
534
- }
531
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
532
+ const {blobKzgCommitments} = builderRes;
533
+ if (blobKzgCommitments === undefined) {
534
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
535
+ }
535
536
 
536
- (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).blobKzgCommitments = blobKzgCommitments;
537
- Object.assign(logMeta, {blobs: blobKzgCommitments.length});
537
+ (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
538
+ Object.assign(logMeta, {blobs: blobKzgCommitments.length});
539
+ }
538
540
 
539
- if (executionRequests === undefined) {
540
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
541
+ if (ForkSeq[fork] >= ForkSeq.electra) {
542
+ const {executionRequests} = builderRes;
543
+ if (executionRequests === undefined) {
544
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
545
+ }
546
+ (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests;
541
547
  }
542
- (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).executionRequests = executionRequests;
543
548
  }
544
549
 
545
550
  // blockType === BlockType.Full
@@ -894,38 +899,45 @@ function preparePayloadAttributes(
894
899
  ): SSEPayloadAttributes["payloadAttributes"] {
895
900
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
896
901
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
897
- if (!isStatePostCapella(prepareState)) {
898
- throw new Error("Expected Capella state for withdrawals");
899
- }
902
+ const payloadAttributes = {
903
+ timestamp,
904
+ prevRandao,
905
+ suggestedFeeRecipient: feeRecipient,
906
+ };
900
907
 
901
- let withdrawals: capella.Withdrawal[];
902
- if (isStatePostGloas(prepareState)) {
903
- const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
904
- if (isExtendingPayload) {
905
- // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
906
- // here means the caller did not apply parent payload to prepareState
907
- if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
908
- throw new Error("Expected state with parent execution payload applied for withdrawals");
908
+ if (ForkSeq[fork] >= ForkSeq.capella) {
909
+ if (!isStatePostCapella(prepareState)) {
910
+ throw new Error("Expected Capella state for withdrawals");
911
+ }
912
+
913
+ if (isStatePostGloas(prepareState)) {
914
+ const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
915
+ if (isExtendingPayload) {
916
+ // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
917
+ // here means the caller did not apply parent payload to prepareState
918
+ if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
919
+ throw new Error("Expected state with parent execution payload applied for withdrawals");
920
+ }
921
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
922
+ prepareState.getExpectedWithdrawals().expectedWithdrawals;
923
+ } else {
924
+ // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
925
+ // already deducted from CL balances but never credited on the EL (the envelope
926
+ // was not delivered). The next payload must carry those same withdrawals to
927
+ // restore CL/EL consistency, otherwise validators permanently lose that balance.
928
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
929
+ prepareState.payloadExpectedWithdrawals;
909
930
  }
910
- withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
911
931
  } else {
912
- // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
913
- // already deducted from CL balances but never credited on the EL (the envelope
914
- // was not delivered). The next payload must carry those same withdrawals to
915
- // restore CL/EL consistency, otherwise validators permanently lose that balance.
916
- withdrawals = prepareState.payloadExpectedWithdrawals;
932
+ // withdrawals logic is now fork aware as it changes on electra fork post capella
933
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
934
+ prepareState.getExpectedWithdrawals().expectedWithdrawals;
917
935
  }
918
- } else {
919
- withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
920
936
  }
921
937
 
922
- const payloadAttributes: deneb.SSEPayloadAttributes["payloadAttributes"] = {
923
- timestamp,
924
- prevRandao,
925
- suggestedFeeRecipient: feeRecipient,
926
- withdrawals,
927
- parentBeaconBlockRoot: parentBlockRoot,
928
- };
938
+ if (ForkSeq[fork] >= ForkSeq.deneb) {
939
+ (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
940
+ }
929
941
 
930
942
  if (ForkSeq[fork] >= ForkSeq.gloas) {
931
943
  if (!isStatePostGloas(prepareState)) {
@@ -1038,28 +1050,37 @@ export async function produceCommonBlockBody<T extends BlockType>(
1038
1050
  step: BlockProductionStep.attestations,
1039
1051
  });
1040
1052
 
1041
- const endSyncAggregate = stepsMetrics?.startTimer();
1042
- const parentBlockRoot = fromHex(parentBlock.blockRoot);
1043
- const previousSlot = slot - 1;
1044
- const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1045
- this.metrics?.production.producedSyncAggregateParticipants.observe(
1046
- syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1047
- );
1048
- endSyncAggregate?.({
1049
- step: BlockProductionStep.syncAggregate,
1050
- });
1051
-
1052
- // Live proposal production is supported from Fulu onward.
1053
- return {
1053
+ const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
1054
1054
  randaoReveal,
1055
1055
  graffiti,
1056
+ // Eth1 data voting is no longer required since electra
1056
1057
  eth1Data: currentState.eth1Data,
1057
1058
  proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1058
1059
  attesterSlashings,
1059
1060
  attestations,
1061
+ // Since electra, deposits are processed by the execution layer,
1062
+ // we no longer support handling deposits from earlier forks.
1060
1063
  deposits: [],
1061
1064
  voluntaryExits,
1062
- blsToExecutionChanges,
1063
- syncAggregate,
1064
1065
  };
1066
+
1067
+ if (ForkSeq[fork] >= ForkSeq.capella) {
1068
+ (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges;
1069
+ }
1070
+
1071
+ const endSyncAggregate = stepsMetrics?.startTimer();
1072
+ if (ForkSeq[fork] >= ForkSeq.altair) {
1073
+ const parentBlockRoot = fromHex(parentBlock.blockRoot);
1074
+ const previousSlot = slot - 1;
1075
+ const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1076
+ this.metrics?.production.producedSyncAggregateParticipants.observe(
1077
+ syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1078
+ );
1079
+ (blockBody as CommonBlockBody).syncAggregate = syncAggregate;
1080
+ }
1081
+ endSyncAggregate?.({
1082
+ step: BlockProductionStep.syncAggregate,
1083
+ });
1084
+
1085
+ return blockBody as CommonBlockBody;
1065
1086
  }
@@ -1,6 +1,22 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ExecutionStatus} from "@lodestar/fork-choice";
3
- import {ForkName, MIN_SEED_LOOKAHEAD, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params";
3
+ import {
4
+ ForkName,
5
+ MAX_ATTESTATIONS_ELECTRA,
6
+ MAX_ATTESTER_SLASHINGS_ELECTRA,
7
+ MAX_BLS_TO_EXECUTION_CHANGES,
8
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
9
+ MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
10
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
11
+ MAX_PAYLOAD_ATTESTATIONS,
12
+ MAX_PROPOSER_SLASHINGS,
13
+ MAX_VOLUNTARY_EXITS,
14
+ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
15
+ MIN_SEED_LOOKAHEAD,
16
+ isForkPostBellatrix,
17
+ isForkPostDeneb,
18
+ isForkPostGloas,
19
+ } from "@lodestar/params";
4
20
  import {
5
21
  computeEpochAtSlot,
6
22
  computeStartSlotAtEpoch,
@@ -157,8 +173,7 @@ export async function validateGossipBlock(
157
173
  }
158
174
 
159
175
  if (isForkPostGloas(fork)) {
160
- const body = (block as gloas.BeaconBlock).body;
161
- const bid = body.signedExecutionPayloadBid.message;
176
+ const bid = (block as gloas.BeaconBlock).body.signedExecutionPayloadBid.message;
162
177
 
163
178
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer
164
179
  // -- i.e. validate that len(bid.blob_kzg_commitments) <= max_blobs_per_block
@@ -181,12 +196,54 @@ export async function validateGossipBlock(
181
196
  });
182
197
  }
183
198
 
184
- // [REJECT] The block contains no deposits.
185
- if (body.deposits.length !== 0) {
186
- throw new BlockGossipError(GossipAction.REJECT, {
187
- code: BlockErrorCode.NON_ZERO_DEPOSITS,
188
- count: body.deposits.length,
189
- });
199
+ // [REJECT] The counts of `block.body.parent_execution_requests` are within
200
+ // their respective limits -- i.e. validate that
201
+ // `len(block.body.parent_execution_requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD`,
202
+ // `len(block.body.parent_execution_requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD`,
203
+ // `len(block.body.parent_execution_requests.builder_deposits) <= MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD`,
204
+ // and
205
+ // `len(block.body.parent_execution_requests.builder_exits) <= MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD`.
206
+ // [REJECT] The counts of the block body operations are within their respective
207
+ // limits -- i.e. validate that
208
+ // `len(block.body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS`,
209
+ // `len(block.body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA`,
210
+ // `len(block.body.attestations) <= MAX_ATTESTATIONS_ELECTRA`,
211
+ // `len(block.body.deposits) == 0`,
212
+ // `len(block.body.voluntary_exits) <= MAX_VOLUNTARY_EXITS`,
213
+ // `len(block.body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES`,
214
+ // and `len(block.body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS`.
215
+ const body = (block as gloas.BeaconBlock).body;
216
+ const requests = body.parentExecutionRequests;
217
+ const countLimits: [string, number, number][] = [
218
+ ["parentExecutionRequests.withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD],
219
+ [
220
+ "parentExecutionRequests.consolidations",
221
+ requests.consolidations.length,
222
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
223
+ ],
224
+ [
225
+ "parentExecutionRequests.builderDeposits",
226
+ requests.builderDeposits.length,
227
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
228
+ ],
229
+ ["parentExecutionRequests.builderExits", requests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD],
230
+ ["proposerSlashings", body.proposerSlashings.length, MAX_PROPOSER_SLASHINGS],
231
+ ["attesterSlashings", body.attesterSlashings.length, MAX_ATTESTER_SLASHINGS_ELECTRA],
232
+ ["attestations", body.attestations.length, MAX_ATTESTATIONS_ELECTRA],
233
+ ["deposits", body.deposits.length, 0],
234
+ ["voluntaryExits", body.voluntaryExits.length, MAX_VOLUNTARY_EXITS],
235
+ ["blsToExecutionChanges", body.blsToExecutionChanges.length, MAX_BLS_TO_EXECUTION_CHANGES],
236
+ ["payloadAttestations", body.payloadAttestations.length, MAX_PAYLOAD_ATTESTATIONS],
237
+ ];
238
+ for (const [name, count, limit] of countLimits) {
239
+ if (count > limit) {
240
+ throw new BlockGossipError(GossipAction.REJECT, {
241
+ code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS,
242
+ name,
243
+ count,
244
+ limit,
245
+ });
246
+ }
190
247
  }
191
248
 
192
249
  // TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation
@@ -1,4 +1,11 @@
1
1
  import {PayloadStatus} from "@lodestar/fork-choice";
2
+ import {
3
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
4
+ MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
5
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
6
+ MAX_WITHDRAWALS_PER_PAYLOAD,
7
+ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
8
+ } from "@lodestar/params";
2
9
  import {
3
10
  computeStartSlotAtEpoch,
4
11
  getExecutionPayloadEnvelopeSignatureSet,
@@ -115,6 +122,36 @@ async function validateExecutionPayloadEnvelope(
115
122
  });
116
123
  }
117
124
 
125
+ // [REJECT] The counts of `execution_requests` are within their respective limits.
126
+ // New in Gloas:EIP7688 — progressive lists are unbounded at the type level, so bounds
127
+ // are enforced here in gossip validation.
128
+ const {executionRequests} = envelope;
129
+ const requestCountLimits: [string, number, number][] = [
130
+ ["withdrawals", executionRequests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD],
131
+ ["consolidations", executionRequests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD],
132
+ ["builderDeposits", executionRequests.builderDeposits.length, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD],
133
+ ["builderExits", executionRequests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD],
134
+ ];
135
+ for (const [name, count, limit] of requestCountLimits) {
136
+ if (count > limit) {
137
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
138
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED,
139
+ name,
140
+ count,
141
+ limit,
142
+ });
143
+ }
144
+ }
145
+
146
+ // [REJECT] The number of withdrawals is within the limit.
147
+ if (payload.withdrawals.length > MAX_WITHDRAWALS_PER_PAYLOAD) {
148
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
149
+ code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED,
150
+ count: payload.withdrawals.length,
151
+ limit: MAX_WITHDRAWALS_PER_PAYLOAD,
152
+ });
153
+ }
154
+
118
155
  // Get the block state to verify the builder's signature.
119
156
  const blockState = await chain.regen
120
157
  .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope)
@@ -1,5 +1,5 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
+ import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params";
3
3
  import {
4
4
  IBeaconStateView,
5
5
  ParticipationFlags,
@@ -1117,11 +1117,7 @@ function isCanonical(rootCache: RootHexCache, block: AttestationBlockInclusion):
1117
1117
 
1118
1118
  /** Returns true if root at slot is the same at slot - 1 == there was no new block at slot */
1119
1119
  function isMissedSlot(rootCache: RootHexCache, slot: Slot): boolean {
1120
- if (slot <= 0) {
1121
- return false;
1122
- }
1123
- const root = rootCache.getBlockRootAtSlot(slot);
1124
- return root !== null && root === rootCache.getBlockRootAtSlot(slot - 1);
1120
+ return slot > 0 && rootCache.getBlockRootAtSlot(slot) === rootCache.getBlockRootAtSlot(slot - 1);
1125
1121
  }
1126
1122
 
1127
1123
  function renderBlockProposalSummary(
@@ -1135,11 +1131,7 @@ function renderBlockProposalSummary(
1135
1131
  return "not_submitted";
1136
1132
  }
1137
1133
 
1138
- const canonicalRoot = rootCache.getBlockRootAtSlot(proposalSlot);
1139
- if (canonicalRoot === null) {
1140
- return "unknown";
1141
- }
1142
- if (canonicalRoot === proposal.blockRoot) {
1134
+ if (rootCache.getBlockRootAtSlot(proposalSlot) === proposal.blockRoot) {
1143
1135
  // Canonical state includes our block
1144
1136
  return "canonical";
1145
1137
  }
@@ -1165,23 +1157,14 @@ function renderBlockProposalSummary(
1165
1157
  * In normal network conditions the same root is read multiple times, specially the target.
1166
1158
  */
1167
1159
  export class RootHexCache {
1168
- private readonly blockRootSlotCache = new Map<Slot, RootHex | null>();
1160
+ private readonly blockRootSlotCache = new Map<Slot, RootHex>();
1169
1161
 
1170
1162
  constructor(private readonly state: IBeaconStateView) {}
1171
1163
 
1172
- /**
1173
- * Block root at `slot`, null if the state does not cover it: slots at or after the state's own slot (an inclusion
1174
- * in a block the head state does not know yet, e.g. an orphaned block, or the slot after the last attestation of
1175
- * the epoch) or slots older than `SLOTS_PER_HISTORICAL_ROOT`. Callers treat null as unknown instead of throwing
1176
- * and aborting the summaries of all remaining validators.
1177
- */
1178
- getBlockRootAtSlot(slot: Slot): RootHex | null {
1164
+ getBlockRootAtSlot(slot: Slot): RootHex {
1179
1165
  let root = this.blockRootSlotCache.get(slot);
1180
- if (root === undefined) {
1181
- root =
1182
- slot < this.state.slot && slot >= this.state.slot - SLOTS_PER_HISTORICAL_ROOT
1183
- ? toRootHex(this.state.getBlockRootAtSlot(slot))
1184
- : null;
1166
+ if (!root) {
1167
+ root = toRootHex(this.state.getBlockRootAtSlot(slot));
1185
1168
  this.blockRootSlotCache.set(slot, root);
1186
1169
  }
1187
1170
  return root;
@@ -296,10 +296,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
296
296
 
297
297
  // 5. Client software MUST update its forkchoice state if payloads referenced by forkchoiceState.headBlockHash and
298
298
  // forkchoiceState.finalizedBlockHash are VALID.
299
- //
300
- // A zero finalizedBlockHash means "no finalized execution block" (e.g. the finalized checkpoint has no execution
301
- // payload, pre-merge/pre-TTD). Real EL clients accept this, so the mock must not treat it as an unknown block.
302
- if (finalizedBlockHash !== ZERO_HASH_HEX && !this.validBlocks.has(finalizedBlockHash)) {
299
+ if (!this.validBlocks.has(finalizedBlockHash)) {
303
300
  throw Error(`Unknown finalizedBlockHash ${finalizedBlockHash}`);
304
301
  }
305
302
  this.headBlockHash = headBlockHash;
@@ -126,8 +126,7 @@ function getRequestCountFn<T extends ReqRespMethod>(
126
126
  const type = requestSszTypeByMethod(fork, config)[method];
127
127
  return (reqData: Uint8Array) => {
128
128
  try {
129
- // A schema-valid request can cost 0 tokens; floor to 1 so it stays charged and bannable, not free.
130
- return Math.max(1, (type && fn(type.deserialize(reqData))) ?? 1);
129
+ return (type && fn(type.deserialize(reqData))) ?? 1;
131
130
  } catch (_e) {
132
131
  return 1;
133
132
  }