@lodestar/beacon-node 1.48.0-dev.df6a2aa429 → 1.48.0-dev.e17b11435f

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 (48) hide show
  1. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  2. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  3. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  4. package/lib/chain/errors/blockError.d.ts +3 -5
  5. package/lib/chain/errors/blockError.d.ts.map +1 -1
  6. package/lib/chain/errors/blockError.js +2 -2
  7. package/lib/chain/errors/blockError.js.map +1 -1
  8. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  9. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  10. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  11. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  12. package/lib/chain/prepareNextSlot.js +13 -13
  13. package/lib/chain/prepareNextSlot.js.map +1 -1
  14. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  15. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  16. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  17. package/lib/chain/validation/block.d.ts.map +1 -1
  18. package/lib/chain/validation/block.js +9 -50
  19. package/lib/chain/validation/block.js.map +1 -1
  20. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  21. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  22. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  23. package/lib/network/gossip/encoding.d.ts.map +1 -1
  24. package/lib/network/gossip/encoding.js +0 -6
  25. package/lib/network/gossip/encoding.js.map +1 -1
  26. package/lib/network/gossip/topic.d.ts +2 -1
  27. package/lib/network/gossip/topic.d.ts.map +1 -1
  28. package/lib/network/gossip/topic.js +4 -20
  29. package/lib/network/gossip/topic.js.map +1 -1
  30. package/lib/network/processor/index.d.ts +49 -3
  31. package/lib/network/processor/index.d.ts.map +1 -1
  32. package/lib/network/processor/index.js +232 -98
  33. package/lib/network/processor/index.js.map +1 -1
  34. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  35. package/lib/network/reqresp/protocols.js +6 -22
  36. package/lib/network/reqresp/protocols.js.map +1 -1
  37. package/package.json +15 -15
  38. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  39. package/src/chain/errors/blockError.ts +3 -3
  40. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  41. package/src/chain/prepareNextSlot.ts +16 -16
  42. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  43. package/src/chain/validation/block.ts +9 -66
  44. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  45. package/src/network/gossip/encoding.ts +0 -6
  46. package/src/network/gossip/topic.ts +3 -25
  47. package/src/network/processor/index.ts +263 -148
  48. package/src/network/reqresp/protocols.ts +7 -24
@@ -1,6 +1,6 @@
1
1
  import { TopicValidatorResult } from "@libp2p/gossipsub";
2
2
  import { routes } from "@lodestar/api";
3
- import { ForkSeq } from "@lodestar/params";
3
+ import { ForkSeq, NUMBER_OF_COLUMNS } from "@lodestar/params";
4
4
  import { computeStartSlotAtEpoch } from "@lodestar/state-transition";
5
5
  import { MapDef, mapValues, sleep } from "@lodestar/utils";
6
6
  import { BlockInputSource } from "../../chain/blocks/blockInput/types.js";
@@ -20,6 +20,28 @@ export * from "./types.js";
20
20
  * Keep up to 3 slot of unknown roots, so we don't always emit to UnknownBlock sync.
21
21
  */
22
22
  const MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE = 3;
23
+ /**
24
+ * Max distinct roots we BUFFER messages for, per slot. Kept tight to avoid the OOM
25
+ * risk. We don't support super forky condition where there are more than this many roots per slot via gossip.
26
+ */
27
+ export const MAX_BUFFERED_ROOTS_PER_SLOT = 5;
28
+ /**
29
+ * Max distinct roots we emit an unknown-root search for, per slot. Higher than
30
+ * the buffer budget because:
31
+ * - in the attack scenario, the genuine root may comes after the first MAX_BUFFERED_ROOTS_PER_SLOT roots
32
+ * - if we cannot search, we'll penalize peers in UnknownBlockInput, not in NetworkProcessor
33
+ */
34
+ export const MAX_SEARCHED_ROOTS_PER_SLOT = 32;
35
+ /**
36
+ * Given the same root and topic, we'll ignore messages after the below cap. Each cap is the number of
37
+ * legitimate messages we expect for one block, plus headroom for some malformed messages that may arrives first
38
+ * they will be penalized by the gossip handler
39
+ */
40
+ export const MAX_AWAITING_MESSAGES_PER_ROOT = {
41
+ [GossipType.data_column_sidecar]: 2 * NUMBER_OF_COLUMNS,
42
+ [GossipType.execution_payload]: 2,
43
+ [GossipType.execution_payload_bid]: 8,
44
+ };
23
45
  /**
24
46
  * This is respective to gossipsub seenTTL (which is 550 * 0.7 = 385s), also it's respective
25
47
  * to beacon_attestation ATTESTATION_PROPAGATION_SLOT_RANGE (32 slots).
@@ -72,6 +94,14 @@ export var ReprocessRejectReason;
72
94
  * There are too many gossip messages that have unknown block root.
73
95
  */
