@lodestar/beacon-node 1.51.0-dev.88c15a0731 → 1.51.0-dev.8d8efcbf5f

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 (180) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +17 -27
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +5 -2
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/beacon/state/utils.d.ts +1 -1
  8. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  9. package/lib/api/impl/validator/index.d.ts.map +1 -1
  10. package/lib/api/impl/validator/index.js +4 -1
  11. package/lib/api/impl/validator/index.js.map +1 -1
  12. package/lib/chain/GetBlobsTracker.d.ts +11 -0
  13. package/lib/chain/GetBlobsTracker.d.ts.map +1 -1
  14. package/lib/chain/GetBlobsTracker.js +85 -27
  15. package/lib/chain/GetBlobsTracker.js.map +1 -1
  16. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  17. package/lib/chain/archiveStore/archiveStore.js +1 -0
  18. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  19. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +4 -0
  20. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  21. package/lib/chain/archiveStore/utils/archiveBlocks.js +7 -1
  22. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  23. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  24. package/lib/chain/blocks/importBlock.js +6 -5
  25. package/lib/chain/blocks/importBlock.js.map +1 -1
  26. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -1
  27. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  28. package/lib/chain/blocks/importExecutionPayload.js +7 -2
  29. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  30. package/lib/chain/blocks/index.js +2 -2
  31. package/lib/chain/blocks/index.js.map +1 -1
  32. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -5
  33. package/lib/chain/blocks/payloadEnvelopeProcessor.js +7 -7
  34. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  35. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  36. package/lib/chain/blocks/verifyBlock.js +1 -1
  37. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  38. package/lib/chain/blocks/verifyBlocksDataAvailability.d.ts.map +1 -1
  39. package/lib/chain/blocks/verifyBlocksDataAvailability.js +8 -1
  40. package/lib/chain/blocks/verifyBlocksDataAvailability.js.map +1 -1
  41. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts +3 -3
  42. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  43. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +13 -6
  44. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  45. package/lib/chain/bls/multithread/index.js +5 -5
  46. package/lib/chain/bls/multithread/index.js.map +1 -1
  47. package/lib/chain/bls/multithread/types.d.ts +1 -1
  48. package/lib/chain/bls/multithread/types.d.ts.map +1 -1
  49. package/lib/chain/bls/multithread/worker.js +1 -1
  50. package/lib/chain/bls/multithread/worker.js.map +1 -1
  51. package/lib/chain/bls/singleThread.js +1 -1
  52. package/lib/chain/bls/singleThread.js.map +1 -1
  53. package/lib/chain/bls/utils.js +1 -1
  54. package/lib/chain/bls/utils.js.map +1 -1
  55. package/lib/chain/chain.d.ts +1 -1
  56. package/lib/chain/chain.d.ts.map +1 -1
  57. package/lib/chain/chain.js +4 -4
  58. package/lib/chain/chain.js.map +1 -1
  59. package/lib/chain/errors/blockError.d.ts +1 -1
  60. package/lib/chain/errors/blockError.d.ts.map +1 -1
  61. package/lib/chain/errors/blockError.js +1 -1
  62. package/lib/chain/errors/blockError.js.map +1 -1
  63. package/lib/chain/interface.d.ts +2 -3
  64. package/lib/chain/interface.d.ts.map +1 -1
  65. package/lib/chain/interface.js.map +1 -1
  66. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  67. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  68. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  69. package/lib/chain/opPools/attestationPool.d.ts +1 -1
  70. package/lib/chain/opPools/attestationPool.d.ts.map +1 -1
  71. package/lib/chain/opPools/attestationPool.js +1 -1
  72. package/lib/chain/opPools/attestationPool.js.map +1 -1
  73. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  74. package/lib/chain/opPools/payloadAttestationPool.js +1 -1
  75. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  76. package/lib/chain/opPools/syncCommitteeMessagePool.d.ts.map +1 -1
  77. package/lib/chain/opPools/syncCommitteeMessagePool.js +1 -1
  78. package/lib/chain/opPools/syncCommitteeMessagePool.js.map +1 -1
  79. package/lib/chain/opPools/syncContributionAndProofPool.d.ts.map +1 -1
  80. package/lib/chain/opPools/syncContributionAndProofPool.js +1 -1
  81. package/lib/chain/opPools/syncContributionAndProofPool.js.map +1 -1
  82. package/lib/chain/opPools/utils.d.ts +1 -1
  83. package/lib/chain/opPools/utils.d.ts.map +1 -1
  84. package/lib/chain/opPools/utils.js +1 -1
  85. package/lib/chain/opPools/utils.js.map +1 -1
  86. package/lib/chain/options.d.ts +0 -1
  87. package/lib/chain/options.d.ts.map +1 -1
  88. package/lib/chain/options.js +0 -1
  89. package/lib/chain/options.js.map +1 -1
  90. package/lib/db/beacon.d.ts +7 -0
  91. package/lib/db/beacon.d.ts.map +1 -1
  92. package/lib/db/beacon.js +32 -0
  93. package/lib/db/beacon.js.map +1 -1
  94. package/lib/db/dataColumnStore.d.ts +1 -1
  95. package/lib/db/dataColumnStore.d.ts.map +1 -1
  96. package/lib/db/dataColumnStore.js +1 -4
  97. package/lib/db/dataColumnStore.js.map +1 -1
  98. package/lib/db/interface.d.ts +1 -0
  99. package/lib/db/interface.d.ts.map +1 -1
  100. package/lib/db/repositories/dataColumnSidecarArchive.d.ts +6 -0
  101. package/lib/db/repositories/dataColumnSidecarArchive.d.ts.map +1 -1
  102. package/lib/db/repositories/dataColumnSidecarArchive.js +19 -0
  103. package/lib/db/repositories/dataColumnSidecarArchive.js.map +1 -1
  104. package/lib/execution/engine/index.d.ts +1 -1
  105. package/lib/execution/engine/index.d.ts.map +1 -1
  106. package/lib/execution/engine/index.js +1 -1
  107. package/lib/execution/engine/index.js.map +1 -1
  108. package/lib/execution/engine/interface.d.ts +1 -1
  109. package/lib/execution/engine/interface.d.ts.map +1 -1
  110. package/lib/execution/engine/interface.js.map +1 -1
  111. package/lib/execution/engine/types.d.ts.map +1 -1
  112. package/lib/execution/engine/types.js +2 -2
  113. package/lib/execution/engine/types.js.map +1 -1
  114. package/lib/monitoring/service.js +1 -1
  115. package/lib/monitoring/service.js.map +1 -1
  116. package/lib/network/network.d.ts.map +1 -1
  117. package/lib/network/network.js +3 -1
  118. package/lib/network/network.js.map +1 -1
  119. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  120. package/lib/network/processor/gossipHandlers.js +6 -5
  121. package/lib/network/processor/gossipHandlers.js.map +1 -1
  122. package/lib/node/nodejs.d.ts +1 -1
  123. package/lib/node/nodejs.d.ts.map +1 -1
  124. package/lib/node/nodejs.js +10 -2
  125. package/lib/node/nodejs.js.map +1 -1
  126. package/lib/node/options.d.ts +1 -1
  127. package/lib/node/options.d.ts.map +1 -1
  128. package/lib/node/options.js +1 -1
  129. package/lib/node/options.js.map +1 -1
  130. package/lib/sync/interface.d.ts.map +1 -1
  131. package/lib/sync/interface.js.map +1 -1
  132. package/lib/util/queue/itemQueue.js +1 -1
  133. package/lib/util/queue/itemQueue.js.map +1 -1
  134. package/lib/util/sszBytes.js +2 -2
  135. package/lib/util/sszBytes.js.map +1 -1
  136. package/package.json +16 -16
  137. package/src/api/impl/beacon/blocks/index.ts +35 -45
  138. package/src/api/impl/beacon/pool/index.ts +5 -1
  139. package/src/api/impl/beacon/state/utils.ts +1 -1
  140. package/src/api/impl/validator/index.ts +4 -1
  141. package/src/chain/GetBlobsTracker.ts +97 -38
  142. package/src/chain/archiveStore/archiveStore.ts +1 -0
  143. package/src/chain/archiveStore/utils/archiveBlocks.ts +12 -4
  144. package/src/chain/blocks/importBlock.ts +8 -7
  145. package/src/chain/blocks/importExecutionPayload.ts +12 -2
  146. package/src/chain/blocks/index.ts +6 -2
  147. package/src/chain/blocks/payloadEnvelopeProcessor.ts +7 -7
  148. package/src/chain/blocks/verifyBlock.ts +1 -0
  149. package/src/chain/blocks/verifyBlocksDataAvailability.ts +10 -1
  150. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +16 -6
  151. package/src/chain/bls/multithread/index.ts +5 -5
  152. package/src/chain/bls/multithread/types.ts +1 -1
  153. package/src/chain/bls/multithread/worker.ts +1 -5
  154. package/src/chain/bls/singleThread.ts +1 -1
  155. package/src/chain/bls/utils.ts +1 -1
  156. package/src/chain/chain.ts +5 -5
  157. package/src/chain/errors/blockError.ts +1 -1
  158. package/src/chain/interface.ts +2 -3
  159. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  160. package/src/chain/opPools/attestationPool.ts +1 -1
  161. package/src/chain/opPools/payloadAttestationPool.ts +1 -1
  162. package/src/chain/opPools/syncCommitteeMessagePool.ts +1 -1
  163. package/src/chain/opPools/syncContributionAndProofPool.ts +1 -1
  164. package/src/chain/opPools/utils.ts +1 -1
  165. package/src/chain/options.ts +0 -2
  166. package/src/db/beacon.ts +39 -0
  167. package/src/db/dataColumnStore.ts +2 -4
  168. package/src/db/interface.ts +2 -0
  169. package/src/db/repositories/dataColumnSidecarArchive.ts +20 -0
  170. package/src/execution/engine/index.ts +1 -1
  171. package/src/execution/engine/interface.ts +1 -1
  172. package/src/execution/engine/types.ts +3 -1
  173. package/src/monitoring/service.ts +1 -1
  174. package/src/network/network.ts +3 -1
  175. package/src/network/processor/gossipHandlers.ts +6 -5
  176. package/src/node/nodejs.ts +24 -7
  177. package/src/node/options.ts +4 -5
  178. package/src/sync/interface.ts +1 -0
  179. package/src/util/queue/itemQueue.ts +1 -1
  180. package/src/util/sszBytes.ts +2 -2
