@lodestar/beacon-node 1.43.0-dev.549a5b8115 → 1.43.0-dev.5adb1cab7b

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 (178) 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.map +1 -1
  33. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  34. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  35. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  36. package/lib/chain/blocks/verifyBlock.js +5 -6
  37. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  38. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  39. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  40. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  41. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  42. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  43. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  44. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  45. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  46. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  47. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  48. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  49. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  50. package/lib/chain/chain.d.ts.map +1 -1
  51. package/lib/chain/chain.js +25 -10
  52. package/lib/chain/chain.js.map +1 -1
  53. package/lib/chain/emitter.d.ts +0 -11
  54. package/lib/chain/emitter.d.ts.map +1 -1
  55. package/lib/chain/emitter.js +0 -4
  56. package/lib/chain/emitter.js.map +1 -1
  57. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  58. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  59. package/lib/chain/errors/proposerPreferences.js +1 -0
  60. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  61. package/lib/chain/initState.d.ts.map +1 -1
  62. package/lib/chain/initState.js +6 -1
  63. package/lib/chain/initState.js.map +1 -1
  64. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  65. package/lib/chain/prepareNextSlot.js +16 -18
  66. package/lib/chain/prepareNextSlot.js.map +1 -1
  67. package/lib/chain/produceBlock/produceBlockBody.d.ts +12 -3
  68. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  69. package/lib/chain/produceBlock/produceBlockBody.js +33 -20
  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/processor/gossipHandlers.d.ts.map +1 -1
  93. package/lib/network/processor/gossipHandlers.js +24 -7
  94. package/lib/network/processor/gossipHandlers.js.map +1 -1
  95. package/lib/network/processor/index.js +5 -5
  96. package/lib/network/processor/index.js.map +1 -1
  97. package/lib/node/nodejs.js +2 -2
  98. package/lib/node/nodejs.js.map +1 -1
  99. package/lib/node/notifier.js +1 -7
  100. package/lib/node/notifier.js.map +1 -1
  101. package/lib/sync/constants.d.ts +7 -1
  102. package/lib/sync/constants.d.ts.map +1 -1
  103. package/lib/sync/constants.js +7 -4
  104. package/lib/sync/constants.js.map +1 -1
  105. package/lib/sync/range/batch.d.ts +23 -3
  106. package/lib/sync/range/batch.d.ts.map +1 -1
  107. package/lib/sync/range/batch.js +191 -36
  108. package/lib/sync/range/batch.js.map +1 -1
  109. package/lib/sync/range/chain.d.ts +16 -2
  110. package/lib/sync/range/chain.d.ts.map +1 -1
  111. package/lib/sync/range/chain.js +97 -8
  112. package/lib/sync/range/chain.js.map +1 -1
  113. package/lib/sync/range/range.d.ts.map +1 -1
  114. package/lib/sync/range/range.js +14 -3
  115. package/lib/sync/range/range.js.map +1 -1
  116. package/lib/sync/sync.d.ts.map +1 -1
  117. package/lib/sync/sync.js +13 -0
  118. package/lib/sync/sync.js.map +1 -1
  119. package/lib/sync/unknownBlock.d.ts +7 -2
  120. package/lib/sync/unknownBlock.d.ts.map +1 -1
  121. package/lib/sync/unknownBlock.js +127 -57
  122. package/lib/sync/unknownBlock.js.map +1 -1
  123. package/lib/sync/utils/downloadByRange.d.ts +29 -8
  124. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  125. package/lib/sync/utils/downloadByRange.js +104 -42
  126. package/lib/sync/utils/downloadByRange.js.map +1 -1
  127. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  128. package/lib/sync/utils/downloadByRoot.js +10 -0
  129. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  130. package/lib/sync/utils/rateLimit.d.ts +2 -0
  131. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  132. package/lib/sync/utils/rateLimit.js +15 -0
  133. package/lib/sync/utils/rateLimit.js.map +1 -0
  134. package/lib/util/sszBytes.d.ts.map +1 -1
  135. package/lib/util/sszBytes.js +8 -6
  136. package/lib/util/sszBytes.js.map +1 -1
  137. package/package.json +16 -16
  138. package/src/api/impl/beacon/blocks/index.ts +13 -0
  139. package/src/api/impl/debug/index.ts +0 -1
  140. package/src/api/impl/validator/index.ts +2 -1
  141. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  142. package/src/chain/blocks/importBlock.ts +16 -49
  143. package/src/chain/blocks/importExecutionPayload.ts +21 -11
  144. package/src/chain/blocks/index.ts +18 -9
  145. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +32 -2
  146. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  147. package/src/chain/blocks/types.ts +2 -1
  148. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  149. package/src/chain/blocks/verifyBlock.ts +7 -5
  150. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  151. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  152. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  153. package/src/chain/chain.ts +29 -9
  154. package/src/chain/emitter.ts +0 -11
  155. package/src/chain/errors/proposerPreferences.ts +9 -1
  156. package/src/chain/initState.ts +9 -1
  157. package/src/chain/prepareNextSlot.ts +21 -29
  158. package/src/chain/produceBlock/produceBlockBody.ts +41 -25
  159. package/src/chain/regen/queued.ts +2 -7
  160. package/src/chain/regen/regen.ts +2 -7
  161. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +78 -22
  162. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  163. package/src/chain/validation/executionPayloadBid.ts +11 -8
  164. package/src/chain/validation/proposerPreferences.ts +37 -18
  165. package/src/network/processor/gossipHandlers.ts +30 -7
  166. package/src/network/processor/index.ts +5 -5
  167. package/src/node/nodejs.ts +2 -2
  168. package/src/node/notifier.ts +1 -8
  169. package/src/sync/constants.ts +8 -4
  170. package/src/sync/range/batch.ts +240 -42
  171. package/src/sync/range/chain.ts +116 -9
  172. package/src/sync/range/range.ts +16 -3
  173. package/src/sync/sync.ts +13 -1
  174. package/src/sync/unknownBlock.ts +157 -60
  175. package/src/sync/utils/downloadByRange.ts +166 -44
  176. package/src/sync/utils/downloadByRoot.ts +12 -0
  177. package/src/sync/utils/rateLimit.ts +16 -0
  178. package/src/util/sszBytes.ts +8 -6
