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

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 (230) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +4 -4
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +39 -39
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/archiveStore/archiveStore.d.ts +1 -0
  8. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  9. package/lib/chain/archiveStore/archiveStore.js +1 -0
  10. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  11. package/lib/chain/archiveStore/historicalState/historicalStateRegen.d.ts.map +1 -1
  12. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js +1 -0
  13. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js.map +1 -1
  14. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  16. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  17. package/lib/chain/archiveStore/historicalState/types.d.ts +2 -0
  18. package/lib/chain/archiveStore/historicalState/types.d.ts.map +1 -1
  19. package/lib/chain/archiveStore/historicalState/types.js.map +1 -1
  20. package/lib/chain/archiveStore/historicalState/worker.js +4 -1
  21. package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
  22. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  23. package/lib/chain/archiveStore/utils/archiveBlocks.js +31 -34
  24. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  25. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  26. package/lib/chain/blocks/importBlock.js +0 -13
  27. package/lib/chain/blocks/importBlock.js.map +1 -1
  28. package/lib/chain/blocks/index.d.ts.map +1 -1
  29. package/lib/chain/blocks/index.js +3 -2
  30. package/lib/chain/blocks/index.js.map +1 -1
  31. package/lib/chain/blocks/writeBlockInputToDb.d.ts.map +1 -1
  32. package/lib/chain/blocks/writeBlockInputToDb.js +9 -16
  33. package/lib/chain/blocks/writeBlockInputToDb.js.map +1 -1
  34. package/lib/chain/chain.d.ts +9 -3
  35. package/lib/chain/chain.d.ts.map +1 -1
  36. package/lib/chain/chain.js +127 -73
  37. package/lib/chain/chain.js.map +1 -1
  38. package/lib/chain/emitter.d.ts +1 -1
  39. package/lib/chain/emitter.js +1 -1
  40. package/lib/chain/errors/blockError.d.ts +19 -7
  41. package/lib/chain/errors/blockError.d.ts.map +1 -1
  42. package/lib/chain/errors/blockError.js +2 -2
  43. package/lib/chain/errors/blockError.js.map +1 -1
  44. package/lib/chain/errors/dataColumnSidecarError.d.ts +15 -2
  45. package/lib/chain/errors/dataColumnSidecarError.d.ts.map +1 -1
  46. package/lib/chain/errors/dataColumnSidecarError.js.map +1 -1
  47. package/lib/chain/errors/executionPayloadEnvelope.d.ts +10 -11
  48. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  49. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  50. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  51. package/lib/chain/interface.d.ts +2 -2
  52. package/lib/chain/interface.d.ts.map +1 -1
  53. package/lib/chain/prepareNextSlot.js +13 -13
  54. package/lib/chain/prepareNextSlot.js.map +1 -1
  55. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  56. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  57. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  58. package/lib/chain/validation/block.d.ts.map +1 -1
  59. package/lib/chain/validation/block.js +35 -54
  60. package/lib/chain/validation/block.js.map +1 -1
  61. package/lib/chain/validation/dataColumnSidecar.d.ts.map +1 -1
  62. package/lib/chain/validation/dataColumnSidecar.js +38 -13
  63. package/lib/chain/validation/dataColumnSidecar.js.map +1 -1
  64. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  65. package/lib/chain/validation/executionPayloadEnvelope.js +10 -29
  66. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  67. package/lib/chain/validatorMonitor.d.ts +7 -1
  68. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  69. package/lib/chain/validatorMonitor.js +22 -5
  70. package/lib/chain/validatorMonitor.js.map +1 -1
  71. package/lib/db/beacon.d.ts +14 -5
  72. package/lib/db/beacon.d.ts.map +1 -1
  73. package/lib/db/beacon.js +17 -3
  74. package/lib/db/beacon.js.map +1 -1
  75. package/lib/db/dataColumnStore.d.ts +39 -0
  76. package/lib/db/dataColumnStore.d.ts.map +1 -0
  77. package/lib/db/dataColumnStore.js +81 -0
  78. package/lib/db/dataColumnStore.js.map +1 -0
  79. package/lib/db/flatFileStore/atomicWrite.d.ts +8 -0
  80. package/lib/db/flatFileStore/atomicWrite.d.ts.map +1 -0
  81. package/lib/db/flatFileStore/atomicWrite.js +61 -0
  82. package/lib/db/flatFileStore/atomicWrite.js.map +1 -0
  83. package/lib/db/flatFileStore/dcolFormat.d.ts +90 -0
  84. package/lib/db/flatFileStore/dcolFormat.d.ts.map +1 -0
  85. package/lib/db/flatFileStore/dcolFormat.js +286 -0
  86. package/lib/db/flatFileStore/dcolFormat.js.map +1 -0
  87. package/lib/db/flatFileStore/errors.d.ts +82 -0
  88. package/lib/db/flatFileStore/errors.d.ts.map +1 -0
  89. package/lib/db/flatFileStore/errors.js +32 -0
  90. package/lib/db/flatFileStore/errors.js.map +1 -0
  91. package/lib/db/flatFileStore/flatFileStore.d.ts +41 -0
  92. package/lib/db/flatFileStore/flatFileStore.d.ts.map +1 -0
  93. package/lib/db/flatFileStore/flatFileStore.js +251 -0
  94. package/lib/db/flatFileStore/flatFileStore.js.map +1 -0
  95. package/lib/db/flatFileStore/index.d.ts +4 -0
  96. package/lib/db/flatFileStore/index.d.ts.map +1 -0
  97. package/lib/db/flatFileStore/index.js +2 -0
  98. package/lib/db/flatFileStore/index.js.map +1 -0
  99. package/lib/db/flatFileStore/interface.d.ts +25 -0
  100. package/lib/db/flatFileStore/interface.d.ts.map +1 -0
  101. package/lib/db/flatFileStore/interface.js +2 -0
  102. package/lib/db/flatFileStore/interface.js.map +1 -0
  103. package/lib/db/flatFileStore/metrics.d.ts +24 -0
  104. package/lib/db/flatFileStore/metrics.d.ts.map +1 -0
  105. package/lib/db/flatFileStore/metrics.js +25 -0
  106. package/lib/db/flatFileStore/metrics.js.map +1 -0
  107. package/lib/db/flatFileStore/path.d.ts +9 -0
  108. package/lib/db/flatFileStore/path.d.ts.map +1 -0
  109. package/lib/db/flatFileStore/path.js +21 -0
  110. package/lib/db/flatFileStore/path.js.map +1 -0
  111. package/lib/db/flatFileStore/slotIndex.d.ts +14 -0
  112. package/lib/db/flatFileStore/slotIndex.d.ts.map +1 -0
  113. package/lib/db/flatFileStore/slotIndex.js +46 -0
  114. package/lib/db/flatFileStore/slotIndex.js.map +1 -0
  115. package/lib/db/flatFileStore/snappy.d.ts +3 -0
  116. package/lib/db/flatFileStore/snappy.d.ts.map +1 -0
  117. package/lib/db/flatFileStore/snappy.js +12 -0
  118. package/lib/db/flatFileStore/snappy.js.map +1 -0
  119. package/lib/db/index.d.ts +1 -0
  120. package/lib/db/index.d.ts.map +1 -1
  121. package/lib/db/interface.d.ts +8 -1
  122. package/lib/db/interface.d.ts.map +1 -1
  123. package/lib/db/repositories/blockArchive.d.ts +8 -1
  124. package/lib/db/repositories/blockArchive.d.ts.map +1 -1
  125. package/lib/db/repositories/blockArchive.js +65 -28
  126. package/lib/db/repositories/blockArchive.js.map +1 -1
  127. package/lib/db/repositories/blockArchiveIndex.d.ts +1 -2
  128. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  129. package/lib/db/repositories/blockArchiveIndex.js +3 -7
  130. package/lib/db/repositories/blockArchiveIndex.js.map +1 -1
  131. package/lib/execution/engine/mock.d.ts.map +1 -1
  132. package/lib/execution/engine/mock.js +4 -1
  133. package/lib/execution/engine/mock.js.map +1 -1
  134. package/lib/metrics/metrics/lodestar.d.ts +18 -0
  135. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  136. package/lib/metrics/metrics/lodestar.js +55 -1
  137. package/lib/metrics/metrics/lodestar.js.map +1 -1
  138. package/lib/monitoring/clientStats.js +1 -1
  139. package/lib/monitoring/clientStats.js.map +1 -1
  140. package/lib/network/gossip/encoding.d.ts.map +1 -1
  141. package/lib/network/gossip/encoding.js +0 -7
  142. package/lib/network/gossip/encoding.js.map +1 -1
  143. package/lib/network/gossip/topic.d.ts +2 -1
  144. package/lib/network/gossip/topic.d.ts.map +1 -1
  145. package/lib/network/gossip/topic.js +4 -20
  146. package/lib/network/gossip/topic.js.map +1 -1
  147. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  148. package/lib/network/processor/gossipHandlers.js +6 -7
  149. package/lib/network/processor/gossipHandlers.js.map +1 -1
  150. package/lib/network/processor/index.d.ts +49 -3
  151. package/lib/network/processor/index.d.ts.map +1 -1
  152. package/lib/network/processor/index.js +232 -98
  153. package/lib/network/processor/index.js.map +1 -1
  154. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  155. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +112 -91
  156. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  157. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.d.ts.map +1 -1
  158. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +2 -0
  159. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  160. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  161. package/lib/network/reqresp/protocols.js +6 -22
  162. package/lib/network/reqresp/protocols.js.map +1 -1
  163. package/lib/network/reqresp/rateLimit.js +2 -1
  164. package/lib/network/reqresp/rateLimit.js.map +1 -1
  165. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts +2 -1
  166. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  167. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +14 -9
  168. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  169. package/lib/node/nodejs.d.ts +2 -1
  170. package/lib/node/nodejs.d.ts.map +1 -1
  171. package/lib/node/nodejs.js +4 -2
  172. package/lib/node/nodejs.js.map +1 -1
  173. package/lib/util/sszBytes.d.ts +6 -0
  174. package/lib/util/sszBytes.d.ts.map +1 -1
  175. package/lib/util/sszBytes.js +1 -1
  176. package/lib/util/sszBytes.js.map +1 -1
  177. package/package.json +18 -19
  178. package/src/api/impl/beacon/blocks/index.ts +8 -4
  179. package/src/api/impl/validator/index.ts +44 -43
  180. package/src/chain/archiveStore/archiveStore.ts +6 -1
  181. package/src/chain/archiveStore/historicalState/historicalStateRegen.ts +1 -0
  182. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  183. package/src/chain/archiveStore/historicalState/types.ts +2 -0
  184. package/src/chain/archiveStore/historicalState/worker.ts +4 -1
  185. package/src/chain/archiveStore/utils/archiveBlocks.ts +34 -39
  186. package/src/chain/blocks/importBlock.ts +0 -13
  187. package/src/chain/blocks/index.ts +13 -2
  188. package/src/chain/blocks/writeBlockInputToDb.ts +10 -17
  189. package/src/chain/chain.ts +149 -80
  190. package/src/chain/emitter.ts +1 -1
  191. package/src/chain/errors/blockError.ts +22 -10
  192. package/src/chain/errors/dataColumnSidecarError.ts +24 -8
  193. package/src/chain/errors/executionPayloadEnvelope.ts +14 -11
  194. package/src/chain/interface.ts +2 -2
  195. package/src/chain/prepareNextSlot.ts +16 -16
  196. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  197. package/src/chain/validation/block.ts +35 -70
  198. package/src/chain/validation/dataColumnSidecar.ts +47 -14
  199. package/src/chain/validation/executionPayloadEnvelope.ts +10 -37
  200. package/src/chain/validatorMonitor.ts +24 -7
  201. package/src/db/beacon.ts +33 -6
  202. package/src/db/dataColumnStore.ts +113 -0
  203. package/src/db/flatFileStore/atomicWrite.ts +65 -0
  204. package/src/db/flatFileStore/dcolFormat.ts +389 -0
  205. package/src/db/flatFileStore/errors.ts +53 -0
  206. package/src/db/flatFileStore/flatFileStore.ts +310 -0
  207. package/src/db/flatFileStore/index.ts +3 -0
  208. package/src/db/flatFileStore/interface.ts +21 -0
  209. package/src/db/flatFileStore/metrics.ts +48 -0
  210. package/src/db/flatFileStore/path.ts +32 -0
  211. package/src/db/flatFileStore/slotIndex.ts +56 -0
  212. package/src/db/flatFileStore/snappy.ts +14 -0
  213. package/src/db/index.ts +1 -0
  214. package/src/db/interface.ts +9 -1
  215. package/src/db/repositories/blockArchive.ts +89 -30
  216. package/src/db/repositories/blockArchiveIndex.ts +4 -9
  217. package/src/execution/engine/mock.ts +4 -1
  218. package/src/metrics/metrics/lodestar.ts +57 -1
  219. package/src/monitoring/clientStats.ts +1 -1
  220. package/src/network/gossip/encoding.ts +0 -7
  221. package/src/network/gossip/topic.ts +3 -25
  222. package/src/network/processor/gossipHandlers.ts +11 -11
  223. package/src/network/processor/index.ts +263 -148
  224. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +139 -105
  225. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +2 -0
  226. package/src/network/reqresp/protocols.ts +7 -24
  227. package/src/network/reqresp/rateLimit.ts +2 -1
  228. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +15 -7
  229. package/src/node/nodejs.ts +6 -1
  230. package/src/util/sszBytes.ts +1 -1
