@lodestar/beacon-node 1.43.0-dev.374360e50a → 1.43.0-dev.38cd4b01e7

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 (151) hide show
  1. package/lib/api/impl/beacon/pool/index.js +3 -3
  2. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  3. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  4. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  5. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  6. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  7. package/lib/chain/blocks/index.d.ts.map +1 -1
  8. package/lib/chain/blocks/index.js +6 -7
  9. package/lib/chain/blocks/index.js.map +1 -1
  10. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +8 -1
  11. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  12. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +19 -0
  13. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  14. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  15. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  16. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  17. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  18. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  19. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  21. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  22. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  23. package/lib/chain/chain.d.ts.map +1 -1
  24. package/lib/chain/chain.js +15 -1
  25. package/lib/chain/chain.js.map +1 -1
  26. package/lib/chain/initState.d.ts.map +1 -1
  27. package/lib/chain/initState.js +6 -1
  28. package/lib/chain/initState.js.map +1 -1
  29. package/lib/chain/lightClient/index.d.ts.map +1 -1
  30. package/lib/chain/lightClient/index.js +1 -1
  31. package/lib/chain/lightClient/index.js.map +1 -1
  32. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  33. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  34. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  35. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  36. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -6
  37. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  38. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +21 -5
  39. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  40. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  41. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  42. package/lib/chain/validation/payloadAttestationMessage.js +5 -3
  43. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  44. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  45. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  46. package/lib/metrics/metrics/lodestar.js +5 -0
  47. package/lib/metrics/metrics/lodestar.js.map +1 -1
  48. package/lib/network/interface.d.ts +1 -0
  49. package/lib/network/interface.d.ts.map +1 -1
  50. package/lib/network/network.d.ts +1 -0
  51. package/lib/network/network.d.ts.map +1 -1
  52. package/lib/network/network.js +3 -0
  53. package/lib/network/network.js.map +1 -1
  54. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  55. package/lib/network/processor/gossipHandlers.js +33 -3
  56. package/lib/network/processor/gossipHandlers.js.map +1 -1
  57. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  58. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  59. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  60. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  61. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  62. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  63. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  64. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  65. package/lib/network/reqresp/handlers/index.js +5 -0
  66. package/lib/network/reqresp/handlers/index.js.map +1 -1
  67. package/lib/network/reqresp/interface.d.ts +1 -1
  68. package/lib/network/reqresp/interface.js +1 -1
  69. package/lib/network/reqresp/protocols.d.ts +1 -0
  70. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  71. package/lib/network/reqresp/protocols.js +5 -0
  72. package/lib/network/reqresp/protocols.js.map +1 -1
  73. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  74. package/lib/network/reqresp/rateLimit.js +4 -0
  75. package/lib/network/reqresp/rateLimit.js.map +1 -1
  76. package/lib/network/reqresp/score.d.ts.map +1 -1
  77. package/lib/network/reqresp/score.js +1 -0
  78. package/lib/network/reqresp/score.js.map +1 -1
  79. package/lib/network/reqresp/types.d.ts +3 -0
  80. package/lib/network/reqresp/types.d.ts.map +1 -1
  81. package/lib/network/reqresp/types.js +3 -0
  82. package/lib/network/reqresp/types.js.map +1 -1
  83. package/lib/sync/constants.d.ts +7 -1
  84. package/lib/sync/constants.d.ts.map +1 -1
  85. package/lib/sync/constants.js +7 -4
  86. package/lib/sync/constants.js.map +1 -1
  87. package/lib/sync/range/batch.d.ts +32 -6
  88. package/lib/sync/range/batch.d.ts.map +1 -1
  89. package/lib/sync/range/batch.js +163 -26
  90. package/lib/sync/range/batch.js.map +1 -1
  91. package/lib/sync/range/chain.d.ts +16 -2
  92. package/lib/sync/range/chain.d.ts.map +1 -1
  93. package/lib/sync/range/chain.js +77 -4
  94. package/lib/sync/range/chain.js.map +1 -1
  95. package/lib/sync/range/range.d.ts.map +1 -1
  96. package/lib/sync/range/range.js +54 -5
  97. package/lib/sync/range/range.js.map +1 -1
  98. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  99. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  100. package/lib/sync/range/utils/peerBalancer.js +8 -4
  101. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  102. package/lib/sync/sync.d.ts.map +1 -1
  103. package/lib/sync/sync.js +13 -0
  104. package/lib/sync/sync.js.map +1 -1
  105. package/lib/sync/unknownBlock.d.ts +7 -0
  106. package/lib/sync/unknownBlock.d.ts.map +1 -1
  107. package/lib/sync/unknownBlock.js +127 -10
  108. package/lib/sync/unknownBlock.js.map +1 -1
  109. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  110. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  111. package/lib/sync/utils/downloadByRange.js +87 -38
  112. package/lib/sync/utils/downloadByRange.js.map +1 -1
  113. package/lib/sync/utils/rateLimit.d.ts +2 -0
  114. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  115. package/lib/sync/utils/rateLimit.js +15 -0
  116. package/lib/sync/utils/rateLimit.js.map +1 -0
  117. package/package.json +15 -16
  118. package/src/api/impl/beacon/pool/index.ts +3 -3
  119. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  120. package/src/chain/blocks/index.ts +5 -7
  121. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +27 -1
  122. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  123. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  124. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  125. package/src/chain/chain.ts +17 -0
  126. package/src/chain/initState.ts +9 -1
  127. package/src/chain/lightClient/index.ts +6 -6
  128. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  129. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +23 -6
  130. package/src/chain/validation/payloadAttestationMessage.ts +6 -4
  131. package/src/metrics/metrics/lodestar.ts +6 -0
  132. package/src/network/interface.ts +1 -0
  133. package/src/network/network.ts +12 -0
  134. package/src/network/processor/gossipHandlers.ts +37 -3
  135. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  136. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  137. package/src/network/reqresp/handlers/index.ts +5 -0
  138. package/src/network/reqresp/interface.ts +1 -1
  139. package/src/network/reqresp/protocols.ts +6 -0
  140. package/src/network/reqresp/rateLimit.ts +4 -0
  141. package/src/network/reqresp/score.ts +1 -0
  142. package/src/network/reqresp/types.ts +5 -0
  143. package/src/sync/constants.ts +8 -4
  144. package/src/sync/range/batch.ts +204 -30
  145. package/src/sync/range/chain.ts +94 -5
  146. package/src/sync/range/range.ts +68 -5
  147. package/src/sync/range/utils/peerBalancer.ts +9 -3
  148. package/src/sync/sync.ts +13 -1
  149. package/src/sync/unknownBlock.ts +157 -10
  150. package/src/sync/utils/downloadByRange.ts +153 -39
  151. package/src/sync/utils/rateLimit.ts +16 -0
