@lodestar/beacon-node 1.43.0-dev.87cbe69c66 → 1.43.0-dev.8ad5d8be4c

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 (153) hide show
  1. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  2. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  3. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  4. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  5. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  6. package/lib/chain/blocks/importBlock.js +0 -16
  7. package/lib/chain/blocks/importBlock.js.map +1 -1
  8. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  9. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  10. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  11. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  12. package/lib/chain/blocks/index.d.ts.map +1 -1
  13. package/lib/chain/blocks/index.js +6 -7
  14. package/lib/chain/blocks/index.js.map +1 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
  16. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
  18. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  19. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  20. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  26. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  27. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  28. package/lib/chain/chain.d.ts.map +1 -1
  29. package/lib/chain/chain.js +25 -10
  30. package/lib/chain/chain.js.map +1 -1
  31. package/lib/chain/initState.d.ts.map +1 -1
  32. package/lib/chain/initState.js +6 -1
  33. package/lib/chain/initState.js.map +1 -1
  34. package/lib/chain/prepareNextSlot.js +1 -1
  35. package/lib/chain/prepareNextSlot.js.map +1 -1
  36. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
  37. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  38. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
  39. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  40. package/lib/chain/seenCache/seenProposerPreferences.d.ts +1 -1
  41. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  42. package/lib/chain/seenCache/seenProposerPreferences.js +6 -11
  43. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  44. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  45. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  46. package/lib/metrics/metrics/lodestar.js +5 -0
  47. package/lib/metrics/metrics/lodestar.js.map +1 -1
  48. package/lib/network/gossip/topic.d.ts +749 -2
  49. package/lib/network/gossip/topic.d.ts.map +1 -1
  50. package/lib/network/interface.d.ts +1 -0
  51. package/lib/network/interface.d.ts.map +1 -1
  52. package/lib/network/network.d.ts +1 -0
  53. package/lib/network/network.d.ts.map +1 -1
  54. package/lib/network/network.js +3 -0
  55. package/lib/network/network.js.map +1 -1
  56. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  57. package/lib/network/processor/gossipHandlers.js +45 -1
  58. package/lib/network/processor/gossipHandlers.js.map +1 -1
  59. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  60. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  61. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  62. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  63. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  64. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  65. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  66. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  67. package/lib/network/reqresp/handlers/index.js +5 -0
  68. package/lib/network/reqresp/handlers/index.js.map +1 -1
  69. package/lib/network/reqresp/interface.d.ts +1 -1
  70. package/lib/network/reqresp/interface.js +1 -1
  71. package/lib/network/reqresp/protocols.d.ts +1 -0
  72. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  73. package/lib/network/reqresp/protocols.js +5 -0
  74. package/lib/network/reqresp/protocols.js.map +1 -1
  75. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  76. package/lib/network/reqresp/rateLimit.js +4 -0
  77. package/lib/network/reqresp/rateLimit.js.map +1 -1
  78. package/lib/network/reqresp/score.d.ts.map +1 -1
  79. package/lib/network/reqresp/score.js +1 -0
  80. package/lib/network/reqresp/score.js.map +1 -1
  81. package/lib/network/reqresp/types.d.ts +3 -0
  82. package/lib/network/reqresp/types.d.ts.map +1 -1
  83. package/lib/network/reqresp/types.js +3 -0
  84. package/lib/network/reqresp/types.js.map +1 -1
  85. package/lib/sync/constants.d.ts +7 -1
  86. package/lib/sync/constants.d.ts.map +1 -1
  87. package/lib/sync/constants.js +7 -4
  88. package/lib/sync/constants.js.map +1 -1
  89. package/lib/sync/range/batch.d.ts +32 -6
  90. package/lib/sync/range/batch.d.ts.map +1 -1
  91. package/lib/sync/range/batch.js +215 -41
  92. package/lib/sync/range/batch.js.map +1 -1
  93. package/lib/sync/range/chain.d.ts +16 -2
  94. package/lib/sync/range/chain.d.ts.map +1 -1
  95. package/lib/sync/range/chain.js +77 -4
  96. package/lib/sync/range/chain.js.map +1 -1
  97. package/lib/sync/range/range.d.ts.map +1 -1
  98. package/lib/sync/range/range.js +54 -5
  99. package/lib/sync/range/range.js.map +1 -1
  100. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  101. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  102. package/lib/sync/range/utils/peerBalancer.js +8 -4
  103. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  104. package/lib/sync/sync.d.ts.map +1 -1
  105. package/lib/sync/sync.js +13 -0
  106. package/lib/sync/sync.js.map +1 -1
  107. package/lib/sync/unknownBlock.d.ts +7 -0
  108. package/lib/sync/unknownBlock.d.ts.map +1 -1
  109. package/lib/sync/unknownBlock.js +127 -10
  110. package/lib/sync/unknownBlock.js.map +1 -1
  111. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  112. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  113. package/lib/sync/utils/downloadByRange.js +89 -42
  114. package/lib/sync/utils/downloadByRange.js.map +1 -1
  115. package/lib/sync/utils/rateLimit.d.ts +2 -0
  116. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  117. package/lib/sync/utils/rateLimit.js +15 -0
  118. package/lib/sync/utils/rateLimit.js.map +1 -0
  119. package/package.json +16 -16
  120. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  121. package/src/chain/blocks/importBlock.ts +0 -18
  122. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  123. package/src/chain/blocks/index.ts +5 -7
  124. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
  125. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  126. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  127. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  128. package/src/chain/chain.ts +29 -9
  129. package/src/chain/initState.ts +9 -1
  130. package/src/chain/prepareNextSlot.ts +1 -1
  131. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
  132. package/src/chain/seenCache/seenProposerPreferences.ts +7 -12
  133. package/src/metrics/metrics/lodestar.ts +6 -0
  134. package/src/network/interface.ts +1 -0
  135. package/src/network/network.ts +12 -0
  136. package/src/network/processor/gossipHandlers.ts +53 -1
  137. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  138. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  139. package/src/network/reqresp/handlers/index.ts +5 -0
  140. package/src/network/reqresp/interface.ts +1 -1
  141. package/src/network/reqresp/protocols.ts +6 -0
  142. package/src/network/reqresp/rateLimit.ts +4 -0
  143. package/src/network/reqresp/score.ts +1 -0
  144. package/src/network/reqresp/types.ts +5 -0
  145. package/src/sync/constants.ts +8 -4
  146. package/src/sync/range/batch.ts +256 -47
  147. package/src/sync/range/chain.ts +94 -5
  148. package/src/sync/range/range.ts +68 -5
  149. package/src/sync/range/utils/peerBalancer.ts +9 -3
  150. package/src/sync/sync.ts +13 -1
  151. package/src/sync/unknownBlock.ts +157 -10
  152. package/src/sync/utils/downloadByRange.ts +156 -44
  153. package/src/sync/utils/rateLimit.ts +16 -0
