@lodestar/beacon-node 1.43.0-dev.0bc48d3b54 → 1.43.0-dev.1540a78cd0

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 (213) hide show
  1. package/lib/api/impl/beacon/blocks/index.js +4 -6
  2. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  3. package/lib/api/impl/beacon/state/utils.d.ts +2 -2
  4. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  6. package/lib/api/impl/lodestar/index.js +1 -1
  7. package/lib/api/impl/lodestar/index.js.map +1 -1
  8. package/lib/api/impl/validator/index.d.ts.map +1 -1
  9. package/lib/api/impl/validator/index.js +0 -4
  10. package/lib/api/impl/validator/index.js.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  12. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  13. package/lib/chain/archiveStore/interface.d.ts +4 -4
  14. package/lib/chain/archiveStore/interface.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts +4 -4
  16. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts.map +1 -1
  17. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js.map +1 -1
  18. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +2 -2
  19. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  20. package/lib/chain/archiveStore/utils/archiveBlocks.js +110 -58
  21. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  22. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  23. package/lib/chain/blocks/importBlock.js +6 -3
  24. package/lib/chain/blocks/importBlock.js.map +1 -1
  25. package/lib/chain/blocks/importExecutionPayload.d.ts +26 -14
  26. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  27. package/lib/chain/blocks/importExecutionPayload.js +73 -86
  28. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  29. package/lib/chain/blocks/index.d.ts +5 -3
  30. package/lib/chain/blocks/index.d.ts.map +1 -1
  31. package/lib/chain/blocks/index.js +28 -10
  32. package/lib/chain/blocks/index.js.map +1 -1
  33. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  34. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  35. package/lib/chain/blocks/types.d.ts +14 -20
  36. package/lib/chain/blocks/types.d.ts.map +1 -1
  37. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  38. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  39. package/lib/chain/blocks/utils/chainSegment.js +81 -12
  40. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  41. package/lib/chain/blocks/verifyBlock.d.ts +3 -2
  42. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  43. package/lib/chain/blocks/verifyBlock.js +30 -5
  44. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  45. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  46. package/lib/chain/blocks/verifyBlocksSanityChecks.js +15 -4
  47. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  48. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  49. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  50. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  51. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  52. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  53. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -1
  54. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +2 -11
  55. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -1
  56. package/lib/chain/chain.d.ts +6 -5
  57. package/lib/chain/chain.d.ts.map +1 -1
  58. package/lib/chain/chain.js +16 -5
  59. package/lib/chain/chain.js.map +1 -1
  60. package/lib/chain/emitter.d.ts +3 -3
  61. package/lib/chain/emitter.d.ts.map +1 -1
  62. package/lib/chain/errors/blockError.d.ts +8 -1
  63. package/lib/chain/errors/blockError.d.ts.map +1 -1
  64. package/lib/chain/errors/blockError.js +2 -0
  65. package/lib/chain/errors/blockError.js.map +1 -1
  66. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  67. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  68. package/lib/chain/errors/executionPayloadBid.js +1 -0
  69. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  70. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  71. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  72. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  73. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  74. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  75. package/lib/chain/forkChoice/index.js +5 -17
  76. package/lib/chain/forkChoice/index.js.map +1 -1
  77. package/lib/chain/interface.d.ts +5 -4
  78. package/lib/chain/interface.d.ts.map +1 -1
  79. package/lib/chain/interface.js.map +1 -1
  80. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  81. package/lib/chain/prepareNextSlot.js +30 -10
  82. package/lib/chain/prepareNextSlot.js.map +1 -1
  83. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  84. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  85. package/lib/chain/produceBlock/produceBlockBody.js +29 -12
  86. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  87. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +11 -4
  88. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  89. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +20 -18
  90. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  91. package/lib/chain/validation/block.d.ts.map +1 -1
  92. package/lib/chain/validation/block.js +1 -0
  93. package/lib/chain/validation/block.js.map +1 -1
  94. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  95. package/lib/chain/validation/executionPayloadBid.js +13 -1
  96. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  97. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  98. package/lib/chain/validation/executionPayloadEnvelope.js +19 -9
  99. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  100. package/lib/db/repositories/executionPayloadEnvelopeArchive.js +1 -1
  101. package/lib/db/repositories/executionPayloadEnvelopeArchive.js.map +1 -1
  102. package/lib/execution/engine/mock.d.ts.map +1 -1
  103. package/lib/execution/engine/mock.js +3 -0
  104. package/lib/execution/engine/mock.js.map +1 -1
  105. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  106. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  107. package/lib/metrics/metrics/lodestar.js +4 -0
  108. package/lib/metrics/metrics/lodestar.js.map +1 -1
  109. package/lib/network/gossip/topic.d.ts +21 -2
  110. package/lib/network/gossip/topic.d.ts.map +1 -1
  111. package/lib/network/network.js +1 -1
  112. package/lib/network/network.js.map +1 -1
  113. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  114. package/lib/network/processor/gossipHandlers.js +5 -4
  115. package/lib/network/processor/gossipHandlers.js.map +1 -1
  116. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  117. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  118. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  119. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  120. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  121. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  122. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  123. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  124. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  125. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  126. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  127. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  128. package/lib/node/notifier.js +7 -1
  129. package/lib/node/notifier.js.map +1 -1
  130. package/lib/sync/range/batch.d.ts +12 -2
  131. package/lib/sync/range/batch.d.ts.map +1 -1
  132. package/lib/sync/range/batch.js +56 -30
  133. package/lib/sync/range/batch.js.map +1 -1
  134. package/lib/sync/range/chain.d.ts +6 -2
  135. package/lib/sync/range/chain.d.ts.map +1 -1
  136. package/lib/sync/range/chain.js +4 -3
  137. package/lib/sync/range/chain.js.map +1 -1
  138. package/lib/sync/range/range.d.ts.map +1 -1
  139. package/lib/sync/range/range.js +17 -6
  140. package/lib/sync/range/range.js.map +1 -1
  141. package/lib/sync/types.d.ts +34 -0
  142. package/lib/sync/types.d.ts.map +1 -1
  143. package/lib/sync/types.js +34 -0
  144. package/lib/sync/types.js.map +1 -1
  145. package/lib/sync/unknownBlock.d.ts +24 -1
  146. package/lib/sync/unknownBlock.d.ts.map +1 -1
  147. package/lib/sync/unknownBlock.js +649 -53
  148. package/lib/sync/unknownBlock.js.map +1 -1
  149. package/lib/sync/utils/downloadByRange.d.ts +46 -10
  150. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  151. package/lib/sync/utils/downloadByRange.js +147 -24
  152. package/lib/sync/utils/downloadByRange.js.map +1 -1
  153. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  154. package/lib/sync/utils/downloadByRoot.js +6 -2
  155. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  156. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  157. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  158. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  159. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  160. package/lib/util/sszBytes.d.ts.map +1 -1
  161. package/lib/util/sszBytes.js +16 -3
  162. package/lib/util/sszBytes.js.map +1 -1
  163. package/package.json +16 -15
  164. package/src/api/impl/beacon/blocks/index.ts +6 -6
  165. package/src/api/impl/beacon/state/utils.ts +2 -2
  166. package/src/api/impl/lodestar/index.ts +1 -1
  167. package/src/api/impl/validator/index.ts +0 -4
  168. package/src/chain/archiveStore/archiveStore.ts +5 -5
  169. package/src/chain/archiveStore/interface.ts +4 -4
  170. package/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +4 -4
  171. package/src/chain/archiveStore/utils/archiveBlocks.ts +153 -94
  172. package/src/chain/blocks/importBlock.ts +4 -2
  173. package/src/chain/blocks/importExecutionPayload.ts +92 -107
  174. package/src/chain/blocks/index.ts +44 -13
  175. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  176. package/src/chain/blocks/types.ts +14 -25
  177. package/src/chain/blocks/utils/chainSegment.ts +106 -17
  178. package/src/chain/blocks/verifyBlock.ts +35 -6
  179. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -7
  180. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  181. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +9 -18
  182. package/src/chain/chain.ts +33 -19
  183. package/src/chain/emitter.ts +3 -3
  184. package/src/chain/errors/blockError.ts +4 -1
  185. package/src/chain/errors/executionPayloadBid.ts +6 -0
  186. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  187. package/src/chain/forkChoice/index.ts +2 -22
  188. package/src/chain/interface.ts +9 -3
  189. package/src/chain/prepareNextSlot.ts +42 -12
  190. package/src/chain/produceBlock/produceBlockBody.ts +32 -10
  191. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +22 -20
  192. package/src/chain/validation/block.ts +1 -0
  193. package/src/chain/validation/executionPayloadBid.ts +14 -0
  194. package/src/chain/validation/executionPayloadEnvelope.ts +20 -10
  195. package/src/db/repositories/executionPayloadEnvelopeArchive.ts +1 -1
  196. package/src/execution/engine/mock.ts +5 -1
  197. package/src/metrics/metrics/lodestar.ts +4 -0
  198. package/src/network/network.ts +1 -1
  199. package/src/network/processor/gossipHandlers.ts +7 -4
  200. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  201. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  202. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  203. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  204. package/src/node/notifier.ts +8 -1
  205. package/src/sync/range/batch.ts +90 -35
  206. package/src/sync/range/chain.ts +13 -5
  207. package/src/sync/range/range.ts +18 -6
  208. package/src/sync/types.ts +72 -0
  209. package/src/sync/unknownBlock.ts +810 -57
  210. package/src/sync/utils/downloadByRange.ts +256 -39
  211. package/src/sync/utils/downloadByRoot.ts +12 -2
  212. package/src/sync/utils/pendingBlocksTree.ts +0 -15
  213. package/src/util/sszBytes.ts +21 -3
