@lodestar/beacon-node 1.48.0-dev.0dbea0de1b → 1.48.0-dev.262a9c1d16

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 (139) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +0 -7
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/utils.js +14 -0
  6. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  7. package/lib/api/impl/config/index.d.ts.map +1 -1
  8. package/lib/api/impl/config/index.js +4 -1
  9. package/lib/api/impl/config/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts.map +1 -1
  11. package/lib/api/impl/validator/index.js +39 -39
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  14. package/lib/chain/archiveStore/archiveStore.js +4 -1
  15. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  16. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +9 -1
  17. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  18. package/lib/chain/archiveStore/utils/archiveBlocks.js +37 -4
  19. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  20. package/lib/chain/blocks/importBlock.d.ts +5 -1
  21. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  22. package/lib/chain/blocks/importBlock.js +12 -18
  23. package/lib/chain/blocks/importBlock.js.map +1 -1
  24. package/lib/chain/blocks/index.d.ts +8 -4
  25. package/lib/chain/blocks/index.d.ts.map +1 -1
  26. package/lib/chain/blocks/index.js +80 -15
  27. package/lib/chain/blocks/index.js.map +1 -1
  28. package/lib/chain/blocks/types.d.ts +8 -0
  29. package/lib/chain/blocks/types.d.ts.map +1 -1
  30. package/lib/chain/blocks/types.js.map +1 -1
  31. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  32. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  33. package/lib/chain/blocks/utils/chainSegment.js +26 -11
  34. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  35. package/lib/chain/chain.d.ts +7 -2
  36. package/lib/chain/chain.d.ts.map +1 -1
  37. package/lib/chain/chain.js +69 -40
  38. package/lib/chain/chain.js.map +1 -1
  39. package/lib/chain/emitter.d.ts +1 -1
  40. package/lib/chain/emitter.js +1 -1
  41. package/lib/chain/errors/blockError.d.ts +3 -5
  42. package/lib/chain/errors/blockError.d.ts.map +1 -1
  43. package/lib/chain/errors/blockError.js +2 -2
  44. package/lib/chain/errors/blockError.js.map +1 -1
  45. package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
  46. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  47. package/lib/chain/errors/executionPayloadBid.js +1 -0
  48. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  49. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  50. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  51. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  52. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  53. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  54. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  55. package/lib/chain/errors/gossipValidation.js +1 -0
  56. package/lib/chain/errors/gossipValidation.js.map +1 -1
  57. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  58. package/lib/chain/forkChoice/index.js +2 -0
  59. package/lib/chain/forkChoice/index.js.map +1 -1
  60. package/lib/chain/interface.d.ts +2 -2
  61. package/lib/chain/interface.d.ts.map +1 -1
  62. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  63. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  64. package/lib/chain/prepareNextSlot.js +13 -13
  65. package/lib/chain/prepareNextSlot.js.map +1 -1
  66. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  67. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  68. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  69. package/lib/chain/validation/block.d.ts.map +1 -1
  70. package/lib/chain/validation/block.js +9 -50
  71. package/lib/chain/validation/block.js.map +1 -1
  72. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  73. package/lib/chain/validation/executionPayloadBid.js +16 -0
  74. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  75. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  76. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  77. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  78. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  79. package/lib/chain/validation/payloadAttestationMessage.js +5 -6
  80. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  81. package/lib/chain/validatorMonitor.d.ts +7 -1
  82. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  83. package/lib/chain/validatorMonitor.js +22 -5
  84. package/lib/chain/validatorMonitor.js.map +1 -1
  85. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  86. package/lib/execution/builder/validateBid.js +3 -0
  87. package/lib/execution/builder/validateBid.js.map +1 -1
  88. package/lib/execution/engine/mock.d.ts.map +1 -1
  89. package/lib/execution/engine/mock.js +4 -1
  90. package/lib/execution/engine/mock.js.map +1 -1
  91. package/lib/metrics/metrics/lodestar.d.ts +4 -3
  92. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  93. package/lib/metrics/metrics/lodestar.js +5 -5
  94. package/lib/metrics/metrics/lodestar.js.map +1 -1
  95. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  96. package/lib/network/processor/gossipHandlers.js +382 -278
  97. package/lib/network/processor/gossipHandlers.js.map +1 -1
  98. package/lib/network/processor/index.d.ts.map +1 -1
  99. package/lib/network/processor/index.js +22 -16
  100. package/lib/network/processor/index.js.map +1 -1
  101. package/lib/network/reqresp/rateLimit.js +2 -1
  102. package/lib/network/reqresp/rateLimit.js.map +1 -1
  103. package/lib/sync/range/range.d.ts.map +1 -1
  104. package/lib/sync/range/range.js +24 -1
  105. package/lib/sync/range/range.js.map +1 -1
  106. package/package.json +15 -15
  107. package/src/api/impl/beacon/blocks/index.ts +0 -10
  108. package/src/api/impl/beacon/state/utils.ts +15 -0
  109. package/src/api/impl/config/index.ts +4 -1
  110. package/src/api/impl/validator/index.ts +44 -43
  111. package/src/chain/archiveStore/archiveStore.ts +6 -0
  112. package/src/chain/archiveStore/utils/archiveBlocks.ts +39 -3
  113. package/src/chain/blocks/importBlock.ts +25 -20
  114. package/src/chain/blocks/index.ts +107 -21
  115. package/src/chain/blocks/types.ts +9 -0
  116. package/src/chain/blocks/utils/chainSegment.ts +26 -12
  117. package/src/chain/chain.ts +78 -50
  118. package/src/chain/emitter.ts +1 -1
  119. package/src/chain/errors/blockError.ts +3 -3
  120. package/src/chain/errors/executionPayloadBid.ts +2 -0
  121. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  122. package/src/chain/errors/gossipValidation.ts +1 -0
  123. package/src/chain/forkChoice/index.ts +2 -0
  124. package/src/chain/interface.ts +2 -2
  125. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  126. package/src/chain/prepareNextSlot.ts +16 -16
  127. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  128. package/src/chain/validation/block.ts +9 -66
  129. package/src/chain/validation/executionPayloadBid.ts +18 -0
  130. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  131. package/src/chain/validation/payloadAttestationMessage.ts +8 -9
  132. package/src/chain/validatorMonitor.ts +24 -7
  133. package/src/execution/builder/validateBid.ts +4 -0
  134. package/src/execution/engine/mock.ts +4 -1
  135. package/src/metrics/metrics/lodestar.ts +6 -5
  136. package/src/network/processor/gossipHandlers.ts +489 -344
  137. package/src/network/processor/index.ts +24 -17
  138. package/src/network/reqresp/rateLimit.ts +2 -1
  139. package/src/sync/range/range.ts +23 -1