@@ -25,7 +25,10 @@ logger.info("Historical state worker started");
25
25
 
26
26
  const config = createBeaconConfig(chainConfigFromJson(workerData.chainConfigJson), workerData.genesisValidatorsRoot);
27
27
 
28
- const db = new BeaconDb(config, await LevelDbController.create({name: workerData.dbLocation}, {logger}));
28
+ const db = new BeaconDb(config, await LevelDbController.create({name: workerData.dbLocation}, {logger}), {
29
+ dataColumnDir: workerData.dataColumnDir,
30
+ logger,
31
+ });
29
32
 
30
33
  const abortController = new AbortController();
31
34
 
@@ -134,6 +134,8 @@ export async function archiveBlocks(
134
134
  logger.verbose("Migrated blobSidecars from hot DB to cold DB", {...logCtx, migratedEntries});
135
135
  }
136
136
 
137
+ // Keep the normal LevelDB hot-to-cold flow for legacy columns. New columns are written
138
+ // directly to flat files, so this only moves fallback data left by older versions.
137
139
  if (finalizedPostFulu) {
138
140
  const migratedSlots = await migrateDataColumnSidecarsFromHotToColdDb(
139
141
  config,
@@ -142,7 +144,7 @@ export async function archiveBlocks(
142
144
  finalizedCanonicalBlocks,
143
145
  currentEpoch
144
146
  );
145
- logger.verbose("Migrated dataColumnSidecars from hot DB to cold DB", {
147
+ logger.verbose("Migrated legacy dataColumnSidecars from hot DB to cold DB", {
146
148
  ...logCtx,
147
149
  migratedEntries: migratedSlots.length,
148
150
  slotRange: prettyPrintIndices(migratedSlots),
@@ -195,6 +197,23 @@ export async function archiveBlocks(
195
197
  slotRange: prettyPrintIndices(nonCanonicalSlots),
196
198
  };
197
199
 
200
+ const columnItems = finalizedNonCanonicalBlocks
201
+ // Gloas EMPTY and FULL variants share a block root. EMPTY has no sidecars, so deleting by its root could
202
+ // remove the canonical FULL variant's columns. Pre-Gloas blocks are always FULL.
203
+ .filter(
204
+ (summary) => config.getForkSeq(summary.slot) >= ForkSeq.fulu && summary.payloadStatus === PayloadStatus.FULL
205
+ )
206
+ .map((summary) => ({slot: summary.slot, blockRoot: summary.blockRoot}));
207
+ if (columnItems.length > 0) {
208
+ // Delete sidecars first so their block roots remain available to retry cleanup after a failure or crash.
209
+ await db.dataColumns.deleteMany(columnItems);
210
+ logger.verbose("Deleted non canonical data columns of blocks", {
211
+ ...logCtx,
212
+ blocks: columnItems.length,
213
+ slotRange: prettyPrintIndices(columnItems.map(({slot}) => slot).sort((a, b) => a - b)),
214
+ });
215
+ }
216
+
198
217
  await db.block.batchDelete(nonCanonicalBlockRoots);
199
218
  logger.verbose("Deleted non canonical blocks from hot DB", nonCanonicalLogCtx);
200
219
 
@@ -203,11 +222,6 @@ export async function archiveBlocks(
203
222
  logger.verbose("Deleted non canonical blobSidecars from hot DB", nonCanonicalLogCtx);
204
223
  }
205
224
 
206
- if (finalizedPostFulu) {
207
- await db.dataColumnSidecar.deleteMany(nonCanonicalBlockRoots);
208
- logger.verbose("Deleted non canonical dataColumnSidecars from hot DB", nonCanonicalLogCtx);
209
- }
210
-
211
225
  if (finalizedPostGloas) {
212
226
  await db.executionPayloadEnvelope.batchDelete(nonCanonicalBlockRoots);
213
227
  logger.verbose("Deleted non canonical executionPayloadEnvelopes from hot DB", nonCanonicalLogCtx);
@@ -245,23 +259,16 @@ export async function archiveBlocks(
245
259
  );
246
260
  const dataColumnSidecarsMinEpoch = currentEpoch - dataColumnSidecarsArchiveWindow;
247
261
  if (dataColumnSidecarsMinEpoch >= config.FULU_FORK_EPOCH) {
248
- const prefixedKeys = await db.dataColumnSidecarArchive.keys({
249
- // The `id` value `0` refers to the column index. So we want to fetch all sidecars less than zero column of `dataColumnSidecarsMinEpoch`
250
- lt: {prefix: computeStartSlotAtEpoch(dataColumnSidecarsMinEpoch), id: 0},
251
- });
252
- // for each slot there could be multiple dataColumnSidecar, so we need to deduplicate it
253
- const slotsToDelete = [...new Set(prefixedKeys.map(({prefix}) => prefix))].sort((a, b) => a - b);
254
-
255
- if (slotsToDelete.length > 0) {
256
- await db.dataColumnSidecarArchive.deleteMany(slotsToDelete);
262
+ const columnsPruneSlot = computeStartSlotAtEpoch(dataColumnSidecarsMinEpoch);
263
+ const prunedColumnSlots = await db.dataColumns.pruneBefore(columnsPruneSlot);
264
+ if (prunedColumnSlots.length > 0) {
257
265
  logger.verbose("dataColumnSidecars prune", {
258
266
  ...logCtx,
259
- slotRange: prettyPrintIndices(slotsToDelete),
260
- numOfSlots: slotsToDelete.length,
261
- totalNumOfSidecars: prefixedKeys.length,
267
+ slotRange: prettyPrintIndices(prunedColumnSlots),
268
+ numOfSlots: prunedColumnSlots.length,
262
269
  });
263
270
  } else {
264
- logger.verbose(`dataColumnSidecars prune: no entries before epoch ${dataColumnSidecarsMinEpoch}`, logCtx);
271
+ logger.verbose("dataColumnSidecars prune: no entries before slot", {...logCtx, slot: columnsPruneSlot});
265
272
  }
266
273
  } else {
267
274
  logger.verbose(
@@ -389,7 +396,6 @@ async function migrateBlobSidecarsFromHotToColdDb(
389
396
  )
390
397
  ).filter((e): e is KeyValue<Slot, Uint8Array> => e !== null);
391
398
 
392
- // put to blockArchive db and delete block db
393
399
  await Promise.all([
394
400
  db.blobSidecarsArchive.batchPutBinary(canonicalBlobSidecarsEntries),
395
401
  db.blobSidecars.batchDelete(canonicalBlocks.map((block) => block.root)),
@@ -402,11 +408,9 @@ async function migrateBlobSidecarsFromHotToColdDb(
402
408
 
403
409
  // TODO: This function can be simplified further by reducing layers of promises in a loop
404
410
  /**
405
- * Post-gloas the data columns of a Gloas block are tied to its execution payload envelope —
406
- * columns only exist once the FULL variant of the block is in the proto-array. Pre-Gloas (Fulu)
407
- * blocks only have a FULL variant, so the `payloadStatus === FULL` filter passes them all.
408
- * Blocks whose canonical variant is PENDING/EMPTY are skipped here — their columns will be picked
409
- * up on a later run once the FULL variant appears in the ancestor walk.
411
+ * Post-gloas the data columns of a Gloas block are tied to its execution payload envelope.
412
+ * Columns only exist once the FULL variant of the block is in the proto-array. Pre-Gloas
413
+ * blocks only have a FULL variant, so the payload-status filter passes them all.
410
414
  */
411
415
  async function migrateDataColumnSidecarsFromHotToColdDb(
412
416
  config: ChainForkConfig,
@@ -428,32 +432,24 @@ async function migrateDataColumnSidecarsFromHotToColdDb(
428
432
  if (batch.length === 0) break;
429
433
 
430
434
  const promises: Promise<void>[] = [];
431
-
432
- // load Buffer instead of ssz deserialized to improve performance
433
435
  for (const block of batch) {
434
- const blockSlot = block.slot;
435
- const blockEpoch = computeEpochAtSlot(blockSlot);
436
-
436
+ const blockEpoch = computeEpochAtSlot(block.slot);
437
437
  if (
438
- config.getForkSeq(blockSlot) < ForkSeq.fulu ||
439
- // if block is out of ${config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS}, skip this step
438
+ config.getForkSeq(block.slot) < ForkSeq.fulu ||
440
439
  blockEpoch < currentEpoch - config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS
441
440
  ) {
442
441
  continue;
443
442
  }
444
443
 
445
- // Here we assume the data column sidecars are already in the hot db
446
444
  const dataColumnSidecarBytes = await fromAsync(db.dataColumnSidecar.valuesStreamBinary(block.root));
447
445
  if (dataColumnSidecarBytes.length === 0) {
448
- // Empty stream: either the block has no blobs, or columns were already archived on a
449
- // previous run (boundary block). Nothing to migrate.
450
446
  logger.debug("DataColumnSidecars in forkchoice but missing in hot db, could be already archived", {
451
447
  slot: block.slot,
452
448
  root: toRootHex(block.root),
453
449
  });
454
450
  continue;
455
451
  }
456
- logger.verbose("Migrated dataColumnSidecars for block", {
452
+ logger.verbose("Migrated legacy dataColumnSidecars for block", {
457
453
  currentEpoch,
458
454
  slot: block.slot,
459
455
  root: toRootHex(block.root),
@@ -462,15 +458,14 @@ async function migrateDataColumnSidecarsFromHotToColdDb(
462
458
  promises.push(
463
459
  db.dataColumnSidecarArchive.putManyBinary(
464
460
  block.slot,
465
- dataColumnSidecarBytes.map((p) => ({key: p.id, value: p.value}))
461
+ dataColumnSidecarBytes.map((entry) => ({key: entry.id, value: entry.value}))
466
462
  )
467
463
  );
468
464
  migratedSlots.push(block.slot);
469
465
  }
470
466
 
471
- promises.push(db.dataColumnSidecar.deleteMany(batch.map((block) => block.root)));
472
-
473
467
  await Promise.all(promises);
468
+ await db.dataColumnSidecar.deleteMany(batch.map((block) => block.root));
474
469
  }
475
470
 
476
471
  // Ancestor walk is newest to oldest; sort ascending so `prettyPrintIndices` renders cleanly.
@@ -524,19 +524,6 @@ export async function importBlock(
524
524
  this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch);
525
525
  this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch);
526
526
  }
527
- const finalizedCheckpoint = checkpointState.finalizedCheckpoint;
528
- const finalizedEpoch = finalizedCheckpoint.epoch;
529
- const preFinalizedEpoch = parentBlockSummary.finalizedEpoch;
530
- if (finalizedEpoch > preFinalizedEpoch) {
531
- this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
532
- block: toRootHex(finalizedCheckpoint.root),
533
- epoch: finalizedCheckpoint.epoch,
534
- state: toRootHex(checkpointState.hashTreeRoot()),
535
- executionOptimistic: false,
536
- });
537
- this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint));
538
- this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch);
539
- }
540
527
  }
541
528
  }
542
529
 
@@ -209,8 +209,19 @@ export async function processBlocks(
209
209
  const blockSlot = signedBlock.message.slot;
210
210
  const {state} = err.type;
211
211
  const forkTypes = this.config.getForkTypes(blockSlot);
212
- this.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `${blockSlot}_invalid_signature`);
213
- this.persistInvalidSszBytes("BeaconState", state.serialize(), `${state.slot}_invalid_signature`);
212
+ const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
213
+ this.persistInvalidSszValue(
214
+ forkTypes.SignedBeaconBlock,
215
+ signedBlock,
216
+ `${blockSlot}_invalid_signature`,
217
+ blockRootHex
218
+ );
219
+ this.persistInvalidSszBytes(
220
+ "BeaconState",
221
+ state.serialize(),
222
+ toRootHex(state.hashTreeRoot()),
223
+ `${state.slot}_invalid_signature`
224
+ );
214
225
  } else if (err.type.code === BlockErrorCode.INVALID_STATE_ROOT) {
215
226
  const {signedBlock} = err;
216
227
  const blockSlot = signedBlock.message.slot;
@@ -1,6 +1,6 @@
1
- import {ForkPostDeneb, isForkPostDeneb} from "@lodestar/params";
1
+ import {ForkPostDeneb, ForkPostFulu, isForkPostDeneb} from "@lodestar/params";
2
2
  import {SignedBeaconBlock} from "@lodestar/types";
3
- import {fromHex, toRootHex} from "@lodestar/utils";
3
+ import {toRootHex} from "@lodestar/utils";
4
4
  import {getBlobKzgCommitments} from "../../util/dataColumns.js";
5
5
  import {BeaconChain} from "../chain.js";
6
6
  import {IBlockInput, IDataColumnsInput, isBlockInputBlobs, isBlockInputColumns} from "./blockInput/index.js";
@@ -76,7 +76,6 @@ async function writeBlockAndBlobsToDb(this: BeaconChain, blockInput: IBlockInput
76
76
  */
77
77
  export async function writeDataColumnsToDb(this: BeaconChain, blockInput: IDataColumnsInput): Promise<void> {
78
78
  const {slot, blockRootHex} = blockInput;
79
- const blockRoot = fromHex(blockRootHex);
80
79
 
81
80
  if (!blockInput.hasComputedAllData()) {
82
81
  // Supernodes may only have a subset of the data columns by the time the block begins to be imported
@@ -89,25 +88,19 @@ export async function writeDataColumnsToDb(this: BeaconChain, blockInput: IDataC
89
88
 
90
89
  const {custodyColumns} = this.custodyConfig;
91
90
  const dataColumnSidecars = blockInput.getCustodyColumns();
91
+ const dataColumnSidecarType = this.config.getForkTypes<ForkPostFulu>(slot).DataColumnSidecar;
92
92
 
93
- const binaryPuts: {key: number; value: Uint8Array}[] = [];
94
- const nonbinaryPuts = [];
93
+ const binaryColumns: {index: number; data: Uint8Array}[] = [];
95
94
  for (const dataColumnSidecar of dataColumnSidecars) {
96
- // skip reserializing column if we already have it
97
95
  const serialized = this.serializedCache.get(dataColumnSidecar);
98
- if (serialized) {
99
- binaryPuts.push({key: dataColumnSidecar.index, value: serialized});
100
- } else {
101
- nonbinaryPuts.push(dataColumnSidecar);
102
- }
96
+ binaryColumns.push({
97
+ index: dataColumnSidecar.index,
98
+ data: serialized ?? dataColumnSidecarType.serialize(dataColumnSidecar),
99
+ });
103
100
  }
101
+ await this.db.dataColumns.putManyBinary({slot, blockRoot: blockRootHex}, binaryColumns);
104
102
 
105
- await Promise.all([
106
- this.db.dataColumnSidecar.putManyBinary(blockRoot, binaryPuts),
107
- this.db.dataColumnSidecar.putMany(blockRoot, nonbinaryPuts),
108
- ]);
109
-
110
- this.logger.debug("Persisted dataColumnSidecars to hot DB", {
103
+ this.logger.debug("Persisted dataColumnSidecars", {
111
104
  slot,
112
105
  root: blockRootHex,
113
106
  dataColumnSidecars: dataColumnSidecars.length,
@@ -268,6 +268,7 @@ export class BeaconChain implements IBeaconChain {
268
268
  db,
269
269
  dbName,
270
270
  dataDir,
271
+ dataColumnDir,
271
272
  logger,
272
273
  processShutdownCallback,
273
274
  clock,
@@ -285,6 +286,7 @@ export class BeaconChain implements IBeaconChain {
285
286
  db: IBeaconDb;
286
287
  dbName: string;
287
288
  dataDir: string;
289
+ dataColumnDir?: string;
288
290
  logger: Logger;
289
291
  processShutdownCallback: ProcessShutdownCallback;
290
292
  /** Used for testing to supply fake clock */
@@ -505,7 +507,12 @@ export class BeaconChain implements IBeaconChain {
505
507
 
506
508
  this.archiveStore = new ArchiveStore(
507
509
  {db, chain: this, logger: logger as LoggerNode, metrics},
508
- {...opts, dbName, anchorState: {finalizedCheckpoint: anchorState.finalizedCheckpoint}},
510
+ {
511
+ ...opts,
512
+ dbName,
513
+ dataColumnDir: dataColumnDir ?? path.join(dataDir, "data_columns"),
514
+ anchorState: {finalizedCheckpoint: anchorState.finalizedCheckpoint},
515
+ },
509
516
  signal
510
517
  );
511
518
 
@@ -969,12 +976,15 @@ export class BeaconChain implements IBeaconChain {
969
976
 
970
977
  async getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise<DataColumnSidecar[]> {
971
978
  const fork = this.config.getForkName(blockSlot);
979
+ const sidecarsByIndex = new Map<number, DataColumnSidecar>();
972
980
 
973
981
  if (isForkPostGloas(fork)) {
974
982
  // After gloas, columns are tracked in PayloadEnvelopeInput
975
983
  const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex);
976
984
  if (payloadInput) {
977
- return payloadInput.getAllColumns();
985
+ for (const sidecar of payloadInput.getAllColumns()) {
986
+ sidecarsByIndex.set(sidecar.index, sidecar);
987
+ }
978
988
  }
979
989
  } else {
980
990
  // Before gloas, columns are tracked in BlockInput
@@ -983,16 +993,17 @@ export class BeaconChain implements IBeaconChain {
983
993
  if (!isBlockInputColumns(blockInput)) {
984
994
  throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`);
985
995
  }
986
- return blockInput.getAllColumns();
996
+ for (const sidecar of blockInput.getAllColumns()) {
997
+ sidecarsByIndex.set(sidecar.index, sidecar);
998
+ }
987
999
  }
988
1000
  }
989
1001
 
990
- const sidecarsUnfinalized = await this.db.dataColumnSidecar.values(fromHex(blockRootHex));
991
- if (sidecarsUnfinalized.length > 0) {
992
- return sidecarsUnfinalized;
1002
+ for (const sidecar of await this.db.dataColumns.getAll({slot: blockSlot, blockRoot: blockRootHex})) {
1003
+ if (!sidecarsByIndex.has(sidecar.index)) sidecarsByIndex.set(sidecar.index, sidecar);
993
1004
  }
994
- const sidecarsFinalized = await this.db.dataColumnSidecarArchive.values(blockSlot);
995
- return sidecarsFinalized;
1005
+
1006
+ return [...sidecarsByIndex.values()].sort((a, b) => a.index - b.index);
996
1007
  }
997
1008
 
998
1009
  async getSerializedDataColumnSidecars(
@@ -1001,22 +1012,25 @@ export class BeaconChain implements IBeaconChain {
1001
1012
  indices: number[]
1002
1013
  ): Promise<(Uint8Array | undefined)[]> {
1003
1014
  const fork = this.config.getForkName(blockSlot);
1015
+ const dataColumnSidecars: (Uint8Array | undefined)[] = indices.map(() => undefined);
1004
1016
 
1005
1017
  if (isForkPostGloas(fork)) {
1006
1018
  // After gloas, columns are tracked in PayloadEnvelopeInput
1007
1019
  const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex);
1008
1020
  if (payloadInput) {
1009
- return indices.map((index) => {
1021
+ for (let i = 0; i < indices.length; i++) {
1022
+ const index = indices[i];
1010
1023
  const sidecar = payloadInput.getColumn(index);
1011
1024
  if (!sidecar) {
1012
- return undefined;
1025
+ continue;
1013
1026
  }
1014
1027
  const serialized = this.serializedCache.get(sidecar);
1015
1028
  if (serialized) {
1016
- return serialized;
1029
+ dataColumnSidecars[i] = serialized;
1030
+ } else {
1031
+ dataColumnSidecars[i] = sszTypesFor(fork as ForkPostGloas).DataColumnSidecar.serialize(sidecar);
1017
1032
  }
1018
- return sszTypesFor(fork as ForkPostGloas).DataColumnSidecar.serialize(sidecar);
1019
- });
1033
+ }
1020
1034
  }
1021
1035
  } else {
1022
1036
  // Before gloas, columns are tracked in BlockInput
@@ -1025,26 +1039,42 @@ export class BeaconChain implements IBeaconChain {
1025
1039
  if (!isBlockInputColumns(blockInput)) {
1026
1040
  throw new Error(`Expected block input to have columns: slot=${blockSlot} root=${blockRootHex}`);
1027
1041
  }
1028
- return indices.map((index) => {
1042
+ for (let i = 0; i < indices.length; i++) {
1043
+ const index = indices[i];
1029
1044
  const sidecar = blockInput.getColumn(index);
1030
1045
  if (!sidecar) {
1031
- return undefined;
1046
+ continue;
1032
1047
  }
1033
1048
  const serialized = this.serializedCache.get(sidecar);
1034
1049
  if (serialized) {
1035
- return serialized;
1050
+ dataColumnSidecars[i] = serialized;
1051
+ } else {
1052
+ dataColumnSidecars[i] = sszTypesFor(blockInput.forkName as ForkPostFulu).DataColumnSidecar.serialize(
1053
+ sidecar
1054
+ );
1036
1055
  }
1037
- return sszTypesFor(blockInput.forkName as ForkPostFulu).DataColumnSidecar.serialize(sidecar);
1038
- });
1056
+ }
1039
1057
  }
1040
1058
  }
1041
1059
 
1042
- const sidecarsUnfinalized = await this.db.dataColumnSidecar.getManyBinary(fromHex(blockRootHex), indices);
1043
- if (sidecarsUnfinalized.some((sidecar) => sidecar != null)) {
1044
- return sidecarsUnfinalized;
1060
+ const missingPositions = dataColumnSidecars
1061
+ .map((sidecar, position) => (sidecar === undefined ? position : -1))
1062
+ .filter((position) => position !== -1);
1063
+
1064
+ if (missingPositions.length > 0) {
1065
+ const persistedSidecars = await this.db.dataColumns.getManyBinary(
1066
+ {slot: blockSlot, blockRoot: blockRootHex},
1067
+ missingPositions.map((position) => indices[position])
1068
+ );
1069
+ for (let i = 0; i < missingPositions.length; i++) {
1070
+ const sidecar = persistedSidecars[i];
1071
+ if (sidecar !== undefined) {
1072
+ dataColumnSidecars[missingPositions[i]] = sidecar;
1073
+ }
1074
+ }
1045
1075
  }
1046
- const sidecarsFinalized = await this.db.dataColumnSidecarArchive.getManyBinary(blockSlot, indices);
1047
- return sidecarsFinalized;
1076
+
1077
+ return dataColumnSidecars;
1048
1078
  }
1049
1079
 
1050
1080
  async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise<CommonBlockBody> {
@@ -1271,59 +1301,65 @@ export class BeaconChain implements IBeaconChain {
1271
1301
  try {
1272
1302
  const prevHead = this.forkChoice.getHead();
1273
1303
  const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
1304
+ this.emitHeadEvents(prevHead, head);
1305
+ return head;
1306
+ } catch (e) {
1307
+ this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1308
+ throw e;
1309
+ } finally {
1310
+ timer?.();
1311
+ }
1312
+ }
1274
1313
 
1275
- const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1276
-
1277
- if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1278
- return head;
1279
- }
1314
+ /**
1315
+ * Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
1316
+ * can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
1317
+ */
1318
+ private emitHeadEvents(prevHead: ProtoBlock, head: ProtoBlock): void {
1319
+ const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1280
1320
 
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
- }
1321
+ if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1322
+ return;
1323
+ }
1298
1324
 
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
- },
1325
+ try {
1326
+ const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1327
+ const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1328
+ const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1329
+ const executionOptimistic = isOptimisticBlock(head);
1330
+
1331
+ if (headRootChanged) {
1332
+ this.emitter.emit(routes.events.EventType.head, {
1333
+ block: head.blockRoot,
1334
+ epochTransition,
1335
+ slot: head.slot,
1336
+ state: head.stateRoot,
1337
+ previousDutyDependentRoot,
1338
+ currentDutyDependentRoot,
1339
+ executionOptimistic,
1311
1340
  });
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
1341
  }
1320
1342
 
1321
- return head;
1343
+ this.emitter.emit(routes.events.EventType.headV2, {
1344
+ version: this.config.getForkName(head.slot),
1345
+ data: {
1346
+ slot: head.slot,
1347
+ block: head.blockRoot,
1348
+ state: head.stateRoot,
1349
+ payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1350
+ epochTransition,
1351
+ currentEpochDependentRoot: previousDutyDependentRoot,
1352
+ nextEpochDependentRoot: currentDutyDependentRoot,
1353
+ executionOptimistic,
1354
+ },
1355
+ });
1322
1356
  } catch (e) {
1323
- this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1324
- throw e;
1325
- } finally {
1326
- timer?.();
1357
+ // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1358
+ this.logger.debug(
1359
+ "Error emitting head/head_v2 event",
1360
+ {slot: head.slot, root: head.blockRoot, headRootChanged},
1361
+ e as Error
1362
+ );
1327
1363
  }
1328
1364
  }
1329
1365
 
@@ -1381,10 +1417,15 @@ export class BeaconChain implements IBeaconChain {
1381
1417
  const slot = data.slot;
1382
1418
  if (isBlindedBeaconBlock(data)) {
1383
1419
  const sszType = this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock;
1384
- void this.persistSszObject("BlindedBeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix);
1420
+ void this.persistSszObject(
1421
+ "BlindedBeaconBlock",
1422
+ sszType.serialize(data),
1423
+ toRootHex(sszType.hashTreeRoot(data)),
1424
+ suffix
1425
+ );
1385
1426
  } else {
1386
1427
  const sszType = this.config.getForkTypes(slot).BeaconBlock;
1387
- void this.persistSszObject("BeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix);
1428
+ void this.persistSszObject("BeaconBlock", sszType.serialize(data), toRootHex(sszType.hashTreeRoot(data)), suffix);
1388
1429
  }
1389
1430
  }
1390
1431
 
@@ -1405,33 +1446,39 @@ export class BeaconChain implements IBeaconChain {
1405
1446
  this.persistSszObject(
1406
1447
  `SignedBeaconBlock_slot_${blockSlot}`,
1407
1448
  blockType.serialize(block),
1408
- blockType.hashTreeRoot(block),
1449
+ toRootHex(this.config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message)),
1409
1450
  `${logStr}_block`
1410
1451
  ),
1411
1452
  this.persistSszObject(
1412
1453
  `preState_slot_${preState.slot}_BeaconState`,
1413
1454
  preState.serialize(),
1414
- preState.hashTreeRoot(),
1455
+ toRootHex(preState.hashTreeRoot()),
1415
1456
  `${logStr}_pre_state`
1416
1457
  ),
1417
1458
  this.persistSszObject(
1418
1459
  `postState_slot_${postState.slot}_BeaconState`,
1419
1460
  postState.serialize(),
1420
- postState.hashTreeRoot(),
1461
+ toRootHex(postStateRoot),
1421
1462
  `${logStr}_post_state`
1422
1463
  ),
1423
1464
  ]);
1424
1465
  }
1425
1466
 
1426
- persistInvalidSszValue<T>(type: Type<T>, sszObject: T, suffix?: string): void {
1467
+ persistInvalidSszValue<T>(type: Type<T>, sszObject: T, suffix?: string, rootHex?: RootHex): void {
1427
1468
  if (this.opts.persistInvalidSszObjects) {
1428
- void this.persistSszObject(type.typeName, type.serialize(sszObject), type.hashTreeRoot(sszObject), suffix);
1469
+ void this.persistSszObject(
1470
+ type.typeName,
1471
+ type.serialize(sszObject),
1472
+ // in SignedBeaconBlock case, we want to use BeaconBlock root instead
1473
+ rootHex ?? toRootHex(type.hashTreeRoot(sszObject)),
1474
+ suffix
1475
+ );
1429
1476
  }
1430
1477
  }
1431
1478
 
1432
- persistInvalidSszBytes(typeName: string, sszBytes: Uint8Array, suffix?: string): void {
1479
+ persistInvalidSszBytes(typeName: string, sszBytes: Uint8Array, rootHex: RootHex, suffix?: string): void {
1433
1480
  if (this.opts.persistInvalidSszObjects) {
1434
- void this.persistSszObject(typeName, sszBytes, sszBytes, suffix);
1481
+ void this.persistSszObject(typeName, sszBytes, rootHex, suffix);
1435
1482
  }
1436
1483
  }
1437
1484
 
@@ -1562,14 +1609,14 @@ export class BeaconChain implements IBeaconChain {
1562
1609
  return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true};
1563
1610
  }
1564
1611
 
1565
- private async persistSszObject(prefix: string, bytes: Uint8Array, root: Uint8Array, logStr?: string): Promise<void> {
1612
+ private async persistSszObject(prefix: string, bytes: Uint8Array, rootHex: RootHex, logStr?: string): Promise<void> {
1566
1613
  const now = new Date();
1567
1614
  // yyyy-MM-dd
1568
1615
  const dateStr = now.toISOString().split("T")[0];
1569
1616
 
1570
1617
  // by default store to lodestar_archive of current dir
1571
1618
  const dirpath = path.join(this.opts.persistInvalidSszObjectsDir ?? "invalid_ssz_objects", dateStr);
1572
- const filepath = path.join(dirpath, `${prefix}_${toRootHex(root)}.ssz`);
1619
+ const filepath = path.join(dirpath, `${prefix}_${rootHex}.ssz`);
1573
1620
 
1574
1621
  await ensureDir(dirpath);
1575
1622
 
@@ -1612,7 +1659,10 @@ export class BeaconChain implements IBeaconChain {
1612
1659
  this.processShutdownCallback(this.forkChoice.irrecoverableError);
1613
1660
  }
1614
1661
 
1662
+ // updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
1663
+ const prevHead = this.forkChoice.getHead();
1615
1664
  this.forkChoice.updateTime(slot);
1665
+ this.emitHeadEvents(prevHead, this.forkChoice.getHead());
1616
1666
  this.metrics?.clockSlot.set(slot);
1617
1667
 
1618
1668
  this.attestationPool.prune(slot);
@@ -1670,6 +1720,25 @@ export class BeaconChain implements IBeaconChain {
1670
1720
 
1671
1721
  private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
1672
1722
  this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex});
1723
+ this.metrics?.finalizedEpoch.set(cp.epoch);
1724
+ const finalizedBlock = this.forkChoice.getBlockHexDefaultStatus(cp.rootHex);
1725
+ if (finalizedBlock) {
1726
+ // The callback runs synchronously inside fork choice checkpoint updates, defer writing to subscribers
1727
+ callInNextEventLoop(() => {
1728
+ this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
1729
+ block: cp.rootHex,
1730
+ epoch: cp.epoch,
1731
+ state: finalizedBlock.stateRoot,
1732
+ executionOptimistic: isOptimisticBlock(finalizedBlock),
1733
+ });
1734
+ });
1735
+ } else {
1736
+ this.logger.debug("Finalized block not found in fork choice, skip finalized_checkpoint event", {
1737
+ epoch: cp.epoch,
1738
+ root: cp.rootHex,
1739
+ });
1740
+ }
1741
+
1673
1742
  const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
1674
1743
  this.seenBlockProposers.prune(finalizedSlot);
1675
1744
 
@@ -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
  /**