@lodestar/beacon-node 1.51.0-dev.2593063a82 → 1.51.0-dev.4157a0dec4

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 (33) hide show
  1. package/lib/chain/ColumnReconstructionTracker.d.ts +11 -11
  2. package/lib/chain/ColumnReconstructionTracker.d.ts.map +1 -1
  3. package/lib/chain/ColumnReconstructionTracker.js +46 -40
  4. package/lib/chain/ColumnReconstructionTracker.js.map +1 -1
  5. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  6. package/lib/chain/archiveStore/utils/archiveBlocks.js +4 -5
  7. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  8. package/lib/chain/blocks/blockInput/utils.js +2 -2
  9. package/lib/chain/blocks/blockInput/utils.js.map +1 -1
  10. package/lib/chain/chain.d.ts.map +1 -1
  11. package/lib/chain/chain.js +1 -0
  12. package/lib/chain/chain.js.map +1 -1
  13. package/lib/metrics/metrics/lodestar.d.ts +7 -0
  14. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  15. package/lib/metrics/metrics/lodestar.js +24 -0
  16. package/lib/metrics/metrics/lodestar.js.map +1 -1
  17. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  18. package/lib/network/gossip/gossipsub.js +3 -1
  19. package/lib/network/gossip/gossipsub.js.map +1 -1
  20. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  21. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +4 -4
  22. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  23. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +3 -3
  24. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  25. package/package.json +15 -15
  26. package/src/chain/ColumnReconstructionTracker.ts +60 -41
  27. package/src/chain/archiveStore/utils/archiveBlocks.ts +6 -7
  28. package/src/chain/blocks/blockInput/utils.ts +2 -2
  29. package/src/chain/chain.ts +1 -0
  30. package/src/metrics/metrics/lodestar.ts +25 -0
  31. package/src/network/gossip/gossipsub.ts +3 -1
  32. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +4 -10
  33. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +3 -3
@@ -1,7 +1,10 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
+ import {NUMBER_OF_COLUMNS} from "@lodestar/params";
3
+ import {RootHex} from "@lodestar/types";
2
4
  import {Logger, sleep} from "@lodestar/utils";
3
5
  import {Metrics} from "../metrics/metrics.js";
4
6
  import {DataColumnReconstructionCode, recoverDataColumnSidecars} from "../util/dataColumns.js";
7
+ import {JobItemQueue} from "../util/queue/index.js";
5
8
  import {BlockInputColumns} from "./blocks/blockInput/index.js";
6
9
  import {PayloadEnvelopeInput} from "./blocks/payloadEnvelopeInput/index.js";
7
10
  import {ChainEventEmitter} from "./emitter.js";
@@ -16,15 +19,22 @@ const RECONSTRUCTION_DELAY_MIN_BPS = 667;
16
19
  */
17
20
  const RECONSTRUCTION_DELAY_MAX_BPS = 1000;
18
21
 
22
+ const QUEUE_MAX_LENGTH = 16;
23
+
24
+ export type ColumnReconstructionInput = BlockInputColumns | PayloadEnvelopeInput;
25
+
19
26
  export type ColumnReconstructionTrackerInit = {
20
27
  logger: Logger;
21
28
  emitter: ChainEventEmitter;
22
29
  metrics: Metrics | null;
23
30
  config: ChainForkConfig;
31
+ signal: AbortSignal;
24
32
  };
25
33
 
26
34
  /**
27
- * Tracks column reconstruction attempts to avoid duplicate and multiple in-flight calls
35
+ * Runs column reconstruction one block root at a time, in trigger order. A trigger for a root that is
36
+ * queued or in flight is a no-op. A root is removed once its attempt finishes so a later column can
37
+ * retry a failed attempt.
28
38
  */
