@lodestar/beacon-node 1.43.0-dev.66d2c102e3 → 1.43.0-dev.6f485b1b61

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 (195) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +13 -3
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +45 -2
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/debug/index.d.ts.map +1 -1
  8. package/lib/api/impl/debug/index.js +0 -1
  9. package/lib/api/impl/debug/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts.map +1 -1
  11. package/lib/api/impl/validator/index.js +68 -2
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  14. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  15. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  16. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  17. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  18. package/lib/chain/blocks/importBlock.js +16 -31
  19. package/lib/chain/blocks/importBlock.js.map +1 -1
  20. package/lib/chain/blocks/importExecutionPayload.d.ts +9 -3
  21. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  22. package/lib/chain/blocks/importExecutionPayload.js +37 -15
  23. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  24. package/lib/chain/blocks/index.d.ts.map +1 -1
  25. package/lib/chain/blocks/index.js +35 -21
  26. package/lib/chain/blocks/index.js.map +1 -1
  27. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +12 -1
  28. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  29. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +28 -2
  30. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  31. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +17 -0
  32. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  33. package/lib/chain/blocks/types.d.ts +2 -1
  34. package/lib/chain/blocks/types.d.ts.map +1 -1
  35. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  36. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  37. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  38. package/lib/chain/blocks/verifyBlock.d.ts +2 -1
  39. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  40. package/lib/chain/blocks/verifyBlock.js +30 -12
  41. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  42. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  43. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  44. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  45. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  46. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  47. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  48. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  49. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  50. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  51. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  52. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  53. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  54. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  55. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +8 -3
  56. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  57. package/lib/chain/chain.d.ts.map +1 -1
  58. package/lib/chain/chain.js +25 -8
  59. package/lib/chain/chain.js.map +1 -1
  60. package/lib/chain/emitter.d.ts +0 -11
  61. package/lib/chain/emitter.d.ts.map +1 -1
  62. package/lib/chain/emitter.js +0 -4
  63. package/lib/chain/emitter.js.map +1 -1
  64. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  65. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  66. package/lib/chain/errors/proposerPreferences.js +1 -0
  67. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  68. package/lib/chain/initState.d.ts.map +1 -1
  69. package/lib/chain/initState.js +6 -1
  70. package/lib/chain/initState.js.map +1 -1
  71. package/lib/chain/opPools/payloadAttestationPool.d.ts +3 -2
  72. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  73. package/lib/chain/opPools/payloadAttestationPool.js +26 -4
  74. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  75. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  76. package/lib/chain/prepareNextSlot.js +16 -18
  77. package/lib/chain/prepareNextSlot.js.map +1 -1
  78. package/lib/chain/produceBlock/produceBlockBody.d.ts +12 -3
  79. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  80. package/lib/chain/produceBlock/produceBlockBody.js +34 -22
  81. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  82. package/lib/chain/regen/queued.d.ts.map +1 -1
  83. package/lib/chain/regen/queued.js +1 -4
  84. package/lib/chain/regen/queued.js.map +1 -1
  85. package/lib/chain/regen/regen.d.ts.map +1 -1
  86. package/lib/chain/regen/regen.js +1 -4
  87. package/lib/chain/regen/regen.js.map +1 -1
  88. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +21 -11
  89. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  90. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +70 -20
  91. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  92. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  93. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  94. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  95. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  96. package/lib/chain/validation/executionPayloadBid.js +11 -8
  97. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  98. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  99. package/lib/chain/validation/proposerPreferences.js +39 -17
  100. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  101. package/lib/network/gossip/topic.d.ts +2 -0
  102. package/lib/network/gossip/topic.d.ts.map +1 -1
  103. package/lib/network/interface.d.ts +1 -0
  104. package/lib/network/interface.d.ts.map +1 -1
  105. package/lib/network/network.d.ts +1 -0
  106. package/lib/network/network.d.ts.map +1 -1
  107. package/lib/network/network.js +5 -0
  108. package/lib/network/network.js.map +1 -1
  109. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  110. package/lib/network/processor/gossipHandlers.js +28 -10
  111. package/lib/network/processor/gossipHandlers.js.map +1 -1
  112. package/lib/network/processor/index.js +5 -5
  113. package/lib/network/processor/index.js.map +1 -1
  114. package/lib/node/nodejs.js +2 -2
  115. package/lib/node/nodejs.js.map +1 -1
  116. package/lib/node/notifier.js +1 -7
  117. package/lib/node/notifier.js.map +1 -1
  118. package/lib/sync/constants.d.ts +3 -1
  119. package/lib/sync/constants.d.ts.map +1 -1
  120. package/lib/sync/constants.js +3 -4
  121. package/lib/sync/constants.js.map +1 -1
  122. package/lib/sync/range/batch.d.ts +23 -3
  123. package/lib/sync/range/batch.d.ts.map +1 -1
  124. package/lib/sync/range/batch.js +191 -36
  125. package/lib/sync/range/batch.js.map +1 -1
  126. package/lib/sync/range/chain.d.ts +13 -2
  127. package/lib/sync/range/chain.d.ts.map +1 -1
  128. package/lib/sync/range/chain.js +61 -9
  129. package/lib/sync/range/chain.js.map +1 -1
  130. package/lib/sync/range/range.d.ts.map +1 -1
  131. package/lib/sync/range/range.js +14 -3
  132. package/lib/sync/range/range.js.map +1 -1
  133. package/lib/sync/sync.d.ts.map +1 -1
  134. package/lib/sync/sync.js +13 -0
  135. package/lib/sync/sync.js.map +1 -1
  136. package/lib/sync/unknownBlock.d.ts +7 -2
  137. package/lib/sync/unknownBlock.d.ts.map +1 -1
  138. package/lib/sync/unknownBlock.js +138 -57
  139. package/lib/sync/unknownBlock.js.map +1 -1
  140. package/lib/sync/utils/downloadByRange.d.ts +29 -8
  141. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  142. package/lib/sync/utils/downloadByRange.js +104 -42
  143. package/lib/sync/utils/downloadByRange.js.map +1 -1
  144. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  145. package/lib/sync/utils/downloadByRoot.js +10 -0
  146. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  147. package/lib/util/sszBytes.d.ts.map +1 -1
  148. package/lib/util/sszBytes.js +8 -6
  149. package/lib/util/sszBytes.js.map +1 -1
  150. package/package.json +15 -15
  151. package/src/api/impl/beacon/blocks/index.ts +16 -3
  152. package/src/api/impl/beacon/pool/index.ts +83 -1
  153. package/src/api/impl/debug/index.ts +0 -1
  154. package/src/api/impl/validator/index.ts +82 -1
  155. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  156. package/src/chain/blocks/importBlock.ts +16 -50
  157. package/src/chain/blocks/importExecutionPayload.ts +51 -20
  158. package/src/chain/blocks/index.ts +32 -15
  159. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +37 -3
  160. package/src/chain/blocks/payloadEnvelopeInput/types.ts +18 -0
  161. package/src/chain/blocks/types.ts +2 -1
  162. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  163. package/src/chain/blocks/verifyBlock.ts +45 -13
  164. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  165. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  166. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  167. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +7 -4
  168. package/src/chain/chain.ts +29 -7
  169. package/src/chain/emitter.ts +0 -11
  170. package/src/chain/errors/proposerPreferences.ts +9 -1
  171. package/src/chain/initState.ts +9 -1
  172. package/src/chain/opPools/payloadAttestationPool.ts +29 -8
  173. package/src/chain/prepareNextSlot.ts +21 -29
  174. package/src/chain/produceBlock/produceBlockBody.ts +45 -27
  175. package/src/chain/regen/queued.ts +2 -7
  176. package/src/chain/regen/regen.ts +2 -7
  177. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +90 -24
  178. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  179. package/src/chain/validation/executionPayloadBid.ts +11 -8
  180. package/src/chain/validation/proposerPreferences.ts +37 -18
  181. package/src/network/interface.ts +1 -0
  182. package/src/network/network.ts +11 -0
  183. package/src/network/processor/gossipHandlers.ts +38 -11
  184. package/src/network/processor/index.ts +5 -5
  185. package/src/node/nodejs.ts +2 -2
  186. package/src/node/notifier.ts +1 -8
  187. package/src/sync/constants.ts +4 -4
  188. package/src/sync/range/batch.ts +240 -42
  189. package/src/sync/range/chain.ts +77 -10
  190. package/src/sync/range/range.ts +16 -3
  191. package/src/sync/sync.ts +13 -1
  192. package/src/sync/unknownBlock.ts +170 -60
  193. package/src/sync/utils/downloadByRange.ts +166 -44
  194. package/src/sync/utils/downloadByRoot.ts +12 -0
  195. 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,7 @@
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 {RequestErrorCode} from "@lodestar/reqresp";
3
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
4
+ import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
4
5
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
5
6
  import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