@@ -1,16 +1,23 @@
1
1
  import {EventEmitter} from "node:events";
2
2
  import {StrictEventEmitter} from "strict-event-emitter-types";
3
3
  import {BeaconConfig} from "@lodestar/config";
4
- import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
+ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
- import {Logger, toRootHex} from "@lodestar/utils";
6
+ import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
8
8
  import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
9
+ import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
10
+ import {BlockError} from "../../chain/errors/index.js";
9
11
  import {IBeaconChain} from "../../chain/index.js";
10
12
  import {Metrics} from "../../metrics/index.js";
11
13
  import {INetwork} from "../../network/index.js";
12
14
  import {PeerIdStr} from "../../util/peerId.js";
13
- import {cacheByRangeResponses, downloadByRange} from "../utils/downloadByRange.js";
15
+ import {
16
+ DownloadByRangeError,
17
+ DownloadByRangeErrorCode,
18
+ cacheByRangeResponses,
19
+ downloadByRange,
20
+ } from "../utils/downloadByRange.js";
14
21
  import {RangeSyncType, getRangeSyncTarget, rangeSyncTypes} from "../utils/remoteSyncType.js";
15
22
  import {ChainTarget, SyncChain, SyncChainDebugState, SyncChainFns} from "./chain.js";
16
23
  import {updateChains} from "./utils/index.js";