@@ -22,7 +22,7 @@ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
22
22
  * - Verifies parent root + slot linearity
23
23
  * - For gloas: verifies bid.parentBlockHash matches the tracked execution hash; if not, the
24
24
  * previous FULL envelope is treated as orphaned (segment continues as if previous slot was EMPTY)
25
- * - If an envelope exists for this slot: verifies it references this block's root
25
+ * - If an envelope exists for this slot (or the parent's slot): verifies it references this block's root
26
26
  * - Advances the tracked execution hash (FULL if envelope present, EMPTY if not)
27
27
  */
28
28
  export function assertLinearChainSegment(
@@ -37,17 +37,6 @@ export function assertLinearChainSegment(
37
37
 
38
38
  const warnings: OrphanedPayloadEnvelope[] = [];
39
39
 
40
- let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
41
- if (parentBlock !== null) {
42
- const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
43
- if (parentPayloadInput?.hasPayloadEnvelope()) {
44
- // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
45
- // hash, not its own payload. Its payload envelope is in this batch, so use that blockHash as
46
- // the real parent EL head.
47
- parentExecHash = parentPayloadInput.getBlockHashHex();
48
- }
49
- }
50
-
51
40
  // Track the expected execution payload block hash through the segment, seeded from the FIRST
52
41
  // block's own bid
53
42
  const firstBlockMessage = blocks[0].getBlock().message;
@@ -55,6 +44,31 @@ export function assertLinearChainSegment(
55
44
  ? toRootHex(firstBlockMessage.body.signedExecutionPayloadBid.message.parentBlockHash)
56
45
  : null;
57
46
 
47
+ let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
48
+ if (parentBlock !== null) {
49
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
50
+ if (parentPayloadInput?.hasPayloadEnvelope()) {
51
+ // Envelopes are served by slot, the one at the parent's slot must reference the parent block. Without this
52
+ // check an envelope of a sibling block is taken as orphaned and only fails downstream in the payload import
53
+ if (parentPayloadInput.blockRootHex !== parentBlock.blockRoot) {
54
+ throw new BlockError(blocks[0].getBlock(), {
55
+ code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
56
+ envelopeBlockRoot: parentPayloadInput.blockRootHex,
57
+ blockRoot: parentBlock.blockRoot,
58
+ });
59
+ }
60
+ const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
61
+ if (currentExecHash === parentPayloadBlockHash) {
62
+ // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
63
+ // hash, its own payload is in this batch and is the real parent EL head
64
+ parentExecHash = parentPayloadBlockHash;
65
+ } else {
66
+ // First block builds on the parent's EMPTY variant, the parent's payload in this batch is orphaned
67
+ warnings.push({slot: parentBlock.slot, payloadEnvelopeInput: parentPayloadInput});
68
+ }
69
+ }
70
+ }
71
+
58
72
  // First block must build on the parent's EL head. PARENT_PAYLOAD_UNKNOWN (vs the in-segment
59
73
  // NON_LINEAR_PAYLOAD_ROOTS below) tells range sync this is a parent-link problem, not an in-batch one.
60
74
  if (parentExecHash !== null && currentExecHash !== null && currentExecHash !== parentExecHash) {
@@ -82,7 +82,7 @@ import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/bloc
82
82
  import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js";
83
83
  import {PayloadEnvelopeInputSource} from "./blocks/payloadEnvelopeInput/index.js";
84
84
  import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js";
85
- import {ImportPayloadOpts} from "./blocks/types.js";
85
+ import {ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
86
86
  import {persistBlockInput} from "./blocks/writeBlockInputToDb.js";
87
87
  import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js";
88
88
  import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js";
@@ -1167,15 +1167,15 @@ export class BeaconChain implements IBeaconChain {
1167
1167
  }
1168
1168
 
1169
1169
  async processBlock(block: IBlockInput, opts?: ImportBlockOpts): Promise<void> {
1170
- return this.blockProcessor.processBlocksJob([block], null, opts);
1170
+ await this.blockProcessor.processBlocksJob([block], null, opts);
1171
1171
  }
1172
1172
 
1173
1173
  async processChainSegment(
1174
1174
  blocks: IBlockInput[],
1175
1175
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
1176
1176
  opts?: ImportBlockOpts
1177
- ): Promise<void> {
1178
- await this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
1177
+ ): Promise<ProcessBlocksResult> {
1178
+ return this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
1179
1179
  }
1180
1180
 
1181
1181
  async processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void> {
@@ -1271,59 +1271,65 @@ 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
+ }
1274
1283
 
1275
- const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1276
-
1277
- if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1278
- return head;
1279
- }
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;
1280
1290
 
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
- }
1291
+ if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1292
+ return;
1293
+ }
1298
1294
 
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
- },
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,
1311
1310
  });
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
- );
1319
1311
  }
