@lodestar/beacon-node 1.43.0-dev.b05ea98d04 → 1.43.0-dev.b0e1143d06

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 (132) hide show
  1. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  2. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  3. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  4. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  5. package/lib/chain/blocks/index.d.ts.map +1 -1
  6. package/lib/chain/blocks/index.js +6 -7
  7. package/lib/chain/blocks/index.js.map +1 -1
  8. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +8 -1
  9. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  10. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +19 -0
  11. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  12. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  13. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  14. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  15. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  16. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  17. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  18. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/chain.d.ts.map +1 -1
  22. package/lib/chain/chain.js +15 -1
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -6
  25. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  26. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +21 -5
  27. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  28. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  29. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  30. package/lib/metrics/metrics/lodestar.js +5 -0
  31. package/lib/metrics/metrics/lodestar.js.map +1 -1
  32. package/lib/network/gossip/topic.d.ts +749 -2
  33. package/lib/network/gossip/topic.d.ts.map +1 -1
  34. package/lib/network/interface.d.ts +1 -0
  35. package/lib/network/interface.d.ts.map +1 -1
  36. package/lib/network/network.d.ts +1 -0
  37. package/lib/network/network.d.ts.map +1 -1
  38. package/lib/network/network.js +3 -0
  39. package/lib/network/network.js.map +1 -1
  40. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  41. package/lib/network/processor/gossipHandlers.js +31 -1
  42. package/lib/network/processor/gossipHandlers.js.map +1 -1
  43. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  44. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  45. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  46. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  47. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  48. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  49. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  50. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  51. package/lib/network/reqresp/handlers/index.js +5 -0
  52. package/lib/network/reqresp/handlers/index.js.map +1 -1
  53. package/lib/network/reqresp/interface.d.ts +1 -1
  54. package/lib/network/reqresp/interface.js +1 -1
  55. package/lib/network/reqresp/protocols.d.ts +1 -0
  56. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  57. package/lib/network/reqresp/protocols.js +5 -0
  58. package/lib/network/reqresp/protocols.js.map +1 -1
  59. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  60. package/lib/network/reqresp/rateLimit.js +4 -0
  61. package/lib/network/reqresp/rateLimit.js.map +1 -1
  62. package/lib/network/reqresp/score.d.ts.map +1 -1
  63. package/lib/network/reqresp/score.js +1 -0
  64. package/lib/network/reqresp/score.js.map +1 -1
  65. package/lib/network/reqresp/types.d.ts +3 -0
  66. package/lib/network/reqresp/types.d.ts.map +1 -1
  67. package/lib/network/reqresp/types.js +3 -0
  68. package/lib/network/reqresp/types.js.map +1 -1
  69. package/lib/sync/constants.d.ts +5 -1
  70. package/lib/sync/constants.d.ts.map +1 -1
  71. package/lib/sync/constants.js +5 -1
  72. package/lib/sync/constants.js.map +1 -1
  73. package/lib/sync/range/batch.d.ts +27 -6
  74. package/lib/sync/range/batch.d.ts.map +1 -1
  75. package/lib/sync/range/batch.js +149 -26
  76. package/lib/sync/range/batch.js.map +1 -1
  77. package/lib/sync/range/chain.d.ts +10 -2
  78. package/lib/sync/range/chain.d.ts.map +1 -1
  79. package/lib/sync/range/chain.js +55 -7
  80. package/lib/sync/range/chain.js.map +1 -1
  81. package/lib/sync/range/range.d.ts.map +1 -1
  82. package/lib/sync/range/range.js +54 -5
  83. package/lib/sync/range/range.js.map +1 -1
  84. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  85. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  86. package/lib/sync/range/utils/peerBalancer.js +8 -4
  87. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  88. package/lib/sync/sync.d.ts.map +1 -1
  89. package/lib/sync/sync.js +13 -0
  90. package/lib/sync/sync.js.map +1 -1
  91. package/lib/sync/unknownBlock.d.ts +7 -0
  92. package/lib/sync/unknownBlock.d.ts.map +1 -1
  93. package/lib/sync/unknownBlock.js +127 -12
  94. package/lib/sync/unknownBlock.js.map +1 -1
  95. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  96. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  97. package/lib/sync/utils/downloadByRange.js +87 -38
  98. package/lib/sync/utils/downloadByRange.js.map +1 -1
  99. package/lib/sync/utils/rateLimit.d.ts +2 -0
  100. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  101. package/lib/sync/utils/rateLimit.js +15 -0
  102. package/lib/sync/utils/rateLimit.js.map +1 -0
  103. package/package.json +16 -16
  104. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  105. package/src/chain/blocks/index.ts +5 -7
  106. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -1
  107. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  108. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  109. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  110. package/src/chain/chain.ts +17 -0
  111. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +23 -6
  112. package/src/metrics/metrics/lodestar.ts +6 -0
  113. package/src/network/interface.ts +1 -0
  114. package/src/network/network.ts +12 -0
  115. package/src/network/processor/gossipHandlers.ts +35 -1
  116. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  117. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  118. package/src/network/reqresp/handlers/index.ts +5 -0
  119. package/src/network/reqresp/interface.ts +1 -1
  120. package/src/network/reqresp/protocols.ts +6 -0
  121. package/src/network/reqresp/rateLimit.ts +4 -0
  122. package/src/network/reqresp/score.ts +1 -0
  123. package/src/network/reqresp/types.ts +5 -0
  124. package/src/sync/constants.ts +5 -1
  125. package/src/sync/range/batch.ts +188 -30
  126. package/src/sync/range/chain.ts +72 -13
  127. package/src/sync/range/range.ts +68 -5
  128. package/src/sync/range/utils/peerBalancer.ts +9 -3
  129. package/src/sync/sync.ts +13 -1
  130. package/src/sync/unknownBlock.ts +157 -12
  131. package/src/sync/utils/downloadByRange.ts +153 -39
  132. package/src/sync/utils/rateLimit.ts +16 -0