@@ -1,18 +1,19 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkName, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
3
- import {Epoch, RootHex, Slot, phase0} from "@lodestar/types";
4
- import {LodestarError} from "@lodestar/utils";
3
+ import {Epoch, RootHex, SignedBeaconBlock, Slot, gloas, phase0} from "@lodestar/types";
4
+ import {LodestarError, byteArrayEquals, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
5
  import {isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
6
6
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
7
7
  import {isDaOutOfRange} from "../../chain/blocks/blockInput/utils.js";
8
8
  import {PayloadEnvelopeInput} from "../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
9
9
  import {BlockError, BlockErrorCode} from "../../chain/errors/index.js";
10
+ import {ZERO_HASH} from "../../constants/constants.js";
10
11
  import {PeerSyncMeta} from "../../network/peers/peersData.js";
11
12
  import {IClock} from "../../util/clock.js";
12
13
  import {CustodyConfig} from "../../util/dataColumns.js";
13
14
  import {PeerIdStr} from "../../util/peerId.js";
14
15
  import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
15
- import {DownloadByRangeRequests} from "../utils/downloadByRange.js";
16
+ import {DownloadByRangeRequests, ParentPayloadCommitments} from "../utils/downloadByRange.js";
16
17
  import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
17
18
 
18
19
  /**
@@ -79,10 +80,36 @@ export type BatchState =
79
80
  };
80
81
 
81
82
  export type BatchMetadata = {
83
+ // Batch-level slot window (always present)
82
84
  startEpoch: Epoch;
85
+ startSlot: Slot;
86
+ count: number;
83
87
  status: BatchStatus;
88
+
89
+ // Per-type outstanding request shapes; only present when that sub-request exists.
90
+ // Format: "startSlot=<n>,count=<n>" (plus ",cols=<indices>" for columns).
91
+ blocksReq?: string;
92
+ blobsReq?: string;
93
+ columnsReq?: string;
94
+ envelopesReq?: string;
95
+
96
+ // Retry counters
97
+ downloadAttempts: number;
98
+ processingAttempts: number;
99
+
100
+ // Cumulative peer attribution for failed attempts (only present when non-empty)
101
+ failedDownloadPeers?: string;
102
+ failedProcessingPeers?: string;
84
103
  };
85
104
 
105
+ function formatRangeReq(req: {startSlot: Slot; count: number}): string {
106
+ return `startSlot=${req.startSlot},count=${req.count}`;
107
+ }
108
+
109
+ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[]}): string {
110
+ return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
111
+ }
112
+
86
113
  /**
87
114
  * Batches are downloaded at the first block of the epoch.
88
115
  *
@@ -115,8 +142,18 @@ export class Batch {
115
142
  private readonly config: ChainForkConfig;
116
143
  private readonly clock: IClock;
117
144
  private readonly custodyConfig: CustodyConfig;
118
-
119
- constructor(startEpoch: Epoch, config: ChainForkConfig, clock: IClock, custodyConfig: CustodyConfig) {
145
+ private readonly isFirstBatchInChain: boolean;
146
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
147
+
148
+ constructor(
149
+ startEpoch: Epoch,
150
+ config: ChainForkConfig,
151
+ clock: IClock,
152
+ custodyConfig: CustodyConfig,
153
+ isFirstBatchInChain: boolean,
154
+ latestBid: gloas.ExecutionPayloadBid | undefined,
155
+ targetSlot: Slot
156
+ ) {
120
157
  this.config = config;
121
158
  this.clock = clock;
122
159
  this.custodyConfig = custodyConfig;
@@ -125,10 +162,40 @@ export class Batch {
125
162
  this.forkName = this.config.getForkName(startSlot);
126
163
  this.startEpoch = startEpoch;
127
164
  this.startSlot = startSlot;
128
- this.count = count;
165
+ this.count = Math.min(count, targetSlot - startSlot + 1);
166
+ this.isFirstBatchInChain = isFirstBatchInChain;
167
+ this.latestBid = latestBid;
129
168
  this.requests = this.getRequests([]);
130
169
  }
131
170
 
171
+ private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
172
+ if (!this.isFirstBatchInChain) return false;
173
+
174
+ if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
175
+ return false;
176
+ }
177
+
178
+ // we only know if we should download parent envelope if firstBlock is downloaded
179
+ if (firstBlock === undefined) return false;
180
+ if (this.latestBid === undefined) return false;
181
+ const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
182
+ .parentBlockHash;
183
+ return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
184
+ }
185
+
186
+ getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
187
+ if (this.latestBid === undefined) {
188
+ throw new Error(
189
+ `Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
190
+ );
191
+ }
192
+ return {
193
+ blockRoot: parentBlockRoot,
194
+ blockRootHex: toRootHex(parentBlockRoot),
195
+ kzgCommitments: this.latestBid.blobKzgCommitments,
196
+ };
197
+ }
198
+
132
199
  /**
133
200
  * Builds ByRange requests for block, blobs and columns
134
201
  */
@@ -176,6 +243,7 @@ export class Batch {
176
243
  const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
177
244
 
178
245
  // ensure blocks are in slot-wise order
246
+ const isPostGloas = isForkPostGloas(this.forkName);
179
247
  for (const blockInput of blocks) {
180
248
  const blockSlot = blockInput.slot;
181
249
  // check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
@@ -191,21 +259,36 @@ export class Batch {
191
259
  if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
192
260
  blockStartSlot = blockSlot + 1;
193
261
  }
194
- if (
195
- blockInput.hasBlock() &&
196
- envelopeStartSlot === blockSlot &&
197
- envelopesBySlot.get(blockSlot)?.hasPayloadEnvelope()
198
- ) {
199
- envelopeStartSlot = blockSlot + 1;
200
- }
201
- if (!blockInput.hasAllData()) {
202
- if (isBlockInputColumns(blockInput)) {
203
- for (const index of blockInput.getMissingSampledColumnMeta().missing) {
262
+
263
+ // Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
264
+ // which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
265
+ // a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
266
+ if (isPostGloas) {
267
+ // Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
268
+ const payloadInput = envelopesBySlot.get(blockSlot);
269
+ if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
270
+ envelopeStartSlot = blockSlot + 1;
271
+ }
272
+ if (payloadInput && !payloadInput.hasComputedAllData()) {
273
+ for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
204
274
  neededColumns.add(index);
205
275
  }
276
+ } else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
277
+ // Only advance dataStartSlot when we know columns for this slot are complete. If
278
+ // payloadInput is missing entirely we cannot tell, so stop here so the next round
279
+ // re-requests columns (and envelopes) starting at this slot.
280
+ dataStartSlot = blockSlot + 1;
281
+ }
282
+ } else {
283
+ if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
284
+ if (isBlockInputColumns(blockInput)) {
285
+ for (const index of blockInput.getMissingSampledColumnMeta().missing) {
286
+ neededColumns.add(index);
287
+ }
288
+ }
289
+ } else if (dataStartSlot === blockSlot) {
290
+ dataStartSlot = blockSlot + 1;
206
291
  }
207
- } else if (dataStartSlot === blockSlot) {
208
- dataStartSlot = blockSlot + 1;
209
292
  }
210
293
  }
211
294
 
@@ -225,11 +308,15 @@ export class Batch {
225
308
  // range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
226
309
  const count = endSlot - dataStartSlot + 1;
227
310
  if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
228
- requests.columnsRequest = {
229
- count,
230
- startSlot: dataStartSlot,
231
- columns: Array.from(neededColumns),
232
- };
311
+ // Skip the column re-request when we have no specific column indices outstanding.
312
+ // Peer rejects an empty `columns` list
313
+ if (neededColumns.size > 0) {
314
+ requests.columnsRequest = {
315
+ count,
316
+ startSlot: dataStartSlot,
317
+ columns: Array.from(neededColumns),
318
+ };
319
+ }
233
320
  } else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
234
321
  requests.blobsRequest = {
235
322
  count,
@@ -246,6 +333,36 @@ export class Batch {
246
333
  };
247
334
  }
248
335
 
336
+ // Only the first batch of a SyncChain may need the dangling-parent payload by-root.
337
+ if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
338
+ // shouldDownloadParentEnvelope() = true means there are at least 1 block
339
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
340
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
341
+ const parentRootHex = toRootHex(parentRoot);
342
+ let parentPayloadInput: PayloadEnvelopeInput | undefined;
343
+ if (this.state.payloadEnvelopes) {
344
+ for (const pi of this.state.payloadEnvelopes.values()) {
345
+ if (pi.blockRootHex === parentRootHex) {
346
+ parentPayloadInput = pi;
347
+ break;
348
+ }
349
+ }
350
+ }
351
+
352
+ const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
353
+ const missingColumns = parentPayloadInput
354
+ ? parentPayloadInput.getMissingSampledColumnMeta().missing
355
+ : this.custodyConfig.sampledColumns;
356
+
357
+ if (needsEnvelope || missingColumns.length > 0) {
358
+ requests.parentPayloadRequest = {
359
+ ...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
360
+ ...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
361
+ };
362
+ }
363
+ }
364
+ }
365
+
249
366
  return requests;
