@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.
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +39 -39
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
- package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
- package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +0 -13
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.js +10 -1
- package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
- package/lib/chain/chain.d.ts +5 -0
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +67 -38
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/emitter.d.ts +1 -1
- package/lib/chain/emitter.js +1 -1
- package/lib/chain/errors/blockError.d.ts +3 -5
- package/lib/chain/errors/blockError.d.ts.map +1 -1
- package/lib/chain/errors/blockError.js +2 -2
- package/lib/chain/errors/blockError.js.map +1 -1
- package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
- package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
- package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
- package/lib/chain/errors/gossipValidation.d.ts +2 -1
- package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
- package/lib/chain/errors/gossipValidation.js +1 -0
- package/lib/chain/errors/gossipValidation.js.map +1 -1
- package/lib/chain/prepareNextSlot.js +13 -13
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +46 -64
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +9 -50
- package/lib/chain/validation/block.js.map +1 -1
- package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
- package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
- package/lib/chain/validatorMonitor.d.ts +7 -1
- package/lib/chain/validatorMonitor.d.ts.map +1 -1
- package/lib/chain/validatorMonitor.js +22 -5
- package/lib/chain/validatorMonitor.js.map +1 -1
- package/lib/execution/engine/mock.d.ts.map +1 -1
- package/lib/execution/engine/mock.js +4 -1
- package/lib/execution/engine/mock.js.map +1 -1
- package/lib/network/gossip/encoding.d.ts.map +1 -1
- package/lib/network/gossip/encoding.js +0 -6
- package/lib/network/gossip/encoding.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +2 -1
- package/lib/network/gossip/topic.d.ts.map +1 -1
- package/lib/network/gossip/topic.js +4 -20
- package/lib/network/gossip/topic.js.map +1 -1
- package/lib/network/processor/index.d.ts +49 -3
- package/lib/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +243 -98
- package/lib/network/processor/index.js.map +1 -1
- package/lib/network/reqresp/protocols.d.ts.map +1 -1
- package/lib/network/reqresp/protocols.js +6 -22
- package/lib/network/reqresp/protocols.js.map +1 -1
- package/lib/network/reqresp/rateLimit.js +2 -1
- package/lib/network/reqresp/rateLimit.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +3 -1
- package/lib/sync/range/range.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/validator/index.ts +44 -43
- package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
- package/src/chain/blocks/importBlock.ts +0 -13
- package/src/chain/blocks/utils/chainSegment.ts +10 -1
- package/src/chain/chain.ts +74 -46
- package/src/chain/emitter.ts +1 -1
- package/src/chain/errors/blockError.ts +3 -3
- package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
- package/src/chain/errors/gossipValidation.ts +1 -0
- package/src/chain/prepareNextSlot.ts +16 -16
- package/src/chain/produceBlock/produceBlockBody.ts +50 -71
- package/src/chain/validation/block.ts +9 -66
- package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
- package/src/chain/validatorMonitor.ts +24 -7
- package/src/execution/engine/mock.ts +4 -1
- package/src/network/gossip/encoding.ts +0 -6
- package/src/network/gossip/topic.ts +3 -25
- package/src/network/processor/index.ts +276 -148
- package/src/network/reqresp/protocols.ts +7 -24
- package/src/network/reqresp/rateLimit.ts +2 -1
- 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
|
-
|
|
143
|
-
|
|
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
|
|
214
|
-
const alreadySearching =
|
|
215
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
364
|
+
searchParentEnvelope = true;
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
}
|
|
317
368
|
else if (!this.chain.forkChoice.hasBlockHexUnsafe(parentRoot)) {
|
|
318
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
447
|
-
if (
|
|
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 -=
|
|
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
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
count
|
|
468
|
-
|
|
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
|
|
474
|
-
if (
|
|
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 -=
|
|
607
|
+
this.awaitingPayloadMessageCount -= total;
|
|
481
608
|
}
|
|
482
609
|
const nowSec = Date.now() / 1000;
|
|
483
610
|
let count = 0;
|
|
484
|
-
for (const
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
count
|
|
492
|
-
|
|
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,
|
|
628
|
+
for (const [slot, searchedRoots] of this.searchedRootsBySlot) {
|
|
500
629
|
if (slot > minSlot)
|
|
501
630
|
continue;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
651
|
+
removed += messages.size;
|
|
652
|
+
}
|
|
653
|
+
if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
|
|
654
|
+
this.awaitingBlockMessageCount -= removed;
|
|
517
655
|
}
|
|
518
656
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
676
|
+
removed += messages.size;
|
|
677
|
+
}
|
|
678
|
+
if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
|
|
679
|
+
this.awaitingPayloadMessageCount -= removed;
|
|
539
680
|
}
|
|
540
681
|
}
|
|
541
|
-
this.
|
|
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() {
|