@lodestar/beacon-node 1.40.0-rc.1 → 1.40.0-rc.3

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 (32) hide show
  1. package/lib/api/rest/base.d.ts.map +1 -1
  2. package/lib/api/rest/base.js +10 -8
  3. package/lib/api/rest/base.js.map +1 -1
  4. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  5. package/lib/chain/archiveStore/archiveStore.js +10 -4
  6. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  7. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  8. package/lib/chain/blocks/importBlock.js +9 -1
  9. package/lib/chain/blocks/importBlock.js.map +1 -1
  10. package/lib/chain/chain.d.ts.map +1 -1
  11. package/lib/chain/chain.js +5 -2
  12. package/lib/chain/chain.js.map +1 -1
  13. package/lib/chain/lightClient/index.d.ts +2 -0
  14. package/lib/chain/lightClient/index.d.ts.map +1 -1
  15. package/lib/chain/lightClient/index.js +10 -4
  16. package/lib/chain/lightClient/index.js.map +1 -1
  17. package/lib/chain/seenCache/seenGossipBlockInput.d.ts +7 -7
  18. package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
  19. package/lib/chain/seenCache/seenGossipBlockInput.js +20 -9
  20. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  21. package/lib/util/queue/itemQueue.d.ts +10 -0
  22. package/lib/util/queue/itemQueue.d.ts.map +1 -1
  23. package/lib/util/queue/itemQueue.js +57 -0
  24. package/lib/util/queue/itemQueue.js.map +1 -1
  25. package/package.json +15 -15
  26. package/src/api/rest/base.ts +11 -9
  27. package/src/chain/archiveStore/archiveStore.ts +10 -4
  28. package/src/chain/blocks/importBlock.ts +9 -1
  29. package/src/chain/chain.ts +5 -2
  30. package/src/chain/lightClient/index.ts +11 -4
  31. package/src/chain/seenCache/seenGossipBlockInput.ts +28 -9
  32. package/src/util/queue/itemQueue.ts +62 -0