250
367
  }
251
368
 
@@ -257,24 +374,28 @@ export class Batch {
257
374
  return this.requests;
258
375
  }
259
376
 
260
- // post-fulu we need to ensure that we only request columns that the peer has advertised
261
- const {columnsRequest} = this.requests;
262
- if (columnsRequest == null) {
263
- return this.requests;
264
- }
377
+ // post-fulu we need to ensure that we only request columns that the peer has advertised.
378
+ const {columnsRequest, parentPayloadRequest} = this.requests;
265
379
 
266
380
  const peerColumns = new Set(peer.custodyColumns ?? []);
267
- const requestedColumns = columnsRequest.columns.filter((c) => peerColumns.has(c));
268
- if (requestedColumns.length === columnsRequest.columns.length) {
269
- return this.requests;
270
- }
381
+ const filteredColumnsRequest =
382
+ columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
383
+ const parentColumns = parentPayloadRequest?.columns;
384
+ const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
385
+
386
+ const updatedColumnRequest =
387
+ columnsRequest != null && filteredColumnsRequest != null
388
+ ? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
389
+ : {};
390
+ const updatedParentPayloadRequest =
391
+ parentPayloadRequest != null && filteredParentColumns != null
392
+ ? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
393
+ : {};
271
394
 
272
395
  return {
273
396
  ...this.requests,
274
- columnsRequest: {
275
- ...columnsRequest,
276
- columns: requestedColumns,
277
- },
397
+ ...updatedColumnRequest,
398
+ ...updatedParentPayloadRequest,
278
399
  };