package/src/sync/sync.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
2
  import {Slot} from "@lodestar/types";
3
- import {Logger} from "@lodestar/utils";
3
+ import {Logger, toRootHex} from "@lodestar/utils";
4
4
  import {IBeaconChain} from "../chain/index.js";
5
5
  import {GENESIS_SLOT} from "../constants/constants.js";
6
6
  import {ExecutionEngineState} from "../execution/index.js";
@@ -188,6 +188,18 @@ export class BeaconSync implements IBeaconSync {
188
188
  private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
189
189
  const localStatus = this.chain.getStatus();
190
190
  const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance);
191
+ this.logger.verbose("Peer sync type classified", {
192
+ peer: data.peer,
193
+ syncType,
194
+ localFinalizedEpoch: localStatus.finalizedEpoch,
195
+ localFinalizedRoot: toRootHex(localStatus.finalizedRoot),
196
+ localHeadSlot: localStatus.headSlot,
197
+ localHeadRoot: toRootHex(localStatus.headRoot),
198
+ remoteFinalizedEpoch: data.status.finalizedEpoch,
199
+ remoteFinalizedRoot: toRootHex(data.status.finalizedRoot),
200
+ remoteHeadSlot: data.status.headSlot,
201
+ remoteHeadRoot: toRootHex(data.status.headRoot),
202
+ });
191
203
 
192
204
  // For metrics only
193
205
  this.peerSyncType.set(data.peer, syncType);