@@ -10,7 +10,7 @@ import {
10
10
  isStatePostBellatrix,
11
11
  isStatePostGloas,
12
12
  } from "@lodestar/state-transition";
13
- import {Bytes32, Slot} from "@lodestar/types";
13
+ import {Bytes32, Slot, electra} from "@lodestar/types";
14
14
  import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils";
15
15
  import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js";
16
16
  import {BuilderStatus} from "../execution/builder/http.js";
@@ -83,7 +83,7 @@ export class PrepareNextSlotScheduler {
83
83
  const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot);
84
84
  const {slot: headSlot, blockRoot: headRoot} = headBlock;
85
85
  // may be updated below if we predict a proposer-boost-reorg
86
- let updatedHeadRoot = headRoot;
86
+ let updatedHead = headBlock;
87
87
 
88
88
  // PS: previously this was comparing slots, but that gave no leway on the skipped
89
89
  // slots on epoch bounday. Making it more fluid.
@@ -148,7 +148,7 @@ export class PrepareNextSlotScheduler {
148
148
  {dontTransferCache: !isEpochTransition},
149
149
  RegenCaller.predictProposerHead
150
150
  );
151
- updatedHeadRoot = proposerHeadRoot;
151
+ updatedHead = proposerHead;
152
152
  }
