@lodestar/beacon-node 1.50.0-dev.814fe8c461 → 1.50.0-dev.82f05c43ff

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 (246) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +6 -1
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/index.js +13 -0
  6. package/lib/api/impl/beacon/index.js.map +1 -1
  7. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  8. package/lib/api/impl/beacon/state/index.js +23 -3
  9. package/lib/api/impl/beacon/state/index.js.map +1 -1
  10. package/lib/api/impl/debug/index.d.ts.map +1 -1
  11. package/lib/api/impl/debug/index.js +26 -18
  12. package/lib/api/impl/debug/index.js.map +1 -1
  13. package/lib/api/impl/validator/index.d.ts +1 -0
  14. package/lib/api/impl/validator/index.d.ts.map +1 -1
  15. package/lib/api/impl/validator/index.js +23 -1
  16. package/lib/api/impl/validator/index.js.map +1 -1
  17. package/lib/chain/archiveStore/archiveStore.d.ts +5 -0
  18. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  19. package/lib/chain/archiveStore/archiveStore.js +14 -24
  20. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  21. package/lib/chain/archiveStore/historicalState/getHistoricalState.d.ts +2 -2
  22. package/lib/chain/archiveStore/historicalState/getHistoricalState.d.ts.map +1 -1
  23. package/lib/chain/archiveStore/historicalState/getHistoricalState.js +5 -7
  24. package/lib/chain/archiveStore/historicalState/getHistoricalState.js.map +1 -1
  25. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js +1 -1
  26. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js.map +1 -1
  27. package/lib/chain/archiveStore/historicalState/types.d.ts +2 -2
  28. package/lib/chain/archiveStore/historicalState/types.d.ts.map +1 -1
  29. package/lib/chain/archiveStore/historicalState/worker.js +1 -1
  30. package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
  31. package/lib/chain/archiveStore/interface.d.ts +1 -1
  32. package/lib/chain/archiveStore/interface.d.ts.map +1 -1
  33. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +1 -1
  34. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  35. package/lib/chain/archiveStore/utils/pruneHistory.js +2 -1
  36. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  37. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  38. package/lib/chain/blocks/importBlock.js +11 -3
  39. package/lib/chain/blocks/importBlock.js.map +1 -1
  40. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
  41. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  42. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  43. package/lib/chain/blocks/verifyBlock.js +1 -1
  44. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  45. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts +2 -1
  46. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  47. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +5 -4
  48. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  49. package/lib/chain/chain.d.ts +9 -5
  50. package/lib/chain/chain.d.ts.map +1 -1
  51. package/lib/chain/chain.js +53 -18
  52. package/lib/chain/chain.js.map +1 -1
  53. package/lib/chain/emitter.d.ts +7 -1
  54. package/lib/chain/emitter.d.ts.map +1 -1
  55. package/lib/chain/emitter.js +5 -0
  56. package/lib/chain/emitter.js.map +1 -1
  57. package/lib/chain/errors/envelopeReconstructionError.d.ts +5 -0
  58. package/lib/chain/errors/envelopeReconstructionError.d.ts.map +1 -1
  59. package/lib/chain/errors/envelopeReconstructionError.js +2 -0
  60. package/lib/chain/errors/envelopeReconstructionError.js.map +1 -1
  61. package/lib/chain/errors/gossipValidation.d.ts +3 -1
  62. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  63. package/lib/chain/errors/gossipValidation.js +2 -0
  64. package/lib/chain/errors/gossipValidation.js.map +1 -1
  65. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  66. package/lib/chain/forkChoice/index.js +2 -0
  67. package/lib/chain/forkChoice/index.js.map +1 -1
  68. package/lib/chain/interface.d.ts +3 -2
  69. package/lib/chain/interface.d.ts.map +1 -1
  70. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +4 -2
  71. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  72. package/lib/chain/opPools/aggregatedAttestationPool.js +30 -8
  73. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  74. package/lib/chain/opPools/proposerPreferencesPool.d.ts +4 -4
  75. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  76. package/lib/chain/opPools/proposerPreferencesPool.js +14 -11
  77. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  78. package/lib/chain/options.d.ts +2 -1
  79. package/lib/chain/options.d.ts.map +1 -1
  80. package/lib/chain/options.js +1 -1
  81. package/lib/chain/options.js.map +1 -1
  82. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  83. package/lib/chain/prepareNextSlot.js +5 -2
  84. package/lib/chain/prepareNextSlot.js.map +1 -1
  85. package/lib/chain/produceBlock/computeNewStateRoot.js +1 -1
  86. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  87. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  88. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  89. package/lib/chain/produceBlock/produceBlockBody.js +6 -2
  90. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  91. package/lib/chain/regen/regen.d.ts +2 -0
  92. package/lib/chain/regen/regen.d.ts.map +1 -1
  93. package/lib/chain/regen/regen.js +10 -4
  94. package/lib/chain/regen/regen.js.map +1 -1
  95. package/lib/chain/seenCache/seenGossipBlockInput.d.ts +7 -0
  96. package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
  97. package/lib/chain/seenCache/seenGossipBlockInput.js +21 -0
  98. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  99. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +9 -2
  100. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  101. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +23 -13
  102. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  103. package/lib/chain/shufflingCache.d.ts +1 -0
  104. package/lib/chain/shufflingCache.d.ts.map +1 -1
  105. package/lib/chain/shufflingCache.js +4 -0
  106. package/lib/chain/shufflingCache.js.map +1 -1
  107. package/lib/chain/validatorMonitor.d.ts +5 -1
  108. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  109. package/lib/chain/validatorMonitor.js +64 -54
  110. package/lib/chain/validatorMonitor.js.map +1 -1
  111. package/lib/db/beacon.d.ts +2 -0
  112. package/lib/db/beacon.d.ts.map +1 -1
  113. package/lib/db/beacon.js +3 -0
  114. package/lib/db/beacon.js.map +1 -1
  115. package/lib/db/buckets.d.ts +2 -1
  116. package/lib/db/buckets.d.ts.map +1 -1
  117. package/lib/db/buckets.js +1 -0
  118. package/lib/db/buckets.js.map +1 -1
  119. package/lib/db/interface.d.ts +2 -0
  120. package/lib/db/interface.d.ts.map +1 -1
  121. package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
  122. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  123. package/lib/db/repositories/earliestAvailableSlot.d.ts +11 -0
  124. package/lib/db/repositories/earliestAvailableSlot.d.ts.map +1 -0
  125. package/lib/db/repositories/earliestAvailableSlot.js +19 -0
  126. package/lib/db/repositories/earliestAvailableSlot.js.map +1 -0
  127. package/lib/execution/engine/disabled.d.ts +0 -2
  128. package/lib/execution/engine/disabled.d.ts.map +1 -1
  129. package/lib/execution/engine/disabled.js +0 -6
  130. package/lib/execution/engine/disabled.js.map +1 -1
  131. package/lib/execution/engine/http.d.ts +1 -3
  132. package/lib/execution/engine/http.d.ts.map +1 -1
  133. package/lib/execution/engine/http.js +2 -17
  134. package/lib/execution/engine/http.js.map +1 -1
  135. package/lib/execution/engine/interface.d.ts +2 -4
  136. package/lib/execution/engine/interface.d.ts.map +1 -1
  137. package/lib/execution/engine/mock.d.ts +0 -2
  138. package/lib/execution/engine/mock.d.ts.map +1 -1
  139. package/lib/execution/engine/mock.js +0 -8
  140. package/lib/execution/engine/mock.js.map +1 -1
  141. package/lib/execution/engine/types.d.ts +0 -8
  142. package/lib/execution/engine/types.d.ts.map +1 -1
  143. package/lib/execution/engine/types.js.map +1 -1
  144. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  145. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  146. package/lib/metrics/metrics/lodestar.js +4 -0
  147. package/lib/metrics/metrics/lodestar.js.map +1 -1
  148. package/lib/metrics/metrics.d.ts +7 -3
  149. package/lib/metrics/metrics.d.ts.map +1 -1
  150. package/lib/metrics/metrics.js +4 -4
  151. package/lib/metrics/metrics.js.map +1 -1
  152. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  153. package/lib/network/processor/gossipHandlers.js +62 -63
  154. package/lib/network/processor/gossipHandlers.js.map +1 -1
  155. package/lib/network/processor/gossipValidatorFn.d.ts +7 -3
  156. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  157. package/lib/network/processor/gossipValidatorFn.js +7 -3
  158. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  159. package/lib/network/processor/index.d.ts +9 -0
  160. package/lib/network/processor/index.d.ts.map +1 -1
  161. package/lib/network/processor/index.js +77 -10
  162. package/lib/network/processor/index.js.map +1 -1
  163. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +1 -1
  164. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  165. package/lib/node/nodejs.d.ts +3 -1
  166. package/lib/node/nodejs.d.ts.map +1 -1
  167. package/lib/node/nodejs.js +27 -4
  168. package/lib/node/nodejs.js.map +1 -1
  169. package/lib/sync/range/batch.d.ts +6 -0
  170. package/lib/sync/range/batch.d.ts.map +1 -1
  171. package/lib/sync/range/batch.js +11 -0
  172. package/lib/sync/range/batch.js.map +1 -1
  173. package/lib/sync/range/chain.d.ts.map +1 -1
  174. package/lib/sync/range/chain.js +6 -1
  175. package/lib/sync/range/chain.js.map +1 -1
  176. package/lib/sync/range/range.d.ts.map +1 -1
  177. package/lib/sync/range/range.js +50 -18
  178. package/lib/sync/range/range.js.map +1 -1
  179. package/lib/sync/unknownBlock.d.ts +5 -2
  180. package/lib/sync/unknownBlock.d.ts.map +1 -1
  181. package/lib/sync/unknownBlock.js +49 -24
  182. package/lib/sync/unknownBlock.js.map +1 -1
  183. package/lib/sync/utils/downloadByRange.d.ts +8 -0
  184. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  185. package/lib/sync/utils/downloadByRange.js +32 -9
  186. package/lib/sync/utils/downloadByRange.js.map +1 -1
  187. package/lib/util/execution.d.ts +9 -11
  188. package/lib/util/execution.d.ts.map +1 -1
  189. package/lib/util/execution.js +5 -1
  190. package/lib/util/execution.js.map +1 -1
  191. package/package.json +17 -17
  192. package/src/api/impl/beacon/blocks/index.ts +7 -4
  193. package/src/api/impl/beacon/index.ts +15 -0
  194. package/src/api/impl/beacon/state/index.ts +31 -3
  195. package/src/api/impl/debug/index.ts +28 -19
  196. package/src/api/impl/validator/index.ts +24 -2
  197. package/src/chain/archiveStore/archiveStore.ts +19 -25
  198. package/src/chain/archiveStore/historicalState/getHistoricalState.ts +5 -7
  199. package/src/chain/archiveStore/historicalState/historicalStateRegen.ts +1 -1
  200. package/src/chain/archiveStore/historicalState/types.ts +2 -2
  201. package/src/chain/archiveStore/historicalState/worker.ts +1 -2
  202. package/src/chain/archiveStore/interface.ts +1 -1
  203. package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
  204. package/src/chain/blocks/importBlock.ts +13 -3
  205. package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
  206. package/src/chain/blocks/verifyBlock.ts +1 -0
  207. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +6 -3
  208. package/src/chain/chain.ts +73 -20
  209. package/src/chain/emitter.ts +7 -1
  210. package/src/chain/errors/envelopeReconstructionError.ts +3 -0
  211. package/src/chain/errors/gossipValidation.ts +2 -0
  212. package/src/chain/forkChoice/index.ts +2 -0
  213. package/src/chain/interface.ts +3 -2
  214. package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
  215. package/src/chain/opPools/proposerPreferencesPool.ts +14 -10
  216. package/src/chain/options.ts +3 -3
  217. package/src/chain/prepareNextSlot.ts +6 -3
  218. package/src/chain/produceBlock/computeNewStateRoot.ts +1 -1
  219. package/src/chain/produceBlock/produceBlockBody.ts +8 -1
  220. package/src/chain/regen/regen.ts +12 -4
  221. package/src/chain/seenCache/seenGossipBlockInput.ts +23 -0
  222. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +31 -13
  223. package/src/chain/shufflingCache.ts +5 -0
  224. package/src/chain/validatorMonitor.ts +75 -57
  225. package/src/db/beacon.ts +3 -0
  226. package/src/db/buckets.ts +1 -0
  227. package/src/db/interface.ts +2 -0
  228. package/src/db/repositories/earliestAvailableSlot.ts +19 -0
  229. package/src/execution/engine/disabled.ts +0 -8
  230. package/src/execution/engine/http.ts +1 -30
  231. package/src/execution/engine/interface.ts +2 -6
  232. package/src/execution/engine/mock.ts +1 -19
  233. package/src/execution/engine/types.ts +0 -10
  234. package/src/metrics/metrics/lodestar.ts +4 -0
  235. package/src/metrics/metrics.ts +21 -6
  236. package/src/network/processor/gossipHandlers.ts +93 -76
  237. package/src/network/processor/gossipValidatorFn.ts +13 -3
  238. package/src/network/processor/index.ts +89 -10
  239. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +1 -1
  240. package/src/node/nodejs.ts +35 -3
  241. package/src/sync/range/batch.ts +12 -0
  242. package/src/sync/range/chain.ts +7 -1
  243. package/src/sync/range/range.ts +50 -18
  244. package/src/sync/unknownBlock.ts +62 -25
  245. package/src/sync/utils/downloadByRange.ts +47 -8
  246. package/src/util/execution.ts +16 -8