279
400
  }
280
401
 
@@ -286,7 +407,26 @@ export class Batch {
286
407
  }
287
408
 
288
409
  getMetadata(): BatchMetadata {
289
- return {startEpoch: this.startEpoch, status: this.state.status};
410
+ const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
411
+ const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
412
+ return {
413
+ startEpoch: this.startEpoch,
414
+ startSlot: this.startSlot,
415
+ count: this.count,
416
+ status: this.state.status,
417
+ ...(blocksRequest && {blocksReq: formatRangeReq(blocksRequest)}),
418
+ ...(blobsRequest && {blobsReq: formatRangeReq(blobsRequest)}),
419
+ ...(columnsRequest && {columnsReq: formatColumnsReq(columnsRequest)}),
420
+ ...(envelopesRequest && {envelopesReq: formatRangeReq(envelopesRequest)}),
421
+ downloadAttempts: this.failedDownloadAttempts.length,
422
+ processingAttempts: this.failedProcessingAttempts.length,
423
+ ...(this.failedDownloadAttempts.length > 0 && {
424
+ failedDownloadPeers: this.failedDownloadAttempts.join(","),
425
+ }),
426
+ ...(failedProcessingPeerList.length > 0 && {
427
+ failedProcessingPeers: failedProcessingPeerList.join(","),
428
+ }),
429
+ };
290
430
  }