153
153
 
154
154
  // Update the builder status, if enabled shoot an api call to check status
@@ -165,14 +165,19 @@ export class PrepareNextSlotScheduler {
165
165
  }
166
166
 
167
167
  let parentBlockHash: Bytes32;
168
+ let isExtendingPayload = false;
168
169
  if (isStatePostGloas(updatedPrepareState)) {
169
- parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHeadRoot)
170
+ isExtendingPayload = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot);
171
+ parentBlockHash = isExtendingPayload
170
172
  ? updatedPrepareState.latestExecutionPayloadBid.blockHash
171
173
  : updatedPrepareState.latestExecutionPayloadBid.parentBlockHash;
172
174
  } else {
173
175
  parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash;
174
176
  }
175
177
 
178
+ // Reused by the SSE emit below to avoid a second DB lookup on cache miss
179
+ let parentExecutionRequests: electra.ExecutionRequests | undefined;
180
+
176
181
  if (feeRecipient) {
177
182
  const preparationTime =
178
183
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
@@ -182,6 +187,13 @@ export class PrepareNextSlotScheduler {
182
187
  const finalizedBlockHash =
183
188
  this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
184
189
 
190
+ if (isExtendingPayload) {
191
+ parentExecutionRequests = await this.chain.getParentExecutionRequests(
192
+ updatedHead.slot,
193
+ updatedHead.blockRoot
194
+ );
195
+ }
196
+
185
197
  // awaiting here instead of throwing an async call because there is no other task
186
198
  // left for scheduler and this gives nice semantics to catch and log errors in the
187
199
  // try/catch wrapper here.
@@ -189,12 +201,13 @@ export class PrepareNextSlotScheduler {
189
201
  this.chain,
190
202
  this.logger,
191
203
  fork as ForkPostBellatrix, // State is of execution type
192
- fromHex(updatedHeadRoot),
204
+ fromHex(updatedHead.blockRoot),
193
205
  parentBlockHash,
194
206
  safeBlockHash,
195
207
  finalizedBlockHash,
196
208
  updatedPrepareState,
197
- feeRecipient
209
+ feeRecipient,
210
+ parentExecutionRequests
198
211
  );
199
212
  this.logger.verbose("PrepareNextSlotScheduler prepared new payload", {
200
213
  prepareSlot,
@@ -203,21 +216,38 @@ export class PrepareNextSlotScheduler {
203
216
  });
204
217
  }
205
218
 
219
+ if (ForkSeq[fork] >= ForkSeq.gloas) {
220
+ // Cutoff = slot of the parent of the block we'll actually build on (post-reorg).
221
+ // Steady state: cache holds just 2 entries — head (parent for next-slot production)
222
+ // and head.parent (proposer-boost-reorg fallback). Anything older is evicted.
223
+ const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot);
224
+ if (updatedHeadParent) {
225
+ this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot);
226
+ }
227
+ }
228
+
206
229
  this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition);
207
230
 