1320
1312
 
1321
- return head;
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
+ });
1322
1326
  } catch (e) {
1323
- this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1324
- throw e;
1325
- } finally {
1326
- timer?.();
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
+ );
1327
1333
  }
1328
1334
  }
1329
1335
 
@@ -1612,7 +1618,10 @@ export class BeaconChain implements IBeaconChain {
1612
1618
  this.processShutdownCallback(this.forkChoice.irrecoverableError);
1613
1619
  }
1614
1620
 
1621
+ // updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
1622
+ const prevHead = this.forkChoice.getHead();
1615
1623
  this.forkChoice.updateTime(slot);
1624
+ this.emitHeadEvents(prevHead, this.forkChoice.getHead());
1616
1625
  this.metrics?.clockSlot.set(slot);
1617
1626
 
1618
1627
  this.attestationPool.prune(slot);
@@ -1670,6 +1679,25 @@ export class BeaconChain implements IBeaconChain {
1670
1679
 
1671
1680
  private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
1672
1681
  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
+
1673
1701
  const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
1674
1702
  this.seenBlockProposers.prune(finalizedSlot);
1675
1703
 
@@ -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 justified checkpoint is updated. This is in response to a newly processed block.
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.
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 body operation or parent execution request list exceeds its per-block limit */
82
- TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS",
81
+ /** A block contains deposits where none are allowed */
82
+ NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
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.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number}
156
+ | {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: 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};
@@ -5,6 +5,7 @@ export enum ExecutionPayloadBidErrorCode {
5
5
  BUILDER_NOT_ELIGIBLE = "EXECUTION_PAYLOAD_BID_ERROR_BUILDER_NOT_ELIGIBLE",
6
6
  INVALID_BUILDER_VERSION = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_BUILDER_VERSION",
7
7
  NON_ZERO_EXECUTION_PAYMENT = "EXECUTION_PAYLOAD_BID_ERROR_NON_ZERO_EXECUTION_PAYMENT",
8
+ BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH",
8
9
  INCOMPATIBLE_WITH_HEAD = "EXECUTION_PAYLOAD_BID_ERROR_INCOMPATIBLE_WITH_HEAD",
9
10
  BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN",
10
11
  BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW",
@@ -63,6 +64,7 @@ export type ExecutionPayloadBidErrorType =
63
64
  }