291
431
 
292
432
  getBlocks(): IBlockInput[] {
@@ -334,7 +474,11 @@ export class Batch {
334
474
  const slots = new Set<number>();
335
475
  for (const block of blocks) {
336
476
  slots.add(block.slot);
337
- if (!block.hasBlockAndAllData()) {
477
+ const dataComplete = isBlockInputColumns(block)
478
+ ? // by_range needs to download all columns
479
+ block.hasBlock() && block.hasComputedAllData()
480
+ : block.hasBlockAndAllData();
481
+ if (!dataComplete) {
338
482
  allComplete = false;
339
483
  }
340
484
  }
@@ -350,11 +494,45 @@ export class Batch {
350
494
  }
351
495
  const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
352
496
 
497
+ if (allComplete && isForkPostGloas(this.forkName)) {
498
+ for (const block of blocks) {
499
+ const payloadInput = newPayloadEnvelopes?.get(block.slot);
500
+ // by_range needs every block's envelope and all sampled columns.
501
+ if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
502
+ allComplete = false;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+
508
+ // First batch of a sync chain must additionally have the dangling-parent payload fully
509
+ // present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
510
+ // `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
511
+ if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
512
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
513
+ // Genesis has no parent payload — nothing to wait for.
514
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
515
+ const parentRootHex = toRootHex(parentRoot);
516
+ let parentPayloadComplete = false;
517
+ if (newPayloadEnvelopes) {
518
+ for (const payloadInput of newPayloadEnvelopes.values()) {
519
+ if (payloadInput.blockRootHex === parentRootHex) {
520
+ parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
521
+ break;
522
+ }
523
+ }
524
+ }
525
+ if (!parentPayloadComplete) {
526
+ allComplete = false;
527
+ }
528
+ }
529
+ }
530
+
353
531
  if (allComplete) {
354
532
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
355
533
  } else {
356
- this.requests = this.getRequests(blocks);
357
534
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
535
+ this.requests = this.getRequests(blocks);
358
536
  }
359
537
 
360
538
  return this.state as DownloadSuccessState;
@@ -380,10 +558,30 @@ export class Batch {
380
558
  };
381
559
  }
382
560
 
561
+ /**
562
+ * Downloading -> AwaitingDownload (without counting as a failed attempt).
563
+ * Used when the peer rate-limited us — the request was never actually served.
564
+ */
565
+ downloadingRateLimited(): void {
566
+ if (this.state.status !== BatchStatus.Downloading) {
567
+ throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
568
+ }
569
+
570
+ this.state = {
571
+ status: BatchStatus.AwaitingDownload,
572
+ blocks: this.state.blocks,
573
+ payloadEnvelopes: this.state.payloadEnvelopes,
574
+ };
575
+ }
576
+
383
577
  /**
384
578
  * AwaitingProcessing -> Processing
385
579
  */
386
- startProcessing(): {blocks: IBlockInput[]; payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null} {
580
+ startProcessing(): {
581
+ blocks: IBlockInput[];
582
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
583
+ peers: PeerIdStr[];
584
+ } {
387
585
  if (this.state.status !== BatchStatus.AwaitingProcessing) {
388
586
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingProcessing));
389
587
  }
@@ -396,7 +594,7 @@ export class Batch {
396
594
  const peers = this.goodPeers;
397
595
  this.goodPeers = [];
398
596
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
399
- return {blocks, payloadEnvelopes};
597
+ return {blocks, payloadEnvelopes, peers};
400
598
  }
401
599
 
402
600
  /**
@@ -479,7 +677,7 @@ export class Batch {
479
677
 
480
678
  /** Helper to construct typed BatchError. Stack traces are correct as the error is thrown above */
481
679
  private errorType(type: BatchErrorType): BatchErrorType & BatchErrorMetadata {
482
- return {...type, ...this.getMetadata()};
680
+ return {...type, startEpoch: this.startEpoch, status: this.state.status};
483
681
  }
484
682
 
485
683
  private wrongStatusErrorType(expectedStatus: BatchStatus): BatchErrorType & BatchErrorMetadata {
@@ -1,6 +1,6 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {Epoch, Root, Slot} from "@lodestar/types";
3
- import {ErrorAborted, LodestarError, Logger, toRootHex} from "@lodestar/utils";
2
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
3
+ import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
4
4
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
5
5
  import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
6
6
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
@@ -17,6 +17,7 @@ import {PeerIdStr} from "../../util/peerId.js";
17
17
  import {WarnResult, wrapError} from "../../util/wrapError.js";
18
18
  import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
19
19
  import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
20
+ import {getRateLimitedUntilMs} from "../utils/rateLimit.js";
20
21
  import {RangeSyncType} from "../utils/remoteSyncType.js";
21
22
  import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
22
23
  import {
@@ -139,20 +140,33 @@ export class SyncChain {
139
140
  private readonly batchProcessor = new ItTrigger();
140
141
  /** Sorted map of batches undergoing some kind of processing. */
141
142
  private readonly batches = new Map<Epoch, Batch>();
143
+ /**
144
+ * `true` until the first `Batch` is constructed via `includeNextBatch`
145
+ */
146
+ private isFirstBatch = true;
142
147
  private readonly peerset = new Map<PeerIdStr, ChainTarget>();
148
+ /**
149
+ * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
150
+ * This is a sync-layer optimization to avoid assigning batches to backed-off peers.
151
+ * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
152
+ */
153
+ private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
154
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
143
155
 
144
156
  private readonly logger: Logger;
145
157
  private readonly config: ChainForkConfig;
146
158
  private readonly clock: IClock;
147
159
  private readonly metrics: Metrics | null;
148
160
  private readonly custodyConfig: CustodyConfig;
161
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
149
162
 
150
163
  constructor(
151
164
  initialBatchEpoch: Epoch,
152
165
  initialTarget: ChainTarget,
153
166
  syncType: RangeSyncType,
154
167
  fns: SyncChainFns,
155
- modules: SyncChainModules
168
+ modules: SyncChainModules,
169
+ latestBid: gloas.ExecutionPayloadBid | undefined
156
170
  ) {
157
171
  const {config, clock, custodyConfig, logger, metrics} = modules;
158
172
  this.firstBatchEpoch = initialBatchEpoch;
@@ -168,6 +182,7 @@ export class SyncChain {
168
182
  this.clock = clock;
169
183
  this.metrics = metrics;
170
184
  this.custodyConfig = custodyConfig;
185
+ this.latestBid = latestBid;
171
186
  this.logger = logger;
172
187
  this.logId = `${syncType}-${nextChainId++}`;
173
188
 
@@ -222,12 +237,16 @@ export class SyncChain {
222
237
  */
223
238
  stopSyncing(): void {
224
239
  this.status = SyncChainStatus.Stopped;
240
+ this.clearRateLimitBackoffTimer();
241
+ this.logger.debug("SyncChain stopSyncing", {id: this.logId});
225
242
  }
226
243
 
227
244
  /**
228
245
  * Permanently remove this chain. Throws the main AsyncIterable
229
246
  */
230
247
  remove(): void {
248
+ this.logger.debug("SyncChain remove", {id: this.logId});
249
+ this.clearRateLimitBackoffTimer();
231
250
  this.batchProcessor.end(new ErrorAborted("SyncChain"));
232
251
  }
233
252
 
@@ -246,6 +265,7 @@ export class SyncChain {
246
265
  */
247
266
  removePeer(peerId: PeerIdStr): boolean {
248
267
  const deleted = this.peerset.delete(peerId);
268
+ this.rateLimitedPeers.delete(peerId);
249
269
  this.computeTarget();
250
270
  return deleted;
251
271
  }
@@ -349,6 +369,8 @@ export class SyncChain {
349
369
  }
350
370
 
351
371
  throw e;
372
+ } finally {
373
+ this.clearRateLimitBackoffTimer();
352
374
  }
353
375
  }
354
376
 
@@ -372,6 +394,44 @@ export class SyncChain {
372
394
  }
373
395
  }
374
396
 
397
+ private scheduleRateLimitBackoffRetry(): void {
398
+ this.clearRateLimitBackoffTimer();
399
+
400
+ if (this.status !== SyncChainStatus.Syncing || this.rateLimitedPeers.size === 0) {
401
+ return;
402
+ }
403
+
404
+ const now = Date.now();
405
+ let retryAt: number | null = null;
406
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedPeers.entries()) {
407
+ if (rateLimitedUntil <= now) {
408
+ this.rateLimitedPeers.delete(peerId);
409
+ continue;
410
+ }
411
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
412
+ }
413
+
414
+ if (retryAt === null) {
415
+ return;
416
+ }
417
+
418
+ this.rateLimitBackoffTimeout = setTimeout(
419
+ () => {
420
+ this.rateLimitBackoffTimeout = undefined;
421
+ this.triggerBatchDownloader();
422
+ this.scheduleRateLimitBackoffRetry();
423
+ },
424
+ Math.max(0, retryAt - now)
425
+ );
426
+ }
427
+
428
+ private clearRateLimitBackoffTimer(): void {
429
+ if (this.rateLimitBackoffTimeout !== undefined) {
430
+ clearTimeout(this.rateLimitBackoffTimeout);
431
+ this.rateLimitBackoffTimeout = undefined;
432
+ }
433
+ }
434
+
375
435
  /**
376
436
  * Attempts to request the next required batches from the peer pool if the chain is syncing.
377
437
  * It will exhaust the peer pool and left over batches until the batch buffer is reached.
@@ -381,8 +441,18 @@ export class SyncChain {
381
441
  return;
382
442
  }
383
443
 
444
+ const now = Date.now();
384
445
  const peersSyncInfo: PeerSyncInfo[] = [];
385
446
  for (const [peerId, target] of this.peerset.entries()) {
447
+ // Skip peers that are currently in rate-limit backoff
448
+ const rateLimitedUntil = this.rateLimitedPeers.get(peerId);
449
+ if (rateLimitedUntil !== undefined) {
450
+ if (now < rateLimitedUntil) {
451
+ continue;
452
+ }
453
+ this.rateLimitedPeers.delete(peerId);
454
+ }
455
+
386
456
  try {
387
457
  peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
388
458
  } catch (e) {
@@ -456,7 +526,17 @@ export class SyncChain {
456
526
  return null;
457
527
  }
458
528
 
459
- const batch = new Batch(startEpoch, this.config, this.clock, this.custodyConfig);
529
+ const batch = new Batch(
530
+ startEpoch,
531
+ this.config,
532
+ this.clock,
533
+ this.custodyConfig,
534
+ this.isFirstBatch,
535
+ // `latestBid` is only meaningful for the first batch's parent-payload check
536
+ this.isFirstBatch ? this.latestBid : undefined,
537
+ this.target.slot
538
+ );
539
+ this.isFirstBatch = false;
460
540
  this.batches.set(startEpoch, batch);
461
541
  return batch;
462
542
  }
@@ -514,7 +594,16 @@ export class SyncChain {
514
594
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
515
595
  res.err
516
596
  );
517
- batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
597
+ const rateLimitedUntilMs = getRateLimitedUntilMs(res.err);
598
+ if (rateLimitedUntilMs !== null) {
599
+ // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
600
+ this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs);
601
+ this.scheduleRateLimitBackoffRetry();
602
+ batch.downloadingRateLimited();
603
+ this.triggerBatchDownloader();
604
+ } else {
605
+ batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
606
+ }
518
607
  } else {
519
608
  this.logger.verbose("Batch download success", {
520
609
  id: this.logId,
@@ -534,7 +623,7 @@ export class SyncChain {
534
623
  this.metrics?.syncRange.downloadByRange.warn.inc({client: peer.client, code: warning.type.code});
535
624
  this.logger.debug(
536
625
  "Batch downloaded with warning",
537
- {id: this.logId, epoch: batch.startEpoch, ...logMeta, peer: prettyPrintPeerIdStr(peer.peerId)},
626
+ {id: this.logId, ...batch.getMetadata(), ...logMeta, peer: prettyPrintPeerIdStr(peer.peerId)},
538
627
  warning
539
628
  );
540
629
  }
@@ -560,10 +649,17 @@ export class SyncChain {
560
649
  // the flow will continue to call triggerBatchDownloader() below
561
650
  }
562
651
 
652
+ const blockSlots = downloadSuccessOutput.blocks.map((b) => b.slot);
653
+ const envelopeSlots = downloadSuccessOutput.payloadEnvelopes
654
+ ? Array.from(downloadSuccessOutput.payloadEnvelopes.keys())
655
+ : null;
656
+
563
657
  this.logger.debug(logMessage, {
564
658
  id: this.logId,
565
- epoch: batch.startEpoch,
659
+ ...batch.getMetadata(),
566
660
  ...logMeta,
661
+ blockSlots: prettyPrintIndices(blockSlots),
662
+ ...(envelopeSlots ? {envelopeSlots: prettyPrintIndices(envelopeSlots)} : {}),
567
663
  peer: prettyPrintPeerIdStr(peer.peerId),
568
664
  });
569
665
  }
@@ -586,13 +682,24 @@ export class SyncChain {
586
682
  * Sends `batch` to the processor. Note: batch may be empty
587
683
  */
588
684
  private async processBatch(batch: Batch): Promise<void> {
589
- const {blocks, payloadEnvelopes} = batch.startProcessing();
685
+ const {blocks, payloadEnvelopes, peers} = batch.startProcessing();
686
+
687
+ const logCtx = {
688
+ id: this.logId,
689
+ ...batch.getMetadata(),
690
+ blockCount: blocks.length,
691
+ blockSlots: prettyPrintIndices(blocks.map((b) => b.slot)),
692
+ ...(payloadEnvelopes ? {envelopeSlots: prettyPrintIndices(Array.from(payloadEnvelopes.keys()))} : {}),
693
+ peers: peers.map(prettyPrintPeerIdStr).join(","),
694
+ };
695
+ this.logger.verbose("Processing batch", logCtx);
590
696
 
591
697
  // wrapError ensures to never call both batch success() and batch error()
592
698
  const res = await wrapError(this.processChainSegment(blocks, payloadEnvelopes, this.syncType));
593
699
 
594
700
  if (!res.err) {
595
701
  batch.processingSuccess();
702
+ this.logger.verbose("Processed batch", {...logCtx, ...batch.getMetadata()});
596
703
 
597
704
  // If the processed batch is not empty, validate previous AwaitingValidation blocks.
598
705
  if (blocks.length > 0) {
@@ -602,7 +709,7 @@ export class SyncChain {
602
709
  // Potentially process next AwaitingProcessing batch
603
710
  this.triggerBatchProcessor();
604
711
  } else {
605
- this.logger.verbose("Batch process error", {id: this.logId, ...batch.getMetadata()}, res.err);
712
+ this.logger.verbose("Batch process error", logCtx, res.err);
606
713
  batch.processingError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS
607
714
 
608
715
  // At least one block was successfully verified and imported, so we can be sure all