208
- // If emitPayloadAttributes is true emit a SSE payloadAttributes event
231
+ // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
232
+ // every slot. Without the flag, only emit the event if we are proposing in the next slot.
209
233
  if (
210
- this.chain.opts.emitPayloadAttributes === true &&
234
+ (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
211
235
  this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
212
236
  ) {
237
+ // if we didn't fetch above (not proposing), SSE still needs it here
238
+ if (!parentExecutionRequests && isExtendingPayload) {
239
+ parentExecutionRequests = await this.chain.getParentExecutionRequests(
240
+ updatedHead.slot,
241
+ updatedHead.blockRoot
242
+ );
243
+ }
213
244
  const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
214
245
  prepareState: updatedPrepareState,
215
246
  prepareSlot,
216
- parentBlockRoot: fromHex(headRoot),
247
+ parentBlockRoot: fromHex(updatedHead.blockRoot),
217
248
  parentBlockHash,
218
- // The likely consumers of this API are builders and will anyway ignore the
219
- // feeRecipient, so just pass zero hash for now till a real use case arises
220
- feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000",
249
+ feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
250
+ parentExecutionRequests,
221
251
  });
222
252
  this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
223
253
  }
@@ -46,6 +46,7 @@ import {
46
46
  electra,
47
47
  fulu,
48
48
  gloas,
49
+ ssz,
49
50
  } from "@lodestar/types";
50
51
  import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
51
52
  import {ZERO_HASH_HEX} from "../../constants/index.js";
@@ -213,9 +214,13 @@ export async function produceBlockBody<T extends BlockType>(
213
214
  });
214
215
 
215
216
  // Get execution payload from EL
216
- const parentBlockHash = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot))
217
+ const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
218
+ const parentBlockHash = isExtendingPayload
217
219
  ? currentState.latestExecutionPayloadBid.blockHash
218
220
  : currentState.latestExecutionPayloadBid.parentBlockHash;
221
+ const parentExecutionRequests = isExtendingPayload
222
+ ? await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot)
223
+ : ssz.electra.ExecutionRequests.defaultValue();
219
224
  const prepareRes = await prepareExecutionPayload(
220
225
  this,
221
226
  this.logger,
@@ -225,7 +230,8 @@ export async function produceBlockBody<T extends BlockType>(
225
230
  safeBlockHash,
226
231
  finalizedBlockHash ?? ZERO_HASH_HEX,
227
232
  currentState,
228
- feeRecipient
233
+ feeRecipient,
234
+ parentExecutionRequests
229
235
  );
230
236
 
231
237
  const {prepType, payloadId} = prepareRes;
@@ -269,6 +275,7 @@ export async function produceBlockBody<T extends BlockType>(
269
275
  value: 0,
270
276
  executionPayment: 0,
271
277
  blobKzgCommitments: blobsBundle.commitments,
278
+ executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests),
272
279
  };
