@lodestar/beacon-node 1.51.0-dev.ec493d509d → 1.51.0-dev.f33a955402

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 (79) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +11 -22
  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 +4 -1
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/GetBlobsTracker.d.ts +11 -0
  8. package/lib/chain/GetBlobsTracker.d.ts.map +1 -1
  9. package/lib/chain/GetBlobsTracker.js +83 -27
  10. package/lib/chain/GetBlobsTracker.js.map +1 -1
  11. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +4 -0
  12. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  13. package/lib/chain/archiveStore/utils/archiveBlocks.js +7 -1
  14. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  15. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -1
  16. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  17. package/lib/chain/blocks/importExecutionPayload.js +2 -1
  18. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  19. package/lib/chain/blocks/index.js +2 -2
  20. package/lib/chain/blocks/index.js.map +1 -1
  21. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -5
  22. package/lib/chain/blocks/payloadEnvelopeProcessor.js +5 -5
  23. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  24. package/lib/chain/blocks/verifyBlock.js +1 -1
  25. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  26. package/lib/chain/blocks/verifyBlocksDataAvailability.d.ts.map +1 -1
  27. package/lib/chain/blocks/verifyBlocksDataAvailability.js +8 -1
  28. package/lib/chain/blocks/verifyBlocksDataAvailability.js.map +1 -1
  29. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts +3 -3
  30. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  31. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +13 -6
  32. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  33. package/lib/chain/errors/blockError.d.ts +1 -1
  34. package/lib/chain/errors/blockError.d.ts.map +1 -1
  35. package/lib/chain/errors/blockError.js +1 -1
  36. package/lib/chain/errors/blockError.js.map +1 -1
  37. package/lib/chain/options.d.ts +0 -1
  38. package/lib/chain/options.d.ts.map +1 -1
  39. package/lib/chain/options.js +0 -1
  40. package/lib/chain/options.js.map +1 -1
  41. package/lib/db/beacon.d.ts +7 -0
  42. package/lib/db/beacon.d.ts.map +1 -1
  43. package/lib/db/beacon.js +32 -0
  44. package/lib/db/beacon.js.map +1 -1
  45. package/lib/db/dataColumnStore.d.ts +1 -1
  46. package/lib/db/dataColumnStore.d.ts.map +1 -1
  47. package/lib/db/dataColumnStore.js +1 -4
  48. package/lib/db/dataColumnStore.js.map +1 -1
  49. package/lib/db/interface.d.ts +1 -0
  50. package/lib/db/interface.d.ts.map +1 -1
  51. package/lib/db/repositories/dataColumnSidecarArchive.d.ts +6 -0
  52. package/lib/db/repositories/dataColumnSidecarArchive.d.ts.map +1 -1
  53. package/lib/db/repositories/dataColumnSidecarArchive.js +19 -0
  54. package/lib/db/repositories/dataColumnSidecarArchive.js.map +1 -1
  55. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  56. package/lib/network/processor/gossipHandlers.js +6 -3
  57. package/lib/network/processor/gossipHandlers.js.map +1 -1
  58. package/lib/node/nodejs.d.ts.map +1 -1
  59. package/lib/node/nodejs.js +9 -1
  60. package/lib/node/nodejs.js.map +1 -1
  61. package/package.json +15 -15
  62. package/src/api/impl/beacon/blocks/index.ts +12 -24
  63. package/src/api/impl/validator/index.ts +4 -1
  64. package/src/chain/GetBlobsTracker.ts +95 -38
  65. package/src/chain/archiveStore/utils/archiveBlocks.ts +12 -4
  66. package/src/chain/blocks/importExecutionPayload.ts +5 -1
  67. package/src/chain/blocks/index.ts +6 -2
  68. package/src/chain/blocks/payloadEnvelopeProcessor.ts +5 -5
  69. package/src/chain/blocks/verifyBlock.ts +1 -0
  70. package/src/chain/blocks/verifyBlocksDataAvailability.ts +10 -1
  71. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +16 -6
  72. package/src/chain/errors/blockError.ts +1 -1
  73. package/src/chain/options.ts +0 -2
  74. package/src/db/beacon.ts +39 -0
  75. package/src/db/dataColumnStore.ts +2 -4
  76. package/src/db/interface.ts +2 -0
  77. package/src/db/repositories/dataColumnSidecarArchive.ts +20 -0
  78. package/src/network/processor/gossipHandlers.ts +6 -3
  79. package/src/node/nodejs.ts +18 -1
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.51.0-dev.ec493d509d",
14
+ "version": "1.51.0-dev.f33a955402",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -127,17 +127,17 @@
127
127
  "@libp2p/prometheus-metrics": "^5.0.31",
128
128
  "@libp2p/tcp": "^11.0.30",
129
129
  "@libp2p/utils": "^7.4.3",
