@lodestar/beacon-node 1.43.0-dev.a45ba75824 → 1.43.0-dev.a58ad80359

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 (156) hide show
  1. package/lib/api/impl/debug/index.d.ts.map +1 -1
  2. package/lib/api/impl/debug/index.js +0 -1
  3. package/lib/api/impl/debug/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +2 -1
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  8. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  9. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  10. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +14 -18
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  15. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  16. package/lib/chain/blocks/importExecutionPayload.js +15 -6
  17. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  18. package/lib/chain/blocks/index.d.ts.map +1 -1
  19. package/lib/chain/blocks/index.js +29 -17
  20. package/lib/chain/blocks/index.js.map +1 -1
  21. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
  22. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  23. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
  24. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  25. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  26. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  27. package/lib/chain/blocks/types.d.ts +2 -1
  28. package/lib/chain/blocks/types.d.ts.map +1 -1
  29. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  30. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  31. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  32. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  33. package/lib/chain/blocks/verifyBlock.js +5 -6
  34. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  35. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  36. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  37. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  38. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  39. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  40. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  42. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  43. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  44. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  45. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  46. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  47. package/lib/chain/chain.d.ts.map +1 -1
  48. package/lib/chain/chain.js +25 -10
  49. package/lib/chain/chain.js.map +1 -1
  50. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  51. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  52. package/lib/chain/errors/proposerPreferences.js +1 -0
  53. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  54. package/lib/chain/initState.d.ts.map +1 -1
  55. package/lib/chain/initState.js +6 -1
  56. package/lib/chain/initState.js.map +1 -1
  57. package/lib/chain/prepareNextSlot.js +1 -1
  58. package/lib/chain/prepareNextSlot.js.map +1 -1
  59. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  60. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  61. package/lib/chain/produceBlock/produceBlockBody.js +2 -7
  62. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  63. package/lib/chain/regen/queued.d.ts.map +1 -1
  64. package/lib/chain/regen/queued.js +1 -4
  65. package/lib/chain/regen/queued.js.map +1 -1
  66. package/lib/chain/regen/regen.d.ts.map +1 -1
  67. package/lib/chain/regen/regen.js +1 -4
  68. package/lib/chain/regen/regen.js.map +1 -1
  69. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
  70. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  71. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
  72. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  73. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  74. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  75. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  76. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  77. package/lib/chain/validation/executionPayloadBid.js +11 -8
  78. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  79. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  80. package/lib/chain/validation/proposerPreferences.js +39 -17
  81. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  82. package/lib/network/gossip/topic.d.ts +2 -0
  83. package/lib/network/gossip/topic.d.ts.map +1 -1
  84. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  85. package/lib/network/processor/gossipHandlers.js +14 -0
  86. package/lib/network/processor/gossipHandlers.js.map +1 -1
  87. package/lib/node/nodejs.js +2 -2
  88. package/lib/node/nodejs.js.map +1 -1
  89. package/lib/node/notifier.js +1 -7
  90. package/lib/node/notifier.js.map +1 -1
  91. package/lib/sync/constants.d.ts +3 -1
  92. package/lib/sync/constants.d.ts.map +1 -1
  93. package/lib/sync/constants.js +3 -4
  94. package/lib/sync/constants.js.map +1 -1
  95. package/lib/sync/range/batch.d.ts +12 -3
  96. package/lib/sync/range/batch.d.ts.map +1 -1
  97. package/lib/sync/range/batch.js +163 -33
  98. package/lib/sync/range/batch.js.map +1 -1
  99. package/lib/sync/range/chain.d.ts +13 -2
  100. package/lib/sync/range/chain.d.ts.map +1 -1
  101. package/lib/sync/range/chain.js +38 -4
  102. package/lib/sync/range/chain.js.map +1 -1
  103. package/lib/sync/range/range.d.ts.map +1 -1
  104. package/lib/sync/range/range.js +14 -3
  105. package/lib/sync/range/range.js.map +1 -1
  106. package/lib/sync/sync.d.ts.map +1 -1
  107. package/lib/sync/sync.js +13 -0
  108. package/lib/sync/sync.js.map +1 -1
  109. package/lib/sync/unknownBlock.d.ts +7 -0
  110. package/lib/sync/unknownBlock.d.ts.map +1 -1
  111. package/lib/sync/unknownBlock.js +138 -10
  112. package/lib/sync/unknownBlock.js.map +1 -1
  113. package/lib/sync/utils/downloadByRange.d.ts +29 -8
  114. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  115. package/lib/sync/utils/downloadByRange.js +87 -31
  116. package/lib/sync/utils/downloadByRange.js.map +1 -1
  117. package/lib/util/sszBytes.d.ts.map +1 -1
  118. package/lib/util/sszBytes.js +8 -6
  119. package/lib/util/sszBytes.js.map +1 -1
  120. package/package.json +16 -16
  121. package/src/api/impl/debug/index.ts +0 -1
  122. package/src/api/impl/validator/index.ts +2 -1
  123. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  124. package/src/chain/blocks/importBlock.ts +14 -21
  125. package/src/chain/blocks/importExecutionPayload.ts +21 -11
  126. package/src/chain/blocks/index.ts +18 -9
  127. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
  128. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  129. package/src/chain/blocks/types.ts +2 -1
  130. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  131. package/src/chain/blocks/verifyBlock.ts +7 -5
  132. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  133. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  134. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  135. package/src/chain/chain.ts +29 -9
  136. package/src/chain/errors/proposerPreferences.ts +9 -1
  137. package/src/chain/initState.ts +9 -1
  138. package/src/chain/prepareNextSlot.ts +1 -1
  139. package/src/chain/produceBlock/produceBlockBody.ts +3 -7
  140. package/src/chain/regen/queued.ts +2 -7
  141. package/src/chain/regen/regen.ts +2 -7
  142. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
  143. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  144. package/src/chain/validation/executionPayloadBid.ts +11 -8
  145. package/src/chain/validation/proposerPreferences.ts +37 -18
  146. package/src/network/processor/gossipHandlers.ts +18 -0
  147. package/src/node/nodejs.ts +2 -2
  148. package/src/node/notifier.ts +1 -8
  149. package/src/sync/constants.ts +4 -4
  150. package/src/sync/range/batch.ts +187 -38
  151. package/src/sync/range/chain.ts +52 -5
  152. package/src/sync/range/range.ts +16 -3
  153. package/src/sync/sync.ts +13 -1
  154. package/src/sync/unknownBlock.ts +170 -10
  155. package/src/sync/utils/downloadByRange.ts +149 -33
  156. package/src/util/sszBytes.ts +8 -6