@@ -40,6 +40,10 @@ export const rateLimitQuotas: (fork: ForkName, config: BeaconConfig) => Record<R
40
40
  },
41
41
  getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByRoot, (req) => req.length),
42
42
  },
43
+ [ReqRespMethod.BeaconBlocksByHead]: {
44
+ byPeer: {quota: config.MAX_REQUEST_BLOCKS_DENEB, quotaTimeMs: 10_000},
45
+ getRequestCount: getRequestCountFn(fork, config, ReqRespMethod.BeaconBlocksByHead, (req) => req.count),
46
+ },
43
47
  [ReqRespMethod.BlobSidecarsByRange]: {
44
48
  // Rationale: MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK
45
49
  byPeer: {
@@ -46,6 +46,7 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
46
46
  return PeerAction.LowToleranceError;
47
47
  case ReqRespMethod.BeaconBlocksByRange:
48
48
  case ReqRespMethod.BeaconBlocksByRoot:
49
+ case ReqRespMethod.BeaconBlocksByHead:
49
50
  case ReqRespMethod.ExecutionPayloadEnvelopesByRoot:
50
51
  case ReqRespMethod.ExecutionPayloadEnvelopesByRange:
51
52
  return PeerAction.MidToleranceError;
@@ -42,6 +42,7 @@ export enum ReqRespMethod {
42
42
  Metadata = "metadata",
43
43
  BeaconBlocksByRange = "beacon_blocks_by_range",
44
44
  BeaconBlocksByRoot = "beacon_blocks_by_root",
45
+ BeaconBlocksByHead = "beacon_blocks_by_head",
45
46
  BlobSidecarsByRange = "blob_sidecars_by_range",
46
47
  BlobSidecarsByRoot = "blob_sidecars_by_root",
47
48
  DataColumnSidecarsByRange = "data_column_sidecars_by_range",
@@ -62,6 +63,7 @@ export type RequestBodyByMethod = {
62
63
  [ReqRespMethod.Metadata]: null;
63
64
  [ReqRespMethod.BeaconBlocksByRange]: phase0.BeaconBlocksByRangeRequest;
64
65
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequest;
66
+ [ReqRespMethod.BeaconBlocksByHead]: fulu.BeaconBlocksByHeadRequest;
65
67
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecarsByRangeRequest;
66
68
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequest;
67
69
  [ReqRespMethod.DataColumnSidecarsByRange]: fulu.DataColumnSidecarsByRangeRequest;
@@ -82,6 +84,7 @@ type ResponseBodyByMethod = {
82
84
  // Do not matter
83
85
  [ReqRespMethod.BeaconBlocksByRange]: SignedBeaconBlock;
84
86
  [ReqRespMethod.BeaconBlocksByRoot]: SignedBeaconBlock;
87
+ [ReqRespMethod.BeaconBlocksByHead]: SignedBeaconBlock;
85
88
  [ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecar;
86
89
  [ReqRespMethod.BlobSidecarsByRoot]: deneb.BlobSidecar;
87
90
  [ReqRespMethod.DataColumnSidecarsByRange]: DataColumnSidecar;
@@ -111,6 +114,7 @@ export const requestSszTypeByMethod: (
111
114
 
112
115
  [ReqRespMethod.BeaconBlocksByRange]: ssz.phase0.BeaconBlocksByRangeRequest,
113
116
  [ReqRespMethod.BeaconBlocksByRoot]: BeaconBlocksByRootRequestType(fork, config),
117
+ [ReqRespMethod.BeaconBlocksByHead]: ssz.fulu.BeaconBlocksByHeadRequest,
114
118
  [ReqRespMethod.BlobSidecarsByRange]: ssz.deneb.BlobSidecarsByRangeRequest,
115
119
  [ReqRespMethod.BlobSidecarsByRoot]: BlobSidecarsByRootRequestType(fork, config),
116
120
  [ReqRespMethod.DataColumnSidecarsByRange]: ssz.fulu.DataColumnSidecarsByRangeRequest,
@@ -142,6 +146,7 @@ export const responseSszTypeByMethod: {[K in ReqRespMethod]: ResponseTypeGetter<
142
146
  version === Version.V1 ? ssz.phase0.Metadata : version === Version.V2 ? ssz.altair.Metadata : ssz.fulu.Metadata,
143
147
  [ReqRespMethod.BeaconBlocksByRange]: blocksResponseType,
144
148
  [ReqRespMethod.BeaconBlocksByRoot]: blocksResponseType,
149
+ [ReqRespMethod.BeaconBlocksByHead]: (fork) => ssz[fork].SignedBeaconBlock,
145
150
  [ReqRespMethod.BlobSidecarsByRange]: () => ssz.deneb.BlobSidecar,
146
151
  [ReqRespMethod.BlobSidecarsByRoot]: () => ssz.deneb.BlobSidecar,
147
152
  [ReqRespMethod.LightClientBootstrap]: (fork) => sszTypesFor(onlyPostAltairFork(fork)).LightClientBootstrap,
@@ -5,10 +5,14 @@ export const PARALLEL_HEAD_CHAINS = 2;
5
5
  export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
6
6
 
7
7
  /** The number of times to retry a batch before it is considered failed. */
8
- // export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
- // this constant is increased a lot for peerDAS because we may have many failed download due to rate limit not implemented yet
10
- // TODO: change it back to 5 when this issue is implemented https://github.com/ChainSafe/lodestar/issues/8033
11
- export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 20;
8
+ export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
+
10
+ /**
11
+ * Backoff before assigning more range-sync batches to a peer that rate-limited us.
12
+ *
13
+ * Note: this is used when rate limited due to MAX_CONCURRENT_REQUESTS
14
+ */
15
+ export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
12
16
 
13
17
  /**
14
18
  * Consider batch faulty after downloading and processing this number of times
@@ -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 {
@@ -460,6 +618,22 @@ export class Batch {
460
618
  };
461
619
  }
462
620
 
621
+ /**
622
+ * Downloading -> AwaitingDownload (without counting as a failed attempt).
623
+ * Used when the peer rate-limited us — the request was never actually served.
624
+ */
625
+ downloadingRateLimited(): void {
626
+ if (this.state.status !== BatchStatus.Downloading) {
627
+ throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
628
+ }
629
+
630
+ this.state = {
631
+ status: BatchStatus.AwaitingDownload,
632
+ blocks: this.state.blocks,
633
+ payloadEnvelopes: this.state.payloadEnvelopes,
634
+ };
635
+ }
636
+
463
637
  /**
464
638
  * AwaitingProcessing -> Processing
465
639
  */
@@ -475,10 +649,10 @@ export class Batch {
475
649
  const blocks = this.state.blocks;
476
650
  const payloadEnvelopes = this.state.payloadEnvelopes;
477
651
  const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
478
- // Reset goodPeers in case another download attempt needs to be made. When Attempt is successful or not the peers
479
- // that the data came from will be handled by the Attempt that goes for processing
480
- const peers = this.goodPeers;
481
- 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();
482
656
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
483
657
  return {blocks, payloadEnvelopes, peers};
484
658
  }
@@ -1,5 +1,5 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {Epoch, Root, Slot} from "@lodestar/types";
2
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
3
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";
@@ -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,6 +237,7 @@ export class SyncChain {
222
237
  */
223
238
  stopSyncing(): void {
224
239
  this.status = SyncChainStatus.Stopped;
240
+ this.clearRateLimitBackoffTimer();
225
241
  this.logger.debug("SyncChain stopSyncing", {id: this.logId});
226
242
  }
227
243
 
@@ -230,6 +246,7 @@ export class SyncChain {
230
246
  */
231
247
  remove(): void {
232
248
  this.logger.debug("SyncChain remove", {id: this.logId});
249
+ this.clearRateLimitBackoffTimer();
233
250
  this.batchProcessor.end(new ErrorAborted("SyncChain"));
234
251
  }
235
252
 
@@ -248,6 +265,7 @@ export class SyncChain {
248
265
  */
249
266
  removePeer(peerId: PeerIdStr): boolean {
250
267
  const deleted = this.peerset.delete(peerId);
268
+ this.rateLimitedPeers.delete(peerId);
251
269
  this.computeTarget();
252
270
  return deleted;
253
271
  }
@@ -351,6 +369,8 @@ export class SyncChain {
351
369
  }
352
370
 
353
371
  throw e;
372
+ } finally {
373
+ this.clearRateLimitBackoffTimer();
354
374
  }
355
375
  }
356
376
 
@@ -374,6 +394,44 @@ export class SyncChain {
374
394
  }
375
395
  }
376
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
+
377
435
  /**
378
436
  * Attempts to request the next required batches from the peer pool if the chain is syncing.
379
437
  * It will exhaust the peer pool and left over batches until the batch buffer is reached.
@@ -383,8 +441,18 @@ export class SyncChain {
383
441
  return;
384
442
  }
385
443
 
444
+ const now = Date.now();
386
445
  const peersSyncInfo: PeerSyncInfo[] = [];
387
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
+
388
456
  try {
389
457
  peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
390
458
  } catch (e) {
@@ -458,7 +526,17 @@ export class SyncChain {
458
526
  return null;
459
527
  }
460
528
 
461
- 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;
462
540
  this.batches.set(startEpoch, batch);
463
541
  return batch;
464
542
  }
@@ -473,7 +551,7 @@ export class SyncChain {
473
551
  peer: prettyPrintPeerIdStr(peer.peerId),
474
552
  });
475
553
  try {
476
- batch.startDownloading(peer.peerId);
554
+ batch.startDownloading(peer);
477
555
 
478
556
  // wrapError ensures to never call both batch success() and batch error()
479
557
  const res = await wrapError(this.downloadByRange(peer, batch, this.syncType));
@@ -503,6 +581,8 @@ export class SyncChain {
503
581
  case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS:
504
582
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
505
583
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
584
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
585
+ case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
506
586
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
507
587
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
508
588
  case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT:
@@ -516,7 +596,16 @@ export class SyncChain {
516
596
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
517
597
  res.err
518
598
  );
519
- batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
599
+ const rateLimitedUntilMs = getRateLimitedUntilMs(res.err);
600
+ if (rateLimitedUntilMs !== null) {
601
+ // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
602
+ this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs);
603
+ this.scheduleRateLimitBackoffRetry();
604
+ batch.downloadingRateLimited();
605
+ this.triggerBatchDownloader();
606
+ } else {
607
+ batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
608
+ }
520
609
  } else {
521
610
  this.logger.verbose("Batch download success", {
522
611
  id: this.logId,