130
- "@lodestar/api": "^1.51.0-dev.ec493d509d",
131
- "@lodestar/config": "^1.51.0-dev.ec493d509d",
132
- "@lodestar/db": "^1.51.0-dev.ec493d509d",
133
- "@lodestar/fork-choice": "^1.51.0-dev.ec493d509d",
134
- "@lodestar/logger": "^1.51.0-dev.ec493d509d",
135
- "@lodestar/params": "^1.51.0-dev.ec493d509d",
136
- "@lodestar/reqresp": "^1.51.0-dev.ec493d509d",
137
- "@lodestar/state-transition": "^1.51.0-dev.ec493d509d",
138
- "@lodestar/types": "^1.51.0-dev.ec493d509d",
139
- "@lodestar/utils": "^1.51.0-dev.ec493d509d",
140
- "@lodestar/validator": "^1.51.0-dev.ec493d509d",
130
+ "@lodestar/api": "^1.51.0-dev.f33a955402",
131
+ "@lodestar/config": "^1.51.0-dev.f33a955402",
132
+ "@lodestar/db": "^1.51.0-dev.f33a955402",
133
+ "@lodestar/fork-choice": "^1.51.0-dev.f33a955402",
134
+ "@lodestar/logger": "^1.51.0-dev.f33a955402",
135
+ "@lodestar/params": "^1.51.0-dev.f33a955402",
136
+ "@lodestar/reqresp": "^1.51.0-dev.f33a955402",
137
+ "@lodestar/state-transition": "^1.51.0-dev.f33a955402",
138
+ "@lodestar/types": "^1.51.0-dev.f33a955402",
139
+ "@lodestar/utils": "^1.51.0-dev.f33a955402",
140
+ "@lodestar/validator": "^1.51.0-dev.f33a955402",
141
141
  "@multiformats/multiaddr": "^13.0.1",
142
142
  "datastore-core": "^12.0.1",
143
143
  "datastore-level": "^13.0.1",
@@ -159,8 +159,8 @@
159
159
  "@ethereumjs/rlp": "^4.0.1",
160
160
  "@libp2p/interface-internal": "^3.2.0",
161
161
  "@libp2p/logger": "^6.2.15",
162
- "@lodestar/spec-test-util": "^1.51.0-dev.ec493d509d",
163
- "@lodestar/test-utils": "^1.51.0-dev.ec493d509d",
162
+ "@lodestar/spec-test-util": "^1.51.0-dev.f33a955402",
163
+ "@lodestar/test-utils": "^1.51.0-dev.f33a955402",
164
164
  "@types/js-yaml": "^4.0.5",
165
165
  "@types/qs": "^6.9.7",
166
166
  "@types/tmp": "^0.2.3",
@@ -175,5 +175,5 @@
175
175
  "beacon",
176
176
  "blockchain"
177
177
  ],
178
- "gitHead": "241f1d27f2082f5d359bfd9d38956187a4566d1b"
178
+ "gitHead": "175cb2656b8e95668eac3b1fa4e2874455f6c58f"
179
179
  }
@@ -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
 
@@ -804,11 +794,6 @@ export function getBeaconBlockApi(
804
794
  };
805
795
  try {
806
796
  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
797
  case routes.beacon.BroadcastValidation.gossip: {
813
798
  await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
814
799
  break;
@@ -847,11 +832,7 @@ export function getBeaconBlockApi(
847
832
  }
848
833
 
849
834
  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);
835
+ throw new ApiError(400, `Invalid broadcast validation level ${broadcastValidation}`);
855
836
  }
856
837
  }
857
838
  } catch (error) {
@@ -1004,7 +985,7 @@ export function getBeaconBlockApi(
1004
985
  // Publish all data column sidecars
1005
986
  ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
1006
987
  // 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
988
+ // known envelope was verified by its original source
1008
989
  () => chain.processExecutionPayload(payloadInput, {validSignature: true}),
1009
990
  ];
1010
991
 
@@ -1109,7 +1090,7 @@ export function getBeaconBlockApi(
1109
1090
  },
1110
1091
 
1111
1092
  async getSignedExecutionPayloadEnvelope({blockId}, context) {
1112
- const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId);
1093
+ const {block, finalized} = await getBlockResponse(chain, blockId);
1113
1094
  const slot = block.message.slot;
1114
1095
  const fork = config.getForkName(slot);
1115
1096
 
@@ -1123,6 +1104,13 @@ export function getBeaconBlockApi(
1123
1104
  const blockRoot = config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message);
1124
1105
  const blockRootHex = toRootHex(blockRoot);
1125
1106
 
1107
+ // The block's own flag is the status of the payload it builds on, the envelope's is that of its
1108
+ // FULL variant. An unfinalized block without one has not imported its envelope yet.
1109
+ const payload = finalized ? null : chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL);
1110
+ if (!finalized && payload === null) {
1111
+ throw new ApiError(404, `Execution payload envelope not found for slot=${slot}, blockRoot=${blockRootHex}`);
1112
+ }
1113
+
1126
1114
  let data: Uint8Array | gloas.SignedExecutionPayloadEnvelope | null;
