@lodestar/beacon-node 1.48.0-dev.e1303d42b0 → 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 (90) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +39 -39
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  5. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  6. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  7. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  8. package/lib/chain/blocks/importBlock.js +0 -13
  9. package/lib/chain/blocks/importBlock.js.map +1 -1
  10. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  11. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  12. package/lib/chain/blocks/utils/chainSegment.js +10 -1
  13. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  14. package/lib/chain/chain.d.ts +5 -0
  15. package/lib/chain/chain.d.ts.map +1 -1
  16. package/lib/chain/chain.js +67 -38
  17. package/lib/chain/chain.js.map +1 -1
  18. package/lib/chain/emitter.d.ts +1 -1
  19. package/lib/chain/emitter.js +1 -1
  20. package/lib/chain/errors/blockError.d.ts +3 -5
  21. package/lib/chain/errors/blockError.d.ts.map +1 -1
  22. package/lib/chain/errors/blockError.js +2 -2
  23. package/lib/chain/errors/blockError.js.map +1 -1
  24. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  25. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  26. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  27. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  28. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  29. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  30. package/lib/chain/errors/gossipValidation.js +1 -0
  31. package/lib/chain/errors/gossipValidation.js.map +1 -1
  32. package/lib/chain/prepareNextSlot.js +13 -13
  33. package/lib/chain/prepareNextSlot.js.map +1 -1
  34. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  35. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  36. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  37. package/lib/chain/validation/block.d.ts.map +1 -1
  38. package/lib/chain/validation/block.js +9 -50
  39. package/lib/chain/validation/block.js.map +1 -1
  40. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  41. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  42. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  43. package/lib/chain/validatorMonitor.d.ts +7 -1
  44. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  45. package/lib/chain/validatorMonitor.js +22 -5
  46. package/lib/chain/validatorMonitor.js.map +1 -1
  47. package/lib/execution/engine/mock.d.ts.map +1 -1
  48. package/lib/execution/engine/mock.js +4 -1
  49. package/lib/execution/engine/mock.js.map +1 -1
  50. package/lib/network/gossip/encoding.d.ts.map +1 -1
  51. package/lib/network/gossip/encoding.js +0 -6
  52. package/lib/network/gossip/encoding.js.map +1 -1
  53. package/lib/network/gossip/topic.d.ts +2 -1
  54. package/lib/network/gossip/topic.d.ts.map +1 -1
  55. package/lib/network/gossip/topic.js +4 -20
  56. package/lib/network/gossip/topic.js.map +1 -1
  57. package/lib/network/processor/index.d.ts +49 -3
  58. package/lib/network/processor/index.d.ts.map +1 -1
  59. package/lib/network/processor/index.js +243 -98
  60. package/lib/network/processor/index.js.map +1 -1
  61. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  62. package/lib/network/reqresp/protocols.js +6 -22
  63. package/lib/network/reqresp/protocols.js.map +1 -1
  64. package/lib/network/reqresp/rateLimit.js +2 -1
  65. package/lib/network/reqresp/rateLimit.js.map +1 -1
  66. package/lib/sync/range/range.d.ts.map +1 -1
  67. package/lib/sync/range/range.js +3 -1
  68. package/lib/sync/range/range.js.map +1 -1
  69. package/package.json +15 -15
  70. package/src/api/impl/validator/index.ts +44 -43
  71. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  72. package/src/chain/blocks/importBlock.ts +0 -13
  73. package/src/chain/blocks/utils/chainSegment.ts +10 -1
  74. package/src/chain/chain.ts +74 -46
  75. package/src/chain/emitter.ts +1 -1
  76. package/src/chain/errors/blockError.ts +3 -3
  77. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  78. package/src/chain/errors/gossipValidation.ts +1 -0
  79. package/src/chain/prepareNextSlot.ts +16 -16
  80. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  81. package/src/chain/validation/block.ts +9 -66
  82. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  83. package/src/chain/validatorMonitor.ts +24 -7
  84. package/src/execution/engine/mock.ts +4 -1
  85. package/src/network/gossip/encoding.ts +0 -6
  86. package/src/network/gossip/topic.ts +3 -25
  87. package/src/network/processor/index.ts +276 -148
  88. package/src/network/reqresp/protocols.ts +7 -24
  89. package/src/network/reqresp/rateLimit.ts +2 -1
  90. package/src/sync/range/range.ts +3 -1
@@ -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.
@@ -280,14 +316,26 @@ export class NetworkProcessor {
280
316
  });
281
317
  return;
282
318
  }