6
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
@@ -15,7 +16,12 @@ import {CustodyConfig} from "../../util/dataColumns.js";
15
16
  import {ItTrigger} from "../../util/itTrigger.js";
16
17
  import {PeerIdStr} from "../../util/peerId.js";
17
18
  import {WarnResult, wrapError} from "../../util/wrapError.js";
18
- import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
19
+ import {
20
+ BATCH_BUFFER_SIZE,
21
+ EPOCHS_PER_BATCH,
22
+ MAX_LOOK_AHEAD_EPOCHS,
23
+ RATE_LIMITED_PEER_BACKOFF_MS,
24
+ } from "../constants.js";
19
25
  import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
20
26
  import {RangeSyncType} from "../utils/remoteSyncType.js";
21
27
  import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
@@ -139,20 +145,32 @@ export class SyncChain {
139
145
  private readonly batchProcessor = new ItTrigger();
140
146
  /** Sorted map of batches undergoing some kind of processing. */
141
147
  private readonly batches = new Map<Epoch, Batch>();
148
+ /**
149
+ * `true` until the first `Batch` is constructed via `includeNextBatch`
150
+ */
151
+ private isFirstBatch = true;
142
152
  private readonly peerset = new Map<PeerIdStr, ChainTarget>();
153
+ /**
154
+ * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
155
+ * This is a sync-layer optimization to avoid assigning batches to backed-off peers.
156
+ * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
157
+ */
158
+ private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
143
159
 
144
160
  private readonly logger: Logger;
145
161
  private readonly config: ChainForkConfig;
146
162
  private readonly clock: IClock;
147
163
  private readonly metrics: Metrics | null;
148
164
  private readonly custodyConfig: CustodyConfig;
165
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
149
166
 
150
167
  constructor(
151
168
  initialBatchEpoch: Epoch,
152
169
  initialTarget: ChainTarget,
153
170
  syncType: RangeSyncType,
154
171
  fns: SyncChainFns,
155
- modules: SyncChainModules
172
+ modules: SyncChainModules,
173
+ latestBid: gloas.ExecutionPayloadBid | undefined
156
174
  ) {
157
175
  const {config, clock, custodyConfig, logger, metrics} = modules;
158
176
  this.firstBatchEpoch = initialBatchEpoch;
@@ -168,6 +186,7 @@ export class SyncChain {
168
186
  this.clock = clock;
169
187
  this.metrics = metrics;
170
188
  this.custodyConfig = custodyConfig;
189
+ this.latestBid = latestBid;
171
190
  this.logger = logger;
172
191
  this.logId = `${syncType}-${nextChainId++}`;
173
192
 
@@ -222,12 +241,14 @@ export class SyncChain {
222
241
  */
223
242
  stopSyncing(): void {
224
243
  this.status = SyncChainStatus.Stopped;
244
+ this.logger.debug("SyncChain stopSyncing", {id: this.logId});
225
245
  }
226
246
 
227
247
  /**
228
248
  * Permanently remove this chain. Throws the main AsyncIterable
229
249
  */
230
250
  remove(): void {
251
+ this.logger.debug("SyncChain remove", {id: this.logId});
231
252
  this.batchProcessor.end(new ErrorAborted("SyncChain"));
232
253
  }
233
254
 
@@ -246,6 +267,7 @@ export class SyncChain {
246
267
  */
247
268
  removePeer(peerId: PeerIdStr): boolean {
248
269
  const deleted = this.peerset.delete(peerId);
270
+ this.rateLimitedPeers.delete(peerId);
249
271
  this.computeTarget();
250
272
  return deleted;
251
273
  }
@@ -381,8 +403,18 @@ export class SyncChain {
381
403
  return;
382
404
  }
383
405
 
406
+ const now = Date.now();
384
407
  const peersSyncInfo: PeerSyncInfo[] = [];
385
408
  for (const [peerId, target] of this.peerset.entries()) {
409
+ // Skip peers that are currently in rate-limit backoff
410
+ const rateLimitedUntil = this.rateLimitedPeers.get(peerId);
411
+ if (rateLimitedUntil !== undefined) {
412
+ if (now < rateLimitedUntil) {
413
+ continue;
414
+ }
415
+ this.rateLimitedPeers.delete(peerId);
416
+ }
417
+
386
418
  try {
387
419
  peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
388
420
  } catch (e) {
@@ -456,7 +488,17 @@ export class SyncChain {
456
488
  return null;
457
489
  }
458
490
 
459
- const batch = new Batch(startEpoch, this.config, this.clock, this.custodyConfig);
491
+ const batch = new Batch(
492
+ startEpoch,
493
+ this.config,
494
+ this.clock,
495
+ this.custodyConfig,
496
+ this.isFirstBatch,
497
+ // `latestBid` is only meaningful for the first batch's parent-payload check
498
+ this.isFirstBatch ? this.latestBid : undefined,
499
+ this.target.slot
500
+ );
501
+ this.isFirstBatch = false;
460
502
  this.batches.set(startEpoch, batch);
461
503
  return batch;
462
504
  }
@@ -514,7 +556,14 @@ export class SyncChain {
514
556
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
515
557
  res.err
516
558
  );
517
- batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
559
+ if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
560
+ // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
561
+ this.rateLimitedPeers.set(peer.peerId, Date.now() + RATE_LIMITED_PEER_BACKOFF_MS);
562
+ batch.downloadingRateLimited();
563
+ this.triggerBatchDownloader();
564
+ } else {
565
+ batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
566
+ }
518
567
  } else {
519
568
  this.logger.verbose("Batch download success", {
520
569
  id: this.logId,
@@ -534,7 +583,7 @@ export class SyncChain {
534
583
  this.metrics?.syncRange.downloadByRange.warn.inc({client: peer.client, code: warning.type.code});
535
584
  this.logger.debug(
536
585
  "Batch downloaded with warning",
537
- {id: this.logId, epoch: batch.startEpoch, ...logMeta, peer: prettyPrintPeerIdStr(peer.peerId)},
586
+ {id: this.logId, ...batch.getMetadata(), ...logMeta, peer: prettyPrintPeerIdStr(peer.peerId)},
538
587
  warning
539
588
  );
540
589
  }
@@ -560,10 +609,17 @@ export class SyncChain {
560
609
  // the flow will continue to call triggerBatchDownloader() below
561
610
  }
562
611
 
612
+ const blockSlots = downloadSuccessOutput.blocks.map((b) => b.slot);
613
+ const envelopeSlots = downloadSuccessOutput.payloadEnvelopes
614
+ ? Array.from(downloadSuccessOutput.payloadEnvelopes.keys())
615
+ : null;
616
+
563
617
  this.logger.debug(logMessage, {
564
618
  id: this.logId,
565
- epoch: batch.startEpoch,
619
+ ...batch.getMetadata(),
566
620
  ...logMeta,
621
+ blockSlots: prettyPrintIndices(blockSlots),
622
+ ...(envelopeSlots ? {envelopeSlots: prettyPrintIndices(envelopeSlots)} : {}),
567
623
  peer: prettyPrintPeerIdStr(peer.peerId),
568
624
  });
569
625
  }
@@ -586,13 +642,24 @@ export class SyncChain {
586
642
  * Sends `batch` to the processor. Note: batch may be empty
587
643
  */
588
644
  private async processBatch(batch: Batch): Promise<void> {
589
- const {blocks, payloadEnvelopes} = batch.startProcessing();
645
+ const {blocks, payloadEnvelopes, peers} = batch.startProcessing();
646
+
647
+ const logCtx = {
648
+ id: this.logId,
649
+ ...batch.getMetadata(),
650
+ blockCount: blocks.length,
651
+ blockSlots: prettyPrintIndices(blocks.map((b) => b.slot)),
652
+ ...(payloadEnvelopes ? {envelopeSlots: prettyPrintIndices(Array.from(payloadEnvelopes.keys()))} : {}),
653
+ peers: peers.map(prettyPrintPeerIdStr).join(","),
654
+ };
655
+ this.logger.verbose("Processing batch", logCtx);
590
656
 
591
657
  // wrapError ensures to never call both batch success() and batch error()
592
658
  const res = await wrapError(this.processChainSegment(blocks, payloadEnvelopes, this.syncType));
593
659
 
594
660
  if (!res.err) {
595
661
  batch.processingSuccess();
662
+ this.logger.verbose("Processed batch", {...logCtx, ...batch.getMetadata()});
596
663
 
597
664
  // If the processed batch is not empty, validate previous AwaitingValidation blocks.
598
665
  if (blocks.length > 0) {
@@ -602,7 +669,7 @@ export class SyncChain {
602
669
  // Potentially process next AwaitingProcessing batch
603
670
  this.triggerBatchProcessor();
604
671
  } else {
605
- this.logger.verbose("Batch process error", {id: this.logId, ...batch.getMetadata()}, res.err);
672
+ this.logger.verbose("Batch process error", logCtx, res.err);
606
673
  batch.processingError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS
607
674
 
608
675
  // At least one block was successfully verified and imported, so we can be sure all
@@ -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