1127
1115
  try {
1128
1116
  data = context?.returnBytes
@@ -1147,7 +1135,7 @@ export function getBeaconBlockApi(
1147
1135
 
1148
1136
  return {
1149
1137
  data,
1150
- meta: {executionOptimistic, finalized, version: fork},
1138
+ meta: {executionOptimistic: payload !== null && isOptimisticBlock(payload), finalized, version: fork},
1151
1139
  };
1152
1140
  },
1153
1141
 
@@ -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,53 @@ 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
+ if (this.pendingTriggers.delete(input.blockRootHex) && requestFailed) {
163
+ this.triggerGetBlobs(input);
164
+ }
165
+ }
166
+
167
+ private recordFailedAttempt(blockRootHex: string): void {
168
+ const failed = this.failedAttempts.get(blockRootHex);
169
+ clearTimeout(failed?.retryTimer);
170
+ this.failedAttempts.set(blockRootHex, {count: (failed?.count ?? 0) + 1, lastAttemptMs: Date.now()});
171
+ pruneSetToMax(this.failedAttempts, MAX_TRACKED_BLOCK_ROOTS);
115
172
  }
116
173
  }
@@ -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);
@@ -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 {
@@ -383,6 +387,6 @@ export async function processExecutionPayload(
383
387
  signal: AbortSignal,
384
388
  opts: ImportPayloadOpts = {}
385
389
  ): Promise<void> {
386
- const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability([payloadInput], signal);
390
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(this.config, [payloadInput], signal);
387
391
  await importExecutionPayload.call(this, payloadInput, dataAvailabilityStatuses[0], opts);
388
392
  }
@@ -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>;
@@ -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);
@@ -72,7 +72,7 @@ export enum BlockErrorCode {
72
72
  * whoever served the block MAY be downscored. Mirrors PayloadErrorCode.EXECUTION_ENGINE_INVALID.
73
73
  */
74
74
  EXECUTION_ENGINE_INVALID = "BLOCK_ERROR_EXECUTION_INVALID",
75
- /** The blobs are unavailable */
75
+ /** The blob or column data did not arrive within the availability wait */
76
76
  DATA_UNAVAILABLE = "BLOCK_ERROR_DATA_UNAVAILABLE",
77
77
  /** Block contains too many kzg commitments */
78
78
  TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
@@ -48,7 +48,6 @@ export type IChainOptions = BlockProcessOpts &
48
48
  /** Max number of produced block roots (blinded or full) cached for broadcast validations */
49
49
  maxCachedProducedRoots?: number;
50
50
  initialCustodyGroupCount?: number;
51
- broadcastValidationStrictness?: string;
52
51
  minSameMessageSignatureSetsToBatch: number;
53
52
  archiveDateEpochs?: number;
54
53
  nHistoricalStatesFileDataStore?: boolean;
@@ -121,7 +120,6 @@ export const defaultChainOptions: IChainOptions = {
121
120
  // for attestation validation, having this value ensures we don't have to regen states most of the time
122
121
  maxSkipSlots: 32,
123
122
  disableProposerSlashings: false,
124
- broadcastValidationStrictness: "warn",
125
123
  // should be less than or equal to MIN_SIGNATURE_SETS_TO_BATCH_VERIFY
126
124
  // batching too much may block the I/O thread so if useWorker=false, suggest this value to be 32
127
125
  // since this batch attestation work is designed to work with useWorker=true, make this the lowest value
package/src/db/beacon.ts CHANGED
@@ -30,6 +30,9 @@ import {
30
30
  VoluntaryExitRepository,
31
31
  } from "./repositories/index.js";
32
32
 
33
+ /** Below this size, seeks over leftover legacy column data stay cheap */
34
+ const LEGACY_DATA_COLUMNS_COMPACTION_MIN_BYTES = 1024 ** 3;
35
+
33
36
  export type BeaconDbOpts = {
34
37
  dataColumnDir: string;
35
38
  logger: Logger;
@@ -67,6 +70,7 @@ export class BeaconDb implements IBeaconDb {
67
70
  readonly dataColumns: IDataColumnStore;
68
71
  lastLegacyArchiveSlot: Slot | null = null;
69
72
  private readonly flatFileStore: FlatFileStore;
73
+ private readonly logger: Logger;
70
74
 
71
75
  constructor(
72
76
  config: ChainForkConfig,
@@ -100,6 +104,7 @@ export class BeaconDb implements IBeaconDb {
100
104
  this.syncCommittee = new SyncCommitteeRepository(config, db);
101
105
  this.syncCommitteeWitness = new SyncCommitteeWitnessRepository(config, db);
102
106
 
107
+ this.logger = opts.logger;
103
108
  this.flatFileStore = new FlatFileStore(opts.dataColumnDir, config, opts.logger, opts.metrics);
104
109
  this.dataColumns = new LegacyDataColumnStore(
105
110
  this.flatFileStore,
@@ -125,6 +130,40 @@ export class BeaconDb implements IBeaconDb {
125
130
  this.flatFileStore.setMetrics(flatFileStoreMetrics);
126
131
  }
127
132
 
133
+ /**
134
+ * Deletes made while an iterator held a snapshot can stay in the bottom LevelDB level, which `compactRange` never
135
+ * rewrites, so every seek into the legacy column buckets walks them. Writing a key at each end of the pruned range
136
+ * makes the compaction overlap those files and rewrite them.
137
+ */
138
+ async compactLegacyDataColumns(
139
+ prunedBeforeSlot: Slot,
140
+ minSizeBytes = LEGACY_DATA_COLUMNS_COMPACTION_MIN_BYTES
141
+ ): Promise<void> {
142
+ const archiveBucket = Bucket.allForks_dataColumnSidecarsArchive;
143
+ const start = encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(0));
144
+ const end = encodeKey(archiveBucket, this.dataColumnSidecarArchive.getMinKeyRaw(prunedBeforeSlot));
145
+ const sizeBytes = await this.db.approximateSize(start, end);
146
+ if (sizeBytes < minSizeBytes) return;
147
+
148
+ // Column indices stay below 0xffff and hot column keys are shorter, so no sentinel matches a real key
149
+ const upperSentinel =
150
+ prunedBeforeSlot > 0
151
+ ? encodeKey(archiveBucket, this.dataColumnSidecarArchive.encodeKeyRaw(prunedBeforeSlot - 1, 0xffff))
152
+ : encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(35, 0xff));
153
+ const sentinels = [start, upperSentinel];
154
+
155
+ this.logger.info("Compacting legacy data columns, this may take a while", {sizeBytes});
156
+ const startTime = Date.now();
157
+ await this.db.batchPut(sentinels.map((key) => ({key, value: new Uint8Array()})));
158
+ await this.db.compactRange(start, end);
159
+ await this.db.batchDelete(sentinels);
160
+ await this.db.compactRange(start, end);
161
+ this.logger.verbose("Compacted legacy data columns", {
162
+ durationMs: Date.now() - startTime,
163
+ sizeBytes: await this.db.approximateSize(start, end),
164
+ });
165
+ }
166
+
128
167
  async pruneHotDb(): Promise<void> {
129
168
  // Prune all hot blobs
130
169
  await this.blobSidecars.batchDelete(await this.blobSidecars.keys());
@@ -13,7 +13,7 @@ export type IndexedDataColumnBytes = {index: ColumnIndex; data: Uint8Array};
13
13
  type LegacyHotDataColumnStore = Pick<DataColumnSidecarRepository, "values" | "getManyBinary" | "deleteMany">;
14
14
  type LegacyArchiveDataColumnStore = Pick<
15
15
  DataColumnSidecarArchiveRepository,
16
- "values" | "getManyBinary" | "keys" | "deleteMany"
16
+ "values" | "getManyBinary" | "pruneBefore"
17
17
  >;
18
18
  type BlockRootIndex = Pick<BlockArchiveRepository, "getSlotByRoot">;
19
19
 
@@ -86,9 +86,7 @@ export class LegacyDataColumnStore implements IDataColumnStore {
86
86
 
87
87
  async pruneBefore(slot: Slot): Promise<Slot[]> {
88
88
  const flatFileSlots = await this.flatFiles.pruneBefore(slot);
89
- const prefixedKeys = await this.legacyArchive.keys({lt: {prefix: slot, id: 0}});
90
- const slots = [...new Set(prefixedKeys.map(({prefix}) => prefix))];
91
- if (slots.length > 0) await this.legacyArchive.deleteMany(slots);
89
+ const slots = await this.legacyArchive.pruneBefore(slot);
92
90
  return [...new Set([...flatFileSlots, ...slots])].sort((a, b) => a - b);
93
91
  }
94
92
  }
@@ -68,6 +68,8 @@ export interface IBeaconDb {
68
68
  readonly lastLegacyArchiveSlot: Slot | null;
69
69
  init(): Promise<void>;
70
70
 
71
+ compactLegacyDataColumns(prunedBeforeSlot: Slot, minSizeBytes?: number): Promise<void>;
72
+
71
73
  pruneHotDb(): Promise<void>;
72
74
 
73
75
  deleteDeprecatedEth1Data(): Promise<void>;