@lodestar/beacon-node 1.43.0-dev.a6d8600051 → 1.43.0-dev.ac258a8def

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 (220) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +10 -0
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/debug/index.d.ts.map +1 -1
  5. package/lib/api/impl/debug/index.js +0 -1
  6. package/lib/api/impl/debug/index.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js +2 -1
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  11. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  12. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  13. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  14. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  15. package/lib/chain/blocks/importBlock.js +16 -29
  16. package/lib/chain/blocks/importBlock.js.map +1 -1
  17. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  18. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  19. package/lib/chain/blocks/importExecutionPayload.js +15 -6
  20. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  21. package/lib/chain/blocks/index.d.ts.map +1 -1
  22. package/lib/chain/blocks/index.js +29 -17
  23. package/lib/chain/blocks/index.js.map +1 -1
  24. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
  25. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  26. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +24 -1
  27. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  28. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  29. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  30. package/lib/chain/blocks/types.d.ts +2 -1
  31. package/lib/chain/blocks/types.d.ts.map +1 -1
  32. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  33. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  34. package/lib/chain/blocks/utils/chainSegment.js +34 -23
  35. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  36. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  37. package/lib/chain/blocks/verifyBlock.js +5 -6
  38. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  39. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  40. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  42. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  43. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  44. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  45. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  46. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  47. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  48. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  49. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  50. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  51. package/lib/chain/chain.d.ts.map +1 -1
  52. package/lib/chain/chain.js +25 -10
  53. package/lib/chain/chain.js.map +1 -1
  54. package/lib/chain/emitter.d.ts +0 -11
  55. package/lib/chain/emitter.d.ts.map +1 -1
  56. package/lib/chain/emitter.js +0 -4
  57. package/lib/chain/emitter.js.map +1 -1
  58. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  59. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  60. package/lib/chain/errors/proposerPreferences.js +1 -0
  61. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  62. package/lib/chain/initState.d.ts.map +1 -1
  63. package/lib/chain/initState.js +6 -1
  64. package/lib/chain/initState.js.map +1 -1
  65. package/lib/chain/prepareNextSlot.js +1 -1
  66. package/lib/chain/prepareNextSlot.js.map +1 -1
  67. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  68. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  69. package/lib/chain/produceBlock/produceBlockBody.js +2 -7
  70. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  71. package/lib/chain/regen/queued.d.ts.map +1 -1
  72. package/lib/chain/regen/queued.js +1 -4
  73. package/lib/chain/regen/queued.js.map +1 -1
  74. package/lib/chain/regen/regen.d.ts.map +1 -1
  75. package/lib/chain/regen/regen.js +1 -4
  76. package/lib/chain/regen/regen.js.map +1 -1
  77. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
  78. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  79. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +63 -19
  80. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  81. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  82. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  83. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  84. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  85. package/lib/chain/validation/executionPayloadBid.js +11 -8
  86. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  87. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  88. package/lib/chain/validation/proposerPreferences.js +39 -17
  89. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  90. package/lib/network/gossip/topic.d.ts +2 -0
  91. package/lib/network/gossip/topic.d.ts.map +1 -1
  92. package/lib/network/interface.d.ts +1 -0
  93. package/lib/network/interface.d.ts.map +1 -1
  94. package/lib/network/network.d.ts +1 -0
  95. package/lib/network/network.d.ts.map +1 -1
  96. package/lib/network/network.js +3 -0
  97. package/lib/network/network.js.map +1 -1
  98. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  99. package/lib/network/processor/gossipHandlers.js +24 -7
  100. package/lib/network/processor/gossipHandlers.js.map +1 -1
  101. package/lib/network/processor/index.js +5 -5
  102. package/lib/network/processor/index.js.map +1 -1
  103. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  104. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  105. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  106. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  107. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  108. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  109. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  110. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  111. package/lib/network/reqresp/handlers/index.js +5 -0
  112. package/lib/network/reqresp/handlers/index.js.map +1 -1
  113. package/lib/network/reqresp/interface.d.ts +1 -1
  114. package/lib/network/reqresp/interface.js +1 -1
  115. package/lib/network/reqresp/protocols.d.ts +1 -0
  116. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  117. package/lib/network/reqresp/protocols.js +5 -0
  118. package/lib/network/reqresp/protocols.js.map +1 -1
  119. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  120. package/lib/network/reqresp/rateLimit.js +4 -0
  121. package/lib/network/reqresp/rateLimit.js.map +1 -1
  122. package/lib/network/reqresp/score.d.ts.map +1 -1
  123. package/lib/network/reqresp/score.js +1 -0
  124. package/lib/network/reqresp/score.js.map +1 -1
  125. package/lib/network/reqresp/types.d.ts +3 -0
  126. package/lib/network/reqresp/types.d.ts.map +1 -1
  127. package/lib/network/reqresp/types.js +3 -0
  128. package/lib/network/reqresp/types.js.map +1 -1
  129. package/lib/node/nodejs.js +2 -2
  130. package/lib/node/nodejs.js.map +1 -1
  131. package/lib/node/notifier.js +1 -7
  132. package/lib/node/notifier.js.map +1 -1
  133. package/lib/sync/constants.d.ts +7 -1
  134. package/lib/sync/constants.d.ts.map +1 -1
  135. package/lib/sync/constants.js +7 -4
  136. package/lib/sync/constants.js.map +1 -1
  137. package/lib/sync/range/batch.d.ts +23 -3
  138. package/lib/sync/range/batch.d.ts.map +1 -1
  139. package/lib/sync/range/batch.js +191 -36
  140. package/lib/sync/range/batch.js.map +1 -1
  141. package/lib/sync/range/chain.d.ts +16 -2
  142. package/lib/sync/range/chain.d.ts.map +1 -1
  143. package/lib/sync/range/chain.js +99 -8
  144. package/lib/sync/range/chain.js.map +1 -1
  145. package/lib/sync/range/range.d.ts.map +1 -1
  146. package/lib/sync/range/range.js +54 -5
  147. package/lib/sync/range/range.js.map +1 -1
  148. package/lib/sync/sync.d.ts.map +1 -1
  149. package/lib/sync/sync.js +13 -0
  150. package/lib/sync/sync.js.map +1 -1
  151. package/lib/sync/unknownBlock.d.ts +7 -2
  152. package/lib/sync/unknownBlock.d.ts.map +1 -1
  153. package/lib/sync/unknownBlock.js +127 -57
  154. package/lib/sync/unknownBlock.js.map +1 -1
  155. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  156. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  157. package/lib/sync/utils/downloadByRange.js +106 -42
  158. package/lib/sync/utils/downloadByRange.js.map +1 -1
  159. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  160. package/lib/sync/utils/downloadByRoot.js +10 -0
  161. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  162. package/lib/sync/utils/rateLimit.d.ts +2 -0
  163. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  164. package/lib/sync/utils/rateLimit.js +15 -0
  165. package/lib/sync/utils/rateLimit.js.map +1 -0
  166. package/lib/util/sszBytes.d.ts.map +1 -1
  167. package/lib/util/sszBytes.js +8 -6
  168. package/lib/util/sszBytes.js.map +1 -1
  169. package/package.json +16 -16
  170. package/src/api/impl/beacon/blocks/index.ts +13 -0
  171. package/src/api/impl/debug/index.ts +0 -1
  172. package/src/api/impl/validator/index.ts +2 -1
  173. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  174. package/src/chain/blocks/importBlock.ts +16 -49
  175. package/src/chain/blocks/importExecutionPayload.ts +21 -11
  176. package/src/chain/blocks/index.ts +18 -9
  177. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +32 -2
  178. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  179. package/src/chain/blocks/types.ts +2 -1
  180. package/src/chain/blocks/utils/chainSegment.ts +35 -24
  181. package/src/chain/blocks/verifyBlock.ts +7 -5
  182. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  183. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  184. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  185. package/src/chain/chain.ts +29 -9
  186. package/src/chain/emitter.ts +0 -11
  187. package/src/chain/errors/proposerPreferences.ts +9 -1
  188. package/src/chain/initState.ts +9 -1
  189. package/src/chain/prepareNextSlot.ts +1 -1
  190. package/src/chain/produceBlock/produceBlockBody.ts +3 -7
  191. package/src/chain/regen/queued.ts +2 -7
  192. package/src/chain/regen/regen.ts +2 -7
  193. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +78 -22
  194. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  195. package/src/chain/validation/executionPayloadBid.ts +11 -8
  196. package/src/chain/validation/proposerPreferences.ts +37 -18
  197. package/src/network/interface.ts +1 -0
  198. package/src/network/network.ts +12 -0
  199. package/src/network/processor/gossipHandlers.ts +30 -7
  200. package/src/network/processor/index.ts +5 -5
  201. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  202. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  203. package/src/network/reqresp/handlers/index.ts +5 -0
  204. package/src/network/reqresp/interface.ts +1 -1
  205. package/src/network/reqresp/protocols.ts +6 -0
  206. package/src/network/reqresp/rateLimit.ts +4 -0
  207. package/src/network/reqresp/score.ts +1 -0
  208. package/src/network/reqresp/types.ts +5 -0
  209. package/src/node/nodejs.ts +2 -2
  210. package/src/node/notifier.ts +1 -8
  211. package/src/sync/constants.ts +8 -4
  212. package/src/sync/range/batch.ts +240 -42
  213. package/src/sync/range/chain.ts +118 -9
  214. package/src/sync/range/range.ts +68 -5
  215. package/src/sync/sync.ts +13 -1
  216. package/src/sync/unknownBlock.ts +157 -60
  217. package/src/sync/utils/downloadByRange.ts +174 -44
  218. package/src/sync/utils/downloadByRoot.ts +12 -0
  219. package/src/sync/utils/rateLimit.ts +16 -0
  220. package/src/util/sszBytes.ts +8 -6
