@lodestar/beacon-node 1.51.0-dev.83da04fef3 → 1.51.0-dev.99edacec52

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.
@@ -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
  }
@@ -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",
@@ -17,10 +17,13 @@ export function deserializeEnrSubnets(bytes: Uint8Array, subnetCount: number): b
17
17
  return getUint8ByteToBitBooleanArray(bytes[0] ?? 0);
18
18
  }
19
19
 
20
- let boolsArr: boolean[] = [];
21
20
  const byteCount = Math.ceil(subnetCount / 8);
21
+ const boolsArr = new Array<boolean>(byteCount * 8);
22
22
  for (let i = 0; i < byteCount; i++) {
23
- boolsArr = boolsArr.concat(getUint8ByteToBitBooleanArray(bytes[i] ?? 0));
23
+ const bits = getUint8ByteToBitBooleanArray(bytes[i] ?? 0);
24
+ for (let j = 0; j < 8; j++) {
25
+ boolsArr[i * 8 + j] = bits[j];
26
+ }
24
27
  }
25
28
 
26
29
  return boolsArr;
@@ -63,7 +63,7 @@ export type NetworkProcessorOpts = GossipHandlerOpts & {
63
63
  /**
64
64
  * Keep up to 3 slot of unknown roots, so we don't always emit to UnknownBlock sync.
65
65
  */
66
- const MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE = 3;
66
+ export const MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE = 3;
67
67
 
68
68
  /**
69
69
  * Max distinct roots we BUFFER messages for, per slot. Kept tight to avoid the OOM
@@ -665,6 +665,7 @@ export class NetworkProcessor {
665
665
  *
666
666
  * For DOS protection, the message is ignored (reject metric incremented) when any cap is hit:
667
667
  * - MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS: global awaiting-block message count
668
+ * - MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE: message slot already pruned by onClockSlot, so not bounded by the per-slot root budget
668
669
  * - MAX_BUFFERED_ROOTS_PER_SLOT: distinct roots we buffer messages for, per slot
669
670
  * - MAX_AWAITING_MESSAGES_PER_ROOT[topic]: messages buffered per (root, topic)
670
671
  */
@@ -675,6 +676,10 @@ export class NetworkProcessor {
675
676
  metric?.reject.inc({reason: ReprocessRejectReason.reached_limit, topic: topicType});
676
677
  return;
677
678
  }
679
+ if (slot <= this.chain.clock.currentSlot - MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE) {
680
+ metric?.reject.inc({reason: ReprocessRejectReason.expired, topic: topicType});
681
+ return;
682
+ }
678
683
  // per-(root, topic) message cap: e.g. <= NUMBER_OF_COLUMNS columns per root
679
684
  const perRootCap = MAX_AWAITING_MESSAGES_PER_ROOT[topicType];
680
685
  if (
@@ -701,6 +706,10 @@ export class NetworkProcessor {
701
706
  metric?.reject.inc({reason: ReprocessRejectReason.reached_limit, topic: topicType});
702
707
  return;
703
708
  }
709
+ if (slot <= this.chain.clock.currentSlot - MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE) {
710
+ metric?.reject.inc({reason: ReprocessRejectReason.expired, topic: topicType});
711
+ return;
712
+ }
704
713
  const perRootCap = MAX_AWAITING_MESSAGES_PER_ROOT[topicType];
705
714
  if (
706
715
  perRootCap !== undefined &&