@@ -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) {
@@ -53,7 +53,7 @@ export async function* onExecutionPayloadEnvelopesByRoot(
53
53
  const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
54
54
  let envelopesBytes: (Uint8Array | null)[];
55
55
  try {
56
- // By-root allows omission, so a mismatched envelope is left out rather than failing the response.
56
+ // By-root allows omission, so an envelope that cannot be rebuilt is left out rather than failing the response.
57
57
  envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
58
58
  } catch (e) {
59
59
  if (e instanceof EnvelopeReconstructionError) {
@@ -1,6 +1,12 @@
1
1
  import {setMaxListeners} from "node:events";
2
2
  import {PrivateKey} from "@libp2p/interface";
3
3
  import {Registry} from "prom-client";
4
+ import {
5
+ init as initNativeMetrics,
6
+ registerLocalValidator as registerNativeLocalValidator,
7
+ scrapeMetrics as scrapeNativeMetrics,
8
+ unregisterLocalValidator as unregisterNativeLocalValidator,
9
+ } from "@chainsafe/lodestar-z/metrics";
4
10
  import {type PubkeyCache} from "@chainsafe/lodestar-z/pubkeys";
5
11
  import {hasher} from "@chainsafe/persistent-merkle-tree";
6
12
  import {BeaconApiMethods} from "@lodestar/api/beacon/server";
@@ -8,6 +14,7 @@ import {BeaconConfig} from "@lodestar/config";
8
14
  import type {LoggerNode} from "@lodestar/logger/node";
9
15
  import {ZERO_HASH_HEX} from "@lodestar/params";
10
16
  import {IBeaconStateView, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
17
+ import {Slot} from "@lodestar/types";
11
18
  import {sleep, toRootHex} from "@lodestar/utils";
12
19
  import {ProcessShutdownCallback} from "@lodestar/validator";
13
20
  import {BeaconRestApiServer, getApi} from "../api/index.js";
@@ -55,6 +62,7 @@ export type BeaconNodeInitModules = {
55
62
  peerStoreDir?: string;
56
63
  anchorState: IBeaconStateView;
57
64
  isAnchorStateFinalized: boolean;
65
+ earliestAvailableSlot: Slot;
58
66
  metricsRegistries?: Registry[];
59
67
  };
60
68
 
@@ -153,6 +161,7 @@ export class BeaconNode {
153
161
  peerStoreDir,
154
162
  anchorState,
155
163
  isAnchorStateFinalized,
164
+ earliestAvailableSlot,
156
165
  metricsRegistries = [],
157
166
  }: BeaconNodeInitModules): Promise<T> {
158
167
  if (hasher.name !== "hashtree") {
@@ -171,7 +180,13 @@ export class BeaconNode {
171
180
  // monitoring relies on metrics data
172
181
  opts.monitoring.endpoint
173
182
  ) {
174
- metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries);
183
+ if (opts.chain.nativeStateTransition) {
184
+ initNativeMetrics();
185
+ }
186
+
187
+ metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries, {
188
+ includeStateTransitionMetrics: !opts.chain.nativeStateTransition,
189
+ });
175
190
  initBeaconMetrics(metrics, anchorState);
176
191
  // Since the db is instantiated before this, metrics must be injected manually afterwards
177
192
  db.setMetrics(metrics.db, metrics.flatFileStore);
@@ -185,7 +200,13 @@ export class BeaconNode {
185
200
  config,
186
201
  anchorState.genesisTime,
187
202
  logger.child({module: LoggerModule.vmon}),
188
- opts.validatorMonitor
203
+ opts.validatorMonitor,
204
+ opts.chain.nativeStateTransition
205
+ ? {
206
+ registerLocalValidator: registerNativeLocalValidator,
207
+ unregisterLocalValidator: unregisterNativeLocalValidator,
208
+ }
209
+ : null
189
210
  )
190
211
  : null;
191
212
 
@@ -248,6 +269,7 @@ export class BeaconNode {
248
269
  validatorMonitor,
249
270
  anchorState,
250
271
  isAnchorStateFinalized,
272
+ earliestAvailableSlot,
251
273
  executionEngine: initializeExecutionEngine(executionEngineOpts, {
252
274
  metrics,
253
275
  signal,
@@ -303,7 +325,17 @@ export class BeaconNode {
303
325
  const metricsServer = opts.metrics.enabled
304
326
  ? await getHttpMetricsServer(opts.metrics, {
305
327
  register: (metrics as Metrics).register,
306
- getOtherMetrics: async () => Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]),
328
+ getOtherMetrics: async () => {
329
+ const otherMetrics = await Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]);
330
+ if (opts.chain.nativeStateTransition) {
331
+ try {
332
+ otherMetrics.push(scrapeNativeMetrics());
333
+ } catch (e) {
334
+ logger.warn("Failed to scrape native state-transition metrics", {}, e as Error);
335
+ }
336
+ }
337
+ return otherMetrics;
338
+ },
307
339
  logger: logger.child({module: LoggerModule.metrics}),
308
340
  })
309
341
  : null;
@@ -224,6 +224,7 @@ export class Batch {
224
224
  return {
225
225
  blockRoot: parentBlockRoot,
226
226
  blockRootHex: toRootHex(parentBlockRoot),
227
+ blockHash: this.latestBid.blockHash,
227
228
  kzgCommitments: this.latestBid.blobKzgCommitments,
228
229
  };
229
230
  }
@@ -773,6 +774,7 @@ export class Batch {
773
774
  // remove any downloaded blocks and re-attempt
774
775
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
775
776
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
777
+ this.resetRequests();
776
778
  }
777
779
 
778
780
  private onProcessingError(attempt: FailedAttempt): void {
@@ -784,6 +786,16 @@ export class Batch {
784
786
  // remove any downloaded blocks and re-attempt
785
787
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
786
788
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
789
+ this.resetRequests();
790
+ }
791
+
792
+ /**
793
+ * After a partial download `requests` only cover the data still missing from the downloaded blocks,
794
+ * e.g. a single column. Once those blocks are dropped, request the whole batch again, otherwise
795
+ * every peer fails the retry for lack of blocks to validate the data against.
796
+ */
797
+ private resetRequests(): void {
798
+ this.requests = this.getRequests([]);
787
799
  }
788
800
 
789
801
  /** Helper to construct typed BatchError. Stack traces are correct as the error is thrown above */
@@ -225,6 +225,9 @@ export class SyncChain {
225
225
  Math.floor((localFinalizedEpoch - this.lastEpochWithProcessBlocks) / EPOCHS_PER_BATCH) * EPOCHS_PER_BATCH;
226
226
  this.advanceChain(lastEpochWithProcessBlocksAligned);
227
227
 
228
+ // stopSyncing() cleared the retry timer, but peers can still be in rate limit backoff
229
+ this.scheduleRateLimitBackoffRetry();
230
+
228
231
  // Potentially download new batches and process pending
229
232
  this.triggerBatchDownloader();
230
233
  this.triggerBatchProcessor();
@@ -417,8 +420,10 @@ export class SyncChain {
417
420
  this.rateLimitBackoffTimeout = setTimeout(
418
421
  () => {
419
422
  this.rateLimitBackoffTimeout = undefined;
420
- this.triggerBatchDownloader();
423
+ // Must run before the downloader, it drops expired peers without triggering a download. If it ran
424
+ // after, a backoff expiring in between would leave the peer idle with no retry scheduled.
421
425
  this.scheduleRateLimitBackoffRetry();
426
+ this.triggerBatchDownloader();
422
427
  },
423
428
  Math.max(0, retryAt - now)
424
429
  );
@@ -581,6 +586,7 @@ export class SyncChain {
581
586
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
582
587
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
583
588
  case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
589
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BLOCK_HASH:
584
590
  case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
585
591
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
586
592
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
@@ -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
 
@@ -73,7 +73,11 @@ type AdvancePendingBlockResult =
73
73
  | "removed";
74
74
 
75
75
  class UnknownBlockRateLimitedError extends Error {
76
- constructor(message: string) {
76
+ constructor(
77
+ message: string,
78
+ /** Earliest backoff expiry seen when the download was deferred, it is retried once that has passed */
79
+ readonly retryAt: number
80
+ ) {
77
81
  super(message);
78
82
  this.name = "UnknownBlockRateLimitedError";
79
83
  }
@@ -849,17 +853,20 @@ export class BlockInputSync {
849
853
  }
850
854
  };
851
855
 
852
- private scheduleRateLimitBackoffRetry(): void {
856
+ /**
857
+ * Returns false if there is no backoff left to wait for, in which case no retry is scheduled.
858
+ */
859
+ private scheduleRateLimitBackoffRetry(): boolean {
853
860
  this.clearRateLimitBackoffTimer();
854
861
 
855
862
  if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
856
- return;
863
+ return false;
857
864
  }
858
865
 
859
866
  const now = Date.now();
860
867
  const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
861
868
  if (retryAt === null) {
862
- return;
869
+ return false;
863
870
  }
864
871
 
865
872
  this.rateLimitBackoffTimeout = setTimeout(
@@ -870,6 +877,7 @@ export class BlockInputSync {
870
877
  },
871
878
  Math.max(0, retryAt - now)
872
879
  );
880
+ return true;
873
881
  }
874
882
 
875
883
  private clearRateLimitBackoffTimer(): void {
@@ -1081,7 +1089,11 @@ export class BlockInputSync {
1081
1089
  pendingBlock.status = PendingBlockInputStatus.pending;
1082
1090
  }
1083
1091
  this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
1084
- this.scheduleRateLimitBackoffRetry();
1092
+ if (!this.scheduleRateLimitBackoffRetry() || res.err.retryAt <= Date.now()) {
1093
+ // The backoff this download waited for expired in the meantime, a timer is only armed if another
1094
+ // peer is still in backoff and would retry it later than needed
1095
+ this.triggerUnknownBlockSearch();
1096
+ }
1085
1097
  return;
1086
1098
  }
1087
1099
 
@@ -1364,6 +1376,14 @@ export class BlockInputSync {
1364
1376
  if (!isPendingPayloadEnvelope(payload)) {
1365
1377
  payload.status = PendingPayloadInputStatus.pending;
1366
1378
  }
1379
+ if (
1380
+ res.err instanceof UnknownBlockRateLimitedError &&
1381
+ (!this.scheduleRateLimitBackoffRetry() || res.err.retryAt <= Date.now())
1382
+ ) {
1383
+ // The backoff this download waited for expired in the meantime, a timer is only armed if another
1384
+ // peer is still in backoff and would retry it later than needed
1385
+ this.triggerUnknownBlockSearch();
1386
+ }
1367
1387
  }
1368
1388
 
1369
1389
  private async processPayload(pendingPayload: PendingPayloadInput): Promise<void> {
@@ -1426,20 +1446,23 @@ export class BlockInputSync {
1426
1446
  pendingPayload.status = PendingPayloadInputStatus.downloaded;
1427
1447
  break;
1428
1448
 
1449
+ // The invalid envelope is evicted from the seen cache in the cases below, otherwise the next
1450
+ // attempt would reuse it instead of fetching another envelope from peers
1429
1451
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1430
1452
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1453
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1431
1454
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
1432
1455
  break;
1433
1456
 
1434
1457
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1435
1458
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1459
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1436
1460
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
1437
1461
  break;
1438
1462
 
1439
1463
  case PayloadErrorCode.INVALID_SIGNATURE:
1440
- // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1441
- // reintroducing envelope replacement / recreation flows.
1442
1464
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1465
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1443
1466
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
1444
1467
  break;
1445
1468
 
@@ -1485,16 +1508,21 @@ export class BlockInputSync {
1485
1508
  const pendingColumns = payloadInput?.hasAllData()
1486
1509
  ? new Set<number>()
1487
1510
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1511
+ // Same `now` for both calls, a backoff expiring in between would be reported as no peer available
1512
+ const now = Date.now();
1488
1513
  // prefer peers that gossiped this payload root to us (#9923)
1489
1514
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1490
1515
  pendingColumns,
1491
1516
  excludedPeers,
1492
- cacheItem.peerIdStrings
1517
+ cacheItem.peerIdStrings,
1518
+ now
1493
1519
  );
1494
1520
  if (peerMeta === null) {
1495
- if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1521
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers, now);
1522
+ if (retryAt !== null) {
1496
1523
  throw new UnknownBlockRateLimitedError(
1497
- `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1524
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`,
1525
+ retryAt
1498
1526
  );
1499
1527
  }
1500
1528
 
@@ -1633,9 +1661,11 @@ export class BlockInputSync {
1633
1661
  );
1634
1662
  this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureMaxAttempts}, i - 1);
1635
1663
 
1636
- if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1664
+ const retryAt = deferredByRateLimit ? this.peerBalancer.getNextRateLimitRetryAt() : null;
1665
+ if (retryAt !== null) {
1637
1666
  throw new UnknownBlockRateLimitedError(
1638
- `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1667
+ `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`,
1668
+ retryAt
1639
1669
  );
1640
1670
  }
1641
1671
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
@@ -1727,16 +1757,21 @@ export class BlockInputSync {
1727
1757
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
1728
1758
  ? new Set(cacheItem.blockInput.getMissingSampledColumnMeta().missing)
1729
1759
  : defaultPendingColumns;
1760
+ // Same `now` for both calls, a backoff expiring in between would be reported as no peer available
1761
+ const now = Date.now();
1730
1762
  // prefer peers that gossiped this block root to us (#9923)
1731
1763
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1732
1764
  pendingColumns,
1733
1765
  excludedPeers,
1734
- cacheItem.peerIdStrings
1766
+ cacheItem.peerIdStrings,
1767
+ now
1735
1768
  );
1736
1769
  if (peerMeta === null) {
1737
- if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1770
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers, now);
1771
+ if (retryAt !== null) {
1738
1772
  throw new UnknownBlockRateLimitedError(
1739
- `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1773
+ `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`,
1774
+ retryAt
1740
1775
  );
1741
1776
  }
1742
1777
 
@@ -1842,8 +1877,9 @@ export class BlockInputSync {
1842
1877
 
1843
1878
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1844
1879
 
1845
- if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1846
- throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1880
+ const retryAt = deferredByRateLimit ? this.peerBalancer.getNextRateLimitRetryAt() : null;
1881
+ if (retryAt !== null) {
1882
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`, retryAt);
1847
1883
  }
1848
1884
 
1849
1885
  if (!isPendingBlockInput(cacheItem)) {
@@ -1879,8 +1915,6 @@ export class BlockInputSync {
1879
1915
 
1880
1916
  // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1881
1917
  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
1918
  if (this.pendingPayloads.delete(rootHex)) {
1885
1919
  this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
1886
1920
  }
@@ -2036,8 +2070,11 @@ export class UnknownBlockPeerBalancer {
2036
2070
  this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
2037
2071
  }
2038
2072
 
2039
- getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
2040
- const now = Date.now();
2073
+ getNextRateLimitRetryAt(
2074
+ pendingColumns?: Set<number>,
2075
+ excludedPeers?: Set<PeerIdStr>,
2076
+ now = Date.now()
2077
+ ): number | null {
2041
2078
  let retryAt: number | null = null;
2042
2079
 
2043
2080
  for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
@@ -2076,9 +2113,10 @@ export class UnknownBlockPeerBalancer {
2076
2113
  bestPeerForPendingColumns(
2077
2114
  pendingColumns: Set<number>,
2078
2115
  excludedPeers: Set<PeerIdStr>,
2079
- preferredPeers?: Set<PeerIdStr>
2116
+ preferredPeers?: Set<PeerIdStr>,
2117
+ now = Date.now()
2080
2118
  ): PeerSyncMeta | null {
2081
- const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers);
2119
+ const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers, now);
2082
2120
  if (eligiblePeers.length === 0) {
2083
2121
  return null;
2084
2122
  }
@@ -2119,8 +2157,7 @@ export class UnknownBlockPeerBalancer {
2119
2157
  return totalActiveRequests;
2120
2158
  }
2121
2159
 
2122
- private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
2123
- const now = Date.now();
2160
+ private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>, now: number): PeerIdStr[] {
2124
2161
  let maxColumnCount = 0;
2125
2162
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
2126
2163
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {