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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  2. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  3. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  4. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  5. package/lib/chain/blocks/index.d.ts.map +1 -1
  6. package/lib/chain/blocks/index.js +6 -7
  7. package/lib/chain/blocks/index.js.map +1 -1
  8. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +8 -1
  9. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  10. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +19 -0
  11. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  12. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  13. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  14. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  15. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  16. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  17. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  18. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/chain.d.ts.map +1 -1
  22. package/lib/chain/chain.js +15 -1
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -6
  25. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  26. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +21 -5
  27. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  28. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  29. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  30. package/lib/metrics/metrics/lodestar.js +5 -0
  31. package/lib/metrics/metrics/lodestar.js.map +1 -1
  32. package/lib/network/gossip/topic.d.ts +749 -2
  33. package/lib/network/gossip/topic.d.ts.map +1 -1
  34. package/lib/network/interface.d.ts +1 -0
  35. package/lib/network/interface.d.ts.map +1 -1
  36. package/lib/network/network.d.ts +1 -0
  37. package/lib/network/network.d.ts.map +1 -1
  38. package/lib/network/network.js +3 -0
  39. package/lib/network/network.js.map +1 -1
  40. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  41. package/lib/network/processor/gossipHandlers.js +31 -1
  42. package/lib/network/processor/gossipHandlers.js.map +1 -1
  43. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  44. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  45. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  46. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  47. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  48. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  49. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  50. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  51. package/lib/network/reqresp/handlers/index.js +5 -0
  52. package/lib/network/reqresp/handlers/index.js.map +1 -1
  53. package/lib/network/reqresp/interface.d.ts +1 -1
  54. package/lib/network/reqresp/interface.js +1 -1
  55. package/lib/network/reqresp/protocols.d.ts +1 -0
  56. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  57. package/lib/network/reqresp/protocols.js +5 -0
  58. package/lib/network/reqresp/protocols.js.map +1 -1
  59. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  60. package/lib/network/reqresp/rateLimit.js +4 -0
  61. package/lib/network/reqresp/rateLimit.js.map +1 -1
  62. package/lib/network/reqresp/score.d.ts.map +1 -1
  63. package/lib/network/reqresp/score.js +1 -0
  64. package/lib/network/reqresp/score.js.map +1 -1
  65. package/lib/network/reqresp/types.d.ts +3 -0
  66. package/lib/network/reqresp/types.d.ts.map +1 -1
  67. package/lib/network/reqresp/types.js +3 -0
  68. package/lib/network/reqresp/types.js.map +1 -1
  69. package/lib/sync/constants.d.ts +5 -1
  70. package/lib/sync/constants.d.ts.map +1 -1
  71. package/lib/sync/constants.js +5 -1
  72. package/lib/sync/constants.js.map +1 -1
  73. package/lib/sync/range/batch.d.ts +27 -6
  74. package/lib/sync/range/batch.d.ts.map +1 -1
  75. package/lib/sync/range/batch.js +149 -26
  76. package/lib/sync/range/batch.js.map +1 -1
  77. package/lib/sync/range/chain.d.ts +10 -2
  78. package/lib/sync/range/chain.d.ts.map +1 -1
  79. package/lib/sync/range/chain.js +55 -7
  80. package/lib/sync/range/chain.js.map +1 -1
  81. package/lib/sync/range/range.d.ts.map +1 -1
  82. package/lib/sync/range/range.js +54 -5
  83. package/lib/sync/range/range.js.map +1 -1
  84. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  85. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  86. package/lib/sync/range/utils/peerBalancer.js +8 -4
  87. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  88. package/lib/sync/sync.d.ts.map +1 -1
  89. package/lib/sync/sync.js +13 -0
  90. package/lib/sync/sync.js.map +1 -1
  91. package/lib/sync/unknownBlock.d.ts +7 -0
  92. package/lib/sync/unknownBlock.d.ts.map +1 -1
  93. package/lib/sync/unknownBlock.js +127 -12
  94. package/lib/sync/unknownBlock.js.map +1 -1
  95. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  96. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  97. package/lib/sync/utils/downloadByRange.js +87 -38
  98. package/lib/sync/utils/downloadByRange.js.map +1 -1
  99. package/lib/sync/utils/rateLimit.d.ts +2 -0
  100. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  101. package/lib/sync/utils/rateLimit.js +15 -0
  102. package/lib/sync/utils/rateLimit.js.map +1 -0
  103. package/package.json +16 -16
  104. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  105. package/src/chain/blocks/index.ts +5 -7
  106. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -1
  107. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  108. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  109. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  110. package/src/chain/chain.ts +17 -0
  111. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +23 -6
  112. package/src/metrics/metrics/lodestar.ts +6 -0
  113. package/src/network/interface.ts +1 -0
  114. package/src/network/network.ts +12 -0
  115. package/src/network/processor/gossipHandlers.ts +35 -1
  116. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  117. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  118. package/src/network/reqresp/handlers/index.ts +5 -0
  119. package/src/network/reqresp/interface.ts +1 -1
  120. package/src/network/reqresp/protocols.ts +6 -0
  121. package/src/network/reqresp/rateLimit.ts +4 -0
  122. package/src/network/reqresp/score.ts +1 -0
  123. package/src/network/reqresp/types.ts +5 -0
  124. package/src/sync/constants.ts +5 -1
  125. package/src/sync/range/batch.ts +188 -30
  126. package/src/sync/range/chain.ts +72 -13
  127. package/src/sync/range/range.ts +68 -5
  128. package/src/sync/range/utils/peerBalancer.ts +9 -3
  129. package/src/sync/sync.ts +13 -1
  130. package/src/sync/unknownBlock.ts +157 -12
  131. package/src/sync/utils/downloadByRange.ts +153 -39
  132. package/src/sync/utils/rateLimit.ts +16 -0
@@ -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, prettyPrintIndices} 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
  /**
@@ -44,6 +45,15 @@ export type Attempt = {
44
45
  hash: RootHex;
45
46
  };
46
47
 
48
+ type TrackedRequest = {
49
+ /** only happen for the 1st batch in checkpoint sync */
50
+ parentPayload: boolean;
51
+ /**
52
+ * we always issue by_range before parent_payload, so we don't model this as null
53
+ */
54
+ byRangeColumns: Set<number>;
55
+ };
56
+
47
57
  export type AwaitingDownloadState = {
48
58
  status: BatchStatus.AwaitingDownload;
49
59
  blocks: IBlockInput[];
@@ -61,6 +71,7 @@ export type BatchState =
61
71
  | {
62
72
  status: BatchStatus.Downloading;
63
73
  peer: PeerIdStr;
74
+ request: TrackedRequest;
64
75
  blocks: IBlockInput[];
65
76
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
66
77
  }
@@ -109,6 +120,13 @@ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[
109
120
  return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
110
121
  }
111
122
 
123
+ function getTrackedRequest({parentPayloadRequest, columnsRequest}: DownloadByRangeRequests): TrackedRequest {
124
+ return {
125
+ parentPayload: parentPayloadRequest != null,
126
+ byRangeColumns: new Set(parentPayloadRequest == null ? (columnsRequest?.columns ?? []) : []),
127
+ };
128
+ }
129
+
112
130
  /**
113
131
  * Batches are downloaded at the first block of the epoch.
114
132
  *
@@ -130,8 +148,8 @@ export class Batch {
130
148
  requests: DownloadByRangeRequests;
131
149
  /** State of the batch. */
132
150
  state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
133
- /** Peers that provided good data */
134
- goodPeers: PeerIdStr[] = [];
151
+ /** Peers that provided good data, with column coverage for by_range requests */
152
+ private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
135
153
  /** The `Attempts` that have been made and failed to send us this batch. */
136
154
  readonly failedProcessingAttempts: Attempt[] = [];
137
155
  /** The `Attempts` that have been made and failed because of execution malfunction. */
@@ -141,8 +159,18 @@ export class Batch {
141
159
  private readonly config: ChainForkConfig;
142
160
  private readonly clock: IClock;
143
161
  private readonly custodyConfig: CustodyConfig;
144
-
145
- constructor(startEpoch: Epoch, config: ChainForkConfig, clock: IClock, custodyConfig: CustodyConfig) {
162
+ private readonly isFirstBatchInChain: boolean;
163
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
164
+
165
+ constructor(
166
+ startEpoch: Epoch,
167
+ config: ChainForkConfig,
168
+ clock: IClock,
169
+ custodyConfig: CustodyConfig,
170
+ isFirstBatchInChain: boolean,
171
+ latestBid: gloas.ExecutionPayloadBid | undefined,
172
+ targetSlot: Slot
173
+ ) {
146
174
  this.config = config;
147
175
  this.clock = clock;
148
176
  this.custodyConfig = custodyConfig;
@@ -151,10 +179,40 @@ export class Batch {
151
179
  this.forkName = this.config.getForkName(startSlot);
152
180
  this.startEpoch = startEpoch;
153
181
  this.startSlot = startSlot;
154
- this.count = count;
182
+ this.count = Math.min(count, targetSlot - startSlot + 1);
183
+ this.isFirstBatchInChain = isFirstBatchInChain;
184
+ this.latestBid = latestBid;
155
185
  this.requests = this.getRequests([]);
156
186
  }
157
187
 
188
+ private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
189
+ if (!this.isFirstBatchInChain) return false;
190
+
191
+ if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
192
+ return false;
193
+ }
194
+
195
+ // we only know if we should download parent envelope if firstBlock is downloaded
196
+ if (firstBlock === undefined) return false;
197
+ if (this.latestBid === undefined) return false;
198
+ const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
199
+ .parentBlockHash;
200
+ return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
201
+ }
202
+
203
+ getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
204
+ if (this.latestBid === undefined) {
205
+ throw new Error(
206
+ `Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
207
+ );
208
+ }
209
+ return {
210
+ blockRoot: parentBlockRoot,
211
+ blockRootHex: toRootHex(parentBlockRoot),
212
+ kzgCommitments: this.latestBid.blobKzgCommitments,
213
+ };
214
+ }
215
+
158
216
  /**
159
217
  * Builds ByRange requests for block, blobs and columns
160
218
  */
@@ -292,6 +350,36 @@ export class Batch {
292
350
  };
293
351
  }
294
352
 
353
+ // Only the first batch of a SyncChain may need the dangling-parent payload by-root.
354
+ if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
355
+ // shouldDownloadParentEnvelope() = true means there are at least 1 block
356
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
357
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
358
+ const parentRootHex = toRootHex(parentRoot);
359
+ let parentPayloadInput: PayloadEnvelopeInput | undefined;
360
+ if (this.state.payloadEnvelopes) {
361
+ for (const pi of this.state.payloadEnvelopes.values()) {
362
+ if (pi.blockRootHex === parentRootHex) {
363
+ parentPayloadInput = pi;
364
+ break;
365
+ }
366
+ }
367
+ }
368
+
369
+ const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
370
+ const missingColumns = parentPayloadInput
371
+ ? parentPayloadInput.getMissingSampledColumnMeta().missing
372
+ : this.custodyConfig.sampledColumns;
373
+
374
+ if (needsEnvelope || missingColumns.length > 0) {
375
+ requests.parentPayloadRequest = {
376
+ ...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
377
+ ...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
378
+ };
379
+ }
380
+ }
381
+ }
382
+
295
383
  return requests;
296
384
  }
297
385
 
@@ -303,24 +391,28 @@ export class Batch {
303
391
  return this.requests;
304
392
  }
305
393
 
306
- // post-fulu we need to ensure that we only request columns that the peer has advertised
307
- const {columnsRequest} = this.requests;
308
- if (columnsRequest == null) {
309
- return this.requests;
310
- }
394
+ // post-fulu we need to ensure that we only request columns that the peer has advertised.
395
+ const {columnsRequest, parentPayloadRequest} = this.requests;
311
396
 
312
397
  const peerColumns = new Set(peer.custodyColumns ?? []);
313
- const requestedColumns = columnsRequest.columns.filter((c) => peerColumns.has(c));
314
- if (requestedColumns.length === columnsRequest.columns.length) {
315
- return this.requests;
316
- }
398
+ const filteredColumnsRequest =
399
+ columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
400
+ const parentColumns = parentPayloadRequest?.columns;
401
+ const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
402
+
403
+ const updatedColumnRequest =
404
+ columnsRequest != null && filteredColumnsRequest != null
405
+ ? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
406
+ : {};
407
+ const updatedParentPayloadRequest =
408
+ parentPayloadRequest != null && filteredParentColumns != null
409
+ ? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
410
+ : {};
317
411
 
318
412
  return {
319
413
  ...this.requests,
320
- columnsRequest: {
321
- ...columnsRequest,
322
- columns: requestedColumns,
323
- },
414
+ ...updatedColumnRequest,
415
+ ...updatedParentPayloadRequest,
324
416
  };
325
417
  }
326
418
 
@@ -331,6 +423,35 @@ export class Batch {
331
423
  return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
332
424
  }
333
425
 
426
+ /**
427
+ * True only if the peer has already returned a successful response for the current request.
428
+ * A by_range success may update `this.requests` to parent_payload, and the same peer is then
429
+ * still eligible for the newly discovered parent payload data.
430
+ * For by_range, a peer that previously succeeded with a superset of requested columns is skipped.
431
+ */
432
+ hasPeerSucceededCurrentRequest(peer: PeerSyncMeta): boolean {
433
+ const successfulDownload = this.successfulDownloads.get(peer.peerId);
434
+ if (successfulDownload == null) return false;
435
+
436
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
437
+ if (request.parentPayload) return successfulDownload.parentPayload;
438
+
439
+ const requestByRangeColumns = request.byRangeColumns;
440
+
441
+ if (requestByRangeColumns.size === 0) {
442
+ // this means a download blocks/envelops by_range only
443
+ // don't do that again if we already did it
444
+ // see https://github.com/ChainSafe/lodestar/issues/9357
445
+ return true;
446
+ }
447
+
448
+ return [...requestByRangeColumns].every((column) => successfulDownload.byRangeColumns.has(column));
449
+ }
450
+
451
+ private getSuccessfulPeers(): PeerIdStr[] {
452
+ return Array.from(this.successfulDownloads.keys());
453
+ }
454
+
334
455
  getMetadata(): BatchMetadata {
335
456
  const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
336
457
  const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
@@ -365,14 +486,17 @@ export class Batch {
365
486
  /**
366
487
  * AwaitingDownload -> Downloading
367
488
  */
368
- startDownloading(peer: PeerIdStr): void {
489
+ startDownloading(peer: PeerSyncMeta): void {
369
490
  if (this.state.status !== BatchStatus.AwaitingDownload) {
370
491
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingDownload));
371
492
  }
372
493
 
494
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
495
+
373
496
  this.state = {
374
497
  status: BatchStatus.Downloading,
375
- peer,
498
+ peer: peer.peerId,
499
+ request,
376
500
  blocks: this.state.blocks,
377
501
  payloadEnvelopes: this.state.payloadEnvelopes,
378
502
  };
@@ -393,7 +517,17 @@ export class Batch {
393
517
  // ensure that blocks are always sorted before getting stored on the batch.state or being used to getRequests
394
518
  blocks.sort((a, b) => a.slot - b.slot);
395
519
 
396
- this.goodPeers.push(peer);
520
+ const successfulDownload = this.successfulDownloads.get(peer) ?? {
521
+ parentPayload: false,
522
+ byRangeColumns: new Set<number>(),
523
+ };
524
+ successfulDownload.parentPayload ||= this.state.request.parentPayload;
525
+ if (!this.state.request.parentPayload) {
526
+ for (const column of this.state.request.byRangeColumns) {
527
+ successfulDownload.byRangeColumns.add(column);
528
+ }
529
+ }
530
+ this.successfulDownloads.set(peer, successfulDownload);
397
531
 
398
532
  let allComplete = true;
399
533
  const slots = new Set<number>();
@@ -422,14 +556,38 @@ export class Batch {
422
556
  if (allComplete && isForkPostGloas(this.forkName)) {
423
557
  for (const block of blocks) {
424
558
  const payloadInput = newPayloadEnvelopes?.get(block.slot);
425
- // by_range needs to download all columns
426
- if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
559
+ // only need to make sure envelope has all columns, not all blocks have payload
560
+ // assertLinearChainSegment() was called before reaching this
561
+ if (payloadInput?.hasPayloadEnvelope() && !payloadInput.hasComputedAllData()) {
427
562
  allComplete = false;
428
563
  break;
429
564
  }
430
565
  }
431
566
  }
432
567
 
568
+ // First batch of a sync chain must additionally have the dangling-parent payload fully
569
+ // present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
570
+ // `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
571
+ if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
572
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
573
+ // Genesis has no parent payload — nothing to wait for.
574
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
575
+ const parentRootHex = toRootHex(parentRoot);
576
+ let parentPayloadComplete = false;
577
+ if (newPayloadEnvelopes) {
578
+ for (const payloadInput of newPayloadEnvelopes.values()) {
579
+ if (payloadInput.blockRootHex === parentRootHex) {
580
+ parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
581
+ break;
582
+ }
583
+ }
584
+ }
585
+ if (!parentPayloadComplete) {
586
+ allComplete = false;
587
+ }
588
+ }
589
+ }
590
+
433
591
  if (allComplete) {
434
592
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
435
593
  } else {
@@ -491,10 +649,10 @@ export class Batch {
491
649
  const blocks = this.state.blocks;
492
650
  const payloadEnvelopes = this.state.payloadEnvelopes;
493
651
  const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
494
- // Reset goodPeers in case another download attempt needs to be made. When Attempt is successful or not the peers
495
- // that the data came from will be handled by the Attempt that goes for processing
496
- const peers = this.goodPeers;
497
- this.goodPeers = [];
652
+ // Reset successfulDownloads in case another download attempt needs to be made. When Attempt is successful or not
653
+ // the peers that the data came from will be handled by the Attempt that goes for processing.
654
+ const peers = this.getSuccessfulPeers();
655
+ this.successfulDownloads.clear();
498
656
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
499
657
  return {blocks, payloadEnvelopes, peers};
500
658
  }
@@ -1,6 +1,5 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {RequestErrorCode} from "@lodestar/reqresp";
3
- import {Epoch, Root, Slot} from "@lodestar/types";
2
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
4
3
  import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
4
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
6
5
  import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
@@ -16,13 +15,9 @@ import {CustodyConfig} from "../../util/dataColumns.js";
16
15
  import {ItTrigger} from "../../util/itTrigger.js";
17
16
  import {PeerIdStr} from "../../util/peerId.js";
18
17
  import {WarnResult, wrapError} from "../../util/wrapError.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";
18
+ import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
25
19
  import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
20
+ import {getRateLimitedUntilMs} from "../utils/rateLimit.js";
26
21
  import {RangeSyncType} from "../utils/remoteSyncType.js";
27
22
  import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
28
23
  import {
@@ -145,6 +140,10 @@ export class SyncChain {
145
140
  private readonly batchProcessor = new ItTrigger();
146
141
  /** Sorted map of batches undergoing some kind of processing. */
147
142
  private readonly batches = new Map<Epoch, Batch>();
143
+ /**
144
+ * `true` until the first `Batch` is constructed via `includeNextBatch`
145
+ */
146
+ private isFirstBatch = true;
148
147
  private readonly peerset = new Map<PeerIdStr, ChainTarget>();
149
148
  /**
150
149
  * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
@@ -152,19 +151,22 @@ export class SyncChain {
152
151
  * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
153
152
  */
154
153
  private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
154
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
155
155
 
156
156
  private readonly logger: Logger;
157
157
  private readonly config: ChainForkConfig;
158
158
  private readonly clock: IClock;
159
159
  private readonly metrics: Metrics | null;
160
160
  private readonly custodyConfig: CustodyConfig;
161
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
161
162
 
162
163
  constructor(
163
164
  initialBatchEpoch: Epoch,
164
165
  initialTarget: ChainTarget,
165
166
  syncType: RangeSyncType,
166
167
  fns: SyncChainFns,
167
- modules: SyncChainModules
168
+ modules: SyncChainModules,
169
+ latestBid: gloas.ExecutionPayloadBid | undefined
168
170
  ) {
169
171
  const {config, clock, custodyConfig, logger, metrics} = modules;
170
172
  this.firstBatchEpoch = initialBatchEpoch;
@@ -180,6 +182,7 @@ export class SyncChain {
180
182
  this.clock = clock;
181
183
  this.metrics = metrics;
182
184
  this.custodyConfig = custodyConfig;
185
+ this.latestBid = latestBid;
183
186
  this.logger = logger;
184
187
  this.logId = `${syncType}-${nextChainId++}`;
185
188
 
@@ -234,6 +237,7 @@ export class SyncChain {
234
237
  */
235
238
  stopSyncing(): void {
236
239
  this.status = SyncChainStatus.Stopped;
240
+ this.clearRateLimitBackoffTimer();
237
241
  this.logger.debug("SyncChain stopSyncing", {id: this.logId});
238
242
  }
239
243
 
@@ -242,6 +246,7 @@ export class SyncChain {
242
246
  */
243
247
  remove(): void {
244
248
  this.logger.debug("SyncChain remove", {id: this.logId});
249
+ this.clearRateLimitBackoffTimer();
245
250
  this.batchProcessor.end(new ErrorAborted("SyncChain"));
246
251
  }
247
252
 
@@ -364,6 +369,8 @@ export class SyncChain {
364
369
  }
365
370
 
366
371
  throw e;
372
+ } finally {
373
+ this.clearRateLimitBackoffTimer();
367
374
  }
368
375
  }
369
376
 
@@ -387,6 +394,44 @@ export class SyncChain {
387
394
  }
388
395
  }
389
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
+
390
435
  /**
391
436
  * Attempts to request the next required batches from the peer pool if the chain is syncing.
392
437
  * It will exhaust the peer pool and left over batches until the batch buffer is reached.
@@ -481,7 +526,17 @@ export class SyncChain {
481
526
  return null;
482
527
  }
483
528
 
484
- 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;
485
540
  this.batches.set(startEpoch, batch);
486
541
  return batch;
487
542
  }
@@ -496,7 +551,7 @@ export class SyncChain {
496
551
  peer: prettyPrintPeerIdStr(peer.peerId),
497
552
  });
498
553
  try {
499
- batch.startDownloading(peer.peerId);
554
+ batch.startDownloading(peer);
500
555
 
501
556
  // wrapError ensures to never call both batch success() and batch error()
502
557
  const res = await wrapError(this.downloadByRange(peer, batch, this.syncType));
@@ -526,6 +581,8 @@ export class SyncChain {
526
581
  case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS:
527
582
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
528
583
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
584
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
585
+ case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
529
586
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
530
587
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
531
588
  case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT:
@@ -539,9 +596,11 @@ export class SyncChain {
539
596
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
540
597
  res.err
541
598
  );
542
- if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
599
+ const rateLimitedUntilMs = getRateLimitedUntilMs(res.err);
600
+ if (rateLimitedUntilMs !== null) {
543
601
  // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
544
- this.rateLimitedPeers.set(peer.peerId, Date.now() + RATE_LIMITED_PEER_BACKOFF_MS);
602
+ this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs);
603
+ this.scheduleRateLimitBackoffRetry();
545
604
  batch.downloadingRateLimited();
546
605
  this.triggerBatchDownloader();
547
606
  } else {
@@ -1,16 +1,23 @@
1
1
  import {EventEmitter} from "node:events";
2
2
  import {StrictEventEmitter} from "strict-event-emitter-types";
3
3
  import {BeaconConfig} from "@lodestar/config";
4
- import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
+ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
- import {Logger, toRootHex} from "@lodestar/utils";
6
+ import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
8
8
  import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
9
+ import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
10
+ import {BlockError} from "../../chain/errors/index.js";
9
11
  import {IBeaconChain} from "../../chain/index.js";
10
12
  import {Metrics} from "../../metrics/index.js";
11
13
  import {INetwork} from "../../network/index.js";
12
14
  import {PeerIdStr} from "../../util/peerId.js";
13
- import {cacheByRangeResponses, downloadByRange} from "../utils/downloadByRange.js";
15
+ import {
16
+ DownloadByRangeError,
17
+ DownloadByRangeErrorCode,
18
+ cacheByRangeResponses,
19
+ downloadByRange,
20
+ } from "../utils/downloadByRange.js";
14
21
  import {RangeSyncType, getRangeSyncTarget, rangeSyncTypes} from "../utils/remoteSyncType.js";
15
22
  import {ChainTarget, SyncChain, SyncChainDebugState, SyncChainFns} from "./chain.js";
16
23
  import {updateChains} from "./utils/index.js";
@@ -206,14 +213,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
206
213
 
207
214
  private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
208
215
  const batchBlocks = batch.getBlocks();
216
+ const requests = batch.getRequestsForPeer(peer);
217
+ const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
218
+ const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
209
219
  const {result, warnings} = await downloadByRange({
210
220
  config: this.config,
211
221
  network: this.network,
212
222
  logger: this.logger,
213
223
  peerIdStr: peer.peerId,
214
224
  batchBlocks,
225
+ parentPayloadCommitments,
215
226
  peerDasMetrics: this.chain.metrics?.peerDas,
216
- ...batch.getRequestsForPeer(peer),
227
+ ...requests,
217
228
  });
218
229
  const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
219
230
  const {blocks, payloadEnvelopes} = cacheByRangeResponses({
@@ -227,6 +238,49 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
227
238
  custodyConfig: this.chain.custodyConfig,
228
239
  seenTimestampSec: Date.now() / 1000,
229
240
  });
241
+
242
+ const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot);
243
+ const envelopeSlots = payloadEnvelopes
244
+ ? Array.from(payloadEnvelopes.entries())
245
+ .filter(([, pi]) => pi.hasPayloadEnvelope())
246
+ .map(([slot]) => slot)
247
+ .sort((a, b) => a - b)
248
+ : [];
249
+ this.logger.verbose("downloadByRange batch ready", {
250
+ peer: peer.peerId,
251
+ blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)),
252
+ envelopeSlots: prettyPrintIndices(envelopeSlots),
253
+ ...batch.getMetadata(),
254
+ });
255
+
256
+ if (segmentBlocks.length > 1) {
257
+ try {
258
+ assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null);
259
+ } catch (err) {
260
+ if (err instanceof BlockError) {
261
+ this.logger.debug(
262
+ "downloadByRange segment validation failed",
263
+ {
264
+ peer: peer.peerId,
265
+ reason: err.type.code,
266
+ slot: err.signedBlock.message.slot,
267
+ detail: JSON.stringify(err.type),
268
+ ...batch.getMetadata(),
269
+ },
270
+ err
271
+ );
272
+ // with this error, the peer will be penalized inside SyncChain
273
+ throw new DownloadByRangeError({
274
+ code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT,
275
+ slot: err.signedBlock.message.slot,
276
+ reason: err.type.code,
277
+ });
278
+ }
279
+
280
+ throw err;
281
+ }
282
+ }
283
+
230
284
  return {result: {blocks, payloadEnvelopes}, warnings};
231
285
  };
232
286
 
@@ -258,6 +312,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
258
312
  private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
259
313
  let syncChain = this.chains.get(syncType);
260
314
  if (!syncChain) {
315
+ // The first batch of a new sync chain may need to detect whether the parent block was an
316
+ // gloas "empty" block (no envelope produced). It does so by comparing the first
317
+ // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
318
+ const headState = this.chain.getHeadState();
319
+ const latestBid = isStatePostGloas(headState)
320
+ ? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
321
+ : undefined;
322
+
261
323
  syncChain = new SyncChain(
262
324
  startEpoch,
263
325
  target,
@@ -276,7 +338,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
276
338
  logger: this.logger,
277
339
  custodyConfig: this.chain.custodyConfig,
278
340
  metrics: this.metrics,
279
- }
341
+ },
342
+ latestBid
280
343
  );
281
344
  this.chains.set(syncType, syncChain);
282
345
 
@@ -52,7 +52,8 @@ export class ChainPeersBalancer {
52
52
 
53
53
  /**
54
54
  * Return the most suitable peer to retry
55
- * Sort peers by (1) no failed request (2) less active requests, then pick first
55
+ * Sort peers by (1) less active requests (2) most columns we need.
56
+ * Peers that failed this batch or already succeeded for the same request are excluded inside `filterPeers`.
56
57
  */
57
58
  bestPeerToRetryBatch(batch: Batch): PeerSyncMeta | undefined {
58
59
  if (batch.state.status !== BatchStatus.AwaitingDownload) {
@@ -63,10 +64,8 @@ export class ChainPeersBalancer {
63
64
  const pendingDataColumns = columnsRequest?.columns ?? this.custodyConfig.sampledColumns;
64
65
  const eligiblePeers = this.filterPeers(batch, pendingDataColumns, false);
65
66
 
66
- const failedPeers = new Set(batch.getFailedPeers());
67
67
  const sortedBestPeers = sortBy(
68
68
  eligiblePeers,
69
- ({syncInfo}) => (failedPeers.has(syncInfo.peerId) ? 1 : 0), // prefer peers without failed requests
70
69
  ({syncInfo}) => this.activeRequestsByPeer.get(syncInfo.peerId) ?? 0, // prefer peers with least active req
71
70
  ({columns}) => -1 * columns // prefer peers with the most columns
72
71
  );
@@ -117,9 +116,16 @@ export class ChainPeersBalancer {
117
116
  return eligiblePeers;
118
117
  }
119
118
 
119
+ // Skip peers that failed this batch, or that already returned the exact current request shape.
120
+ const failedPeers = new Set<PeerIdStr>(batch.getFailedPeers());
121
+
120
122
  for (const peer of this.peers) {
121
123
  const {earliestAvailableSlot, target, peerId} = peer;
122
124
 
125
+ if (failedPeers.has(peerId) || batch.hasPeerSucceededCurrentRequest(peer)) {
126
+ continue;
127
+ }
128
+
123
129
  const activeRequest = this.activeRequestsByPeer.get(peerId) ?? 0;
124
130
  if (noActiveRequest && activeRequest > 0) {
125
131
  // consumer wants to find peer with no active request, but this peer has active request