@@ -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,
@@ -155,6 +164,7 @@ export class BlockInputSync {
155
164
 
156
165
  unsubscribeFromNetwork(): void {
157
166
  this.logger.verbose("BlockInputSync disabled.");
167
+ this.clearRateLimitBackoffTimer();
158
168
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
159
169
  this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
160
170
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
@@ -402,6 +412,7 @@ export class BlockInputSync {
402
412
  private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
403
413
  const peerId = data.peer;
404
414
  this.peerBalancer.onPeerDisconnected(peerId);
415
+ this.scheduleRateLimitBackoffRetry();
405
416
  };
406
417
 
407
418
  /**
@@ -519,7 +530,7 @@ export class BlockInputSync {
519
530
  */
520
531
  private triggerUnknownBlockSearch = (): void => {
521
532
  // Cheap early stop to prevent calling the network.getConnectedPeers()
522
- if (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0) {
533
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
523
534
  return;
524
535
  }
525
536
 
@@ -607,6 +618,36 @@ export class BlockInputSync {
607
618
  }
608
619
  };
609
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
+
610
651
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
611
652
  if (block.status !== PendingBlockInputStatus.pending) {
612
653
  return;
@@ -678,6 +719,16 @@ export class BlockInputSync {
678
719
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
679
720
  }
680
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
+
681
732
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
682
733
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
683
734
  this.removeAndDownScoreAllDescendants(block);
@@ -994,12 +1045,19 @@ export class BlockInputSync {
994
1045
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
995
1046
 
996
1047
  let i = 0;
1048
+ let deferredByRateLimit = false;
997
1049
  while (i++ < this.getMaxDownloadAttempts()) {
998
1050
  const pendingColumns = payloadInput?.hasAllData()
999
1051
  ? new Set<number>()
1000
1052
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1001
1053
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1002
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
+
1003
1061
  throw Error(
1004
1062
  `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1005
1063
  );
@@ -1076,7 +1134,12 @@ export class BlockInputSync {
1076
1134
  e as Error
1077
1135
  );
1078
1136
 
1079
- 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) {
1080
1143
  switch (e.type.code) {
1081
1144
  case RequestErrorCode.REQUEST_RATE_LIMITED:
1082
1145
  case RequestErrorCode.REQUEST_TIMEOUT:
@@ -1093,6 +1156,12 @@ export class BlockInputSync {
1093
1156
  }
1094
1157
  }
1095
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
+
1096
1165
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1097
1166
  }
1098
1167
 
@@ -1176,6 +1245,7 @@ export class BlockInputSync {
1176
1245
  }
1177
1246
 
1178
1247
  let i = 0;
1248
+ let deferredByRateLimit = false;
1179
1249
  while (i++ < this.getMaxDownloadAttempts()) {
1180
1250
  const pendingColumns =
1181
1251
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
@@ -1183,6 +1253,12 @@ export class BlockInputSync {
1183
1253
  : defaultPendingColumns;
1184
1254
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1185
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
+
1186
1262
  // no more peer with needed columns to try, throw error
1187
1263
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
1188
1264
  this.metrics?.blockInputSync.fetchTimeSec.observe(
@@ -1238,16 +1314,23 @@ export class BlockInputSync {
1238
1314
  } else if (e instanceof RequestError) {
1239
1315
  // should look into req_resp metrics in this case
1240
1316
  downloadByRootMetrics?.error.inc({code: "req_resp", client: peerClient});
1241
- switch (e.type.code) {
1242
- case RequestErrorCode.REQUEST_RATE_LIMITED:
1243
- case RequestErrorCode.RESP_RATE_LIMITED:
1244
- case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
1245
- case RequestErrorCode.REQUEST_TIMEOUT:
1246
- // do not exclude peer for these errors
1247
- break;
1248
- default:
1249
- excludedPeers.add(peerId);
1250
- 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
+ }
1251
1334
  }
1252
1335
  } else {
1253
1336
  // investigate if this happens
@@ -1275,6 +1358,10 @@ export class BlockInputSync {
1275
1358
 
1276
1359
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1277
1360
 
1361
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1362
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1363
+ }
1364
+
1278
1365
  if (!isPendingBlockInput(cacheItem)) {
1279
1366
  throw Error(`${message} No block and no data was found.`);
1280
1367
  }
@@ -1406,10 +1493,12 @@ export class BlockInputSync {
1406
1493
  export class UnknownBlockPeerBalancer {
1407
1494
  readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
1408
1495
  readonly activeRequests: Map<PeerIdStr, number>;
1496
+ readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
1409
1497
 
1410
1498
  constructor() {
1411
1499
  this.peersMeta = new Map();
1412
1500
  this.activeRequests = new Map();
1501
+ this.rateLimitedUntilByPeer = new Map();
1413
1502
  }
1414
1503
 
1415
1504
  /** Trigger on each peer re-status */
@@ -1424,6 +1513,41 @@ export class UnknownBlockPeerBalancer {
1424
1513
  onPeerDisconnected(peerId: PeerIdStr): void {
1425
1514
  this.peersMeta.delete(peerId);
1426
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;
1427
1551
  }
1428
1552
 
1429
1553
  /**
@@ -1472,6 +1596,7 @@ export class UnknownBlockPeerBalancer {
1472
1596
  }
1473
1597
 
1474
1598
  private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
1599
+ const now = Date.now();
1475
1600
  let maxColumnCount = 0;
1476
1601
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
1477
1602
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -1480,12 +1605,24 @@ export class UnknownBlockPeerBalancer {
1480
1605
  continue;
1481
1606
  }
1482
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
+
1483
1616
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
1484
1617
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
1485
1618
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
1486
1619
  continue;
1487
1620
  }
1488
1621
 
1622
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1623
+ continue;
1624
+ }
1625
+
1489
1626
  if (pendingDataColumns.size === 0) {
1490
1627
  considerPeers.push({peerId, columnCount: 0});
1491
1628
  continue;
@@ -1519,4 +1656,12 @@ export class UnknownBlockPeerBalancer {
1519
1656
 
1520
1657
  return eligiblePeers;
1521
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
+ }
1522
1667
  }