@@ -42,6 +42,7 @@ import {
42
42
  } from "./types.js";
43
43
  import {DownloadByRootError, downloadByRoot} from "./utils/downloadByRoot.js";
44
44
  import {getAllDescendantBlocks, getUnknownAndAncestorBlocks} from "./utils/pendingBlocksTree.js";
45
+ import {getRateLimitedUntilMs} from "./utils/rateLimit.js";
45
46
 
46
47
  const MAX_ATTEMPTS_PER_BLOCK = 5;
47
48
  const MAX_KNOWN_BAD_BLOCKS = 500;
@@ -63,6 +64,13 @@ enum FetchResult {
63
64
  FailureMaxAttempts = "failure_max_attempts",
64
65
  }
65
66
 
67
+ class UnknownBlockRateLimitedError extends Error {
68
+ constructor(message: string) {
69
+ super(message);
70
+ this.name = "UnknownBlockRateLimitedError";
71
+ }
72
+ }
73
+
66
74
  /**
67
75
  * BlockInputSync is a class that handles ReqResp to find blocks and data related to a specific blockRoot. The
68
76
  * blockRoot may have been found via object gossip, or the API. Gossip objects that can trigger a search are block,
@@ -106,6 +114,7 @@ export class BlockInputSync {
106
114
  private readonly maxPendingBlocks;
107
115
  private subscribedToNetworkEvents = false;
108
116
  private peerBalancer: UnknownBlockPeerBalancer;
117
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
109
118
 
110
119
  constructor(
111
120
  private readonly config: ChainForkConfig,
@@ -145,7 +154,6 @@ export class BlockInputSync {
145
154
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
146
155
  this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
147
156
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
148
- this.chain.emitter.on(ChainEvent.envelopeUnknownBlock, this.onEnvelopeUnknownBlock);
149
157
  this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
150
158
  this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
151
159
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -156,12 +164,12 @@ export class BlockInputSync {
156
164
 
157
165
  unsubscribeFromNetwork(): void {
158
166
  this.logger.verbose("BlockInputSync disabled.");
167
+ this.clearRateLimitBackoffTimer();
159
168
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
160
169
  this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
161
170
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
162
171
  this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
163
172
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
164
- this.chain.emitter.off(ChainEvent.envelopeUnknownBlock, this.onEnvelopeUnknownBlock);
165
173
  this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
166
174
  this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
167
175
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -263,19 +271,6 @@ export class BlockInputSync {
263
271
  }
264
272
  };
265
273
 
266
- private onEnvelopeUnknownBlock = (data: ChainEventData[ChainEvent.envelopeUnknownBlock]): void => {
267
- try {
268
- const blockRootHex = toRootHex(data.envelope.message.beaconBlockRoot);
269
- this.addByRootHex(blockRootHex, data.peer);
270
- this.addByPayloadEnvelope(data.envelope, data.peer);
271
- this.triggerUnknownBlockSearch();
272
- this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
273
- this.metrics?.blockInputSync.source.inc({source: data.source});
274
- } catch (e) {
275
- this.logger.debug("Error handling envelopeUnknownBlock event", {}, e as Error);
276
- }
277
- };
278
-
279
274
  private onBlockImported = (): void => {
280
275
  if (this.pendingPayloads.size > 0) {
281
276
  this.triggerUnknownBlockSearch();
@@ -381,41 +376,6 @@ export class BlockInputSync {
381
376
  return added;
382
377
  };
383
378
 
384
- private addByPayloadEnvelope = (envelope: gloas.SignedExecutionPayloadEnvelope, peerIdStr?: PeerIdStr): void => {
385
- const rootHex = toRootHex(envelope.message.beaconBlockRoot);
386
- const existingPendingPayload = this.pendingPayloads.get(rootHex);
387
- let pendingPayload = this.pendingPayloads.get(rootHex);
388
- if (!pendingPayload || !isPendingPayloadEnvelope(pendingPayload)) {
389
- pendingPayload = {
390
- status: PendingPayloadInputStatus.waitingForBlock,
391
- envelope,
392
- peerIdStrings: new Set(existingPendingPayload?.peerIdStrings ?? []),
393
- timeAddedSec: existingPendingPayload?.timeAddedSec ?? Date.now() / 1000,
394
- };
395
- this.pendingPayloads.set(rootHex, pendingPayload);
396
-
397
- this.logger.verbose("Added payload envelope to BlockInputSync.pendingPayloads", {
398
- slot: envelope.message.payload.slotNumber,
399
- root: rootHex,
400
- });
401
- } else {
402
- this.logger.debug("Overwriting pending payload envelope for root already waiting for block", {
403
- slot: envelope.message.payload.slotNumber,
404
- root: rootHex,
405
- });
406
- pendingPayload.envelope = envelope;
407
- }
408
-
409
- if (peerIdStr) {
410
- pendingPayload.peerIdStrings.add(peerIdStr);
411
- }
412
-
413
- const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
414
- if (prunedItemCount > 0) {
415
- this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
416
- }
417
- };
418
-
419
379
  private addByPayloadInput = (
420
380
  payloadInput: PayloadEnvelopeInput,
421
381
  peerIdStr?: PeerIdStr,
@@ -452,6 +412,7 @@ export class BlockInputSync {
452
412
  private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
453
413
  const peerId = data.peer;
454
414
  this.peerBalancer.onPeerDisconnected(peerId);
415
+ this.scheduleRateLimitBackoffRetry();
455
416
  };
456
417
 
457
418
  /**
@@ -569,7 +530,7 @@ export class BlockInputSync {
569
530
  */