319
+ // DOS protection: drop far-future messages before allocating any unknown-root tracking entry.
320
+ // Bound each topic to what its gossip validator accepts; only execution_payload_bid accepts the next slot.
321
+ const latestPermissableSlot = this.chain.clock.currentSlotWithGossipDisparity + (topicType === GossipType.execution_payload_bid ? 1 : 0);
322
+ if (slot > latestPermissableSlot) {
323
+ // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
324
+ this.metrics?.networkProcessor.gossipValidationError.inc({
325
+ topic: topicType,
326
+ error: GossipErrorCode.FUTURE_SLOT,
327
+ });
328
+ return;
329
+ }
283
330
  message.msgSlot = slot;
331
+ const peerId = message.propagationSource.toString();
284
332
  // this determines whether this message needs to wait for a Block or Envelope
285
- // 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.
286
334
  let preprocessResult = { action: PreprocessAction.PushToQueue };
287
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
288
336
  if (root && !this.chain.forkChoice.hasBlockHexUnsafe(root)) {
289
337
  // starting from GLOAS, unknown root from data_column_sidecar also falls into this case
290
- this.searchUnknownBlock({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
338
+ this.searchUnknownRoot({ slot, root }, true, false, peerId);
291
339
  // for beacon_attestation and beacon_aggregate_and_proof messages, this is only temporary.
292
340
  // if "index = 1" we need to await for the Envelope instead
293
341
  preprocessResult = { action: PreprocessAction.AwaitBlock, root };
@@ -295,28 +343,32 @@ export class NetworkProcessor {
295
343
  // 2nd extract round for some specific topics
296
344
  // we separate the search action from the await action
297
345
  // beacon_block: proactively search for parent block/envelope across all forks, but never queue.
298
- // 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).
299
348
  if (topicType === GossipType.beacon_block) {
300
349
  const parentRoot = getParentRootFromSignedBeaconBlockSerialized(message.msg.data);
301
350
  if (parentRoot) {
351
+ let searchParentBlock = false;
352
+ let searchParentEnvelope = false;
302
353
  if (ForkSeq[fork] >= ForkSeq.gloas) {
303
354
  // GLOAS: also check parent envelope, same logic as execution_payload_bid
304
355
  const parentBlockHash = getParentBlockHashFromGloasSignedBeaconBlockSerialized(message.msg.data);
305
356
  if (parentBlockHash && !this.chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash)) {
306
357
  const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentRoot);
307
358
  if (protoBlock === null) {
308
- this.searchUnknownBlock({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
359
+ searchParentBlock = true;
309
360
  }
310
361
  else if (protoBlock.executionPayloadBlockHash &&
311
362
  protoBlock.executionPayloadBlockHash !== parentBlockHash) {
312
363
  // only search for the envelope by block root if we're sure there is one. Otherwise UnknownBlockSync will penalize the peer.
313
- this.searchUnknownEnvelope({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
364
+ searchParentEnvelope = true;
314
365
  }
315
366
  }
316
367
  }
317
368
  else if (!this.chain.forkChoice.hasBlockHexUnsafe(parentRoot)) {
318
- this.searchUnknownBlock({ slot, root: parentRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
369
+ searchParentBlock = true;
319
370
  }
371
+ this.searchUnknownRoot({ slot, root: parentRoot }, searchParentBlock, searchParentEnvelope, peerId);
320
372
  }
321
373
  preprocessResult = { action: PreprocessAction.PushToQueue };
322
374
  }
@@ -334,7 +386,7 @@ export class NetworkProcessor {
334
386
  : getDataIndexFromSignedAggregateAndProofSerialized(message.msg.data);
335
387
  if (attIndex === 1 && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
336
388
  // attestation votes that the payload is available but it is not yet known
337
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
389
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
338
390
  preprocessResult = { action: PreprocessAction.AwaitEnvelope, root };
339
391
  }
340
392
  break;
@@ -345,7 +397,7 @@ export class NetworkProcessor {
345
397
  const payloadPresent = getPayloadPresentFromPayloadAttestationMessageSerialized(message.msg.data);
346
398
  if (payloadPresent && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
347
399
  // payload attestation votes that the payload is available but it is not yet known
348
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
400
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
349
401
  // do not await the envelope, payload attestation processing only requires that the block is known
350
402
  // also do not reset preprocessResult, we may already await for the block
351
403
  }
@@ -355,7 +407,7 @@ export class NetworkProcessor {
355
407
  if (root == null)
356
408
  break;
357
409
  if (!this.chain.forkChoice.hasPayloadHexUnsafe(root)) {
358
- this.searchUnknownEnvelope({ slot, root }, BlockInputSource.network_processor, message.propagationSource.toString());
410
+ this.searchUnknownRoot({ slot, root }, false, true, peerId);
359
411
  // do not await the envelope, we can do gossip validation
360
412
  // also do not reset preprocessResult, we may already await for the block
361
413
  }
@@ -366,7 +418,7 @@ export class NetworkProcessor {
366
418
  // Extract beacon_block_root directly
367
419
  const blockRoot = getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(message.msg.data);
368
420
  if (blockRoot && !this.chain.forkChoice.hasBlockHexUnsafe(blockRoot)) {
369
- this.searchUnknownBlock({ slot, root: blockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
421
+ this.searchUnknownRoot({ slot, root: blockRoot }, true, false, peerId);
370
422
  // We always want to await the block
371
423
  // This allows us to properly forward the payload envelope
372
424
  preprocessResult = { action: PreprocessAction.AwaitBlock, root: blockRoot };
@@ -383,12 +435,12 @@ export class NetworkProcessor {
383
435
  !this.chain.forkChoice.getBlockHexAndBlockHash(parentBlockRoot, parentBlockHash)) {
384
436
  const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentBlockRoot);
385
437
  if (protoBlock === null) {
386
- this.searchUnknownBlock({ slot, root: parentBlockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
438
+ this.searchUnknownRoot({ slot, root: parentBlockRoot }, true, false, peerId);
387
439
  preprocessResult = { action: PreprocessAction.AwaitBlock, root: parentBlockRoot };
388
440
  }
389
441
  else if (protoBlock.executionPayloadBlockHash &&
390
442
  protoBlock.executionPayloadBlockHash !== parentBlockHash) {
391
- this.searchUnknownEnvelope({ slot, root: parentBlockRoot }, BlockInputSource.network_processor, message.propagationSource.toString());
443
+ this.searchUnknownRoot({ slot, root: parentBlockRoot }, false, true, peerId);
392
444
  preprocessResult = { action: PreprocessAction.AwaitEnvelope, root: parentBlockRoot };
393
445
  }
394
446
  }
@@ -400,37 +452,96 @@ export class NetworkProcessor {
400
452
  case PreprocessAction.PushToQueue:
401
453
  this.pushPendingGossipsubMessageToQueue(message);
402
454
  break;
403
- case PreprocessAction.AwaitBlock: {
404
- if (this.awaitingBlockMessageCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
405
- // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
406
- this.metrics?.awaitingBlockGossipMessages.reject.inc({
407
- reason: ReprocessRejectReason.reached_limit,
408
- topic: topicType,
409
- });
410
- return;
411
- }
412
- this.metrics?.awaitingBlockGossipMessages.queue.inc({ topic: topicType });
413
- const awaitingGossipsubMessages = this.awaitingMessagesByBlockRoot.getOrDefault(preprocessResult.root);
414
- awaitingGossipsubMessages.add(message);
415
- this.awaitingBlockMessageCount++;
455
+ case PreprocessAction.AwaitBlock:
456
+ this.maybeAwaitBlock(preprocessResult.root, topicType, slot, message);
416
457
  break;
417
- }
418
- case PreprocessAction.AwaitEnvelope: {
419
- if (this.awaitingPayloadMessageCount > MAX_QUEUED_UNKNOWN_PAYLOAD_GOSSIP_OBJECTS) {
420
- this.metrics?.awaitingPayloadGossipMessages.reject.inc({
421
- reason: ReprocessRejectReason.reached_limit,
422
- topic: topicType,
423
- });
424
- return;
425
- }
426
- this.metrics?.awaitingPayloadGossipMessages.queue.inc({ topic: topicType });
427
- const awaitingPayloadGossipsubMessages = this.awaitingMessagesByPayloadBlockRoot.getOrDefault(preprocessResult.root);
428
- awaitingPayloadGossipsubMessages.add(message);
429
- this.awaitingPayloadMessageCount++;
458
+ case PreprocessAction.AwaitEnvelope:
459
+ this.maybeAwaitPayload(preprocessResult.root, topicType, slot, message);
430
460
  break;
431
- }
432
461
  }
433
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
+ }
434
545
  pushPendingGossipsubMessageToQueue(message) {
435
546
  const topicType = message.topic.type;
436
547
  message.queueAddedMs = Date.now();
@@ -443,66 +554,92 @@ export class NetworkProcessor {
443
554
  this.executeWork();
444
555
  }
445
556
  onBlockProcessed = async ({ block: rootHex }) => {
446
- const waitingGossipsubMessages = this.awaitingMessagesByBlockRoot.get(rootHex);
447
- 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) {
448
566
  return;
449
567
  }
450
568
  // Atomically remove from map and update counter before async iteration to
451
569
  // prevent double-decrement race with onClockSlot during yield points below
452
570
  if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
453
- this.awaitingBlockMessageCount -= waitingGossipsubMessages.size;
571
+ this.awaitingBlockMessageCount -= total;
454
572
  }
455
573
  const nowSec = Date.now() / 1000;
456
574
  let count = 0;
457
575
  // TODO: we can group attestations to process in batches but since we have the SeenAttestationDatas
458
576
  // cache, it may not be necessary at this time
459
- for (const message of waitingGossipsubMessages) {
460
- const topicType = message.topic.type;
461
- this.metrics?.awaitingBlockGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
462
- this.metrics?.awaitingBlockGossipMessages.resolve.inc({ topic: topicType });
463
- this.pushPendingGossipsubMessageToQueue(message);
464
- count++;
465
- // 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
466
- if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
467
- count = 0;
468
- 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
+ }
469
589
  }
470
590
  }
471
591
  };
472
592
  onPayloadEnvelopeProcessed = async ({ blockRoot: rootHex }) => {
473
- const waitingGossipsubMessages = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
474
- 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) {
475
602
  return;
476
603
  }
477
604
  // Atomically remove from map and update counter before async iteration to
478
605
  // prevent double-decrement race with onClockSlot during yield points below
479
606
  if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
480
- this.awaitingPayloadMessageCount -= waitingGossipsubMessages.size;
607
+ this.awaitingPayloadMessageCount -= total;
481
608
  }
482
609
  const nowSec = Date.now() / 1000;
483
610
  let count = 0;
484
- for (const message of waitingGossipsubMessages) {
485
- const topicType = message.topic.type;
486
- this.metrics?.awaitingPayloadGossipMessages.waitSecBeforeResolve.set({ topic: topicType }, nowSec - message.seenTimestampSec);
487
- this.metrics?.awaitingPayloadGossipMessages.resolve.inc({ topic: topicType });
488
- this.pushPendingGossipsubMessageToQueue(message);
489
- count++;
490
- if (count === MAX_AWAITING_GOSSIP_OBJECTS_PER_TICK) {
491
- count = 0;
492
- 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
+ }
493
622
  }
494
623
  }
495
624
  };
496
625
  onClockSlot = (clockSlot) => {
497
626
  const nowSec = Date.now() / 1000;
498
627
  const minSlot = clockSlot - MAX_UNKNOWN_ROOTS_SLOT_CACHE_SIZE;
499
- for (const [slot, roots] of this.unknownBlocksBySlot) {
628
+ for (const [slot, searchedRoots] of this.searchedRootsBySlot) {
500
629
  if (slot > minSlot)
501
630
  continue;
502
- for (const rootHex of roots.keys()) {
503
- const gossipMessages = this.awaitingMessagesByBlockRoot.get(rootHex);
504
- if (gossipMessages !== undefined) {
505
- 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) {
506
643
  const topicType = message.topic.type;
507
644
  this.metrics?.awaitingBlockGossipMessages.reject.inc({
508
645
  topic: topicType,
@@ -511,20 +648,23 @@ export class NetworkProcessor {
511
648
  this.metrics?.awaitingBlockGossipMessages.waitSecBeforeReject.set({ topic: topicType, reason: ReprocessRejectReason.expired }, nowSec - message.seenTimestampSec);
512
649
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
513
650
  }
514
- if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
515
- this.awaitingBlockMessageCount -= gossipMessages.size;
516
- }
651
+ removed += messages.size;
652
+ }
653
+ if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
654
+ this.awaitingBlockMessageCount -= removed;
517
655
  }
518
656
  }
519
- this.unknownBlocksBySlot.delete(slot);
520
- }
521
- for (const [slot, roots] of this.unknownEnvelopesBySlot) {
522
- if (slot > minSlot)
523
- continue;
524
- for (const rootHex of roots.keys()) {
525
- const gossipMessages = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
526
- if (gossipMessages !== undefined) {
527
- 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) {
528
668
  const topicType = message.topic.type;
529
669
  this.metrics?.awaitingPayloadGossipMessages.reject.inc({
530
670
  topic: topicType,
@@ -533,12 +673,17 @@ export class NetworkProcessor {
533
673
  this.metrics?.awaitingPayloadGossipMessages.waitSecBeforeReject.set({ topic: topicType, reason: ReprocessRejectReason.expired }, nowSec - message.seenTimestampSec);
534
674
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
535
675
  }
536
- if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
537
- this.awaitingPayloadMessageCount -= gossipMessages.size;
538
- }
676
+ removed += messages.size;
677
+ }
678
+ if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
679
+ this.awaitingPayloadMessageCount -= removed;
539
680
  }
540
681
  }
541
- 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);
542
687
  }
543
688
  };
544
689
  executeWork() {