@@ -1,10 +1,19 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {CheckpointWithHex} from "@lodestar/fork-choice";
3
- import {ForkName, ForkPostFulu, ForkPreGloas, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
3
+ import {
4
+ ForkName,
5
+ ForkPostFulu,
6
+ ForkPreGloas,
7
+ SLOTS_PER_EPOCH,
8
+ isForkPostDeneb,
9
+ isForkPostFulu,
10
+ isForkPostGloas,
11
+ } from "@lodestar/params";
4
12
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
5
13
  import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
6
14
  import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils";
7
15
  import {Metrics} from "../../metrics/metrics.js";
16
+ import {MAX_LOOK_AHEAD_EPOCHS} from "../../sync/constants.js";
8
17
  import {IClock} from "../../util/clock.js";
9
18
  import {CustodyConfig} from "../../util/dataColumns.js";
10
19
  import {
@@ -26,7 +35,17 @@ import {
26
35
  } from "../blocks/blockInput/index.js";
27
36
  import {ChainEvent, ChainEventEmitter} from "../emitter.js";
28
37
 
29
- const MAX_BLOCK_INPUT_CACHE_SIZE = 5;
38
+ // Target size for the block input cache, enforced by pruneToMaxSize() which runs after prune()
39
+ // and onFinalized() — NOT on insertion. The cache can temporarily exceed this during range sync
40
+ // (e.g. 32 blocks inserted per batch) but is trimmed back after blocks are processed.
41
+ //
42
+ // Must be large enough to hold blocks from all concurrently downloaded range sync batches.
43
+ // Range sync downloads up to MAX_LOOK_AHEAD_EPOCHS batches ahead of the processing head,
44
+ // so up to (MAX_LOOK_AHEAD_EPOCHS + 1) batches (current + look-ahead) of SLOTS_PER_EPOCH
45
+ // blocks can be in the cache simultaneously. If this value is too small, pruneToMaxSize()
46
+ // will evict blocks from the batch being processed before they are persisted to the database,
47
+ // causing errors when async handlers like onForkChoiceFinalized run.
48
+ const MAX_BLOCK_INPUT_CACHE_SIZE = (MAX_LOOK_AHEAD_EPOCHS + 1) * SLOTS_PER_EPOCH;
30
49
 
31
50
  export type SeenBlockInputCacheModules = {
32
51
  config: ChainForkConfig;
@@ -64,14 +83,14 @@ export type GetByBlobOptions = {
64
83
  * - onFinalized event handler will help to prune any non-canonical forks once the chain finalizes. Any block-slots that
65
84
  * are before the finalized checkpoint will be pruned.
66
85
  * - Range-sync periods. The range process uses this cache to store and sync blocks with DA data as the chain is pulled
67
- * from peers. We pull batches, by epoch, so 32 slots are pulled at a time and several batches are pulled concurrently.
68
- * It is important to set the MAX_BLOCK_INPUT_CACHE_SIZE high enough to support range sync activities. Currently the
69
- * value is set for 5 batches of 32 slots. As process block is called (similar to following head) the BlockInput and
70
- * its ancestors will be pruned.
86
+ * from peers. We pull batches, by epoch, so 32 slots are pulled at a time and several batches are downloaded
87
+ * concurrently (up to MAX_LOOK_AHEAD_EPOCHS ahead). All downloaded blocks are added to this shared cache, so it
88
+ * must be large enough to hold blocks from all concurrent batches. If pruneToMaxSize() evicts blocks from the batch
89
+ * currently being processed, those blocks may not yet be persisted to the database, causing getBlockByRoot() to fail
90
+ * when async event handlers (e.g. onForkChoiceFinalized) try to look them up.
71
91
  * - Non-Finality times. This is a bit more tricky. There can be long periods of non-finality and storing everything
72
- * will cause OOM. The pruneToMax will help ensure a hard limit on the number of stored blocks (with DA) that are held
73
- * in memory at any one time. The value for MAX_BLOCK_INPUT_CACHE_SIZE is set to accommodate range-sync but in
74
- * practice this value may need to be massaged in the future if we find issues when debugging non-finality
92
+ * will cause OOM. The pruneToMaxSize will help ensure the number of stored blocks (with DA) is trimmed back to
93
+ * MAX_BLOCK_INPUT_CACHE_SIZE after each prune() or onFinalized() call
75
94
  */
76
95
 
77
96
  export class SeenBlockInput {
@@ -24,6 +24,8 @@ export class JobItemQueue<Args extends any[], R> {
24
24
  private readonly metrics?: QueueMetrics;
25
25
  private runningJobs = 0;
26
26
  private lastYield = 0;
27
+ /** Resolvers waiting for space in the queue */
28
+ private spaceWaiters: (() => void)[] = [];
27
29
 
28
30
  constructor(
29
31
  private readonly itemProcessor: (...args: Args) => Promise<R>,
@@ -72,12 +74,57 @@ export class JobItemQueue<Args extends any[], R> {
72
74
  });
73
75
  }
74
76
 
77
+ /**
78
+ * Returns a promise that resolves when there is space in the queue.
79
+ * If the queue already has space, resolves immediately (noop).
80
+ * Use this to apply backpressure when the caller should wait rather than
81
+ * have push() throw QUEUE_MAX_LENGTH.
82
+ */
83
+ async waitForSpace(): Promise<void> {
84
+ if (this.opts.signal.aborted) {
85
+ throw new QueueError({code: QueueErrorCode.QUEUE_ABORTED});
86
+ }
87
+
88
+ if (this.jobs.length < this.opts.maxLength) {
89
+ return;
90
+ }
91
+
92
+ return new Promise<void>((resolve, reject) => {
93
+ let settled = false;
94
+
95
+ const onAbort = (): void => {
96
+ if (settled) return;
97
+ settled = true;
98
+ const index = this.spaceWaiters.indexOf(wrappedResolve);
99
+ if (index >= 0) {
100
+ this.spaceWaiters.splice(index, 1);
101
+ }
102
+ reject(new QueueError({code: QueueErrorCode.QUEUE_ABORTED}));
103
+ };
104
+
105
+ const wrappedResolve = (): void => {
106
+ if (settled) return;
107
+ settled = true;
108
+ this.opts.signal.removeEventListener("abort", onAbort);
109
+ resolve();
110
+ };
111
+
112
+ this.spaceWaiters.push(wrappedResolve);
113
+ this.opts.signal.addEventListener("abort", onAbort, {once: true});
114
+
115
+ // Re-check after attaching listener to close the race window where
116
+ // signal.abort() fires between the initial check and addEventListener
117
+ if (this.opts.signal.aborted) onAbort();
118
+ });
119
+ }
120
+
75
121
  getItems(): {args: Args; addedTimeMs: number}[] {
76
122
  return this.jobs.map((job) => ({args: job.args, addedTimeMs: job.addedTimeMs}));
77
123
  }
78
124
 
79
125
  dropAllJobs = (): void => {
80
126
  this.jobs.clear();
127
+ this.notifySpaceWaiters();
81
128
  };
82
129
 
83
130
  private runJob = async (): Promise<void> => {
@@ -115,10 +162,25 @@ export class JobItemQueue<Args extends any[], R> {
115
162
 
116
163
  this.runningJobs = Math.max(0, this.runningJobs - 1);
117
164
 
165
+ // Notify any waiters that space is available
166
+ this.notifySpaceWaiters();
167
+
118
168
  // Potentially run a new job
119
169
  void this.runJob();
120
170
  };
121
171
 
172
+ private notifySpaceWaiters(): void {
173
+ // Compute available slots once to avoid thundering herd: resolved waiters
174
+ // won't push() until the next microtask, so jobs.length doesn't change
175
+ // inside this loop. Without the cap we'd wake ALL waiters on a single slot.
176
+ let available = this.opts.maxLength - this.jobs.length;
177
+ while (available > 0 && this.spaceWaiters.length > 0) {
178
+ const resolve = this.spaceWaiters.shift();
179
+ if (resolve) resolve();
180
+ available--;
181
+ }
182
+ }
183
+
122
184
  private abortAllJobs = (): void => {
123
185
  while (this.jobs.length > 0) {
124
186
  const job = this.jobs.pop();