273
280
  const signedBid: gloas.SignedExecutionPayloadBid = {
274
281
  message: bid,
@@ -280,6 +287,7 @@ export async function produceBlockBody<T extends BlockType>(
280
287
  gloasBody.signedExecutionPayloadBid = signedBid;
281
288
  // TODO GLOAS: Get payload attestations from pool for previous slot
282
289
  gloasBody.payloadAttestations = [];
290
+ gloasBody.parentExecutionRequests = parentExecutionRequests;
283
291
  blockBody = gloasBody as AssembledBodyType<T>;
284
292
 
285
293
  // Store execution payload data required to construct execution payload envelope later
@@ -616,7 +624,8 @@ export async function prepareExecutionPayload(
616
624
  safeBlockHash: RootHex,
617
625
  finalizedBlockHash: RootHex,
618
626
  state: IBeaconStateViewBellatrix,
619
- suggestedFeeRecipient: string
627
+ suggestedFeeRecipient: string,
628
+ parentExecutionRequests?: electra.ExecutionRequests
620
629
  ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
621
630
  const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
622
631
  const prevRandao = state.getRandaoMix(state.epoch);
@@ -653,6 +662,7 @@ export async function prepareExecutionPayload(
653
662
  parentBlockRoot,
654
663
  parentBlockHash,
655
664
  feeRecipient: suggestedFeeRecipient,
665
+ parentExecutionRequests,
656
666
  });
657
667
 
658
668
  payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
@@ -711,12 +721,14 @@ export function getPayloadAttributesForSSE(
711
721
  parentBlockRoot,
712
722
  parentBlockHash,
713
723
  feeRecipient,
724
+ parentExecutionRequests,
714
725
  }: {
715
726
  prepareState: IBeaconStateViewBellatrix;
716
727
  prepareSlot: Slot;
717
728
  parentBlockRoot: Root;
718
729
  parentBlockHash: Bytes32;
719
730
  feeRecipient: string;
731
+ parentExecutionRequests?: electra.ExecutionRequests;
720
732
  }
721
733
  ): SSEPayloadAttributes {
722
734
  const payloadAttributes = preparePayloadAttributes(fork, chain, {
@@ -725,6 +737,7 @@ export function getPayloadAttributesForSSE(
725
737
  parentBlockRoot,
726
738
  parentBlockHash,
727
739
  feeRecipient,
740
+ parentExecutionRequests,
728
741
  });
729
742
 
730
743
  let parentBlockNumber: number;
@@ -763,12 +776,14 @@ function preparePayloadAttributes(
763
776
  parentBlockRoot,
764
777
  parentBlockHash,
765
778
  feeRecipient,
779
+ parentExecutionRequests,
766
780
  }: {
767
781
  prepareState: IBeaconStateViewBellatrix;
768
782
  prepareSlot: Slot;
769
783
  parentBlockRoot: Root;
770
784
  parentBlockHash: Bytes32;
771
785
  feeRecipient: string;
786
+ parentExecutionRequests?: electra.ExecutionRequests;
772
787
  }
773
788
  ): SSEPayloadAttributes["payloadAttributes"] {
774
789
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
@@ -786,13 +801,20 @@ function preparePayloadAttributes(
786
801
 
787
802
  if (isStatePostGloas(prepareState)) {
788
803
  const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
789
- // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
790
- // already deducted from CL balances but never credited on the EL (the envelope
791
- // was not delivered). The next payload must carry those same withdrawals to
792
- // restore CL/EL consistency, otherwise validators permanently lose that balance.
793
- (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = isExtendingPayload
794
- ? prepareState.getExpectedWithdrawals().expectedWithdrawals
795
- : prepareState.payloadExpectedWithdrawals;
804
+ if (isExtendingPayload) {
805
+ if (parentExecutionRequests === undefined) {
806
+ throw new Error("parentExecutionRequests required when extending full parent");
807
+ }
808
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
809
+ prepareState.getExpectedWithdrawalsForFullParent(parentExecutionRequests);
810
+ } else {
811
+ // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
812
+ // already deducted from CL balances but never credited on the EL (the envelope
813
+ // was not delivered). The next payload must carry those same withdrawals to
814
+ // restore CL/EL consistency, otherwise validators permanently lose that balance.
815
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
816
+ prepareState.payloadExpectedWithdrawals;
817
+ }
796
818
  } else {
797
819
  // withdrawals logic is now fork aware as it changes on electra fork post capella
798
820
  (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
@@ -1,6 +1,6 @@
1
1
  import {CheckpointWithHex} from "@lodestar/fork-choice";
2
2
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
- import {RootHex} from "@lodestar/types";
3
+ import {RootHex, Slot} from "@lodestar/types";
4
4
  import {Logger} from "@lodestar/utils";
5
5
  import {Metrics} from "../../metrics/metrics.js";
6
6
  import {SerializedCache} from "../../util/serializedCache.js";
@@ -21,8 +21,15 @@ export type SeenPayloadEnvelopeInputModules = {
21
21
  /**
22
22
  * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root.
23
23
  *
24
- * Created during block import when a block is processed.
25
- * Pruned on finalization and after payload is written to DB.
24
+ * Created during block import when a block is processed. Two pruning paths:
25
+ * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build
26
+ * on is known.
27
+ * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup.
28
+ *
29
+ * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
30
+ * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
31
+ * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips
32
+ * ticks; subsequent ticks settle it back.
26
33
  */
27
34
  export class SeenPayloadEnvelopeInput {
28
35
  private readonly chainEvents: ChainEventEmitter;
@@ -58,16 +65,7 @@ export class SeenPayloadEnvelopeInput {
58
65
  }
59
66
 
60
67
  private onFinalized = (checkpoint: CheckpointWithHex): void => {
61
- // Prune all entries with slot < finalized slot
62
- const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch);
63
- let deletedCount = 0;
64
- for (const [, input] of this.payloadInputs) {
65
- if (input.slot < finalizedSlot) {
66
- this.evictPayloadInput(input);
67
- deletedCount++;
68
- }
69
- }
70
- this.logger?.debug("SeenPayloadEnvelopeInput.onFinalized deleted cached entries", {deletedCount});
68
+ this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch));
71
69
  };
72
70
 
73
71
  add(props: CreateFromBlockProps): PayloadEnvelopeInput {
@@ -88,17 +86,21 @@ export class SeenPayloadEnvelopeInput {
88
86
  return this.payloadInputs.get(blockRootHex)?.hasPayloadEnvelope() ?? false;
89
87
  }
90
88
 
91
- prune(blockRootHex: RootHex): void {
92
- const payloadInput = this.payloadInputs.get(blockRootHex);
93
- if (payloadInput) {
94
- this.evictPayloadInput(payloadInput);
95
- }
96
- }
97
-
98
89
  size(): number {
99
90
  return this.payloadInputs.size;
100
91
  }
101
92
 
93
+ pruneBelow(slot: Slot): void {
94
+ let deletedCount = 0;
95
+ for (const [, input] of this.payloadInputs) {
96
+ if (input.slot < slot) {
97
+ this.evictPayloadInput(input);
98
+ deletedCount++;
99
+ }
100
+ }
101
+ this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount});
102
+ }
103
+
102
104
  private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void {
103
105
  this.serializedCache.delete(payloadInput.getSerializedCacheKeys());
104
106
  this.payloadInputs.delete(payloadInput.blockRootHex);
@@ -103,6 +103,7 @@ export async function validateGossipBlock(
103
103
  if (chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) {
104
104
  throw new BlockGossipError(GossipAction.IGNORE, {
105
105
  code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
106
+ parentRoot,
106
107
  parentBlockHash: parentBlockHashHex,
107
108
  });
108
109
  }
@@ -1,5 +1,6 @@
1
1
  import {PublicKey} from "@chainsafe/blst";
2
2
  import {
3
+ computeEpochAtSlot,
3
4
  createSingleSignatureSetFromComponents,
4
5
  getExecutionPayloadBidSigningRoot,
5
6
  isActiveBuilder,
@@ -76,6 +77,19 @@ async function validateExecutionPayloadBid(
76
77
  // `SignedProposerPreferences` associated with `bid.slot`.
77
78
  // TODO GLOAS: Implement this along with proposer preference
78
79
 
80
+ // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
81
+ // consensus layer -- i.e. validate that
82
+ // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
83
+ const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
84
+ const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
85
+ if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
86
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
87
+ code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
88
+ blobKzgCommitmentsLen,
89
+ commitmentLimit: maxBlobsPerBlock,
90
+ });
91
+ }
92
+
79
93
  // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot.
80
94
  if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) {
81
95
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
@@ -4,8 +4,8 @@ import {
4
4
  getExecutionPayloadEnvelopeSignatureSet,
5
5
  isStatePostGloas,
6
6
  } from "@lodestar/state-transition";
7
- import {gloas} from "@lodestar/types";
8
- import {toRootHex} from "@lodestar/utils";
7
+ import {gloas, ssz} from "@lodestar/types";
8
+ import {byteArrayEquals, toRootHex} from "@lodestar/utils";
9
9
  import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
11
  import {RegenCaller} from "../regen/index.js";
@@ -53,7 +53,7 @@ async function validateExecutionPayloadEnvelope(
53
53
  throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, {
54
54
  code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN,
55
55
  blockRoot: blockRootHex,
56
- slot: envelope.slot,
56
+ slot: payload.slotNumber,
57
57
  });
58
58
  }
59
59
 
@@ -65,13 +65,13 @@ async function validateExecutionPayloadEnvelope(
65
65
  });
66
66
  }
67
67
 
68
- // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)`
68
+ // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `payload.slotNumber >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)`
69
69
  const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint();
70
70
  const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch);
71
- if (envelope.slot < finalizedSlot) {
71
+ if (payload.slotNumber < finalizedSlot) {
72
72
  throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, {
73
73
  code: ExecutionPayloadEnvelopeErrorCode.BELONG_TO_FINALIZED_BLOCK,
74
- envelopeSlot: envelope.slot,
74
+ envelopeSlot: payload.slotNumber,
75
75
  finalizedSlot,
76
76
  });
77
77
  }
@@ -80,11 +80,11 @@ async function validateExecutionPayloadEnvelope(
80
80
  // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice,
81
81
  // it is possible that the block didn't pass the validation
82
82
 
83
- // [REJECT] `block.slot` equals `envelope.slot`.
84
- if (block.slot !== envelope.slot) {
83
+ // [REJECT] `block.slot` equals `payload.slotNumber`.
84
+ if (block.slot !== payload.slotNumber) {
85
85
  throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
86
86
  code: ExecutionPayloadEnvelopeErrorCode.SLOT_MISMATCH,
87
- envelopeSlot: envelope.slot,
87
+ envelopeSlot: payload.slotNumber,
88
88
  blockSlot: block.slot,
89
89
  });
90
90
  }
@@ -107,6 +107,16 @@ async function validateExecutionPayloadEnvelope(
107
107
  });
108
108
  }