@@ -206,14 +213,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
206
213
 
207
214
  private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
208
215
  const batchBlocks = batch.getBlocks();
216
+ const requests = batch.getRequestsForPeer(peer);
217
+ const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
218
+ const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
209
219
  const {result, warnings} = await downloadByRange({
210
220
  config: this.config,
211
221
  network: this.network,
212
222
  logger: this.logger,
213
223
  peerIdStr: peer.peerId,
214
224
  batchBlocks,
225
+ parentPayloadCommitments,
215
226
  peerDasMetrics: this.chain.metrics?.peerDas,
216
- ...batch.getRequestsForPeer(peer),
227
+ ...requests,
217
228
  });
218
229
  const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
219
230
  const {blocks, payloadEnvelopes} = cacheByRangeResponses({
@@ -227,6 +238,49 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
227
238
  custodyConfig: this.chain.custodyConfig,
228
239
  seenTimestampSec: Date.now() / 1000,
229
240
  });
241
+
242
+ const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot);
243
+ const envelopeSlots = payloadEnvelopes
244
+ ? Array.from(payloadEnvelopes.entries())
245
+ .filter(([, pi]) => pi.hasPayloadEnvelope())
246
+ .map(([slot]) => slot)
247
+ .sort((a, b) => a - b)
248
+ : [];
249
+ this.logger.verbose("downloadByRange batch ready", {
250
+ peer: peer.peerId,
251
+ blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)),
252
+ envelopeSlots: prettyPrintIndices(envelopeSlots),
253
+ ...batch.getMetadata(),
254
+ });
255
+
256
+ if (segmentBlocks.length > 1) {
257
+ try {
258
+ assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null);
259
+ } catch (err) {
260
+ if (err instanceof BlockError) {
261
+ this.logger.debug(
262
+ "downloadByRange segment validation failed",
263
+ {
264
+ peer: peer.peerId,
265
+ reason: err.type.code,
266
+ slot: err.signedBlock.message.slot,
267
+ detail: JSON.stringify(err.type),
268
+ ...batch.getMetadata(),
269
+ },
270
+ err
271
+ );
272
+ // with this error, the peer will be penalized inside SyncChain
273
+ throw new DownloadByRangeError({
274
+ code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT,
275
+ slot: err.signedBlock.message.slot,
276
+ reason: err.type.code,
277
+ });
278
+ }
279
+
280
+ throw err;
281
+ }
282
+ }
283
+
230
284
  return {result: {blocks, payloadEnvelopes}, warnings};
231
285
  };
232
286
 
@@ -258,6 +312,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
258
312
  private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
259
313
  let syncChain = this.chains.get(syncType);
260
314
  if (!syncChain) {
315
+ // The first batch of a new sync chain may need to detect whether the parent block was an
316
+ // gloas "empty" block (no envelope produced). It does so by comparing the first
317
+ // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
318
+ const headState = this.chain.getHeadState();
319
+ const latestBid = isStatePostGloas(headState)
320
+ ? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
321
+ : undefined;
322
+
261
323
  syncChain = new SyncChain(
262
324
  startEpoch,
263
325
  target,
@@ -276,7 +338,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
276
338
  logger: this.logger,
277
339
  custodyConfig: this.chain.custodyConfig,
278
340
  metrics: this.metrics,
279
- }
341
+ },
342
+ latestBid
280
343
  );
281
344
  this.chains.set(syncType, syncChain);
282
345
 