64
65
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
65
66
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
67
+ | {code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH; blockHash: RootHex}
66
68
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
67
69
  | {code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
68
70
  | {
@@ -12,8 +12,6 @@ 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",
17
15
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
18
16
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
19
17
  }
@@ -44,13 +42,6 @@ export type ExecutionPayloadEnvelopeErrorType =
44
42
  envelopeRequestsRoot: RootHex;
45
43
  bidRequestsRoot: RootHex;
46
44
  }
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}
54
45
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
55
46
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
56
47
 
@@ -8,6 +8,7 @@ export enum GossipAction {
8
8
  export enum GossipErrorCode {
9
9
  INVALID_SERIALIZED_BYTES_ERROR_CODE = "GOSSIP_ERROR_INVALID_SERIALIZED_BYTES",
10
10
  PAST_SLOT = "GOSSIP_ERROR_PAST_SLOT",
11
+ FUTURE_SLOT = "GOSSIP_ERROR_FUTURE_SLOT",
11
12
  }
12
13
 
13
14
  export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
@@ -135,6 +135,7 @@ export function initializeForkChoiceFromFinalizedState(
135
135
  blockRoot: toRootHex(checkpoint.root),
136
136
  timeliness: true, // Optimistically assume is timely
137
137
  ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
138
+ importedTimely: true, // Optimistically assume is timely
138
139
  proposerIndex: blockHeader.proposerIndex,
139
140
 
140
141
  justifiedEpoch: justifiedCheckpoint.epoch,
@@ -233,6 +234,7 @@ export function initializeForkChoiceFromUnfinalizedState(
233
234
  targetRoot: headRoot,
234
235
  timeliness: true, // Optimistically assume is timely
235
236
  ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
237
+ importedTimely: true, // Optimistically assume is timely
236
238
  proposerIndex: blockHeader.proposerIndex,
237
239
 
238
240
  justifiedEpoch: justifiedCheckpoint.epoch,
@@ -35,7 +35,7 @@ import {IArchiveStore} from "./archiveStore/interface.js";
35
35
  import {CheckpointBalancesCache} from "./balancesCache.js";
36
36
  import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache.js";
37
37
  import {IBlockInput} from "./blocks/blockInput/index.js";
38
- import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js";
38
+ import {ImportBlockOpts, ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
39
39
  import {IBlsVerifier} from "./bls/index.js";
40
40
  import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js";
41
41
  import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js";
@@ -262,7 +262,7 @@ export interface IBeaconChain {
262
262
  blocks: IBlockInput[],
263
263
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
264
264
  opts?: ImportBlockOpts
265
- ): Promise<void>;
265
+ ): Promise<ProcessBlocksResult>;
266
266
 
267
267
  /** Process execution payload envelope: verify, import to fork choice, and persist to DB */
268
268
  processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void>;
@@ -363,7 +363,7 @@ export class AggregatedAttestationPool {
363
363
  stateEpoch,
364
364
  rootCache,
365
365
  gloasState?.executionPayloadAvailability ?? null,
366
- gloasState?.latestExecutionPayloadBid.slot ?? null
366
+ gloasState?.latestBlockHeader.slot ?? null
367
367
  );
368
368
 
369
369
  const weight =
@@ -243,6 +243,22 @@ 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
+
246
262
  if (feeRecipient) {
247
263
  const preparationTime =
248
264
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
@@ -273,22 +289,6 @@ export class PrepareNextSlotScheduler {
273
289
  }
274
290
 
275
291
  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,23 +528,18 @@ export async function produceBlockBody<T extends BlockType>(
528
528
  }
529
529
  }
530
530
 
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
- }
536
-
537
- (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
538
- Object.assign(logMeta, {blobs: blobKzgCommitments.length});
531
+ const {blobKzgCommitments, executionRequests} = builderRes;
532
+ if (blobKzgCommitments === undefined) {
533
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
539
534
  }
540
535
 
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;
536
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).blobKzgCommitments = blobKzgCommitments;
537
+ Object.assign(logMeta, {blobs: blobKzgCommitments.length});
538
+
539
+ if (executionRequests === undefined) {
540
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
547
541
  }
542
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).executionRequests = executionRequests;
548
543
  }
