@lodestar/beacon-node 1.50.0-dev.f2d839a25c → 1.50.0-dev.fd4f342a40

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 (124) hide show
  1. package/lib/api/impl/beacon/pool/index.js +4 -4
  2. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  3. package/lib/chain/archiveStore/archiveStore.d.ts +1 -0
  4. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  5. package/lib/chain/archiveStore/archiveStore.js +6 -2
  6. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  7. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +4 -1
  8. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  9. package/lib/chain/archiveStore/utils/pruneHistory.js +4 -3
  10. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +4 -0
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  16. package/lib/chain/chain.js +3 -3
  17. package/lib/chain/chain.js.map +1 -1
  18. package/lib/chain/errors/gossipValidation.d.ts +3 -1
  19. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  20. package/lib/chain/errors/gossipValidation.js +2 -0
  21. package/lib/chain/errors/gossipValidation.js.map +1 -1
  22. package/lib/chain/initState.d.ts +2 -9
  23. package/lib/chain/initState.d.ts.map +1 -1
  24. package/lib/chain/initState.js +5 -38
  25. package/lib/chain/initState.js.map +1 -1
  26. package/lib/chain/opPools/opPool.d.ts +5 -4
  27. package/lib/chain/opPools/opPool.d.ts.map +1 -1
  28. package/lib/chain/opPools/opPool.js +62 -18
  29. package/lib/chain/opPools/opPool.js.map +1 -1
  30. package/lib/chain/opPools/utils.d.ts +4 -1
  31. package/lib/chain/opPools/utils.d.ts.map +1 -1
  32. package/lib/chain/opPools/utils.js +11 -1
  33. package/lib/chain/opPools/utils.js.map +1 -1
  34. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  35. package/lib/chain/prepareNextSlot.js +5 -2
  36. package/lib/chain/prepareNextSlot.js.map +1 -1
  37. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  38. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  39. package/lib/chain/produceBlock/produceBlockBody.js +6 -2
  40. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  41. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +6 -0
  42. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  43. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +10 -0
  44. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  45. package/lib/chain/validation/attesterSlashing.d.ts +4 -4
  46. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  47. package/lib/chain/validation/attesterSlashing.js +3 -0
  48. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  49. package/lib/chain/validation/proposerSlashing.d.ts +3 -3
  50. package/lib/chain/validation/proposerSlashing.d.ts.map +1 -1
  51. package/lib/chain/validation/proposerSlashing.js +3 -0
  52. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  53. package/lib/db/beacon.d.ts.map +1 -1
  54. package/lib/db/beacon.js +2 -0
  55. package/lib/db/beacon.js.map +1 -1
  56. package/lib/db/repositories/proposerSlashing.d.ts +1 -0
  57. package/lib/db/repositories/proposerSlashing.d.ts.map +1 -1
  58. package/lib/db/repositories/proposerSlashing.js +5 -1
  59. package/lib/db/repositories/proposerSlashing.js.map +1 -1
  60. package/lib/index.d.ts +1 -1
  61. package/lib/index.d.ts.map +1 -1
  62. package/lib/index.js +1 -1
  63. package/lib/index.js.map +1 -1
  64. package/lib/network/metadata.d.ts +1 -1
  65. package/lib/network/metadata.d.ts.map +1 -1
  66. package/lib/network/metadata.js +17 -17
  67. package/lib/network/metadata.js.map +1 -1
  68. package/lib/network/peers/datastore.d.ts +9 -9
  69. package/lib/network/peers/datastore.d.ts.map +1 -1
  70. package/lib/network/peers/datastore.js +36 -36
  71. package/lib/network/peers/datastore.js.map +1 -1
  72. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  73. package/lib/network/processor/gossipHandlers.js +7 -4
  74. package/lib/network/processor/gossipHandlers.js.map +1 -1
  75. package/lib/network/processor/gossipValidatorFn.d.ts +7 -3
  76. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  77. package/lib/network/processor/gossipValidatorFn.js +7 -3
  78. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  79. package/lib/network/processor/index.d.ts +9 -0
  80. package/lib/network/processor/index.d.ts.map +1 -1
  81. package/lib/network/processor/index.js +77 -10
  82. package/lib/network/processor/index.js.map +1 -1
  83. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  84. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +41 -26
  85. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  86. package/lib/sync/range/range.d.ts.map +1 -1
  87. package/lib/sync/range/range.js +50 -18
  88. package/lib/sync/range/range.js.map +1 -1
  89. package/lib/sync/unknownBlock.d.ts +9 -5
  90. package/lib/sync/unknownBlock.d.ts.map +1 -1
  91. package/lib/sync/unknownBlock.js +88 -46
  92. package/lib/sync/unknownBlock.js.map +1 -1
  93. package/lib/util/execution.d.ts +2 -0
  94. package/lib/util/execution.d.ts.map +1 -1
  95. package/lib/util/execution.js +1 -1
  96. package/lib/util/execution.js.map +1 -1
  97. package/package.json +15 -15
  98. package/src/api/impl/beacon/pool/index.ts +4 -4
  99. package/src/chain/archiveStore/archiveStore.ts +9 -3
  100. package/src/chain/archiveStore/utils/pruneHistory.ts +6 -4
  101. package/src/chain/blocks/importBlock.ts +4 -0
  102. package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
  103. package/src/chain/chain.ts +3 -3
  104. package/src/chain/errors/gossipValidation.ts +2 -0
  105. package/src/chain/initState.ts +8 -60
  106. package/src/chain/opPools/opPool.ts +91 -18
  107. package/src/chain/opPools/utils.ts +31 -3
  108. package/src/chain/prepareNextSlot.ts +6 -3
  109. package/src/chain/produceBlock/produceBlockBody.ts +8 -1
  110. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +11 -0
  111. package/src/chain/validation/attesterSlashing.ts +7 -4
  112. package/src/chain/validation/proposerSlashing.ts +7 -4
  113. package/src/db/beacon.ts +2 -0
  114. package/src/db/repositories/proposerSlashing.ts +6 -1
  115. package/src/index.ts +1 -1
  116. package/src/network/metadata.ts +17 -17
  117. package/src/network/peers/datastore.ts +36 -36
  118. package/src/network/processor/gossipHandlers.ts +7 -4
  119. package/src/network/processor/gossipValidatorFn.ts +13 -3
  120. package/src/network/processor/index.ts +89 -10
  121. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +42 -28
  122. package/src/sync/range/range.ts +50 -18
  123. package/src/sync/unknownBlock.ts +99 -50
  124. package/src/util/execution.ts +1 -1