@@ -1,7 +1,7 @@
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
6
  import {Logger, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
@@ -206,14 +206,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
206
206
 
207
207
  private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
208
208
  const batchBlocks = batch.getBlocks();
209
+ const requests = batch.getRequestsForPeer(peer);
210
+ const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
211
+ const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
209
212
  const {result, warnings} = await downloadByRange({
210
213
  config: this.config,
211
214
  network: this.network,
212
215
  logger: this.logger,
213
216
  peerIdStr: peer.peerId,
214
217
  batchBlocks,
218
+ parentPayloadCommitments,
215
219
  peerDasMetrics: this.chain.metrics?.peerDas,
216
- ...batch.getRequestsForPeer(peer),
220
+ ...requests,
217
221
  });
218
222
  const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
219
223
  const {blocks, payloadEnvelopes} = cacheByRangeResponses({
@@ -258,6 +262,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
258
262
  private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
259
263
  let syncChain = this.chains.get(syncType);
260
264
  if (!syncChain) {
265
+ // The first batch of a new sync chain may need to detect whether the parent block was an
266
+ // gloas "empty" block (no envelope produced). It does so by comparing the first
267
+ // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
268
+ const headState = this.chain.getHeadState();
269
+ const latestBid = isStatePostGloas(headState)
270
+ ? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
271
+ : undefined;
272
+
261
273
  syncChain = new SyncChain(
262
274
  startEpoch,
263
275
  target,
@@ -276,7 +288,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
276
288
  logger: this.logger,
277
289
  custodyConfig: this.chain.custodyConfig,
278
290
  metrics: this.metrics,
279
- }
291
+ },
292
+ latestBid
280
293
  );
281
294
  this.chains.set(syncType, syncChain);
282
295
 
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);
@@ -63,6 +63,27 @@ enum FetchResult {
63
63
  FailureMaxAttempts = "failure_max_attempts",
64
64
  }
65
65
 
66
+ class UnknownBlockRateLimitedError extends Error {
67
+ constructor(message: string) {
68
+ super(message);
69
+ this.name = "UnknownBlockRateLimitedError";
70
+ }
71
+ }
72
+
73
+ function getRateLimitedUntilMs(e: unknown): number | null {
74
+ if (!(e instanceof RequestError)) {
75
+ return null;
76
+ }
77
+
78
+ switch (e.type.code) {
79
+ case RequestErrorCode.RESP_RATE_LIMITED:
80
+ case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
81
+ return e.type.rateLimitedUntilMs ?? null;
82
+ default:
83
+ return null;
84
+ }
85
+ }
86
+
66
87
  /**
67
88
  * BlockInputSync is a class that handles ReqResp to find blocks and data related to a specific blockRoot. The
68
89
  * blockRoot may have been found via object gossip, or the API. Gossip objects that can trigger a search are block,
@@ -106,6 +127,7 @@ export class BlockInputSync {
106
127
  private readonly maxPendingBlocks;
107
128
  private subscribedToNetworkEvents = false;
108
129
  private peerBalancer: UnknownBlockPeerBalancer;
130
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
109
131
 
110
132
  constructor(
111
133
  private readonly config: ChainForkConfig,
@@ -155,6 +177,7 @@ export class BlockInputSync {
155
177
 
156
178
  unsubscribeFromNetwork(): void {
157
179
  this.logger.verbose("BlockInputSync disabled.");
180
+ this.clearRateLimitBackoffTimer();
158
181
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
159
182
  this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
160
183
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
@@ -402,6 +425,7 @@ export class BlockInputSync {
402
425
  private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
403
426
  const peerId = data.peer;
404
427
  this.peerBalancer.onPeerDisconnected(peerId);
428
+ this.scheduleRateLimitBackoffRetry();
405
429
  };
406
430
 
407
431
  /**
@@ -519,7 +543,7 @@ export class BlockInputSync {
519
543
  */
520
544
  private triggerUnknownBlockSearch = (): void => {
521
545
  // Cheap early stop to prevent calling the network.getConnectedPeers()
522
- if (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0) {
546
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
523
547
  return;
524
548
  }
525
549
 
@@ -607,6 +631,36 @@ export class BlockInputSync {
607
631
  }
608
632
  };
609
633
 
634
+ private scheduleRateLimitBackoffRetry(): void {
635
+ this.clearRateLimitBackoffTimer();
636
+
637
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
638
+ return;
639
+ }
640
+
641
+ const now = Date.now();
642
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
643
+ if (retryAt === null) {
644
+ return;
645
+ }
646
+
647
+ this.rateLimitBackoffTimeout = setTimeout(
648
+ () => {
649
+ this.rateLimitBackoffTimeout = undefined;
650
+ this.triggerUnknownBlockSearch();
651
+ this.scheduleRateLimitBackoffRetry();
652
+ },
653
+ Math.max(0, retryAt - now)
654
+ );
655
+ }
656
+
657
+ private clearRateLimitBackoffTimer(): void {
658
+ if (this.rateLimitBackoffTimeout !== undefined) {
659
+ clearTimeout(this.rateLimitBackoffTimeout);
660
+ this.rateLimitBackoffTimeout = undefined;
661
+ }
662
+ }
663
+
610
664
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
611
665
  if (block.status !== PendingBlockInputStatus.pending) {
612
666
  return;
@@ -678,6 +732,16 @@ export class BlockInputSync {
678
732
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
679
733
  }
680
734
  } else {
735
+ if (res.err instanceof UnknownBlockRateLimitedError) {
736
+ const pendingBlock = this.pendingBlocks.get(rootHex);
737
+ if (pendingBlock) {
738
+ pendingBlock.status = PendingBlockInputStatus.pending;
739
+ }
740
+ this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
741
+ this.scheduleRateLimitBackoffRetry();
742
+ return;
743
+ }
744
+
681
745
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
682
746
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
683
747
  this.removeAndDownScoreAllDescendants(block);
@@ -994,12 +1058,19 @@ export class BlockInputSync {
994
1058
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
995
1059
 
996
1060
  let i = 0;
1061
+ let deferredByRateLimit = false;
997
1062
  while (i++ < this.getMaxDownloadAttempts()) {
998
1063
  const pendingColumns = payloadInput?.hasAllData()
999
1064
  ? new Set<number>()
1000
1065
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1001
1066
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1002
1067
  if (peerMeta === null) {
1068
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1069
+ throw new UnknownBlockRateLimitedError(
1070
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1071
+ );
1072
+ }
1073
+
1003
1074
  throw Error(
1004
1075
  `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1005
1076
  );
@@ -1076,7 +1147,12 @@ export class BlockInputSync {
1076
1147
  e as Error
1077
1148
  );
1078
1149
 
1079
- if (e instanceof RequestError) {
1150
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1151
+ if (rateLimitedUntilMs !== null) {
1152
+ deferredByRateLimit = true;
1153
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1154
+ this.scheduleRateLimitBackoffRetry();
1155
+ } else if (e instanceof RequestError) {
1080
1156
  switch (e.type.code) {
1081
1157
  case RequestErrorCode.REQUEST_RATE_LIMITED:
1082
1158
  case RequestErrorCode.REQUEST_TIMEOUT:
@@ -1093,6 +1169,12 @@ export class BlockInputSync {
1093
1169
  }
1094
1170
  }
1095
1171
 
1172
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1173
+ throw new UnknownBlockRateLimitedError(
1174
+ `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1175
+ );
1176
+ }
1177
+
1096
1178
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1097
1179
  }
1098
1180
 
@@ -1176,6 +1258,7 @@ export class BlockInputSync {
1176
1258
  }
1177
1259
 
1178
1260
  let i = 0;
1261
+ let deferredByRateLimit = false;
1179
1262
  while (i++ < this.getMaxDownloadAttempts()) {
1180
1263
  const pendingColumns =
1181
1264
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
@@ -1183,6 +1266,12 @@ export class BlockInputSync {
1183
1266
  : defaultPendingColumns;
1184
1267
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1185
1268
  if (peerMeta === null) {
1269
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1270
+ throw new UnknownBlockRateLimitedError(
1271
+ `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1272
+ );
1273
+ }
1274
+
1186
1275
  // no more peer with needed columns to try, throw error
1187
1276
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
1188
1277
  this.metrics?.blockInputSync.fetchTimeSec.observe(
@@ -1238,14 +1327,23 @@ export class BlockInputSync {
1238
1327
  } else if (e instanceof RequestError) {
1239
1328
  // should look into req_resp metrics in this case
1240
1329
  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;
1330
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1331
+ if (rateLimitedUntilMs !== null) {
1332
+ deferredByRateLimit = true;
1333
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1334
+ this.scheduleRateLimitBackoffRetry();
1335
+ } else {
1336
+ switch (e.type.code) {
1337
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1338
+ case RequestErrorCode.RESP_RATE_LIMITED:
1339
+ case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
1340
+ case RequestErrorCode.REQUEST_TIMEOUT:
1341
+ // do not exclude peer for these errors
1342
+ break;
1343
+ default:
1344
+ excludedPeers.add(peerId);
1345
+ break;
1346
+ }
1249
1347
  }
1250
1348
  } else {
1251
1349
  // investigate if this happens
@@ -1273,6 +1371,10 @@ export class BlockInputSync {
1273
1371
 
1274
1372
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1275
1373
 
1374
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1375
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1376
+ }
1377
+
1276
1378
  if (!isPendingBlockInput(cacheItem)) {
1277
1379
  throw Error(`${message} No block and no data was found.`);
1278
1380
  }
@@ -1404,10 +1506,12 @@ export class BlockInputSync {
1404
1506
  export class UnknownBlockPeerBalancer {
1405
1507
  readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
1406
1508
  readonly activeRequests: Map<PeerIdStr, number>;
1509
+ readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
1407
1510
 
1408
1511
  constructor() {
1409
1512
  this.peersMeta = new Map();
1410
1513
  this.activeRequests = new Map();
1514
+ this.rateLimitedUntilByPeer = new Map();
1411
1515
  }
1412
1516
 
1413
1517
  /** Trigger on each peer re-status */
@@ -1422,6 +1526,41 @@ export class UnknownBlockPeerBalancer {
1422
1526
  onPeerDisconnected(peerId: PeerIdStr): void {
1423
1527
  this.peersMeta.delete(peerId);
1424
1528
  this.activeRequests.delete(peerId);
1529
+ this.rateLimitedUntilByPeer.delete(peerId);
1530
+ }
1531
+
1532
+ onRateLimited(peerId: PeerIdStr, rateLimitedUntilMs: number): void {
1533
+ this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
1534
+ }
1535
+
1536
+ getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
1537
+ const now = Date.now();
1538
+ let retryAt: number | null = null;
1539
+
1540
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
1541
+ if (rateLimitedUntil <= now) {
1542
+ this.rateLimitedUntilByPeer.delete(peerId);
1543
+ continue;
1544
+ }
1545
+
1546
+ if (excludedPeers?.has(peerId)) {
1547
+ continue;
1548
+ }
1549
+
1550
+ const syncMeta = this.peersMeta.get(peerId);
1551
+ if (syncMeta === undefined) {
1552
+ this.rateLimitedUntilByPeer.delete(peerId);
1553
+ continue;
1554
+ }
1555
+
1556
+ if (pendingColumns !== undefined && !this.peerHasPendingColumns(syncMeta, pendingColumns)) {
1557
+ continue;
1558
+ }
1559
+
1560
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
1561
+ }
1562
+
1563
+ return retryAt;
1425
1564
  }
1426
1565
 
1427
1566
  /**
@@ -1470,6 +1609,7 @@ export class UnknownBlockPeerBalancer {
1470
1609
  }
1471
1610
 
1472
1611
  private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
1612
+ const now = Date.now();
1473
1613
  let maxColumnCount = 0;
1474
1614
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
1475
1615
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -1478,12 +1618,24 @@ export class UnknownBlockPeerBalancer {
1478
1618
  continue;
1479
1619
  }
1480
1620
 
1621
+ const rateLimitedUntil = this.rateLimitedUntilByPeer.get(peerId);
1622
+ if (rateLimitedUntil !== undefined) {
1623
+ if (now < rateLimitedUntil) {
1624
+ continue;
1625
+ }
1626
+ this.rateLimitedUntilByPeer.delete(peerId);
1627
+ }
1628
+
1481
1629
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
1482
1630
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
1483
1631
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
1484
1632
  continue;
1485
1633
  }
1486
1634
 
1635
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1636
+ continue;
1637
+ }
1638
+
1487
1639
  if (pendingDataColumns.size === 0) {
1488
1640
  considerPeers.push({peerId, columnCount: 0});
1489
1641
  continue;
@@ -1517,4 +1669,12 @@ export class UnknownBlockPeerBalancer {
1517
1669
 
1518
1670
  return eligiblePeers;
1519
1671
  }
1672
+
1673
+ private peerHasPendingColumns(syncMeta: PeerSyncMeta, pendingDataColumns: Set<number>): boolean {
1674
+ if (pendingDataColumns.size === 0) {
1675
+ return true;
1676
+ }
1677
+
1678
+ return syncMeta.custodyColumns.some((column) => pendingDataColumns.has(column));
1679
+ }
1520
1680
  }