74
96
  ReprocessRejectReason["reached_limit"] = "reached_limit";
97
+ /**
98
+ * Too many distinct unknown roots are already tracked for the message's slot (MAX_UNKNOWN_ROOTS_PER_SLOT).
99
+ */
100
+ ReprocessRejectReason["reached_root_limit"] = "reached_root_limit";
101
+ /**
102
+ * Too many gossip messages are already buffered for this (root, topic) (MAX_AWAITING_MESSAGES_PER_ROOT).
103
+ */
104
+ ReprocessRejectReason["reached_topic_limit"] = "reached_topic_limit";
75
105
  /**
76
106
  * The awaiting gossip message is pruned per clock slot.
77
107
  */
@@ -132,15 +162,16 @@ export class NetworkProcessor {
132
162
  gossipTopicConcurrency;
133
163
  extractBlockSlotRootFns = createExtractBlockSlotRootFns();
134
164
  // we may not receive the block for messages like Attestation and SignedAggregateAndProof messages, in that case PendingGossipsubMessage needs
135
- // to be stored in this Map and reprocessed once the block comes
165
+ // to be stored in this Map and reprocessed once the block comes. Keyed by topic per root so we can cap
166
+ // the number of buffered messages per (root, topic) - see MAX_AWAITING_MESSAGES_PER_ROOT.
136
167
  awaitingMessagesByBlockRoot;
137
168
  awaitingBlockMessageCount = 0;
138
169
  // we may not receive the payload for messages that require the FULL payload variant to be processed,
139
170
  // in that case PendingGossipsubMessage needs to be stored in this Map and reprocessed once the payload comes
140
171
  awaitingMessagesByPayloadBlockRoot;
141
172
  awaitingPayloadMessageCount = 0;
142
- unknownBlocksBySlot = new MapDef(() => new MapDef(() => new Set()));
143
- unknownEnvelopesBySlot = new MapDef(() => new MapDef(() => new Set()));
173
+ searchedRootsBySlot = new MapDef(() => new MapDef(() => ({})));
174
+ bufferedRootsBySlot = new MapDef(() => new Set());
144
175
  constructor(modules, opts) {
145
176
  this.opts = opts;
146
177
  const { chain, events, logger, metrics } = modules;
@@ -157,8 +188,8 @@ export class NetworkProcessor {
157
188
  this.chain.emitter.on(routes.events.EventType.block, this.onBlockProcessed);
158
189
  this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadEnvelopeProcessed);
159
190
  this.chain.clock.on(ClockEvent.slot, this.onClockSlot);
160
- this.awaitingMessagesByBlockRoot = new MapDef(() => new Set());
161
- this.awaitingMessagesByPayloadBlockRoot = new MapDef(() => new Set());
191
+ this.awaitingMessagesByBlockRoot = new MapDef(() => new MapDef(() => new Set()));
192
+ this.awaitingMessagesByPayloadBlockRoot = new MapDef(() => new MapDef(() => new Set()));
162
193
  // TODO: Implement queues and priorization for ReqResp incoming requests
163
194
  // Listens to NetworkEvent.reqRespIncomingRequest event
164
195
  if (metrics) {
@@ -210,9 +241,12 @@ export class NetworkProcessor {
210
241
  if (this.chain.seenBlock(root)) {
211
242
  return;
212
243
  }
213
- const peersForRoot = this.unknownBlocksBySlot.getOrDefault(slot);
214
- const alreadySearching = peersForRoot.has(root) || this.awaitingMessagesByBlockRoot.has(root);
215
- const forwardedPeers = peersForRoot.getOrDefault(root);
244
+ const entry = this.searchedRootsBySlot.getOrDefault(slot).getOrDefault(root);
245
+ const alreadySearching = entry.blockPeerIds !== undefined;
246
+ if (entry.blockPeerIds === undefined) {
247
+ entry.blockPeerIds = new Set();
248
+ }
249
+ const forwardedPeers = entry.blockPeerIds;
216
250
  // brand-new search always emits (peer may be undefined)
217
251
  let shouldEmit = !alreadySearching;
218
252
  if (peer !== undefined && !forwardedPeers.has(peer) && forwardedPeers.size < MAX_PEERS_PER_ROOT) {
@@ -234,10 +268,12 @@ export class NetworkProcessor {
234
268
  if (this.chain.seenPayloadEnvelope(root)) {
235
269
  return;
236
270
  }
237
- const peersForRoot = this.unknownEnvelopesBySlot.getOrDefault(slot);
238
- // capture "already searching" BEFORE getOrDefault(root) creates the entry
239
- const alreadySearching = peersForRoot.has(root) || this.awaitingMessagesByPayloadBlockRoot.has(root);
240
- const forwardedPeers = peersForRoot.getOrDefault(root);
271
+ const entry = this.searchedRootsBySlot.getOrDefault(slot).getOrDefault(root);
272
+ const alreadySearching = entry.envelopePeerIds !== undefined;
273
+ if (entry.envelopePeerIds === undefined) {
274
+ entry.envelopePeerIds = new Set();
275
+ }
276
+ const forwardedPeers = entry.envelopePeerIds;
241
277
  let shouldEmit = !alreadySearching; // brand-new search always emits (peer may be undefined)
242
278
  if (peer !== undefined && !forwardedPeers.has(peer) && forwardedPeers.size < MAX_PEERS_PER_ROOT) {
243
279
  // First time we forward THIS peer for THIS root: emit so BlockInputSync accumulates it.
@@ -292,13 +328,14 @@ export class NetworkProcessor {
292
328
  return;
293
329
  }
294
330
  message.msgSlot = slot;
331
+ const peerId = message.propagationSource.toString();
295
332
  // this determines whether this message needs to wait for a Block or Envelope
296
- // a message should only wait for what it voted for, hence we don't want to put it on both queues
333
+ // a message should only wait for what it voted for, hence we don't want to put it on both queues.
297
334
  let preprocessResult = { action: PreprocessAction.PushToQueue };
298
335
  // no need to check if root is a descendant of the current finalized block, it will be checked once we validate the message if needed
299
336
  if (root && !this.chain.forkChoice.hasBlockHexUnsafe(root)) {
300
337
  // starting from GLOAS, unknown root from data_column_sidecar also falls into this case
301
- this.searchUnknownBlock({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
338
+ this.searchUnknownRoot({ slot, root }, true, false, peerId);
302
339
  // for beacon_attestation and beacon_aggregate_and_proof messages, this is only temporary.
303
340
  // if "index = 1" we need to await for the Envelope instead
304
341
  preprocessResult = { action: PreprocessAction.AwaitBlock, root };
@@ -306,28 +343,32 @@ export class NetworkProcessor {
306
343
  // 2nd extract round for some specific topics
307
344
  // we separate the search action from the await action
308
345
  // beacon_block: proactively search for parent block/envelope across all forks, but never queue.
309
- // BlockInputSync handles cascading recovery if the gossip handler throws.
346
+ // BlockInputSync handles cascading recovery if the gossip handler throws. The parent root differs from
347
+ // the message root and this message never awaits, so its search flushes inline here (gated by the budget).
310
348
  if (topicType === GossipType.beacon_block) {
311
349
  const parentRoot = getParentRootFromSignedBeaconBlockSerialized(message.msg.data);
312
350
  if (parentRoot) {
351
+ let searchParentBlock = false;
352
+ let searchParentEnvelope = false;
313
353
  if (ForkSeq[fork] >= ForkSeq.gloas) {
314
354
  // GLOAS: also check parent envelope, same logic as execution_payload_bid
315
355
  const parentBlockHash = getParentBlockHashFromGloasSignedBeaconBlockSerialized(message.msg.data);
316
356
  if (parentBlockHash && !this.chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash)) {
317
357
  const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentRoot);
318
358
  if (protoBlock === null) {
319
- this.searchUnknownBlock({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
359
+ searchParentBlock = true;
320
360
  }
321
361
  else if (protoBlock.executionPayloadBlockHash &&
322
362
  protoBlock.executionPayloadBlockHash !== parentBlockHash) {
323
363
  // only search for the envelope by block root if we're sure there is one. Otherwise UnknownBlockSync will penalize the peer.
324
- this.searchUnknownEnvelope({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
364
+ searchParentEnvelope = true;
325
365
  }
326
366
  }
327
367
  }
328
368
  else if (!this.chain.forkChoice.hasBlockHexUnsafe(parentRoot)) {
329
- this.searchUnknownBlock({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
369
+ searchParentBlock = true;
330
370
  }
371
+ this.searchUnknownRoot({ slot, root: parentRoot }, searchParentBlock, searchParentEnvelope, peerId);
331
372
  }
332
373
  preprocessResult = { action: PreprocessAction.PushToQueue };
333
374
  }
@@ -345,7 +386,7 @@ export class NetworkProcessor {
345
386
  : getDataIndexFromSignedAggregateAndProofSerialized(message.msg.data);
346
387
  if (attIndex === 1 && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
347
388
  // attestation votes that the payload is available but it is not yet known
348
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
389
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
349
390
  preprocessResult = { action: PreprocessAction.AwaitEnvelope, root };
350
391
  }
351
392
  break;
@@ -356,7 +397,7 @@ export class NetworkProcessor {
356
397
  const payloadPresent = getPayloadPresentFromPayloadAttestationMessageSerialized(message.msg.data);
357
398
  if (payloadPresent && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
358
399
  // payload attestation votes that the payload is available but it is not yet known
359
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
400
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
360
401
  // do not await the envelope, payload attestation processing only requires that the block is known
361
402
  // also do not reset preprocessResult, we may already await for the block
362
403
  }
@@ -366,7 +407,7 @@ export class NetworkProcessor {
366
407
  if (root == null)
367
408
  break;
368
409
  if (!this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
369
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
410
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
370
411
  // do not await the envelope, we can do gossip validation
371
412
  // also do not reset preprocessResult, we may already await for the block
372
413
  }
@@ -377,7 +418,7 @@ export class NetworkProcessor {
377
418
  // Extract beacon_block_root directly
378
419
  const blockRoot = getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(message.msg.data);
379
420
  if (blockRoot && !this.chain.forkChoice.hasBlockHexUnsafe(blockRoot)) {
380
- this.searchUnknownBlock({ slot, root: blockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
421
+ this.searchUnknownRoot({ slot, root: blockRoot }, true, false, peerId);
381
422
  // We always want to await the block
382
423
  // This allows us to properly forward the payload envelope
383
424
  preprocessResult = { action: PreprocessAction.AwaitBlock, root: blockRoot };
@@ -394,12 +435,12 @@ export class NetworkProcessor {
394
435
  !this.chain.forkChoice.getBlockHexAndBlockHash(parentBlockRoot, parentBlockHash)) {
395
436
  const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentBlockRoot);
396
437
  if (protoBlock === null) {
397
- this.searchUnknownBlock({ slot, root: parentBlockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
438
+ this.searchUnknownRoot({ slot, root: parentBlockRoot }, true, false, peerId);
398
439
  preprocessResult = { action: PreprocessAction.AwaitBlock, root: parentBlockRoot };
399
440
  }
400
441
  else if (protoBlock.executionPayloadBlockHash &&
401
442
  protoBlock.executionPayloadBlockHash !== parentBlockHash) {
402
- this.searchUnknownEnvelope({ slot, root: parentBlockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
443
+ this.searchUnknownRoot({ slot, root: parentBlockRoot }, false, true, peerId);
403
444
  preprocessResult = { action: PreprocessAction.AwaitEnvelope, root: parentBlockRoot };
404
445
  }
405
446
  }
@@ -411,37 +452,96 @@ export class NetworkProcessor {
411
452
  case PreprocessAction.PushToQueue:
412
453
  this.pushPendingGossipsubMessageToQueue(message);
413
454
  break;
414
- case PreprocessAction.AwaitBlock: {
415
- if (this.awaitingBlockMessageCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
416
- // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
417
- this.metrics?.awaitingBlockGossipMessages.reject.inc({
418
- reason: ReprocessRejectReason.reached_limit,
419
- topic: topicType,
420
- });
421
- return;
422
- }
423
- this.metrics?.awaitingBlockGossipMessages.queue.inc({ topic: topicType });
424
- const awaitingGossipsubMessages = this.awaitingMessagesByBlockRoot.getOrDefault(preprocessResult.root);
425
- awaitingGossipsubMessages.add(message);
426
- this.awaitingBlockMessageCount++;
455
+ case PreprocessAction.AwaitBlock:
456
+ this.maybeAwaitBlock(preprocessResult.root, topicType, slot, message);
427
457
  break;
428
- }
429
- case PreprocessAction.AwaitEnvelope: {
430
- if (this.awaitingPayloadMessageCount > MAX_QUEUED_UNKNOWN_PAYLOAD_GOSSIP_OBJECTS) {
431
- this.metrics?.awaitingPayloadGossipMessages.reject.inc({
432
- reason: ReprocessRejectReason.reached_limit,
433
- topic: topicType,
434
- });
435
- return;
436
- }
437
- this.metrics?.awaitingPayloadGossipMessages.queue.inc({ topic: topicType });
438
- const awaitingPayloadGossipsubMessages = this.awaitingMessagesByPayloadBlockRoot.getOrDefault(preprocessResult.root);
439
- awaitingPayloadGossipsubMessages.add(message);
440
- this.awaitingPayloadMessageCount++;
458
+ case PreprocessAction.AwaitEnvelope:
459
+ this.maybeAwaitPayload(preprocessResult.root, topicType, slot, message);
441
460
  break;
442
- }
443
461
  }
444
462
  };
463
+ /**
464
+ * Buffer a gossip message that must wait for an unknown block before it can be validated.
465
+ *
466
+ * For DOS protection, the message is ignored (reject metric incremented) when any cap is hit:
467
+ * - MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS: global awaiting-block message count
468
+ * - MAX_BUFFERED_ROOTS_PER_SLOT: distinct roots we buffer messages for, per slot
469
+ * - MAX_AWAITING_MESSAGES_PER_ROOT[topic]: messages buffered per (root, topic)
470
+ */
471
+ maybeAwaitBlock(root, topicType, slot, message) {
472
+ const metric = this.metrics?.awaitingBlockGossipMessages;
473
+ // global message-count cap - cheapest check first
474
+ if (this.awaitingBlockMessageCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
475
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_limit, topic: topicType });
476
+ return;
477
+ }
478
+ // per-(root, topic) message cap: e.g. <= NUMBER_OF_COLUMNS columns per root
479
+ const perRootCap = MAX_AWAITING_MESSAGES_PER_ROOT[topicType];
480
+ if (perRootCap !== undefined &&
481
+ (this.awaitingMessagesByBlockRoot.get(root)?.get(topicType)?.size ?? 0) >= perRootCap) {
482
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_topic_limit, topic: topicType });
483
+ return;
484
+ }
485
+ if (!this.tryReserveBufferedRoot(slot, root)) {
486
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_root_limit, topic: topicType });
487
+ return;
488
+ }
489
+ metric?.queue.inc({ topic: topicType });
490
+ this.awaitingMessagesByBlockRoot.getOrDefault(root).getOrDefault(topicType).add(message);
491
+ this.awaitingBlockMessageCount++;
492
+ }
493
+ /** Payload counterpart of maybeAwaitBlock */
494
+ maybeAwaitPayload(root, topicType, slot, message) {
495
+ const metric = this.metrics?.awaitingPayloadGossipMessages;
496
+ if (this.awaitingPayloadMessageCount > MAX_QUEUED_UNKNOWN_PAYLOAD_GOSSIP_OBJECTS) {
497
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_limit, topic: topicType });
498
+ return;
499
+ }
500
+ const perRootCap = MAX_AWAITING_MESSAGES_PER_ROOT[topicType];
501
+ if (perRootCap !== undefined &&
502
+ (this.awaitingMessagesByPayloadBlockRoot.get(root)?.get(topicType)?.size ?? 0) >= perRootCap) {
503
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_topic_limit, topic: topicType });
504
+ return;
505
+ }
506
+ if (!this.tryReserveBufferedRoot(slot, root)) {
507
+ metric?.reject.inc({ reason: ReprocessRejectReason.reached_root_limit, topic: topicType });
508
+ return;
509
+ }
510
+ metric?.queue.inc({ topic: topicType });
511
+ this.awaitingMessagesByPayloadBlockRoot.getOrDefault(root).getOrDefault(topicType).add(message);
512
+ this.awaitingPayloadMessageCount++;
513
+ }
514
+ /**
515
+ * Cap distinct roots we emit an unknown-root search for, an already-tracked root (dedup) never counts as "too many".
516
+ */
517
+ tooManySearchedRoots(slot, root) {
518
+ const roots = this.searchedRootsBySlot.get(slot);
519
+ if (roots === undefined)
520
+ return false;
521
+ return !roots.has(root) && roots.size >= MAX_SEARCHED_ROOTS_PER_SLOT;
522
+ }
523
+ /**
524
+ * Cap distinct roots we buffer per slot.
525
+ */
526
+ tryReserveBufferedRoot(slot, root) {
527
+ const roots = this.bufferedRootsBySlot.get(slot);
528
+ if (roots?.has(root))
529
+ return true;
530
+ if ((roots?.size ?? 0) >= MAX_BUFFERED_ROOTS_PER_SLOT)
531
+ return false;
532
+ this.bufferedRootsBySlot.getOrDefault(slot).add(root);
533
+ return true;
534
+ }
535
+ searchUnknownRoot(slotRoot, searchBlock, searchEnvelope, peerId) {
536
+ if (!searchBlock && !searchEnvelope)
537
+ return;
538
+ if (this.tooManySearchedRoots(slotRoot.slot, slotRoot.root))
539
+ return;
540
+ if (searchBlock)
541
+ this.searchUnknownBlock(slotRoot, BlockInputSource.network_processor, peerId);
542
+ if (searchEnvelope)
543
+ this.searchUnknownEnvelope(slotRoot, BlockInputSource.network_processor, peerId);
544
+ }
445
545
  pushPendingGossipsubMessageToQueue(message) {
446
546
  const topicType = message.topic.type;
447
547
  message.queueAddedMs = Date.now();
@@ -454,66 +554,92 @@ export class NetworkProcessor {
454
554
  this.executeWork();
455
555
  }
456
556
  onBlockProcessed = async ({ block: rootHex }) => {
457
- const waitingGossipsubMessages = this.awaitingMessagesByBlockRoot.get(rootHex);
458
- if (!waitingGossipsubMessages || waitingGossipsubMessages.size === 0) {
557
+ const messagesByTopic = this.awaitingMessagesByBlockRoot.get(rootHex);
558
+ if (messagesByTopic === undefined) {
559
+ return;
560
+ }
561
+ let total = 0;
562
+ for (const messages of messagesByTopic.values()) {
563
+ total += messages.size;
564
+ }
565
+ if (total === 0) {
459
566
  return;
460
567
  }
461
568
  // Atomically remove from map and update counter before async iteration to
462
569
  // prevent double-decrement race with onClockSlot during yield points below
463
570
  if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
464
- this.awaitingBlockMessageCount -= waitingGossipsubMessages.size;
571
+ this.awaitingBlockMessageCount -= total;
465
572
  }
466
573
  const nowSec = Date.now() / 1000;
467
574
  let count = 0;
468
575
  // TODO: we can group attestations to process in batches but since we have the SeenAttestationDatas
469
576
  // cache, it may not be necessary at this time
470
- for (const message of waitingGossipsubMessages) {
471
- const topicType = message.topic.type;
472
- this.metrics?.awaitingBlockGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
473
- this.metrics?.awaitingBlockGossipMessages.resolve.inc({ topic: topicType });
474
- this.pushPendingGossipsubMessageToQueue(message);
475
- count++;
476
- // don't want to block the event loop, worse case it'd wait for 16_084 / 1024 * 50ms = 800ms which is not a big deal
477
- if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
478
- count = 0;
479
- await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
577
+ for (const messages of messagesByTopic.values()) {
578
+ for (const message of messages) {
579
+ const topicType = message.topic.type;
580
+ this.metrics?.awaitingBlockGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
581
+ this.metrics?.awaitingBlockGossipMessages.resolve.inc({ topic: topicType });
582
+ this.pushPendingGossipsubMessageToQueue(message);
583
+ count++;
584
+ // don't want to block the event loop, worse case it'd wait for 16_084 / 1024 * 50ms = 800ms which is not a big deal
585
+ if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
586
+ count = 0;
587
+ await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
588
+ }
480
589
  }
481
590
  }
482
591
  };
483
592
  onPayloadEnvelopeProcessed = async ({ blockRoot: rootHex }) => {
484
- const waitingGossipsubMessages = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
485
- if (!waitingGossipsubMessages || waitingGossipsubMessages.size === 0) {
593
+ const messagesByTopic = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
594
+ if (messagesByTopic === undefined) {
595
+ return;
596
+ }
597
+ let total = 0;
598
+ for (const messages of messagesByTopic.values()) {
599
+ total += messages.size;
600
+ }
601
+ if (total === 0) {
486
602
  return;
487
603
  }
488
604
  // Atomically remove from map and update counter before async iteration to
489
605
  // prevent double-decrement race with onClockSlot during yield points below
490
606
  if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
491
- this.awaitingPayloadMessageCount -= waitingGossipsubMessages.size;
607
+ this.awaitingPayloadMessageCount -= total;
492
608
  }
493
609
  const nowSec = Date.now() / 1000;
494
610
  let count = 0;
495
- for (const message of waitingGossipsubMessages) {
496
- const topicType = message.topic.type;
497
- this.metrics?.awaitingPayloadGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
498
- this.metrics?.awaitingPayloadGossipMessages.resolve.inc({ topic: topicType });
499
- this.pushPendingGossipsubMessageToQueue(message);
500
- count++;
501
- if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
502
- count = 0;
503
- await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
611
+ for (const messages of messagesByTopic.values()) {
612
+ for (const message of messages) {
613
+ const topicType = message.topic.type;
614
+ this.metrics?.awaitingPayloadGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
615
+ this.metrics?.awaitingPayloadGossipMessages.resolve.inc({ topic: topicType });
616
+ this.pushPendingGossipsubMessageToQueue(message);
617
+ count++;
618
+ if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
619
+ count = 0;
620
+ await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
621
+ }
504
622
  }
505
623
  }
506
624
  };
507
625
  onClockSlot = (clockSlot) => {
508
626
  const nowSec = Date.now() / 1000;
509
627
  const minSlot = clockSlot - MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE;
510
- for (const [slot, roots] of this.unknownBlocksBySlot) {
628
+ for (const [slot, searchedRoots] of this.searchedRootsBySlot) {
511
629
  if (slot > minSlot)
512
630
  continue;
513
- for (const rootHex of roots.keys()) {
514
- const gossipMessages = this.awaitingMessagesByBlockRoot.get(rootHex);
515
- if (gossipMessages !== undefined) {
516
- for (const message of gossipMessages) {
631
+ // expire messages awaiting an unknown block for these roots. Only roots this slot did a BLOCK search
632
+ // for (blockPeerIds set) have block waits keyed here; skipping the rest avoids deleting a payload wait
633
+ // that a newer slot created for the same root (block/envelope searches share searchedRootsBySlot).
634
+ for (const [rootHex, entry] of searchedRoots) {
635
+ if (entry.blockPeerIds === undefined)
636
+ continue;
637
+ const messagesByTopic = this.awaitingMessagesByBlockRoot.get(rootHex);
638
+ if (messagesByTopic === undefined)
639
+ continue;
640
+ let removed = 0;
641
+ for (const messages of messagesByTopic.values()) {
642
+ for (const message of messages) {
517
643
  const topicType = message.topic.type;
518
644
  this.metrics?.awaitingBlockGossipMessages.reject.inc({
519
645
  topic: topicType,
@@ -522,20 +648,23 @@ export class NetworkProcessor {
522
648
  this.metrics?.awaitingBlockGossipMessages.waitSecBeforeReject.set({ topic: topicType, reason: ReprocessRejectReason.expired }, nowSec - message.seenTimestampSec);
523
649
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
524
650
  }
525
- if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
526
- this.awaitingBlockMessageCount -= gossipMessages.size;
527
- }
651
+ removed += messages.size;
652
+ }
653
+ if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
654
+ this.awaitingBlockMessageCount -= removed;
528
655
  }
529
656
  }
530
- this.unknownBlocksBySlot.delete(slot);
531
- }
532
- for (const [slot, roots] of this.unknownEnvelopesBySlot) {
533
- if (slot > minSlot)
534
- continue;
535
- for (const rootHex of roots.keys()) {
536
- const gossipMessages = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
537
- if (gossipMessages !== undefined) {
538
- for (const message of gossipMessages) {
657
+ // expire messages awaiting an unknown payload envelope for these roots. Symmetric to the block pass:
658
+ // only roots this slot did an ENVELOPE search for (envelopePeerIds set) have payload waits keyed here.
659
+ for (const [rootHex, entry] of searchedRoots) {
660
+ if (entry.envelopePeerIds === undefined)
661
+ continue;
662
+ const messagesByTopic = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
663
+ if (messagesByTopic === undefined)
664
+ continue;
665
+ let removed = 0;
666
+ for (const messages of messagesByTopic.values()) {
667
+ for (const message of messages) {
539
668
  const topicType = message.topic.type;
540
669
  this.metrics?.awaitingPayloadGossipMessages.reject.inc({
541
670
  topic: topicType,
@@ -544,12 +673,17 @@ export class NetworkProcessor {
544
673
  this.metrics?.awaitingPayloadGossipMessages.waitSecBeforeReject.set({ topic: topicType, reason: ReprocessRejectReason.expired }, nowSec - message.seenTimestampSec);
545
674
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
546
675
  }
547
- if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
548
- this.awaitingPayloadMessageCount -= gossipMessages.size;
549
- }
676
+ removed += messages.size;
677
+ }
678
+ if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
679
+ this.awaitingPayloadMessageCount -= removed;
550
680
  }
551
681
  }
552
- this.unknownEnvelopesBySlot.delete(slot);
682
+ this.searchedRootsBySlot.delete(slot);
683
+ }
684
+ for (const slot of this.bufferedRootsBySlot.keys()) {
685
+ if (slot <= minSlot)
686
+ this.bufferedRootsBySlot.delete(slot);
553
687
  }
554
688
  };
555
689
  executeWork() {