@@ -22,6 +22,7 @@ import {
22
22
  getParentRootFromSignedBeaconBlockSerialized,
23
23
  getPayloadPresentFromPayloadAttestationMessageSerialized,
24
24
  } from "../../util/sszBytes.js";
25
+ import {INetworkCore} from "../core/index.js";
25
26
  import {NetworkEvent, NetworkEventBus} from "../events.js";
26
27
  import {
27
28
  GossipHandlers,
@@ -34,7 +35,12 @@ import {MAX_PEERS_PER_ROOT} from "./constants.js";
34
35
  import {createExtractBlockSlotRootFns} from "./extractSlotRootFns.js";
35
36
  import {GossipHandlerOpts, ValidatorFnsModules, getGossipHandlers} from "./gossipHandlers.js";
36
37
  import {createGossipQueues} from "./gossipQueues/index.js";
37
- import {ValidatorFnModules, getGossipValidatorBatchFn, getGossipValidatorFn} from "./gossipValidatorFn.js";
38
+ import {
39
+ ValidatorFnModules,
40
+ getGossipValidatorBatchFn,
41
+ getGossipValidatorFn,
42
+ rejectPeerAction,
43
+ } from "./gossipValidatorFn.js";
38
44
  import {PendingGossipsubMessage} from "./types.js";
39
45
 
40
46
  export * from "./types.js";
@@ -102,6 +108,12 @@ type SearchedRootEntry = {
102
108
  */
103
109
  const DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE = 32;
104
110
 
111
+ /**
112
+ * The network worker bans a Fatal peer only on its next PeerManager heartbeat (30s). Until then the main thread
113
+ * would keep validating that peer's messages, so drop them here. 3 slots (36s) outlasts one heartbeat.
114
+ */
115
+ export const FATAL_PEER_EXPIRY_SLOTS = 3;
116
+
105
117
  type WorkOpts = {
106
118
  bypassQueue?: boolean;
107
119
  };
@@ -232,6 +244,7 @@ type SearchUnknownRootTarget =
232
244
  */
233
245
  export class NetworkProcessor {
234
246
  private readonly chain: IBeaconChain;
247
+ private readonly core: INetworkCore;
235
248
  private readonly events: NetworkEventBus;
236
249
  private readonly logger: Logger;
237
250
  private readonly metrics: Metrics | null;
@@ -256,23 +269,31 @@ export class NetworkProcessor {
256
269
  () => new MapDef<RootHex, SearchedRootEntry>(() => ({}))
257
270
  );
258
271
  private bufferedRootsBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
272
+ /** peer -> clock slot when it was Fatal'ed */
273
+ private readonly fatalPeers = new Map<PeerIdStr, Slot>();
259
274
 
260
275
  constructor(
261
276
  modules: NetworkProcessorModules,
262
277
  private readonly opts: NetworkProcessorOpts
263
278
  ) {
264
- const {chain, events, logger, metrics} = modules;
279
+ const {chain, core, events, logger, metrics} = modules;
265
280
  this.chain = chain;
281
+ this.core = core;
266
282
  this.events = events;
267
283
  this.metrics = metrics;
268
284
  this.logger = logger;
269
285
  this.events = events;
270
286
  this.gossipQueues = createGossipQueues();
271
287
  this.gossipTopicConcurrency = mapValues(this.gossipQueues, () => 0);
272
- this.gossipValidatorFn = getGossipValidatorFn(modules.gossipHandlers ?? getGossipHandlers(modules, opts), modules);
288
+ this.gossipValidatorFn = getGossipValidatorFn(
289
+ modules.gossipHandlers ?? getGossipHandlers(modules, opts),
290
+ modules,
291
+ this.onFatalPeer
292
+ );
273
293
  this.gossipValidatorBatchFn = getGossipValidatorBatchFn(
274
294
  modules.gossipHandlers ?? getGossipHandlers(modules, opts),
275
- modules
295
+ modules,
296
+ this.onFatalPeer
276
297
  );
277
298
 
278
299
  events.on(NetworkEvent.pendingGossipsubMessage, this.onPendingGossipsubMessage);
@@ -406,19 +427,47 @@ export class NetworkProcessor {
406
427
  }
407
428
  }
408
429
 
430
+ private onFatalPeer = (peer: PeerIdStr): void => {
431
+ this.fatalPeers.set(peer, this.chain.clock.currentSlot);
432
+ };
433
+
409
434
  private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => {
410
435
  const topicType = message.topic.type;
436
+ if (this.fatalPeers.has(message.propagationSource)) {
437
+ this.metrics?.networkProcessor.gossipValidationError.inc({topic: topicType, error: GossipErrorCode.FATAL_PEER});
438
+ // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
439
+ return;
440
+ }
441
+
411
442
  const extractBlockSlotRootFn = this.extractBlockSlotRootFns[topicType];
443
+ if (extractBlockSlotRootFn === undefined) {
444
+ // some messages don't have slot and root
445
+ this.pushPendingGossipsubMessageToQueue(message);
446
+ return;
447
+ }
412
448
 
413
449
  // 1st extract round: make sure slot is in range and if block root is not available
414
450
  // proactively search for it + queue the message
415
- const slotRoot = extractBlockSlotRootFn
416
- ? extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork)
417
- : null;
451
+ const slotRoot = extractBlockSlotRootFn(message.msg.data, message.topic.boundary.fork);
418
452
  if (slotRoot === null) {
419
- // some messages don't have slot and root
420
- // if the msg.data is invalid, message will be rejected when deserializing data in later phase (gossipValidatorFn)
421
- this.pushPendingGossipsubMessageToQueue(message);
453
+ // DOS protection: null means malformed bytes or slot >= 2^32, neither is forwarded by honest peers.
454
+ const {propagationSource, clientAgent, clientVersion} = message;
455
+ const code = GossipErrorCode.INVALID_SLOT;
456
+ const peerAction = rejectPeerAction(topicType, code);
457
+ this.metrics?.networkProcessor.gossipValidationReject.inc({topic: topicType});
458
+ this.core.reportPeer(propagationSource, peerAction, code);
459
+ this.logger.debug(`Gossip validation ${topicType} rejected due to null slot`, {
460
+ peer: propagationSource,
461
+ clientAgent,
462
+ clientVersion,
463
+ peerAction,
464
+ code,
465
+ });
466
+ this.events.emit(NetworkEvent.gossipMessageValidationResult, {
467
+ msgId: message.msgId,
468
+ propagationSource,
469
+ acceptance: TopicValidatorResult.Reject,
470
+ });
422
471
  return;
423
472
  }
424
473
 
@@ -872,6 +921,10 @@ export class NetworkProcessor {
872
921
  for (const slot of this.bufferedRootsBySlot.keys()) {
873
922
  if (slot <= minSlot) this.bufferedRootsBySlot.delete(slot);
874
923
  }
924
+
925
+ for (const [peer, slot] of this.fatalPeers) {
926
+ if (clockSlot - slot > FATAL_PEER_EXPIRY_SLOTS) this.fatalPeers.delete(peer);
927
+ }
875
928
  };
876
929
 
877
930
  private executeWork(): void {
@@ -927,6 +980,32 @@ export class NetworkProcessor {
927
980
  private async processPendingGossipsubMessage(
928
981
  messageOrArray: PendingGossipsubMessage | PendingGossipsubMessage[]
929
982
  ): Promise<void> {
983
+ // drop messages from peers Fatal'ed after these were queued or started awaiting a block/envelope
984
+ // No need to report the dropped jobs to gossip. They will be eventually pruned from the mcache
985
+ if (this.fatalPeers.size > 0) {
986
+ if (Array.isArray(messageOrArray)) {
987
+ const allowed: PendingGossipsubMessage[] = [];
988
+ for (const msg of messageOrArray) {
989
+ if (this.fatalPeers.has(msg.propagationSource)) {
990
+ this.metrics?.networkProcessor.gossipValidationError.inc({
991
+ topic: msg.topic.type,
992
+ error: GossipErrorCode.FATAL_PEER,
993
+ });
994
+ } else {
995
+ allowed.push(msg);
996
+ }
997
+ }
998
+ if (allowed.length === 0) return;
999
+ messageOrArray = allowed;
1000
+ } else if (this.fatalPeers.has(messageOrArray.propagationSource)) {
1001
+ this.metrics?.networkProcessor.gossipValidationError.inc({
1002
+ topic: messageOrArray.topic.type,
1003
+ error: GossipErrorCode.FATAL_PEER,
1004
+ });
1005
+ return;
1006
+ }
1007
+ }
1008
+
930
1009
  const nowSec = Date.now() / 1000;
931
1010
  if (Array.isArray(messageOrArray)) {
932
1011
  for (const msg of messageOrArray) {
@@ -6,6 +6,7 @@ import {toRootHex} from "@lodestar/utils";
6
6
  import {EnvelopeReconstructionError} from "../../../chain/errors/index.js";
7
7
  import {IBeaconChain} from "../../../chain/index.js";
8
8
  import {IBeaconDb} from "../../../db/index.js";
9
+ import {MAX_BODIES_PER_REQUEST} from "../../../util/execution.js";
9
10
  import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js";
10
11
  import {prettyPrintPeerId} from "../../util.js";
11
12
 
@@ -19,10 +20,15 @@ export async function* onExecutionPayloadEnvelopesByRoot(
19
20
  // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
20
21
  // Archival nodes may still serve older retained payloads to allow genesis sync.
21
22
 
22
- // Resolve slots first so archived header envelopes are rebuilt in one EL batch, not one call per root
23
+ // Resolve slots first so archived envelopes can be reconstructed in EL batches.
24
+ // Duplicate roots are served once, since each archived envelope costs an EL fetch and a rebuild.
23
25
  const requests: {blockSlot: Slot; blockRootHex: RootHex}[] = [];
26
+ const seenRoots = new Set<RootHex>();
24
27
  for (const root of requestBody) {
25
28
  const rootHex = toRootHex(root);
29
+ if (seenRoots.has(rootHex)) continue;
30
+ seenRoots.add(rootHex);
31
+
26
32
  const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex);
27
33
  // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive
28
34
  const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root);
@@ -41,35 +47,43 @@ export async function* onExecutionPayloadEnvelopesByRoot(
41
47
  requests.push({blockSlot: slot, blockRootHex: rootHex});
42
48
  }
43
49
 
44
- let envelopesBytes: (Uint8Array | null)[];
45
- try {
46
- // by-root allows omission, so a mismatched envelope is left out rather than failing the response
47
- envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(requests, "omit");
48
- } catch (e) {
49
- if (e instanceof EnvelopeReconstructionError) {
50
- throw new ResponseError(
51
- RespStatus.RESOURCE_UNAVAILABLE,
52
- `Failed to reconstruct archived envelopes: ${e.message}`
53
- );
50
+ // Yield between EL batches so the first response does not wait for the entire request.
51
+ let yielded = 0;
52
+ for (let i = 0; i < requests.length; i += MAX_BODIES_PER_REQUEST) {
53
+ const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
54
+ let envelopesBytes: (Uint8Array | null)[];
55
+ try {
56
+ // By-root allows omission, so a mismatched envelope is left out rather than failing the response.
57
+ envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
58
+ } catch (e) {
59
+ if (e instanceof EnvelopeReconstructionError) {
60
+ // By-root permits partial responses, so an EL failure can end the response here.
61
+ if (yielded > 0) return;
62
+ throw new ResponseError(
63
+ RespStatus.RESOURCE_UNAVAILABLE,
64
+ `Failed to reconstruct archived envelopes: ${e.message}`
65
+ );
66
+ }
67
+ throw e;
54
68
  }
55
- throw e;
56
- }
57
69
 
58
- for (let i = 0; i < requests.length; i++) {
59
- const {blockSlot, blockRootHex} = requests[i];
60
- const envelopeBytes = envelopesBytes[i];
61
- if (envelopeBytes) {
62
- yield {
63
- data: envelopeBytes,
64
- boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
65
- };
66
- } else {
67
- chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
68
- slot: blockSlot,
69
- root: blockRootHex,
70
- peer: prettyPrintPeerId(peerId),
71
- client: peerClient,
72
- });
70
+ for (let j = 0; j < batch.length; j++) {
71
+ const {blockSlot, blockRootHex} = batch[j];
72
+ const envelopeBytes = envelopesBytes[j];
73
+ if (envelopeBytes) {
74
+ yielded++;
75
+ yield {
76
+ data: envelopeBytes,
77
+ boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
78
+ };
79
+ } else {
80
+ chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
81
+ slot: blockSlot,
82
+ root: blockRootHex,
83
+ peer: prettyPrintPeerId(peerId),
84
+ client: peerClient,
85
+ });
86
+ }
73
87
  }
74
88
  }
75
89
  }
@@ -5,6 +5,7 @@ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
6
  import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
8
+ import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
8
9
  import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
9
10
  import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
10
11
  import {BlockError} from "../../chain/errors/index.js";
@@ -207,22 +208,50 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
207
208
  blsVerifyOnMainThread: false,
208
209
  };
209
210
 
210
- if (this.opts?.disableProcessAsChainSegment) {
211
- // Should only be used for debugging or testing
212
- for (const block of blocks) {
213
- await this.chain.processBlock(block, flags);
214
- const payloadEnvelope = payloadEnvelopes?.get(block.slot);
215
- if (payloadEnvelope?.hasPayloadEnvelope()) {
216
- await this.chain.processExecutionPayload(payloadEnvelope);
211
+ try {
212
+ if (this.opts?.disableProcessAsChainSegment) {
213
+ // Should only be used for debugging or testing
214
+ for (const block of blocks) {
215
+ await this.chain.processBlock(block, flags);
216
+ const payloadEnvelope = payloadEnvelopes?.get(block.slot);
217
+ if (payloadEnvelope?.hasPayloadEnvelope()) {
218
+ await this.chain.processExecutionPayload(payloadEnvelope);
219
+ }
220
+ }
221
+ } else {
222
+ const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
223
+ // We log orphaned payloads to work with different clients
224
+ // in the future, consider applying penalties in certain conditions
225
+ // make sure the payload source was from range sync in that case
226
+ for (const {slot, payloadEnvelopeInput} of orphaned) {
227
+ const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
228
+ let client = "unknown";
229
+ if (peerIdStr !== undefined) {
230
+ try {
231
+ client = this.getConnectedPeerSyncMeta(peerIdStr).client;
232
+ } catch {
233
+ // peer disconnected since serving the envelope, keep "unknown"
234
+ }
235
+ }
236
+ this.logger.debug("Orphaned payload envelope in range sync batch", {
237
+ slot,
238
+ root: payloadEnvelopeInput.blockRootHex,
239
+ peer: peerIdStr ?? "unknown",
240
+ source,
241
+ client,
242
+ skipped,
243
+ });
217
244
  }
218
245
  }
219
- } else {
220
- const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
221
- // We log orphaned payloads to work with different clients
222
- // in the future, consider applying penalties in certain conditions
223
- // make sure the payload source was from range sync in that case
224
- for (const {slot, payloadEnvelopeInput} of orphaned) {
225
- const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
246
+ } catch (e) {
247
+ if (
248
+ e instanceof PayloadError &&
249
+ (e.type.code === PayloadErrorCode.INVALID_SIGNATURE ||
250
+ e.type.code === PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR ||
251
+ e.type.code === PayloadErrorCode.EXECUTION_ENGINE_INVALID)
252
+ ) {
253
+ const {payloadInput} = e;
254
+ const {peerIdStr, source} = payloadInput.getPayloadEnvelopeSource();
226
255
  let client = "unknown";
227
256
  if (peerIdStr !== undefined) {
228
257
  try {
@@ -231,15 +260,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
231
260
  // peer disconnected since serving the envelope, keep "unknown"
232
261
  }
233
262
  }
234
- this.logger.debug("Orphaned payload envelope in range sync batch", {
235
- slot,
236
- root: payloadEnvelopeInput.blockRootHex,
263
+ this.logger.debug("Removing invalid payload envelope in range sync batch", {
264
+ slot: payloadInput.slot,
265
+ root: payloadInput.blockRootHex,
237
266
  peer: peerIdStr ?? "unknown",
238
267
  source,
239
268
  client,
240
- skipped,
269
+ code: e.type.code,
241
270
  });
271
+ // The batch is downloaded again, it would otherwise reuse the cached envelope and fail the same way
272
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(payloadInput);
242
273
  }
274
+ throw e;
243
275
  }
244
276
  };
245
277
 
@@ -15,7 +15,7 @@ import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js";
15
15
  import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js";
16
16
  import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/executionPayloadEnvelope.js";
17
17
  import {Metrics} from "../metrics/index.js";
18
- import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
18
+ import {INetwork, NetworkEvent, NetworkEventData, PeerAction, prettyPrintPeerIdStr} from "../network/index.js";
19
19
  import {PeerSyncMeta} from "../network/peers/peersData.js";
20
20
  import {MAX_PEERS_PER_ROOT} from "../network/processor/constants.js";
21
21
  import {ClockEvent} from "../util/clock.js";
@@ -442,6 +442,11 @@ export class BlockInputSync {
442
442
  };
443
443
 
444
444
  private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
445
+ if (this.knownBadBlocks.has(rootHex)) {
446
+ this.logger.debug("Ignoring known bad block root", {root: rootHex, peerIdStr: peerIdStr ?? "unknown peer"});
447
+ return false;
448
+ }
449
+
445
450
  let pendingBlock = this.pendingBlocks.get(rootHex);
446
451
  let added = false;
447
452
  if (!pendingBlock) {
@@ -477,6 +482,11 @@ export class BlockInputSync {
477
482
  };
478
483
 
479
484
  private addByBlockInput = (blockInput: IBlockInput, peerIdStr?: string): void => {
485
+ if (this.knownBadBlocks.has(blockInput.blockRootHex)) {
486
+ this.logger.debug("Ignoring known bad block", {slot: blockInput.slot, root: blockInput.blockRootHex, peerIdStr});
487
+ return;
488
+ }
489
+
480
490
  let pendingBlock = this.pendingBlocks.get(blockInput.blockRootHex);
481
491
  // if entry is missing or was added via rootHex and now we have more complete information overwrite
482
492
  // the existing information with the more complete cache entry
@@ -513,6 +523,15 @@ export class BlockInputSync {
513
523
  };
514
524
 
515
525
  private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr, slot?: Slot): boolean => {
526
+ if (this.knownBadBlocks.has(rootHex)) {
527
+ this.logger.debug("Ignoring payload for known bad block root", {
528
+ slot: slot ?? "unknown",
529
+ root: rootHex,
530
+ peerIdStr,
531
+ });
532
+ return false;
533
+ }
534
+
516
535
  let pendingPayload = this.pendingPayloads.get(rootHex);
517
536
  let added = false;
518
537
  if (!pendingPayload) {
@@ -581,6 +600,15 @@ export class BlockInputSync {
581
600
  peerIdStr?: PeerIdStr,
582
601
  envelope?: gloas.SignedExecutionPayloadEnvelope
583
602
  ): void => {
603
+ if (this.knownBadBlocks.has(payloadInput.blockRootHex)) {
604
+ this.logger.debug("Ignoring payload input for known bad block root", {
605
+ slot: payloadInput.slot,
606
+ root: payloadInput.blockRootHex,
607
+ peerIdStr,
608
+ });
609
+ return;
610
+ }
611
+
584
612
  const pendingPayload = this.toPendingPayloadInput(
585
613
  payloadInput,
586
614
  this.pendingPayloads.get(payloadInput.blockRootHex),
@@ -654,6 +682,7 @@ export class BlockInputSync {
654
682
  return {kind: "ready"};
655
683
  }
656
684
 
685
+ // The parent's payload is known but does not match `parentBlockHashHex`
657
686
  if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
658
687
  return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
659
688
  }
@@ -1032,7 +1061,8 @@ export class BlockInputSync {
1032
1061
  }
1033
1062
  } else if (blockSlot <= finalizedSlot) {
1034
1063
  // the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
1035
- // we should found it through forkchoice. If not, we should penalize all peers sending us this block chain
1064
+ // we should found it through forkchoice. If not, drop the chain without penalizing peers because pending entries are
1065
+ // block retained on failed download, so finalization can pass the block while it is still pending
1036
1066
  // 0 - 1 - ... - n - finalizedSlot
1037
1067
  // \
1038
1068
  // parent 1 - parent 2 - ... - unknownParent block
@@ -1040,7 +1070,7 @@ export class BlockInputSync {
1040
1070
  ...logCtx2,
1041
1071
  finalizedSlot,
1042
1072
  });
1043
- this.removeAndDownScoreAllDescendants(block, DroppedItemReason.belowFinalized);
1073
+ this.removeAllDescendants(block, DroppedItemReason.belowFinalized);
1044
1074
  } else {
1045
1075
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
1046
1076
  }
@@ -1172,15 +1202,18 @@ export class BlockInputSync {
1172
1202
  break;
1173
1203
 
1174
1204
  case BlockErrorCode.EXECUTION_ENGINE_INVALID:
1175
- // the peer served a bad block
1205
+ // Ban but do not downscore: optimistic-sync peers may forward and serve blocks whose payload is later
1206
+ // rejected by the execution engine. Per the spec, an INVALID execution payload SHOULD NOT cause a peer
1207
+ // to be down-scored or disconnected, only consensus-layer invalidity MAY.
1208
+ // https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
1176
1209
  this.logger.debug("Execution engine rejected block from unknown parent sync", errorData, res.err);
1177
- this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
1210
+ this.removeAndBanAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
1178
1211
  break;
1179
1212
 
1180
1213
  default:
1181
1214
  // Block is not correct with respect to our chain. Log error loudly
1182
1215
  this.logger.debug("Error processing block from unknown parent sync", errorData, res.err);
1183
- this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
1216
+ this.removeAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
1184
1217
  }
1185
1218
  }
1186
1219
 
@@ -1393,20 +1426,23 @@ export class BlockInputSync {
1393
1426
  pendingPayload.status = PendingPayloadInputStatus.downloaded;
1394
1427
  break;
1395
1428
 
1429
+ // The invalid envelope is evicted from the seen cache in the cases below, otherwise the next
1430
+ // attempt would reuse it instead of fetching another envelope from peers
1396
1431
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1397
1432
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1433
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1398
1434
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
1399
1435
  break;
1400
1436
 
1401
1437
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1402
1438
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1439
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1403
1440
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
1404
1441
  break;
1405
1442
 
1406
1443
  case PayloadErrorCode.INVALID_SIGNATURE:
1407
- // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1408
- // reintroducing envelope replacement / recreation flows.
1409
1444
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1445
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1410
1446
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
1411
1447
  break;
1412
1448
 
@@ -1478,11 +1514,11 @@ export class BlockInputSync {
1478
1514
  }
1479
1515
 
1480
1516
  const {peerId, client: peerClient} = peerMeta;
1481
- cacheItem.peerIdStrings.add(peerId);
1482
1517
 
1483
1518
  try {
1484
1519
  if (!envelope) {
1485
1520
  envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1521
+ cacheItem.peerIdStrings.add(peerId);
1486
1522
  const slotWasUnknown = typeof slot !== "number";
1487
1523
  slot = envelope.message.payload.slotNumber;
1488
1524
  if (slotWasUnknown) {
@@ -1524,6 +1560,8 @@ export class BlockInputSync {
1524
1560
  const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
1525
1561
  if (missing.length > 0) {
1526
1562
  const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
1563
+ // toPendingPayloadInput copied cacheItem's set, so add to the object that is carried forward
1564
+ pendingPayload.peerIdStrings.add(peerId);
1527
1565
  const seenTimestampSec = Date.now() / 1000;
1528
1566
  for (const columnSidecar of columnSidecars) {
1529
1567
  if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
@@ -1716,8 +1754,6 @@ export class BlockInputSync {
1716
1754
  }
1717
1755
  const {peerId, client: peerClient} = peerMeta;
1718
1756
 
1719
- cacheItem.peerIdStrings.add(peerId);
1720
-
1721
1757
  try {
1722
1758
  const downloadResult = await downloadByRoot({
1723
1759
  config: this.config,
@@ -1728,6 +1764,7 @@ export class BlockInputSync {
1728
1764
  cacheItem,
1729
1765
  });
1730
1766
  cacheItem = downloadResult.result;
1767
+ cacheItem.peerIdStrings.add(peerId);
1731
1768
  if (slot === undefined) {
1732
1769
  slot = cacheItem.blockInput.slot;
1733
1770
  // we were not able to observe the time into slot when starting the fetch, do it now
@@ -1843,44 +1880,8 @@ export class BlockInputSync {
1843
1880
  throw Error(message);
1844
1881
  }
1845
1882
 
1846
- /**
1847
- * Gets all descendant blocks of `block` recursively from `pendingBlocks`.
1848
- * Assumes that if a parent block does not exist or is not processable, all descendant blocks are bad too.
1849
- * Downscore all peers that have referenced any of this bad blocks. May report peers multiple times if they have
1850
- * referenced more than one bad block.
1851
- */
1852
- private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1853
- // Get all blocks that are a descendant of this one
1854
- const badPendingBlocks = this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent);
1855
- // just console log and do not penalize on pending/bad blocks for debugging
1856
- // console.log("removeAndDownscoreAllDescendants", {block});
1857
-
1858
- for (const block of badPendingBlocks) {
1859
- //
1860
- // TODO(fulu): why is this commented out here?
1861
- //
1862
- // this.knownBadBlocks.add(block.blockRootHex);
1863
- // for (const peerIdStr of block.peerIdStrings) {
1864
- // // TODO: Refactor peerRpcScores to work with peerIdStr only
1865
- // this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadBlockByRoot");
1866
- // }
1867
- this.logger.debug("ignored Banning unknown block", {
1868
- slot: getBlockInputSyncCacheItemSlot(block),
1869
- root: getBlockInputSyncCacheItemRootHex(block),
1870
- peerIdStrings: Array.from(block.peerIdStrings)
1871
- .map((id) => prettyPrintPeerIdStr(id))
1872
- .join(","),
1873
- });
1874
- }
1875
-
1876
- // Prune knownBadBlocks
1877
- pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
1878
- }
1879
-
1880
1883
  // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1881
1884
  private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
1882
- // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1883
- // later validation/finalization logic decides when it can leave memory.
1884
1885
  if (this.pendingPayloads.delete(rootHex)) {
1885
1886
  this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
1886
1887
  }
@@ -1903,13 +1904,61 @@ export class BlockInputSync {
1903
1904
  }
1904
1905
  }
1905
1906
 
1907
+ /**
1908
+ * Same as `removeAndBanAllDescendants`, and downscores every peer that referenced or served a banned item, once per
1909
+ * peer. Only call for consensus-layer invalidity, never for an INVALID execution payload:
1910
+ * https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
1911
+ */
1912
+ private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1913
+ const peersToReport = new Set<PeerIdStr>();
1914
+ for (const bannedBlock of this.removeAndBanAllDescendants(block, headReason)) {
1915
+ for (const peerIdStr of bannedBlock.peerIdStrings) {
1916
+ peersToReport.add(peerIdStr);
1917
+ }
1918
+ }
1919
+
1920
+ for (const peerIdStr of peersToReport) {
1921
+ this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, headReason);
1922
+ }
1923
+ }
1924
+
1925
+ /**
1926
+ * Removes `block` and all its pending descendants and bans their roots in `knownBadBlocks`. Only call when the chain
1927
+ * is provably bad: a banned root is never searched for again. Returns the banned items.
1928
+ */
1929
+ private removeAndBanAllDescendants(
1930
+ block: BlockInputSyncCacheItem,
1931
+ headReason: DroppedItemReason
1932
+ ): BlockInputSyncCacheItem[] {
1933
+ const bannedBlocks: BlockInputSyncCacheItem[] = [];
1934
+ for (const badBlock of this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent)) {
1935
+ const rootHex = getBlockInputSyncCacheItemRootHex(badBlock);
1936
+ // a gossip import may race a failing sync path, never ban a root fork choice has imported
1937
+ if (this.chain.forkChoice.hasBlockHex(rootHex)) {
1938
+ continue;
1939
+ }
1940
+ this.knownBadBlocks.add(rootHex);
1941
+ bannedBlocks.push(badBlock);
1942
+ this.logger.debug("Banning unknown block", {
1943
+ slot: getBlockInputSyncCacheItemSlot(badBlock),
1944
+ root: rootHex,
1945
+ reason: headReason,
1946
+ peerIdStrings: Array.from(badBlock.peerIdStrings)
1947
+ .map((id) => prettyPrintPeerIdStr(id))
1948
+ .join(","),
1949
+ });
1950
+ }
1951
+
1952
+ pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
1953
+ return bannedBlocks;
1954
+ }
1955
+
1906
1956
  private removeAllDescendants(
1907
1957
  block: BlockInputSyncCacheItem,
1908
1958
  headReason: DroppedItemReason,
1909
1959
  descendantReason: DroppedItemReason = DroppedItemReason.invalidParent
1910
1960
  ): BlockInputSyncCacheItem[] {
1911
1961
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1912
- const slot = getBlockInputSyncCacheItemSlot(block);
1913
1962
  // Get all blocks that are a descendant of this one (index 0 is the head item itself)
1914
1963
  const badPendingBlocks = [block, ...getAllDescendantBlocks(rootHex, this.pendingBlocks)];
1915
1964
 
@@ -1926,8 +1975,8 @@ export class BlockInputSync {
1926
1975
  // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1927
1976
  // importBlock()-owned lifecycle.
1928
1977
  this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
1929
- slot,
1930
- blockRoot: rootHex,
1978
+ slot: getBlockInputSyncCacheItemSlot(block),
1979
+ root: rootHex,
1931
1980
  });
1932
1981
  }
1933
1982
 
@@ -260,7 +260,7 @@ export async function getDataColumnSidecarsFromExecution(
260
260
  }
261
261
 
262
262
  /** engine_getPayloadBodiesByHashV2: every EL must accept requests of up to 32 hashes, larger ones may fail with -38004 */
263
- const MAX_BODIES_PER_REQUEST = 32;
263
+ export const MAX_BODIES_PER_REQUEST = 32;
264
264
 
265
265
  type SlotEnvelopeBytes = {slot: Slot; envelopeBytes: Uint8Array};
266
266