29
39
  export class ColumnReconstructionTracker {
30
40
  logger: Logger;
@@ -32,62 +42,71 @@ export class ColumnReconstructionTracker {
32
42
  metrics: Metrics | null;
33
43
  config: ChainForkConfig;
34
44
 
35
- /**
36
- * Track last attempted block root
37
- *
38
- * This is sufficient to avoid duplicate calls since we only call this
39
- * function when we see a new data column sidecar from gossip.
40
- */
41
- lastBlockRootHex: string | null = null;
42
- /** Track if a reconstruction attempt is in-flight */
43
- running = false;
45
+ private readonly signal: AbortSignal;
46
+ private readonly pending = new Set<RootHex>();
47
+ private readonly queue: JobItemQueue<[ColumnReconstructionInput, number], void>;
44
48
 
45
49
  constructor(init: ColumnReconstructionTrackerInit) {
46
50
  this.logger = init.logger;
47
51
  this.emitter = init.emitter;
48
52
  this.metrics = init.metrics;
49
53
  this.config = init.config;
54
+ this.signal = init.signal;
55
+ this.queue = new JobItemQueue(
56
+ this.reconstruct,
57
+ {maxLength: QUEUE_MAX_LENGTH, signal: init.signal},
58
+ init.metrics?.columnReconstructionQueue
59
+ );
50
60
  }
51
61
 
52
- triggerColumnReconstruction(input: BlockInputColumns | PayloadEnvelopeInput): void {
53
- if (this.running) {
62
+ triggerColumnReconstruction(input: ColumnReconstructionInput): void {
63
+ if (this.pending.has(input.blockRootHex)) {
54
64
  return;
55
65
  }
56
66
 
57
- if (this.lastBlockRootHex === input.blockRootHex) {
67
+ if (input.getAllColumns().length < NUMBER_OF_COLUMNS / 2) {
58
68
  return;
59
69
  }
60
70
 
61
- // We don't care about the outcome of this call,
62
- // just that it has been triggered for this block root.
63
- this.running = true;
64
- this.lastBlockRootHex = input.blockRootHex;
71
+ this.pending.add(input.blockRootHex);
72
+ void this.enqueue(input);
73
+ }
74
+
75
+ private async enqueue(input: ColumnReconstructionInput): Promise<void> {
76
+ try {
77
+ await this.queue.push(input, Date.now());
78
+ } catch (e) {
79
+ this.logger.debug(
80
+ "Data column sidecar reconstruction dropped",
81
+ {slot: input.slot, root: input.blockRootHex},
82
+ e as Error
83
+ );
84
+ } finally {
85
+ this.pending.delete(input.blockRootHex);
86
+ }
87
+ }
88
+
89
+ private reconstruct = async (input: ColumnReconstructionInput, queuedAtMs: number): Promise<void> => {
65
90
  const fork = this.config.getForkName(input.slot);
66
91
  const minDelayMs = this.config.getSlotComponentDurationMs(fork, RECONSTRUCTION_DELAY_MIN_BPS);
67
92
  const maxDelayMs = this.config.getSlotComponentDurationMs(fork, RECONSTRUCTION_DELAY_MAX_BPS);
68
93
  const delay = minDelayMs + Math.random() * (maxDelayMs - minDelayMs);
69
- sleep(delay)
70
- .then(() => {
71
- const logCtx = {slot: input.slot, root: input.blockRootHex};
72
- this.logger.debug("Attempting data column sidecar reconstruction", logCtx);
73
- recoverDataColumnSidecars(input, this.emitter, this.metrics)
74
- .then((result) => {
75
- this.metrics?.recoverDataColumnSidecars.reconstructionResult.inc({result});
76
- this.logger.debug("Data column sidecar reconstruction complete", {...logCtx, result});
77
- })
78
- .catch((e) => {
79
- this.metrics?.recoverDataColumnSidecars.reconstructionResult.inc({
80
- result: DataColumnReconstructionCode.Failed,
81
- });
82
- this.logger.debug("Error during data column sidecar reconstruction", logCtx, e as Error);
83
- })
84
- .finally(() => {
85
- this.logger.debug("Data column sidecar reconstruction attempt finished", logCtx);
86
- this.running = false;
87
- });
88
- })
89
- .catch((err) => {
90
- this.logger.debug("ColumnReconstructionTracker unreachable error", {}, err);
91
- });
92
- }
94
+
95
+ // The delay gives gossip a chance to deliver the remaining columns, so time already spent in the
96
+ // queue behind another root counts towards it
97
+ await sleep(delay - (Date.now() - queuedAtMs), this.signal);
98
+
99
+ const logCtx = {slot: input.slot, root: input.blockRootHex};
100
+ this.logger.debug("Attempting data column sidecar reconstruction", logCtx);
101
+ try {
102
+ const result = await recoverDataColumnSidecars(input, this.emitter, this.metrics);
103
+ this.metrics?.recoverDataColumnSidecars.reconstructionResult.inc({result});
104
+ this.logger.debug("Data column sidecar reconstruction complete", {...logCtx, result});
105
+ } catch (e) {
106
+ this.metrics?.recoverDataColumnSidecars.reconstructionResult.inc({result: DataColumnReconstructionCode.Failed});
107
+ this.logger.debug("Error during data column sidecar reconstruction", logCtx, e as Error);
108
+ } finally {
109
+ this.logger.debug("Data column sidecar reconstruction attempt finished", logCtx);
110
+ }
111
+ };
93
112
  }
@@ -3,7 +3,7 @@ import {ChainForkConfig} from "@lodestar/config";
3
3
  import {KeyValue} from "@lodestar/db";
4
4
  import {CheckpointWithHex, ExecutionStatus, IForkChoice, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice";
5
5
  import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
6
- import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
6
+ import {computeEpochAtSlot, computeStartSlotAtEpoch, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
7
7
  import {Epoch, Slot} from "@lodestar/types";
8
8
  import {Logger, fromAsync, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
9
9
  import {IBeaconDb} from "../../../db/index.js";
@@ -253,14 +253,13 @@ export async function archiveBlocks(
253
253
  }
254
254
 
255
255
  // Delete expired data column sidecars
256
- // Keep only `[current_epoch - max(MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, archiveDataEpochs)]`
256
+ // Keep only `[min(compute_blob_data_retention_start_epoch(current_epoch), current_epoch - archiveDataEpochs)]`
257
257
  if (finalizedPostFulu) {
258
258
  if (archiveDataEpochs !== Infinity) {
259
- const dataColumnSidecarsArchiveWindow = Math.max(
260
- config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS,
261
- archiveDataEpochs ?? 0
259
+ const dataColumnSidecarsMinEpoch = Math.min(
260
+ getBlobDataRetentionStartEpoch(config, currentEpoch),
261
+ currentEpoch - (archiveDataEpochs ?? 0)
262
262
  );
263
- const dataColumnSidecarsMinEpoch = currentEpoch - dataColumnSidecarsArchiveWindow;
264
263
  if (dataColumnSidecarsMinEpoch >= config.FULU_FORK_EPOCH) {
265
264
  const columnsPruneSlot = computeStartSlotAtEpoch(dataColumnSidecarsMinEpoch);
266
265
  const prunedColumnSlots = await db.dataColumns.pruneBefore(columnsPruneSlot);
@@ -439,7 +438,7 @@ async function migrateDataColumnSidecarsFromHotToColdDb(
439
438
  const blockEpoch = computeEpochAtSlot(block.slot);
440
439
  if (
441
440
  config.getForkSeq(block.slot) < ForkSeq.fulu ||
442
- blockEpoch < currentEpoch - config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS
441
+ blockEpoch < getBlobDataRetentionStartEpoch(config, currentEpoch)
443
442
  ) {
444
443
  continue;
445
444
  }
@@ -1,6 +1,6 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkName, isForkPostDeneb, isForkPostFulu} from "@lodestar/params";
3
- import {computeEpochAtSlot} from "@lodestar/state-transition";
3
+ import {computeEpochAtSlot, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
4
4
  import {Epoch, Slot} from "@lodestar/types";
5
5
 
6
6
  export function isDaOutOfRange(
@@ -10,7 +10,7 @@ export function isDaOutOfRange(
10
10
  currentEpoch: Epoch
11
11
  ): boolean {
12
12
  if (isForkPostFulu(forkName)) {
13
- return computeEpochAtSlot(blockSlot) < currentEpoch - config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS;
13
+ return computeEpochAtSlot(blockSlot) < getBlobDataRetentionStartEpoch(config, currentEpoch);
14
14
  }
15
15
 
16
16
  if (isForkPostDeneb(forkName)) {
@@ -509,6 +509,7 @@ export class BeaconChain implements IBeaconChain {
509
509
  emitter,
510
510
  metrics,
511
511
  config,
512
+ signal,
512
513
  });
513
514
 
514
515
  this.archiveStore = new ArchiveStore(
@@ -330,6 +330,31 @@ export function createLodestarMetrics(
330
330
  }),
331
331
  },
332
332
 
333
+ columnReconstructionQueue: {
334
+ length: register.gauge({
335
+ name: "lodestar_column_reconstruction_queue_length",
336
+ help: "Count of total column reconstruction queue length",
337
+ }),
338
+ droppedJobs: register.gauge({
339
+ name: "lodestar_column_reconstruction_queue_dropped_jobs_total",
340
+ help: "Count of total column reconstruction queue dropped jobs",
341
+ }),
342
+ jobTime: register.histogram({
343
+ name: "lodestar_column_reconstruction_queue_job_time_seconds",
344
+ help: "Time to process column reconstruction queue job in seconds",
345
+ buckets: [0.01, 0.1, 1, 4, 12],
346
+ }),
347
+ jobWaitTime: register.histogram({
348
+ name: "lodestar_column_reconstruction_queue_job_wait_time_seconds",
349
+ help: "Time from job added to the column reconstruction queue to starting in seconds",
350
+ buckets: [0.01, 0.1, 1, 4, 12],
351
+ }),
352
+ concurrency: register.gauge({
353
+ name: "lodestar_column_reconstruction_queue_concurrency",
354
+ help: "Current concurrency of column reconstruction queue",
355
+ }),
356
+ },
357
+
333
358
  engineHttpProcessorQueue: {
334
359
  length: register.gauge({
335
360
  name: "lodestar_engine_http_processor_queue_length",
@@ -21,6 +21,7 @@ import {
21
21
  SLOTS_PER_EPOCH,
22
22
  SYNC_COMMITTEE_SUBNET_COUNT,
23
23
  } from "@lodestar/params";
24
+ import {getSlotDurations} from "@lodestar/state-transition";
24
25
  import {SubnetID} from "@lodestar/types";
25
26
  import {Logger, Map2d, Map2dArr} from "@lodestar/utils";
26
27
  import {RegistryMetricCreator} from "../../metrics/index.js";
@@ -172,7 +173,8 @@ export class Eth2Gossipsub {
172
173
  mcacheLength: 6,
173
174
  mcacheGossip: 3,
174
175
  // this should be in ms
175
- seenTTL: config.SLOT_DURATION_MS * SLOTS_PER_EPOCH * 2,
176
+ // use the largest configured slot duration so the TTL stays conservative across duration changes
177
+ seenTTL: Math.max(...getSlotDurations(config).map(([, slotDurationMs]) => slotDurationMs)) * SLOTS_PER_EPOCH * 2,
176
178
  scoreParams,
177
179
  scoreThresholds: gossipScoreThresholds,
178
180
  // For a single stream, await processing each RPC before processing the next
@@ -3,7 +3,7 @@ import {ChainConfig} from "@lodestar/config";
3
3
  import {PayloadStatus} from "@lodestar/fork-choice";
4
4
  import {ForkSeq, GENESIS_SLOT, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
5
5
  import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
6
- import {computeEpochAtSlot} from "@lodestar/state-transition";
6
+ import {computeEpochAtSlot, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
7
7
  import {ColumnIndex, Epoch, RootHex, fulu} from "@lodestar/types";
8
8
  import {fromHex, toRootHex} from "@lodestar/utils";
9
9
  import {IBeaconChain} from "../../../chain/index.js";
@@ -216,16 +216,10 @@ export function validateDataColumnSidecarsByRangeRequest(
216
216
  throw new ResponseError(RespStatus.INVALID_REQUEST, "startSlot < genesis");
217
217
  }
218
218
 
219
- // Spec: [max(current_epoch - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, FULU_FORK_EPOCH), current_epoch]
220
- const minimumRequestEpoch = Math.max(
221
- currentEpoch - config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS,
222
- config.FULU_FORK_EPOCH
223
- );
219
+ // Spec: [max(compute_blob_data_retention_start_epoch(current_epoch), FULU_FORK_EPOCH), current_epoch]
220
+ const minimumRequestEpoch = Math.max(getBlobDataRetentionStartEpoch(config, currentEpoch), config.FULU_FORK_EPOCH);
224
221
  if (computeEpochAtSlot(startSlot) < minimumRequestEpoch) {
225
- throw new ResponseError(
226
- RespStatus.RESOURCE_UNAVAILABLE,
227
- "startSlot is before MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS"
228
- );
222
+ throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, "startSlot is before the blob data retention window");
229
223
  }
230
224
 
231
225
  if (count > config.MAX_REQUEST_BLOCKS_DENEB) {
@@ -1,6 +1,6 @@
1
1
  import {PeerId} from "@libp2p/interface";
2
2
  import {ResponseOutgoing} from "@lodestar/reqresp";
3
- import {computeEpochAtSlot} from "@lodestar/state-transition";
3
+ import {computeEpochAtSlot, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
4
4
  import {ColumnIndex} from "@lodestar/types";
5
5
  import {toRootHex} from "@lodestar/utils";
6
6
  import {IBeaconChain} from "../../../chain/index.js";
@@ -19,10 +19,10 @@ export async function* onDataColumnSidecarsByRoot(
19
19
  peerId: PeerId,
20
20
  peerClient: string
21
21
  ): AsyncIterable<ResponseOutgoing> {
22
- // SPEC: minimum_request_epoch = max(current_epoch - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, FULU_FORK_EPOCH)
22
+ // SPEC: minimum_request_epoch = max(compute_blob_data_retention_start_epoch(current_epoch), FULU_FORK_EPOCH)
23
23
  const currentEpoch = chain.clock.currentEpoch;
24
24
  const minimumRequestEpoch = Math.max(
25
- currentEpoch - chain.config.MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS,
25
+ getBlobDataRetentionStartEpoch(chain.config, currentEpoch),
26
26
  chain.config.FULU_FORK_EPOCH
27
27
  );
28
28