570
531
  private triggerUnknownBlockSearch = (): void => {
571
532
  // Cheap early stop to prevent calling the network.getConnectedPeers()
572
- if (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0) {
533
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
573
534
  return;
574
535
  }
575
536
 
@@ -657,6 +618,36 @@ export class BlockInputSync {
657
618
  }
658
619
  };
659
620
 
621
+ private scheduleRateLimitBackoffRetry(): void {
622
+ this.clearRateLimitBackoffTimer();
623
+
624
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
625
+ return;
626
+ }
627
+
628
+ const now = Date.now();
629
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
630
+ if (retryAt === null) {
631
+ return;
632
+ }
633
+
634
+ this.rateLimitBackoffTimeout = setTimeout(
635
+ () => {
636
+ this.rateLimitBackoffTimeout = undefined;
637
+ this.triggerUnknownBlockSearch();
638
+ this.scheduleRateLimitBackoffRetry();
639
+ },
640
+ Math.max(0, retryAt - now)
641
+ );
642
+ }
643
+
644
+ private clearRateLimitBackoffTimer(): void {
645
+ if (this.rateLimitBackoffTimeout !== undefined) {
646
+ clearTimeout(this.rateLimitBackoffTimeout);
647
+ this.rateLimitBackoffTimeout = undefined;
648
+ }
649
+ }
650
+
660
651
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
661
652
  if (block.status !== PendingBlockInputStatus.pending) {
662
653
  return;
@@ -728,6 +719,16 @@ export class BlockInputSync {
728
719
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
729
720
  }
730
721
  } else {
722
+ if (res.err instanceof UnknownBlockRateLimitedError) {
723
+ const pendingBlock = this.pendingBlocks.get(rootHex);
724
+ if (pendingBlock) {
725
+ pendingBlock.status = PendingBlockInputStatus.pending;
726
+ }
727
+ this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
728
+ this.scheduleRateLimitBackoffRetry();
729
+ return;
730
+ }
731
+
731
732
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
732
733
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
733
734
  this.removeAndDownScoreAllDescendants(block);
@@ -1044,12 +1045,19 @@ export class BlockInputSync {
1044
1045
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1045
1046
 
1046
1047
  let i = 0;
1048
+ let deferredByRateLimit = false;
1047
1049
  while (i++ < this.getMaxDownloadAttempts()) {
1048
1050
  const pendingColumns = payloadInput?.hasAllData()
1049
1051
  ? new Set<number>()
1050
1052
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1051
1053
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1052
1054
  if (peerMeta === null) {
1055
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1056
+ throw new UnknownBlockRateLimitedError(
1057
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1058
+ );
1059
+ }
1060
+
1053
1061
  throw Error(
1054
1062
  `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1055
1063
  );
@@ -1126,7 +1134,12 @@ export class BlockInputSync {
1126
1134
  e as Error
1127
1135
  );
1128
1136
 
1129
- if (e instanceof RequestError) {
1137
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1138
+ if (rateLimitedUntilMs !== null) {
1139
+ deferredByRateLimit = true;
1140
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1141
+ this.scheduleRateLimitBackoffRetry();
1142
+ } else if (e instanceof RequestError) {
1130
1143
  switch (e.type.code) {
1131
1144
  case RequestErrorCode.REQUEST_RATE_LIMITED:
1132
1145
  case RequestErrorCode.REQUEST_TIMEOUT:
@@ -1143,6 +1156,12 @@ export class BlockInputSync {
1143
1156
  }
1144
1157
  }
1145
1158
 
1159
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1160
+ throw new UnknownBlockRateLimitedError(
1161
+ `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1162
+ );
1163
+ }
1164
+
1146
1165
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1147
1166
  }
1148
1167
 
@@ -1226,6 +1245,7 @@ export class BlockInputSync {
1226
1245
  }
1227
1246
 
1228
1247
  let i = 0;
1248
+ let deferredByRateLimit = false;
1229
1249
  while (i++ < this.getMaxDownloadAttempts()) {
1230
1250
  const pendingColumns =
1231
1251
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
@@ -1233,6 +1253,12 @@ export class BlockInputSync {
1233
1253
  : defaultPendingColumns;
1234
1254
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1235
1255
  if (peerMeta === null) {
1256
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1257
+ throw new UnknownBlockRateLimitedError(
1258
+ `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1259
+ );
1260
+ }
1261
+
1236
1262
  // no more peer with needed columns to try, throw error
1237
1263
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
1238
1264
  this.metrics?.blockInputSync.fetchTimeSec.observe(
@@ -1288,14 +1314,23 @@ export class BlockInputSync {
1288
1314
  } else if (e instanceof RequestError) {
1289
1315
  // should look into req_resp metrics in this case
1290
1316
  downloadByRootMetrics?.error.inc({code: "req_resp", client: peerClient});
1291
- switch (e.type.code) {
1292
- case RequestErrorCode.REQUEST_RATE_LIMITED:
1293
- case RequestErrorCode.REQUEST_TIMEOUT:
1294
- // do not exclude peer for these errors
1295
- break;
1296
- default:
1297
- excludedPeers.add(peerId);
1298
- break;
1317
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1318
+ if (rateLimitedUntilMs !== null) {
1319
+ deferredByRateLimit = true;
1320
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1321
+ this.scheduleRateLimitBackoffRetry();
1322
+ } else {
1323
+ switch (e.type.code) {
1324
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1325
+ case RequestErrorCode.RESP_RATE_LIMITED:
1326
+ case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
1327
+ case RequestErrorCode.REQUEST_TIMEOUT:
1328
+ // do not exclude peer for these errors
1329
+ break;
1330
+ default:
1331
+ excludedPeers.add(peerId);
1332
+ break;
1333
+ }
1299
1334
  }
1300
1335
  } else {
1301
1336
  // investigate if this happens
@@ -1323,6 +1358,10 @@ export class BlockInputSync {
1323
1358
 
1324
1359
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1325
1360
 
1361
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1362
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1363
+ }
1364
+
1326
1365
  if (!isPendingBlockInput(cacheItem)) {
1327
1366
  throw Error(`${message} No block and no data was found.`);
1328
1367
  }
@@ -1454,10 +1493,12 @@ export class BlockInputSync {
1454
1493
  export class UnknownBlockPeerBalancer {
1455
1494
  readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
1456
1495
  readonly activeRequests: Map<PeerIdStr, number>;
1496
+ readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
1457
1497
 
1458
1498
  constructor() {
1459
1499
  this.peersMeta = new Map();
1460
1500
  this.activeRequests = new Map();
1501
+ this.rateLimitedUntilByPeer = new Map();
1461
1502
  }
1462
1503
 
1463
1504
  /** Trigger on each peer re-status */
@@ -1472,6 +1513,41 @@ export class UnknownBlockPeerBalancer {
1472
1513
  onPeerDisconnected(peerId: PeerIdStr): void {
1473
1514
  this.peersMeta.delete(peerId);
1474
1515
  this.activeRequests.delete(peerId);
1516
+ this.rateLimitedUntilByPeer.delete(peerId);
1517
+ }
1518
+
1519
+ onRateLimited(peerId: PeerIdStr, rateLimitedUntilMs: number): void {
1520
+ this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
1521
+ }
1522
+
1523
+ getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
1524
+ const now = Date.now();
1525
+ let retryAt: number | null = null;
1526
+
1527
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
1528
+ if (rateLimitedUntil <= now) {
1529
+ this.rateLimitedUntilByPeer.delete(peerId);
1530
+ continue;
1531
+ }
1532
+
1533
+ if (excludedPeers?.has(peerId)) {
1534
+ continue;
1535
+ }
1536
+
1537
+ const syncMeta = this.peersMeta.get(peerId);
1538
+ if (syncMeta === undefined) {
1539
+ this.rateLimitedUntilByPeer.delete(peerId);
1540
+ continue;
1541
+ }
1542
+
1543
+ if (pendingColumns !== undefined && !this.peerHasPendingColumns(syncMeta, pendingColumns)) {
1544
+ continue;
1545
+ }
1546
+
1547
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
1548
+ }
1549
+
1550
+ return retryAt;
1475
1551
  }
1476
1552
 
1477
1553
  /**
@@ -1520,6 +1596,7 @@ export class UnknownBlockPeerBalancer {
1520
1596
  }
1521
1597
 
1522
1598
  private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
1599
+ const now = Date.now();
1523
1600
  let maxColumnCount = 0;
1524
1601
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
1525
1602
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -1528,12 +1605,24 @@ export class UnknownBlockPeerBalancer {
1528
1605
  continue;
1529
1606
  }
1530
1607
 
1608
+ const rateLimitedUntil = this.rateLimitedUntilByPeer.get(peerId);
1609
+ if (rateLimitedUntil !== undefined) {
1610
+ if (now < rateLimitedUntil) {
1611
+ continue;
1612
+ }
1613
+ this.rateLimitedUntilByPeer.delete(peerId);
1614
+ }
1615
+
1531
1616
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
1532
1617
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
1533
1618
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
1534
1619
  continue;
1535
1620
  }
1536
1621
 
1622
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1623
+ continue;
1624
+ }
1625
+
1537
1626
  if (pendingDataColumns.size === 0) {
1538
1627
  considerPeers.push({peerId, columnCount: 0});
1539
1628
  continue;
@@ -1567,4 +1656,12 @@ export class UnknownBlockPeerBalancer {
1567
1656
 
1568
1657
  return eligiblePeers;
1569
1658
  }
1659
+
1660
+ private peerHasPendingColumns(syncMeta: PeerSyncMeta, pendingDataColumns: Set<number>): boolean {
1661
+ if (pendingDataColumns.size === 0) {
1662
+ return true;
1663
+ }
1664
+
1665
+ return syncMeta.custodyColumns.some((column) => pendingDataColumns.has(column));
1666
+ }
1570
1667
  }