109
109
 
110
+ // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root`
111
+ const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests);
112
+ if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) {
113
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
114
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH,
115
+ envelopeRequestsRoot: toRootHex(requestsRoot),
116
+ bidRequestsRoot: toRootHex(payloadInput.getBid().executionRequestsRoot),
117
+ });
118
+ }
119
+
110
120
  // Get the block state to verify the builder's signature.
111
121
  const blockState = await chain.regen
112
122
  .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope)
@@ -114,7 +124,7 @@ async function validateExecutionPayloadEnvelope(
114
124
  throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, {
115
125
  code: ExecutionPayloadEnvelopeErrorCode.UNKNOWN_BLOCK_STATE,
116
126
  blockRoot: blockRootHex,
117
- slot: envelope.slot,
127
+ slot: payload.slotNumber,
118
128
  });
119
129
  });
120
130
  if (!isStatePostGloas(blockState)) {
@@ -19,7 +19,7 @@ export class ExecutionPayloadEnvelopeArchiveRepository extends Repository<Slot,
19
19
  * Id is the slot from the envelope
20
20
  */
21
21
  getId(value: gloas.SignedExecutionPayloadEnvelope): Slot {
22
- return value.message.slot;
22
+ return value.message.payload.slotNumber;
23
23
  }
24
24
 
25
25
  encodeKey(id: Slot): Uint8Array {
@@ -11,7 +11,7 @@ import {
11
11
  SLOTS_PER_EPOCH,
12
12
  } from "@lodestar/params";
13
13
  import {computeTimeAtSlot} from "@lodestar/state-transition";
14
- import {ExecutionPayload, RootHex, bellatrix, deneb, ssz} from "@lodestar/types";
14
+ import {ExecutionPayload, RootHex, bellatrix, deneb, gloas, ssz} from "@lodestar/types";
15
15
  import {fromHex, toRootHex} from "@lodestar/utils";
16
16
  import {ZERO_HASH_HEX} from "../../constants/index.js";
17
17
  import {INTEROP_BLOCK_HASH} from "../../node/utils/interop/state.js";
@@ -382,6 +382,10 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
382
382
  (executionPayload as ExecutionPayload<ForkPostCapella>).withdrawals = ssz.capella.Withdrawals.defaultValue();
383
383
  }
384
384
 
385
+ if (ForkSeq[fork] >= ForkSeq.gloas && payloadAttributes.slotNumber != null) {
386
+ (executionPayload as gloas.ExecutionPayload).slotNumber = payloadAttributes.slotNumber;
387
+ }
388
+
385
389
  this.preparingPayloads.set(payloadId, {
386
390
  executionPayload: serializeExecutionPayload(fork, executionPayload),
387
391
  blobsBundle: serializeBlobsBundle({
@@ -613,6 +613,10 @@ export function createLodestarMetrics(
613
613
  name: "lodestar_sync_unknown_block_pending_blocks_size",
614
614
  help: "Current size of UnknownBlockSync pending blocks cache",
615
615
  }),
616
+ pendingPayloads: register.gauge({
617
+ name: "lodestar_sync_unknown_block_pending_payloads_size",
618
+ help: "Current size of UnknownBlockSync pending payloads cache",
619
+ }),
616
620
  knownBadBlocks: register.gauge({
617
621
  name: "lodestar_sync_unknown_block_known_bad_blocks_size",
618
622
  help: "Current size of UnknownBlockSync known bad blocks cache",
@@ -505,7 +505,7 @@ export class Network implements INetwork {
505
505
  }
506
506
 
507
507
  async publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number> {
508
- const epoch = computeEpochAtSlot(signedEnvelope.message.slot);
508
+ const epoch = computeEpochAtSlot(signedEnvelope.message.payload.slotNumber);
509
509
  const boundary = this.config.getForkBoundaryAtEpoch(epoch);
510
510
 
511
511
  return this.publishGossip<GossipType.execution_payload>(
@@ -198,7 +198,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
198
198
  } catch (e) {
199
199
  if (e instanceof BlockGossipError) {
200
200
  logger.debug("Gossip block has error", {slot, root: blockShortHex, code: e.type.code});
201
- if (e.type.code === BlockErrorCode.PARENT_UNKNOWN && blockInput) {
201
+ if (
202
+ (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) &&
203
+ blockInput
204
+ ) {
202
205
  chain.emitter.emit(ChainEvent.blockUnknownParent, {
203
206
  blockInput,
204
207
  peer: peerIdStr,
@@ -1065,7 +1068,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1065
1068
  await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope);
1066
1069
  } catch (e) {
1067
1070
  if (e instanceof ExecutionPayloadEnvelopeError) {
1068
- const {slot, beaconBlockRoot} = signedEnvelope.message;
1071
+ const {beaconBlockRoot} = signedEnvelope.message;
1072
+ const slot = signedEnvelope.message.payload.slotNumber;
1069
1073
  logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code});
1070
1074
  if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN) {
1071
1075
  // TODO GLOAS: UnknownBlockSync to handle this
@@ -1088,7 +1092,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1088
1092
  throw e;
1089
1093
  }
1090
1094
 
1091
- const slot = envelope.slot;
1095
+ const slot = envelope.payload.slotNumber;
1092
1096
  const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
1093
1097
  metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1094
1098
  chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope);
@@ -1118,7 +1122,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1118
1122
  builderIndex: envelope.builderIndex,
1119
1123
  blockHash: toRootHex(envelope.payload.blockHash),
1120
1124
  blockRoot: blockRootHex,
1121
- stateRoot: toRootHex(envelope.stateRoot),
1122
1125
  });
1123
1126
 
1124
1127
  chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
@@ -24,6 +24,10 @@ export async function* onBeaconBlocksByRange(
24
24
  // in the case of initializing from a non-finalized state, we don't have the finalized block so this api does not work
25
25
  // chain.forkChoice.getFinalizeBlock().slot
26
26
  const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot();
27
+ // Blocks are migrated to blockArchive at finalization (including the finalized block itself),
28
+ // so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain loop
29
+ // starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
30
+ const archiveMaxSlot = finalizedSlot;
27
31
 
28
32
  const forkName = chain.config.getForkName(startSlot);
29
33
  if (isForkPostFulu(forkName) && startSlot < chain.earliestAvailableSlot) {
@@ -35,9 +39,12 @@ export async function* onBeaconBlocksByRange(
35
39
  }
36
40
 
37
41
  // Finalized range of blocks
38
- if (startSlot <= finalizedSlot) {
42
+ if (startSlot <= archiveMaxSlot) {
39
43
  // Chain of blobs won't change
40
- for await (const {key, value} of finalized.binaryEntriesStream({gte: startSlot, lt: endSlot})) {
44
+ for await (const {key, value} of finalized.binaryEntriesStream({
45
+ gte: startSlot,
46
+ lt: Math.min(endSlot, archiveMaxSlot + 1),
47
+ })) {
41
48
  yield {
42
49
  data: value,
43
50
  boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(finalized.decodeKey(key))),
@@ -46,19 +53,20 @@ export async function* onBeaconBlocksByRange(
46
53
  }
47
54
 
48
55
  // Non-finalized range of blocks
49
- if (endSlot > finalizedSlot) {
56
+ if (endSlot > archiveMaxSlot) {
50
57
  const headBlock = chain.forkChoice.getHead();
51
58
  const headRoot = headBlock.blockRoot;
52
59
  // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg
53
60
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
54
- // getAllAncestorBlocks response includes the head node, so it's the full chain.
61
+ // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary.
55
62
 
56
63
  // Iterate head chain with ascending block numbers
57
64
  for (let i = headChain.length - 1; i >= 0; i--) {
58
65
  const block = headChain[i];
59
66
 
60
- // Must include only blocks in the range requested
61
- if (block.slot >= startSlot && block.slot < endSlot) {
67
+ // Must include only blocks in the range requested, and skip anything the archive loop
68
+ // above already served via the block.slot > archiveMaxSlot filter.
69
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
62
70
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
63
71
  // at the time of the start of the request. Spec is clear the chain of blobs must be consistent, but on
64
72
  // re-org there's no need to abort the request
@@ -20,31 +20,37 @@ export async function* onBlobSidecarsByRange(
20
20
  const finalized = db.blobSidecarsArchive;
21
21
  const unfinalized = db.blobSidecars;
22
22
  const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot;
23
+ // Blobs are migrated to blobSidecarsArchive at finalization (including the finalized block
24
+ // itself), so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain
25
+ // loop starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
26
+ const archiveMaxSlot = finalizedSlot;
23
27
 
24
28
  // Finalized range of blobs
25
- if (startSlot <= finalizedSlot) {
29
+ if (startSlot <= archiveMaxSlot) {
26
30
  // Chain of blobs won't change
27
31
  for await (const {key, value: blobSideCarsBytesWrapped} of finalized.binaryEntriesStream({
28
32
  gte: startSlot,
29
- lt: endSlot,
33
+ lt: Math.min(endSlot, archiveMaxSlot + 1),
30
34
  })) {
31
35
  yield* iterateBlobBytesFromWrapper(chain, blobSideCarsBytesWrapped, finalized.decodeKey(key));
32
36
  }
33
37
  }
34
38
 
35
39
  // Non-finalized range of blobs
36
- if (endSlot > finalizedSlot) {
40
+ if (endSlot > archiveMaxSlot) {
37
41
  const headBlock = chain.forkChoice.getHead();
38
42
  const headRoot = headBlock.blockRoot;
39
43
  // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg
40
44
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
45
+ // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary.
41
46
 
42
47
  // Iterate head chain with ascending block numbers
43
48
  for (let i = headChain.length - 1; i >= 0; i--) {
44
49
  const block = headChain[i];
45
50
 
46
- // Must include only blobs in the range requested
47
- if (block.slot >= startSlot && block.slot < endSlot) {
51
+ // Must include only blobs in the range requested, and skip anything the archive loop
52
+ // above already served via the block.slot > archiveMaxSlot filter.
53
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
48
54
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
49
55
  // at the time of the start of the request. Spec is clear the chain of blobs must be consistent, but on
50
56
  // re-org there's no need to abort the request