@@ -316,18 +316,8 @@ export function getBeaconBlockApi(
316
316
  break;
317
317
  }
318
318
 
319
- case routes.beacon.BroadcastValidation.none: {
320
- chain.logger.debug("Skipping broadcast validation", valLogMeta);
321
- break;
322
- }
323
-
324
319
  default: {
325
- // error or log warning we do not support this validation
326
- const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
327
- if (chain.opts.broadcastValidationStrictness === "error") {
328
- throw Error(message);
329
- }
330
- chain.logger.warn(message, valLogMeta);
320
+ throw new ApiError(400, `Invalid broadcast validation level ${broadcastValidation}`);
331
321
  }
332
322
  }
333
323
 
@@ -397,26 +387,24 @@ export function getBeaconBlockApi(
397
387
  ...blobSidecars.map((blobSidecar) => () => network.publishBlobSidecar(blobSidecar)),
398
388
  () =>
399
389
  // there is no rush to persist block since we published it to gossip anyway
400
- chain
401
- .processBlock(blockForImport, opts)
402
- .catch((e) => {
403
- if (e instanceof BlockError) {
404
- switch (e.type.code) {
405
- case BlockErrorCode.ALREADY_KNOWN:
406
- // Block was imported while publishing, e.g. received via gossip
407
- chain.logger.debug("Ignoring already-known block during publishing", valLogMeta);
408
- return;
409
- case BlockErrorCode.PARENT_BLOCK_UNKNOWN:
410
- case BlockErrorCode.PARENT_PAYLOAD_UNKNOWN:
411
- chain.emitter.emit(ChainEvent.blockUnknownParent, {
412
- blockInput: blockForImport,
413
- peer: IDENTITY_PEER_ID,
414
- source: BlockInputSource.api,
415
- });
416
- }
390
+ chain.processBlock(blockForImport, opts).catch((e) => {
391
+ if (e instanceof BlockError) {
392
+ switch (e.type.code) {
393
+ case BlockErrorCode.ALREADY_KNOWN:
394
+ // Block was imported while publishing, e.g. received via gossip
395
+ chain.logger.debug("Ignoring already-known block during publishing", valLogMeta);
396
+ return;
397
+ case BlockErrorCode.PARENT_BLOCK_UNKNOWN:
398
+ case BlockErrorCode.PARENT_PAYLOAD_UNKNOWN:
399
+ chain.emitter.emit(ChainEvent.blockUnknownParent, {
400
+ blockInput: blockForImport,
401
+ peer: IDENTITY_PEER_ID,
402
+ source: BlockInputSource.api,
403
+ });
417
404
  }
418
- throw e;
419
- }),
405
+ }
406
+ throw e;
407
+ }),
420
408
  ];
421
409
  const sentPeersArr = await promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
422
410
  publishPromises
@@ -804,11 +792,6 @@ export function getBeaconBlockApi(
804
792
  };
805
793
  try {
806
794
  switch (broadcastValidation) {
807
- case routes.beacon.BroadcastValidation.none: {
808
- chain.logger.debug("Skipping broadcast validation of execution payload envelope", valLogMeta);
809
- break;
810
- }
811
-
812
795
  case routes.beacon.BroadcastValidation.gossip: {
813
796
  await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
814
797
  break;
@@ -847,11 +830,7 @@ export function getBeaconBlockApi(
847
830
  }
848
831
 
849
832
  default: {
850
- const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
851
- if (chain.opts.broadcastValidationStrictness === "error") {
852
- throw Error(message);
853
- }
854
- chain.logger.warn(message, valLogMeta);
833
+ throw new ApiError(400, `Invalid broadcast validation level ${broadcastValidation}`);
855
834
  }
856
835
  }
857
836
  } catch (error) {
@@ -1003,9 +982,13 @@ export function getBeaconBlockApi(
1003
982
  () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope),
1004
983
  // Publish all data column sidecars
1005
984
  ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
1006
- // Import execution payload. Signature is verified during broadcast validation, an already
1007
- // known envelope was verified by its original source and `none` deliberately skips validation
1008
- () => chain.processExecutionPayload(payloadInput, {validSignature: true}),
985
+ // Import execution payload
986
+ () =>
987
+ chain.processExecutionPayload(payloadInput, {
988
+ // Signature is verified during broadcast validation only for the submitted envelope,
989
+ // an envelope cached by another source is verified at import
990
+ validSignature: payloadInput.getPayloadEnvelope() === signedExecutionPayloadEnvelope,
991
+ }),
1009
992
  ];
1010
993
 
1011
994
  const publishPromise = promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
@@ -1109,7 +1092,7 @@ export function getBeaconBlockApi(
1109
1092
  },
1110
1093
 
1111
1094
  async getSignedExecutionPayloadEnvelope({blockId}, context) {
1112
- const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId);
1095
+ const {block, finalized} = await getBlockResponse(chain, blockId);
1113
1096
  const slot = block.message.slot;
1114
1097
  const fork = config.getForkName(slot);
1115
1098
 
@@ -1123,6 +1106,13 @@ export function getBeaconBlockApi(
1123
1106
  const blockRoot = config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message);
1124
1107
  const blockRootHex = toRootHex(blockRoot);
1125
1108
 
1109
+ // The block's own flag is the status of the payload it builds on, the envelope's is that of its
1110
+ // FULL variant. An unfinalized block without one has not imported its envelope yet.
1111
+ const payload = finalized ? null : chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL);
1112
+ if (!finalized && payload === null) {
1113
+ throw new ApiError(404, `Execution payload envelope not found for slot=${slot}, blockRoot=${blockRootHex}`);
1114
+ }
1115
+
1126
1116
  let data: Uint8Array | gloas.SignedExecutionPayloadEnvelope | null;
1127
1117
  try {
1128
1118
  data = context?.returnBytes
@@ -1147,7 +1137,7 @@ export function getBeaconBlockApi(
1147
1137
 
1148
1138
  return {
1149
1139
  data,
1150
- meta: {executionOptimistic, finalized, version: fork},
1140
+ meta: {executionOptimistic: payload !== null && isOptimisticBlock(payload), finalized, version: fork},
1151
1141
  };
1152
1142
  },
1153
1143
 
@@ -17,6 +17,7 @@ import {
17
17
  PayloadAttestationError,
18
18
  PayloadAttestationErrorCode,
19
19
  SyncCommitteeError,
20
+ SyncCommitteeErrorCode,
20
21
  } from "../../../../chain/errors/index.js";
21
22
  import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
22
23
  import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
@@ -340,7 +341,10 @@ export function getBeaconPoolApi({
340
341
  const synCommittee = state.getIndexedSyncCommittee(signature.slot);
341
342
  const indexesInCommittee = synCommittee.validatorIndexMap.get(signature.validatorIndex);
342
343
  if (indexesInCommittee === undefined || indexesInCommittee.length === 0) {
343
- return; // Not a sync committee member
344
+ throw new SyncCommitteeError(GossipAction.REJECT, {
345
+ code: SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE,
346
+ validatorIndex: signature.validatorIndex,
347
+ });
344
348
  }
345
349
 
346
350
  // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node.
@@ -1,6 +1,6 @@
1
- import {type PubkeyCache} from "@chainsafe/lodestar-z/pubkeys";
2
1
  import {routes} from "@lodestar/api";
3
2
  import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice";
3
+ import {type PubkeyCache} from "@lodestar/native/pubkeys";
4
4
  import {GENESIS_SLOT} from "@lodestar/params";
5
5
  import {IBeaconStateView, IBeaconStateViewGloas} from "@lodestar/state-transition";
6
6
  import {
@@ -1756,7 +1756,10 @@ export function getValidatorApi(
1756
1756
  }
1757
1757
  }
1758
1758
 
1759
- const dependentRoot = fromHex(state.getShufflingDecisionRoot(epoch));
1759
+ // The fork epoch PTC is computed at the fork from effective balances updated at the end of the
1760
+ // last pre-fork epoch, so its dependent root is one epoch later than for other epochs
1761
+ const dependentRootEpoch = epoch === config.GLOAS_FORK_EPOCH ? epoch + 1 : epoch;
1762
+ const dependentRoot = fromHex(state.getShufflingDecisionRoot(dependentRootEpoch));
1760
1763
 
1761
1764
  return {
1762
1765
  data: duties,
@@ -1,10 +1,9 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {computeEpochAtSlot} from "@lodestar/state-transition";
3
- import {Logger} from "@lodestar/utils";
3
+ import {Logger, pruneSetToMax} from "@lodestar/utils";
4
4
  import {BLOB_AND_PROOF_V2_RPC_BYTES} from "../execution/engine/types.js";
5
5
  import {IExecutionEngine} from "../execution/index.js";
6
6
  import {Metrics} from "../metrics/metrics.js";
7
- import {callInNextEventLoop} from "../util/eventLoop.js";
8
7
  import {
9
8
  DataColumnEngineResult,
10
9
  getBlobSidecarsFromExecution,
@@ -14,6 +13,11 @@ import {IBlockInput, isBlockInputBlobs} from "./blocks/blockInput/index.js";
14
13
  import {PayloadEnvelopeInput} from "./blocks/payloadEnvelopeInput/index.js";
15
14
  import {ChainEventEmitter} from "./emitter.js";
16
15
 
16
+ /** A `null` answer only changes if the EL receives the transactions later, so retries are few and spaced out */
17
+ export const MAX_GET_BLOBS_ATTEMPTS = 3;
18
+ export const GET_BLOBS_RETRY_INTERVAL_MS = 1000;
19
+ const MAX_TRACKED_BLOCK_ROOTS = 64;
20
+
17
21
  export type GetBlobsTrackerInit = {
18
22
  logger: Logger;
19
23
  executionEngine: IExecutionEngine;
@@ -32,6 +36,8 @@ export class GetBlobsTracker {
32
36
  metrics: Metrics | null;
33
37
  config: ChainForkConfig;
34
38
  activeReconstructions = new Set<string>();
39
+ failedAttempts = new Map<string, {count: number; lastAttemptMs: number; retryTimer?: NodeJS.Timeout}>();
40
+ pendingTriggers = new Set<string>();
35
41
  // Preallocate buffers for getBlobsV2 RPC calls
36
42
  // See https://github.com/ChainSafe/lodestar/pull/8282 for context
37
43
  blobsAndProofsBuffers: {buffers: Uint8Array[]; inUse: boolean}[] = [];
@@ -46,23 +52,55 @@ export class GetBlobsTracker {
46
52
 
47
53
  triggerGetBlobs(input: IBlockInput | PayloadEnvelopeInput): void {
48
54
  if (this.activeReconstructions.has(input.blockRootHex)) {
55
+ this.pendingTriggers.add(input.blockRootHex);
49
56
  return;
50
57
  }
51
58
 
59
+ const failed = this.failedAttempts.get(input.blockRootHex);
60
+ if (failed) {
61
+ if (failed.count >= MAX_GET_BLOBS_ATTEMPTS) {
62
+ return;
63
+ }
64
+ const waitMs = GET_BLOBS_RETRY_INTERVAL_MS - (Date.now() - failed.lastAttemptMs);
65
+ if (waitMs > 0) {
66
+ // Columns tend to arrive in one burst, so keep a single trailing retry instead of relying on a later trigger
67
+ failed.retryTimer ??= setTimeout(() => {
68
+ failed.retryTimer = undefined;
69
+ if (this.failedAttempts.get(input.blockRootHex) === failed && !input.hasAllData()) {
70
+ this.triggerGetBlobs(input);
71
+ }
72
+ }, waitMs).unref();
73
+ return;
74
+ }
75
+ }
76
+
77
+ // The request is sent right away, before block processing issues newPayload: ELs that drop a
78
+ // transaction's blobs once the payload is validated can only answer a request that gets there first
52
79
  if (!(input instanceof PayloadEnvelopeInput) && isBlockInputBlobs(input)) {
53
80
  // there is not preallocation for blob sidecars like there is for columns sidecars so no need to
54
81
  // store the index for the preallocated buffers
55
82
  this.activeReconstructions.add(input.blockRootHex);
56
- callInNextEventLoop(() => {
57
- const logCtx = {slot: input.slot, root: input.blockRootHex};
58
- this.logger.verbose("Trigger getBlobsV1 for block", logCtx);
59
- getBlobSidecarsFromExecution(this.config, this.executionEngine, this.metrics, this.emitter, input).finally(
60
- () => {
61
- this.logger.verbose("Completed getBlobsV1 for block", logCtx);
62
- this.activeReconstructions.delete(input.blockRootHex);
83
+ const logCtx = {slot: input.slot, root: input.blockRootHex};
84
+ this.logger.verbose("Trigger getBlobsV1 for block", logCtx);
85
+ let requestFailed = false;
86
+ getBlobSidecarsFromExecution(this.config, this.executionEngine, this.metrics, this.emitter, input)
87
+ .then(() => {
88
+ // EL errors and misses are handled inside the helper, blobs still missing means the attempt failed
89
+ if (!input.hasAllData()) {
90
+ requestFailed = true;
91
+ this.recordFailedAttempt(input.blockRootHex);
63
92
  }
64
- );
65
- });
93
+ })
94
+ .catch((error) => {
95
+ this.logger.debug("Error during getBlobsV1 for block", logCtx, error as Error);
96
+ requestFailed = true;
97
+ this.recordFailedAttempt(input.blockRootHex);
98
+ })
99
+ .finally(() => {
100
+ this.logger.verbose("Completed getBlobsV1 for block", logCtx);
101
+ this.activeReconstructions.delete(input.blockRootHex);
102
+ this.retryPendingTrigger(input, requestFailed);
103
+ });
66
104
 
67
105
  return;
68
106
  }
@@ -83,34 +121,55 @@ export class GetBlobsTracker {
83
121
  }
84
122
  timer?.();
85
123
 
86
- // We don't care about the outcome of this call,
87
- // just that it has been triggered for this block root.
88
124
  this.activeReconstructions.add(input.blockRootHex);
89
125
  this.blobsAndProofsBuffers[freeIndex].inUse = true;
90
- callInNextEventLoop(() => {
91
- const logCtx = {slot: input.slot, root: input.blockRootHex};
92
- this.logger.verbose("Trigger getBlobsV2 for block", logCtx);
93
- getDataColumnSidecarsFromExecution(
94
- this.config,
95
- this.executionEngine,
96
- this.emitter,
97
- input,
98
- this.metrics,
99
- this.blobsAndProofsBuffers[freeIndex].buffers
100
- )
101
- .then((result) => {
102
- this.logger.debug("getBlobsV2 result for block", {...logCtx, result});
103
- this.metrics?.dataColumns.dataColumnEngineResult.inc({result});
104
- })
105
- .catch((error) => {
106
- this.logger.debug("Error during getBlobsV2 for block", logCtx, error as Error);
107
- this.metrics?.dataColumns.dataColumnEngineResult.inc({result: DataColumnEngineResult.Failed});
108
- })
109
- .finally(() => {
110
- this.logger.verbose("Completed getBlobsV2 for block", logCtx);
111
- this.activeReconstructions.delete(input.blockRootHex);
112
- this.blobsAndProofsBuffers[freeIndex].inUse = false;
113
- });
114
- });
126
+ const logCtx = {slot: input.slot, root: input.blockRootHex};
127
+ this.logger.verbose("Trigger getBlobsV2 for block", logCtx);
128
+ let requestFailed = false;
129
+ getDataColumnSidecarsFromExecution(
130
+ this.config,
131
+ this.executionEngine,
132
+ this.emitter,
133
+ input,
134
+ this.metrics,
135
+ this.blobsAndProofsBuffers[freeIndex].buffers
136
+ )
137
+ .then((result) => {
138
+ this.logger.debug("getBlobsV2 result for block", {...logCtx, result});
139
+ this.metrics?.dataColumns.dataColumnEngineResult.inc({result});
140
+ if (result === DataColumnEngineResult.NullResponse) {
141
+ requestFailed = true;
142
+ this.recordFailedAttempt(input.blockRootHex);
143
+ }
144
+ })
145
+ .catch((error) => {
146
+ this.logger.debug("Error during getBlobsV2 for block", logCtx, error as Error);
147
+ this.metrics?.dataColumns.dataColumnEngineResult.inc({result: DataColumnEngineResult.Failed});
148
+ requestFailed = true;
149
+ this.recordFailedAttempt(input.blockRootHex);
150
+ })
151
+ .finally(() => {
152
+ this.logger.verbose("Completed getBlobsV2 for block", logCtx);
153
+ this.activeReconstructions.delete(input.blockRootHex);
154
+ this.blobsAndProofsBuffers[freeIndex].inUse = false;
155
+ this.retryPendingTrigger(input, requestFailed);
156
+ });
157
+ }
158
+
159
+ private retryPendingTrigger(input: IBlockInput | PayloadEnvelopeInput, requestFailed: boolean): void {
160
+ // Triggers dropped while the request was in flight re-enter once it failed, the attempt that was just
161
+ // recorded makes this arm the trailing retry instead of sending another request right away.
162
+ // Post-gloas only the bid triggers a request, so every failed attempt re-enters to arm its own retry
163
+ const hadPendingTrigger = this.pendingTriggers.delete(input.blockRootHex);
164
+ if (requestFailed && (hadPendingTrigger || input instanceof PayloadEnvelopeInput)) {
165
+ this.triggerGetBlobs(input);
166
+ }
167
+ }
168
+
169
+ private recordFailedAttempt(blockRootHex: string): void {
170
+ const failed = this.failedAttempts.get(blockRootHex);
171
+ clearTimeout(failed?.retryTimer);
172
+ this.failedAttempts.set(blockRootHex, {count: (failed?.count ?? 0) + 1, lastAttemptMs: Date.now()});
173
+ pruneSetToMax(this.failedAttempts, MAX_TRACKED_BLOCK_ROOTS);
115
174
  }
116
175
  }
@@ -197,6 +197,7 @@ export class ArchiveStore {
197
197
  //
198
198
  // `jobQueue.push()` can throw synchronously (queue aborted / full) or reject asynchronously;
199
199
  // awaiting it inside the deferred callback funnels both failure modes into the one catch.
200
+ // biome-ignore lint/nursery/noMisusedPromises: the callback catches all errors itself
200
201
  callInNextEventLoop(async () => {
201
202
  try {
202
203
  await this.jobQueue.push(finalized);
@@ -56,6 +56,17 @@ async function persistOrphanedBlock(
56
56
  await writeIfNotExist(filepath, bytes);
57
57
  }
58
58
 
59
+ /**
60
+ * Oldest epoch for which data column sidecars are kept
61
+ */
62
+ export function getDataColumnSidecarsMinEpoch(
63
+ config: ChainForkConfig,
64
+ currentEpoch: Epoch,
65
+ archiveDataEpochs?: number
66
+ ): Epoch {
67
+ return Math.min(getBlobDataRetentionStartEpoch(config, currentEpoch), currentEpoch - (archiveDataEpochs ?? 0));
68
+ }
69
+
59
70
  /**
60
71
  * Archives finalized blocks from active bucket to archive bucket.
61
72
  *
@@ -260,10 +271,7 @@ export async function archiveBlocks(
260
271
  // Keep only `[min(compute_blob_data_retention_start_epoch(current_epoch), current_epoch - archiveDataEpochs)]`
261
272
  if (finalizedPostFulu) {
262
273
  if (archiveDataEpochs !== Infinity) {
263
- const dataColumnSidecarsMinEpoch = Math.min(
264
- getBlobDataRetentionStartEpoch(config, currentEpoch),
265
- currentEpoch - (archiveDataEpochs ?? 0)
266
- );
274
+ const dataColumnSidecarsMinEpoch = getDataColumnSidecarsMinEpoch(config, currentEpoch, archiveDataEpochs);
267
275
  if (dataColumnSidecarsMinEpoch >= config.FULU_FORK_EPOCH) {
268
276
  const columnsPruneSlot = computeStartSlotAtEpoch(dataColumnSidecarsMinEpoch);
269
277
  const prunedColumnSlots = await db.dataColumns.pruneBefore(columnsPruneSlot);
@@ -21,7 +21,7 @@ import {
21
21
  IBeaconStateView,
22
22
  RootCache,
23
23
  computeEpochAtSlot,
24
- computeTimeAtSlot,
24
+ computeTimeAtSlotMs,
25
25
  getBeaconCommittees,
26
26
  isStatePostAltair,
27
27
  isStatePostBellatrix,
@@ -105,8 +105,9 @@ export async function importBlock(
105
105
  const currentEpoch = computeEpochAtSlot(currentSlot);
106
106
  const blockEpoch = computeEpochAtSlot(blockSlot);
107
107
  const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
108
- const receiveDelaySec =
109
- fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime);
108
+ const receiveDelayMs =
109
+ Math.round(fullyVerifiedBlock.seenTimestampSec * 1000) -
110
+ computeTimeAtSlotMs(this.config, blockSlot, postState.genesisTime * 1000);
110
111
  const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
111
112
  const fork = this.config.getForkSeq(blockSlot);
112
113
 
@@ -148,12 +149,12 @@ export async function importBlock(
148
149
  executionStatus = parentBlock.executionStatus;
149
150
  }
150
151
 
151
- const importDelaySec = this.clock.secFromSlot(blockSlot);
152
+ const importDelayMs = this.clock.msFromSlot(blockSlot);
152
153
  const blockSummary = this.forkChoice.onBlock(
153
154
  block.message,
154
155
  postState,
155
- receiveDelaySec,
156
- importDelaySec,
156
+ receiveDelayMs,
157
+ importDelayMs,
157
158
  currentSlot,
158
159
  executionStatus,
159
160
  dataAvailabilityStatus
@@ -417,7 +418,7 @@ export async function importBlock(
417
418
  // 4) Block meets the criteria of being re-orged out (this is also checked inside the function)
418
419
  const result = this.forkChoice.shouldOverrideForkChoiceUpdate(
419
420
  blockSummary,
420
- this.clock.secFromSlot(currentSlot),
421
+ this.clock.msFromSlot(currentSlot),
421
422
  currentSlot
422
423
  );
423
424
  shouldOverrideFcu = result.shouldOverrideFcu;
@@ -37,6 +37,7 @@ export enum PayloadErrorCode {
37
37
  MISS_BLOCK_STATE = "PAYLOAD_ERROR_MISS_BLOCK_STATE",
38
38
  ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
39
39
  INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
40
+ DATA_UNAVAILABLE = "PAYLOAD_ERROR_DATA_UNAVAILABLE",
40
41
  }
41
42
 
42
43
  export type PayloadErrorType =
@@ -64,6 +65,9 @@ export type PayloadErrorType =
64
65
  }
65
66
  | {
66
67
  code: PayloadErrorCode.INVALID_SIGNATURE;
68
+ }
69
+ | {
70
+ code: PayloadErrorCode.DATA_UNAVAILABLE;
67
71
  };
68
72
 
69
73
  export class PayloadError extends Error {
@@ -329,7 +333,13 @@ export async function importExecutionPayload(
329
333
 
330
334
  // 7. Queue notifyForkchoiceUpdate to engine api
331
335
  const head = this.forkChoice.getHead();
332
- if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) {
336
+ if (
337
+ !this.opts.disableImportExecutionFcU &&
338
+ blockRootHex === head.blockRoot &&
339
+ // Skip a payload that does not satisfy the inclusion list constraints, fork choice keeps the EMPTY
340
+ // variant as head and the next prepare fcU keeps the engine on the parent payload
341
+ this.forkChoice.isPayloadInclusionListSatisfied(blockRootHex)
342
+ ) {
333
343
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
334
344
  const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
335
345
  this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => {
@@ -383,6 +393,6 @@ export async function processExecutionPayload(
383
393
  signal: AbortSignal,
384
394
  opts: ImportPayloadOpts = {}
385
395
  ): Promise<void> {
386
- const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability([payloadInput], signal);
396
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(this.config, [payloadInput], signal);
387
397
  await importExecutionPayload.call(this, payloadInput, dataAvailabilityStatuses[0], opts);
388
398
  }
@@ -147,7 +147,7 @@ export async function processBlocks(
147
147
  proposerBalanceDelta: proposerBalanceDeltas[i],
148
148
  indexedAttestations: indexedAttestationsByBlock[i],
149
149
  // TODO: Make this param mandatory and capture in gossip
150
- seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
150
+ seenTimestampSec: opts.seenTimestampSec ?? Date.now() / 1000,
151
151
  });
152
152
  }
153
153
 
@@ -292,7 +292,11 @@ async function importPayloadEnvelopesOfKnownBlocks(
292
292
  return result;
293
293
  }
294
294
 
295
- const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(payloadInputs, new AbortController().signal);
295
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(
296
+ this.config,
297
+ payloadInputs,
298
+ new AbortController().signal
299
+ );
296
300
  for (let i = 0; i < payloadInputs.length; i++) {
297
301
  this.logger.debug("Importing payload envelope of known block", {
298
302
  slot: payloadInputs[i].slot,
@@ -13,11 +13,11 @@ const QUEUE_MAX_LENGTH = 16;
13
13
  *
14
14
  * Jobs are enqueued only on envelope arrival (gossip or API). The envelope may reach us before
15
15
  * the sampled data columns; importExecutionPayload awaits `verifyPayloadsDataAvailability`
16
- * internally, so a queued job can pend for up to `PAYLOAD_DATA_AVAILABILITY_TIMEOUT` while
17
- * waiting for columns. Duplicate triggers for the same payloadInput are deduped by sharing the
18
- * in-flight import promise: every caller observes the real import outcome. Resolving duplicates
19
- * early instead would let BlockInputSync mark a still-pending payload as processed, re-queue it,
20
- * and spin re-processing the same root until OOM while the import is blocked (e.g. EL syncing).
16
+ * internally, so a queued job can pend for up to one slot while waiting for columns. Duplicate
17
+ * triggers for the same payloadInput are deduped by sharing the in-flight import promise: every
18
+ * caller observes the real import outcome. Resolving duplicates early instead would let
19
+ * BlockInputSync mark a still-pending payload as processed, re-queue it, and spin re-processing
20
+ * the same root until OOM while the import is blocked (e.g. EL syncing).
21
21
  */
22
22
  export class PayloadEnvelopeProcessor {
23
23
  readonly jobQueue: JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>;
@@ -35,7 +35,7 @@ export class PayloadEnvelopeProcessor {
35
35
 
36
36
  async processPayloadEnvelopeJob(payloadInput: PayloadEnvelopeInput, opts: ImportPayloadOpts = {}): Promise<void> {
37
37
  const existing = this.imports.get(payloadInput);
38
- if (existing) {
38
+ if (existing !== undefined) {
39
39
  return existing;
40
40
  }
41
41
 
@@ -43,7 +43,7 @@ export class PayloadEnvelopeProcessor {
43
43
 
44
44
  // Re-check after await, as another call may have queued this payload.
45
45
  const queued = this.imports.get(payloadInput);
46
- if (queued) {
46
+ if (queued !== undefined) {
47
47
  return queued;
48
48
  }
49
49
 
@@ -143,6 +143,7 @@ export async function verifyBlocksInEpoch(
143
143
  ? Array.from(payloadEnvelopes.values()).filter((payloadInput) => payloadInput.hasPayloadEnvelope())
144
144
  : [];
145
145
  const {dataAvailabilityStatuses, availableTime} = await verifyPayloadsDataAvailability(
146
+ this.config,
146
147
  payloadInputsForDa,
147
148
  abortController.signal
148
149
  );
@@ -1,4 +1,6 @@
1
1
  import {DataAvailabilityStatus} from "@lodestar/state-transition";
2
+ import {TimeoutError} from "@lodestar/utils";
3
+ import {BlockError, BlockErrorCode} from "../errors/index.js";
2
4
  import {DAData, DAType, IBlockInput} from "./blockInput/index.js";
3
5
 
4
6
  // we can now wait for full 12 seconds because unavailable block sync will try pulling
@@ -22,7 +24,14 @@ export async function verifyBlocksDataAvailability(
22
24
  for (const blockInput of blocks) {
23
25
  // block verification is triggered on a verified gossip block so we only need to wait for all data
24
26
  if (!blockInput.hasAllData()) {
25
- promises.push(blockInput.waitForAllData(BLOB_AVAILABILITY_TIMEOUT, signal));
27
+ promises.push(
28
+ blockInput.waitForAllData(BLOB_AVAILABILITY_TIMEOUT, signal).catch((e) => {
29
+ if (e instanceof TimeoutError) {
30
+ throw new BlockError(blockInput.getBlock(), {code: BlockErrorCode.DATA_UNAVAILABLE});
31
+ }
32
+ throw e;
33
+ })
34
+ );
26
35
  }
27
36
  }
28
37
  await Promise.all(promises);
@@ -1,18 +1,18 @@
1
+ import {ChainForkConfig} from "@lodestar/config";
1
2
  import {DataAvailabilityStatus} from "@lodestar/state-transition";
2
3
  import {gloas} from "@lodestar/types";
4
+ import {TimeoutError} from "@lodestar/utils";
3
5
  import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
4
-
5
- // we can now wait for full 12 seconds because sync and reconstruction will try pulling
6
- // the data columns from the network anyway while the envelope is being processed
7
- export const PAYLOAD_DATA_AVAILABILITY_TIMEOUT = 12_000;
6
+ import {PayloadError, PayloadErrorCode} from "./importExecutionPayload.js";
8
7
 
9
8
  /**
10
9
  * Verifies that all payload envelope inputs have their data columns available.
11
- * - Waits a max of PAYLOAD_DATA_AVAILABILITY_TIMEOUT for all data to be available
10
+ * - Waits a max of one slot duration of the payload's fork for all data to be available
12
11
  * - Returns the time at which all data was available
13
12
  * - Returns the data availability status for each payload input
14
13
  */
15
14
  export async function verifyPayloadsDataAvailability(
15
+ config: ChainForkConfig,
16
16
  payloadInputs: PayloadEnvelopeInput[],
17
17
  signal: AbortSignal
18
18
  ): Promise<{
@@ -22,7 +22,17 @@ export async function verifyPayloadsDataAvailability(
22
22
  const promises: Promise<gloas.DataColumnSidecar[]>[] = [];
23
23
  for (const payloadInput of payloadInputs) {
24
24
  if (!payloadInput.hasAllData()) {
25
- promises.push(payloadInput.waitForAllData(PAYLOAD_DATA_AVAILABILITY_TIMEOUT, signal));
25
+ // Sync and reconstruction keep pulling the data columns from the network while we wait. The wait is
26
+ // capped at one slot because the payload queue is serial: a longer wait lets consecutive unavailable
27
+ // payloads push every later payload import further behind with each slot.
28
+ promises.push(
29
+ payloadInput.waitForAllData(config.getSlotDurationMs(payloadInput.forkName), signal).catch((e) => {
30
+ if (e instanceof TimeoutError) {
31
+ throw new PayloadError(payloadInput, {code: PayloadErrorCode.DATA_UNAVAILABLE});
32
+ }
33
+ throw e;
34
+ })
35
+ );
26
36
  }
27
37
  }
28
38
  await Promise.all(promises);