549
544
 
550
545
  // blockType === BlockType.Full
@@ -899,45 +894,38 @@ function preparePayloadAttributes(
899
894
  ): SSEPayloadAttributes["payloadAttributes"] {
900
895
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
901
896
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
902
- const payloadAttributes = {
903
- timestamp,
904
- prevRandao,
905
- suggestedFeeRecipient: feeRecipient,
906
- };
907
-
908
- if (ForkSeq[fork] >= ForkSeq.capella) {
909
- if (!isStatePostCapella(prepareState)) {
910
- throw new Error("Expected Capella state for withdrawals");
911
- }
897
+ if (!isStatePostCapella(prepareState)) {
898
+ throw new Error("Expected Capella state for withdrawals");
899
+ }
912
900
 
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;
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");
930
909
  }
910
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
931
911
  } else {
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;
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;
935
917
  }
918
+ } else {
919
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
936
920
  }
937
921
 
938
- if (ForkSeq[fork] >= ForkSeq.deneb) {
939
- (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
940
- }
922
+ const payloadAttributes: deneb.SSEPayloadAttributes["payloadAttributes"] = {
923
+ timestamp,
924
+ prevRandao,
925
+ suggestedFeeRecipient: feeRecipient,
926
+ withdrawals,
927
+ parentBeaconBlockRoot: parentBlockRoot,
928
+ };
941
929
 
942
930
  if (ForkSeq[fork] >= ForkSeq.gloas) {
943
931
  if (!isStatePostGloas(prepareState)) {
@@ -1050,37 +1038,28 @@ export async function produceCommonBlockBody<T extends BlockType>(
1050
1038
  step: BlockProductionStep.attestations,
1051
1039
  });
1052
1040
 
1053
- const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
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 {
1054
1054
  randaoReveal,
1055
1055
  graffiti,
1056
- // Eth1 data voting is no longer required since electra
1057
1056
  eth1Data: currentState.eth1Data,
1058
1057
  proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1059
1058
  attesterSlashings,
1060
1059
  attestations,
1061
- // Since electra, deposits are processed by the execution layer,
1062
- // we no longer support handling deposits from earlier forks.
1063
1060
  deposits: [],
1064
1061
  voluntaryExits,
1062
+ blsToExecutionChanges,
1063
+ syncAggregate,
1065
1064
  };
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;
1086
1065
  }