@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.
- package/lib/api/impl/validator/index.js +2 -2
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/ColumnReconstructionTracker.d.ts +11 -11
- package/lib/chain/ColumnReconstructionTracker.d.ts.map +1 -1
- package/lib/chain/ColumnReconstructionTracker.js +46 -40
- package/lib/chain/ColumnReconstructionTracker.js.map +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +1 -0
- package/lib/chain/chain.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +7 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +24 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/peers/utils/enrSubnetsDeserialize.d.ts.map +1 -1
- package/lib/network/peers/utils/enrSubnetsDeserialize.js +5 -2
- package/lib/network/peers/utils/enrSubnetsDeserialize.js.map +1 -1
- package/lib/network/processor/index.d.ts +5 -0
- package/lib/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +10 -1
- package/lib/network/processor/index.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/validator/index.ts +2 -2
- package/src/chain/ColumnReconstructionTracker.ts +60 -41
- package/src/chain/chain.ts +1 -0
- package/src/metrics/metrics/lodestar.ts +25 -0
- package/src/network/peers/utils/enrSubnetsDeserialize.ts +5 -2
- package/src/network/processor/index.ts +10 -1
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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:
|
|
53
|
-
if (this.
|
|
62
|
+
triggerColumnReconstruction(input: ColumnReconstructionInput): void {
|
|
63
|
+
if (this.pending.has(input.blockRootHex)) {
|
|
54
64
|
return;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
if (
|
|
67
|
+
if (input.getAllColumns().length < NUMBER_OF_COLUMNS / 2) {
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
package/src/chain/chain.ts
CHANGED
|
@@ -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
|
-
|
|
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 &&
|