@@ -52,7 +52,8 @@ export class ChainPeersBalancer {
52
52
 
53
53
  /**
54
54
  * Return the most suitable peer to retry
55
- * Sort peers by (1) no failed request (2) less active requests, then pick first
55
+ * Sort peers by (1) less active requests (2) most columns we need.
56
+ * Peers that failed this batch or already succeeded for the same request are excluded inside `filterPeers`.
56
57
  */
57
58
  bestPeerToRetryBatch(batch: Batch): PeerSyncMeta | undefined {
58
59
  if (batch.state.status !== BatchStatus.AwaitingDownload) {
@@ -63,10 +64,8 @@ export class ChainPeersBalancer {
63
64
  const pendingDataColumns = columnsRequest?.columns ?? this.custodyConfig.sampledColumns;
64
65
  const eligiblePeers = this.filterPeers(batch, pendingDataColumns, false);
65
66
 
66
- const failedPeers = new Set(batch.getFailedPeers());
67
67
  const sortedBestPeers = sortBy(
68
68
  eligiblePeers,
69
- ({syncInfo}) => (failedPeers.has(syncInfo.peerId) ? 1 : 0), // prefer peers without failed requests
70
69
  ({syncInfo}) => this.activeRequestsByPeer.get(syncInfo.peerId) ?? 0, // prefer peers with least active req
71
70
  ({columns}) => -1 * columns // prefer peers with the most columns
72
71
  );
@@ -117,9 +116,16 @@ export class ChainPeersBalancer {
117
116
  return eligiblePeers;
118
117
  }
119
118
 
119
+ // Skip peers that failed this batch, or that already returned the exact current request shape.
120
+ const failedPeers = new Set<PeerIdStr>(batch.getFailedPeers());
121
+
120
122
  for (const peer of this.peers) {
121
123
  const {earliestAvailableSlot, target, peerId} = peer;
122
124
 
125
+ if (failedPeers.has(peerId) || batch.hasPeerSucceededCurrentRequest(peer)) {
126
+ continue;
127
+ }
128
+
123
129
  const activeRequest = this.activeRequestsByPeer.get(peerId) ?? 0;
124
130
  if (noActiveRequest && activeRequest > 0) {
125
131
  // consumer wants to find peer with no active request, but this peer has active request
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,14 +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.REQUEST_TIMEOUT:
1244
- // do not exclude peer for these errors
1245
- break;
1246
- default:
1247
- excludedPeers.add(peerId);
1248
- 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
+ }
1249
1334
  }
1250
1335
  } else {
1251
1336
  // investigate if this happens
@@ -1273,6 +1358,10 @@ export class BlockInputSync {
1273
1358
 
1274
1359
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1275
1360
 
1361
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1362
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1363
+ }
1364
+
1276
1365
  if (!isPendingBlockInput(cacheItem)) {
1277
1366
  throw Error(`${message} No block and no data was found.`);
1278
1367
  }
@@ -1404,10 +1493,12 @@ export class BlockInputSync {
1404
1493
  export class UnknownBlockPeerBalancer {
1405
1494
  readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
1406
1495
  readonly activeRequests: Map<PeerIdStr, number>;
1496
+ readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
1407
1497
 
1408
1498
  constructor() {
1409
1499
  this.peersMeta = new Map();
1410
1500
  this.activeRequests = new Map();
1501
+ this.rateLimitedUntilByPeer = new Map();
1411
1502
  }
1412
1503
 
1413
1504
  /** Trigger on each peer re-status */
@@ -1422,6 +1513,41 @@ export class UnknownBlockPeerBalancer {
1422
1513
  onPeerDisconnected(peerId: PeerIdStr): void {
1423
1514
  this.peersMeta.delete(peerId);
1424
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;
1425
1551
  }
1426
1552
 
1427
1553
  /**
@@ -1470,6 +1596,7 @@ export class UnknownBlockPeerBalancer {
1470
1596
  }
1471
1597
 
1472
1598
  private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
1599
+ const now = Date.now();
1473
1600
  let maxColumnCount = 0;
1474
1601
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
1475
1602
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -1478,12 +1605,24 @@ export class UnknownBlockPeerBalancer {
1478
1605
  continue;
1479
1606
  }
1480
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
+
1481
1616
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
1482
1617
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
1483
1618
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
1484
1619
  continue;
1485
1620
  }
1486
1621
 
1622
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1623
+ continue;
1624
+ }
1625
+
1487
1626
  if (pendingDataColumns.size === 0) {
1488
1627
  considerPeers.push({peerId, columnCount: 0});
1489
1628
  continue;
@@ -1517,4 +1656,12 @@ export class UnknownBlockPeerBalancer {
1517
1656
 
1518
1657
  return eligiblePeers;
1519
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
+ }
1520
1667
  }