@lodestar/beacon-node 1.43.0-dev.4358217e12 → 1.43.0-dev.4451fec75a

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 (254) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +10 -0
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/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 +6 -1
  21. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  22. package/lib/chain/blocks/importExecutionPayload.js +19 -10
  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 +1 -3
  36. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  37. package/lib/chain/blocks/utils/chainSegment.js +34 -23
  38. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  39. package/lib/chain/blocks/verifyBlock.d.ts +2 -1
  40. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlock.js +30 -12
  42. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  43. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  44. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  45. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  46. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  47. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  48. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  49. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  50. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  51. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  52. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  53. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  54. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  55. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  56. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +8 -3
  57. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  58. package/lib/chain/chain.d.ts.map +1 -1
  59. package/lib/chain/chain.js +25 -8
  60. package/lib/chain/chain.js.map +1 -1
  61. package/lib/chain/emitter.d.ts +0 -11
  62. package/lib/chain/emitter.d.ts.map +1 -1
  63. package/lib/chain/emitter.js +0 -4
  64. package/lib/chain/emitter.js.map +1 -1
  65. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  66. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  67. package/lib/chain/errors/proposerPreferences.js +1 -0
  68. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  69. package/lib/chain/initState.d.ts.map +1 -1
  70. package/lib/chain/initState.js +6 -1
  71. package/lib/chain/initState.js.map +1 -1
  72. package/lib/chain/lightClient/index.d.ts.map +1 -1
  73. package/lib/chain/lightClient/index.js +1 -1
  74. package/lib/chain/lightClient/index.js.map +1 -1
  75. package/lib/chain/opPools/payloadAttestationPool.d.ts +2 -1
  76. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  77. package/lib/chain/opPools/payloadAttestationPool.js +52 -10
  78. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  79. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  80. package/lib/chain/prepareNextSlot.js +16 -18
  81. package/lib/chain/prepareNextSlot.js.map +1 -1
  82. package/lib/chain/produceBlock/produceBlockBody.d.ts +12 -3
  83. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  84. package/lib/chain/produceBlock/produceBlockBody.js +33 -20
  85. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  86. package/lib/chain/regen/queued.d.ts.map +1 -1
  87. package/lib/chain/regen/queued.js +1 -4
  88. package/lib/chain/regen/queued.js.map +1 -1
  89. package/lib/chain/regen/regen.d.ts.map +1 -1
  90. package/lib/chain/regen/regen.js +1 -4
  91. package/lib/chain/regen/regen.js.map +1 -1
  92. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +21 -11
  93. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  94. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +70 -20
  95. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  96. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  97. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  98. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  99. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  100. package/lib/chain/validation/executionPayloadBid.js +11 -8
  101. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  102. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  103. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  104. package/lib/chain/validation/payloadAttestationMessage.js +5 -3
  105. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  106. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  107. package/lib/chain/validation/proposerPreferences.js +39 -17
  108. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  109. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  110. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  111. package/lib/metrics/metrics/lodestar.js +5 -0
  112. package/lib/metrics/metrics/lodestar.js.map +1 -1
  113. package/lib/network/gossip/topic.d.ts +751 -2
  114. package/lib/network/gossip/topic.d.ts.map +1 -1
  115. package/lib/network/interface.d.ts +2 -0
  116. package/lib/network/interface.d.ts.map +1 -1
  117. package/lib/network/network.d.ts +2 -0
  118. package/lib/network/network.d.ts.map +1 -1
  119. package/lib/network/network.js +8 -0
  120. package/lib/network/network.js.map +1 -1
  121. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  122. package/lib/network/processor/gossipHandlers.js +61 -13
  123. package/lib/network/processor/gossipHandlers.js.map +1 -1
  124. package/lib/network/processor/index.js +5 -5
  125. package/lib/network/processor/index.js.map +1 -1
  126. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  127. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  128. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  129. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  130. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  131. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  132. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  133. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  134. package/lib/network/reqresp/handlers/index.js +5 -0
  135. package/lib/network/reqresp/handlers/index.js.map +1 -1
  136. package/lib/network/reqresp/interface.d.ts +1 -1
  137. package/lib/network/reqresp/interface.js +1 -1
  138. package/lib/network/reqresp/protocols.d.ts +1 -0
  139. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  140. package/lib/network/reqresp/protocols.js +5 -0
  141. package/lib/network/reqresp/protocols.js.map +1 -1
  142. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  143. package/lib/network/reqresp/rateLimit.js +4 -0
  144. package/lib/network/reqresp/rateLimit.js.map +1 -1
  145. package/lib/network/reqresp/score.d.ts.map +1 -1
  146. package/lib/network/reqresp/score.js +1 -0
  147. package/lib/network/reqresp/score.js.map +1 -1
  148. package/lib/network/reqresp/types.d.ts +3 -0
  149. package/lib/network/reqresp/types.d.ts.map +1 -1
  150. package/lib/network/reqresp/types.js +3 -0
  151. package/lib/network/reqresp/types.js.map +1 -1
  152. package/lib/node/nodejs.js +2 -2
  153. package/lib/node/nodejs.js.map +1 -1
  154. package/lib/node/notifier.js +1 -7
  155. package/lib/node/notifier.js.map +1 -1
  156. package/lib/sync/constants.d.ts +7 -1
  157. package/lib/sync/constants.d.ts.map +1 -1
  158. package/lib/sync/constants.js +7 -4
  159. package/lib/sync/constants.js.map +1 -1
  160. package/lib/sync/range/batch.d.ts +43 -6
  161. package/lib/sync/range/batch.d.ts.map +1 -1
  162. package/lib/sync/range/batch.js +243 -44
  163. package/lib/sync/range/batch.js.map +1 -1
  164. package/lib/sync/range/chain.d.ts +16 -2
  165. package/lib/sync/range/chain.d.ts.map +1 -1
  166. package/lib/sync/range/chain.js +100 -9
  167. package/lib/sync/range/chain.js.map +1 -1
  168. package/lib/sync/range/range.d.ts.map +1 -1
  169. package/lib/sync/range/range.js +54 -5
  170. package/lib/sync/range/range.js.map +1 -1
  171. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  172. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  173. package/lib/sync/range/utils/peerBalancer.js +8 -4
  174. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  175. package/lib/sync/sync.d.ts.map +1 -1
  176. package/lib/sync/sync.js +13 -0
  177. package/lib/sync/sync.js.map +1 -1
  178. package/lib/sync/unknownBlock.d.ts +7 -2
  179. package/lib/sync/unknownBlock.d.ts.map +1 -1
  180. package/lib/sync/unknownBlock.js +127 -57
  181. package/lib/sync/unknownBlock.js.map +1 -1
  182. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  183. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  184. package/lib/sync/utils/downloadByRange.js +106 -42
  185. package/lib/sync/utils/downloadByRange.js.map +1 -1
  186. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  187. package/lib/sync/utils/downloadByRoot.js +10 -0
  188. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  189. package/lib/sync/utils/rateLimit.d.ts +2 -0
  190. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  191. package/lib/sync/utils/rateLimit.js +15 -0
  192. package/lib/sync/utils/rateLimit.js.map +1 -0
  193. package/lib/util/sszBytes.d.ts.map +1 -1
  194. package/lib/util/sszBytes.js +8 -6
  195. package/lib/util/sszBytes.js.map +1 -1
  196. package/package.json +15 -16
  197. package/src/api/impl/beacon/blocks/index.ts +13 -0
  198. package/src/api/impl/beacon/pool/index.ts +83 -1
  199. package/src/api/impl/debug/index.ts +0 -1
  200. package/src/api/impl/validator/index.ts +82 -1
  201. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  202. package/src/chain/blocks/importBlock.ts +16 -50
  203. package/src/chain/blocks/importExecutionPayload.ts +32 -15
  204. package/src/chain/blocks/index.ts +32 -15
  205. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +37 -3
  206. package/src/chain/blocks/payloadEnvelopeInput/types.ts +18 -0
  207. package/src/chain/blocks/types.ts +2 -1
  208. package/src/chain/blocks/utils/chainSegment.ts +35 -24
  209. package/src/chain/blocks/verifyBlock.ts +45 -13
  210. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  211. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  212. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  213. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +7 -4
  214. package/src/chain/chain.ts +29 -7
  215. package/src/chain/emitter.ts +0 -11
  216. package/src/chain/errors/proposerPreferences.ts +9 -1
  217. package/src/chain/initState.ts +9 -1
  218. package/src/chain/lightClient/index.ts +6 -6
  219. package/src/chain/opPools/payloadAttestationPool.ts +59 -10
  220. package/src/chain/prepareNextSlot.ts +21 -29
  221. package/src/chain/produceBlock/produceBlockBody.ts +41 -25
  222. package/src/chain/regen/queued.ts +2 -7
  223. package/src/chain/regen/regen.ts +2 -7
  224. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +90 -24
  225. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  226. package/src/chain/validation/executionPayloadBid.ts +11 -8
  227. package/src/chain/validation/payloadAttestationMessage.ts +6 -4
  228. package/src/chain/validation/proposerPreferences.ts +37 -18
  229. package/src/metrics/metrics/lodestar.ts +6 -0
  230. package/src/network/interface.ts +2 -0
  231. package/src/network/network.ts +23 -0
  232. package/src/network/processor/gossipHandlers.ts +75 -14
  233. package/src/network/processor/index.ts +5 -5
  234. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  235. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  236. package/src/network/reqresp/handlers/index.ts +5 -0
  237. package/src/network/reqresp/interface.ts +1 -1
  238. package/src/network/reqresp/protocols.ts +6 -0
  239. package/src/network/reqresp/rateLimit.ts +4 -0
  240. package/src/network/reqresp/score.ts +1 -0
  241. package/src/network/reqresp/types.ts +5 -0
  242. package/src/node/nodejs.ts +2 -2
  243. package/src/node/notifier.ts +1 -8
  244. package/src/sync/constants.ts +8 -4
  245. package/src/sync/range/batch.ts +309 -51
  246. package/src/sync/range/chain.ts +119 -10
  247. package/src/sync/range/range.ts +68 -5
  248. package/src/sync/range/utils/peerBalancer.ts +9 -3
  249. package/src/sync/sync.ts +13 -1
  250. package/src/sync/unknownBlock.ts +157 -60
  251. package/src/sync/utils/downloadByRange.ts +174 -44
  252. package/src/sync/utils/downloadByRoot.ts +12 -0
  253. package/src/sync/utils/rateLimit.ts +16 -0
  254. 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
  /**
@@ -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
  }
@@ -79,10 +90,43 @@ export type BatchState =
79
90
  };
80
91
 
81
92
  export type BatchMetadata = {
93
+ // Batch-level slot window (always present)
82
94
  startEpoch: Epoch;
95
+ startSlot: Slot;
96
+ count: number;
83
97
  status: BatchStatus;
98
+
99
+ // Per-type outstanding request shapes; only present when that sub-request exists.
100
+ // Format: "startSlot=<n>,count=<n>" (plus ",cols=<indices>" for columns).
101
+ blocksReq?: string;
102
+ blobsReq?: string;
103
+ columnsReq?: string;
104
+ envelopesReq?: string;
105
+
106
+ // Retry counters
107
+ downloadAttempts: number;
108
+ processingAttempts: number;
109
+
110
+ // Cumulative peer attribution for failed attempts (only present when non-empty)
111
+ failedDownloadPeers?: string;
112
+ failedProcessingPeers?: string;
84
113
  };
85
114
 
115
+ function formatRangeReq(req: {startSlot: Slot; count: number}): string {
116
+ return `startSlot=${req.startSlot},count=${req.count}`;
117
+ }
118
+
119
+ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[]}): string {
120
+ return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
121
+ }
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
+
86
130
  /**
87
131
  * Batches are downloaded at the first block of the epoch.
88
132
  *
@@ -104,8 +148,8 @@ export class Batch {
104
148
  requests: DownloadByRangeRequests;
105
149
  /** State of the batch. */
106
150
  state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
107
- /** Peers that provided good data */
108
- goodPeers: PeerIdStr[] = [];
151
+ /** Peers that provided good data, with column coverage for by_range requests */
152
+ private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
109
153
  /** The `Attempts` that have been made and failed to send us this batch. */
110
154
  readonly failedProcessingAttempts: Attempt[] = [];
111
155
  /** The `Attempts` that have been made and failed because of execution malfunction. */
@@ -115,8 +159,18 @@ export class Batch {
115
159
  private readonly config: ChainForkConfig;
116
160
  private readonly clock: IClock;
117
161
  private readonly custodyConfig: CustodyConfig;
118
-
119
- 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
+ ) {
120
174
  this.config = config;
121
175
  this.clock = clock;
122
176
  this.custodyConfig = custodyConfig;
@@ -125,10 +179,40 @@ export class Batch {
125
179
  this.forkName = this.config.getForkName(startSlot);
126
180
  this.startEpoch = startEpoch;
127
181
  this.startSlot = startSlot;
128
- this.count = count;
182
+ this.count = Math.min(count, targetSlot - startSlot + 1);
183
+ this.isFirstBatchInChain = isFirstBatchInChain;
184
+ this.latestBid = latestBid;
129
185
  this.requests = this.getRequests([]);
130
186
  }
131
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
+
132
216
  /**
133
217
  * Builds ByRange requests for block, blobs and columns
134
218
  */
@@ -176,6 +260,7 @@ export class Batch {
176
260
  const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
177
261
 
178
262
  // ensure blocks are in slot-wise order
263
+ const isPostGloas = isForkPostGloas(this.forkName);
179
264
  for (const blockInput of blocks) {
180
265
  const blockSlot = blockInput.slot;
181
266
  // check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
@@ -191,21 +276,36 @@ export class Batch {
191
276
  if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
192
277
  blockStartSlot = blockSlot + 1;
193
278
  }
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) {
279
+
280
+ // Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
281
+ // which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
282
+ // a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
283
+ if (isPostGloas) {
284
+ // Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
285
+ const payloadInput = envelopesBySlot.get(blockSlot);
286
+ if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
287
+ envelopeStartSlot = blockSlot + 1;
288
+ }
289
+ if (payloadInput && !payloadInput.hasComputedAllData()) {
290
+ for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
204
291
  neededColumns.add(index);
205
292
  }
293
+ } else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
294
+ // Only advance dataStartSlot when we know columns for this slot are complete. If
295
+ // payloadInput is missing entirely we cannot tell, so stop here so the next round
296
+ // re-requests columns (and envelopes) starting at this slot.
297
+ dataStartSlot = blockSlot + 1;
298
+ }
299
+ } else {
300
+ if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
301
+ if (isBlockInputColumns(blockInput)) {
302
+ for (const index of blockInput.getMissingSampledColumnMeta().missing) {
303
+ neededColumns.add(index);
304
+ }
305
+ }
306
+ } else if (dataStartSlot === blockSlot) {
307
+ dataStartSlot = blockSlot + 1;
206
308
  }
207
- } else if (dataStartSlot === blockSlot) {
208
- dataStartSlot = blockSlot + 1;
209
309
  }
210
310
  }
211
311
 
@@ -225,11 +325,15 @@ export class Batch {
225
325
  // range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
226
326
  const count = endSlot - dataStartSlot + 1;
227
327
  if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
228
- requests.columnsRequest = {
229
- count,
230
- startSlot: dataStartSlot,
231
- columns: Array.from(neededColumns),
232
- };
328
+ // Skip the column re-request when we have no specific column indices outstanding.
329
+ // Peer rejects an empty `columns` list
330
+ if (neededColumns.size > 0) {
331
+ requests.columnsRequest = {
332
+ count,
333
+ startSlot: dataStartSlot,
334
+ columns: Array.from(neededColumns),
335
+ };
336
+ }
233
337
  } else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
234
338
  requests.blobsRequest = {
235
339
  count,
@@ -246,6 +350,36 @@ export class Batch {
246
350
  };
247
351
  }
248
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
+
249
383
  return requests;
250
384
  }
251
385
 
@@ -257,24 +391,28 @@ export class Batch {
257
391
  return this.requests;
258
392
  }
259
393
 
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
- }
394
+ // post-fulu we need to ensure that we only request columns that the peer has advertised.
395
+ const {columnsRequest, parentPayloadRequest} = this.requests;
265
396
 
266
397
  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
- }
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
+ : {};
271
411
 
272
412
  return {
273
413
  ...this.requests,
274
- columnsRequest: {
275
- ...columnsRequest,
276
- columns: requestedColumns,
277
- },
414
+ ...updatedColumnRequest,
415
+ ...updatedParentPayloadRequest,
278
416
  };
279
417
  }
280
418
 
@@ -285,8 +423,56 @@ export class Batch {
285
423
  return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
286
424
  }
287
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
+
288
455
  getMetadata(): BatchMetadata {
289
- return {startEpoch: this.startEpoch, status: this.state.status};
456
+ const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
457
+ const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
458
+ return {
459
+ startEpoch: this.startEpoch,
460
+ startSlot: this.startSlot,
461
+ count: this.count,
462
+ status: this.state.status,
463
+ ...(blocksRequest && {blocksReq: formatRangeReq(blocksRequest)}),
464
+ ...(blobsRequest && {blobsReq: formatRangeReq(blobsRequest)}),
465
+ ...(columnsRequest && {columnsReq: formatColumnsReq(columnsRequest)}),
466
+ ...(envelopesRequest && {envelopesReq: formatRangeReq(envelopesRequest)}),
467
+ downloadAttempts: this.failedDownloadAttempts.length,
468
+ processingAttempts: this.failedProcessingAttempts.length,
469
+ ...(this.failedDownloadAttempts.length > 0 && {
470
+ failedDownloadPeers: this.failedDownloadAttempts.join(","),
471
+ }),
472
+ ...(failedProcessingPeerList.length > 0 && {
473
+ failedProcessingPeers: failedProcessingPeerList.join(","),
474
+ }),
475
+ };
290
476
  }
291
477
 
292
478
  getBlocks(): IBlockInput[] {
@@ -300,14 +486,17 @@ export class Batch {
300
486
  /**
301
487
  * AwaitingDownload -> Downloading
302
488
  */
303
- startDownloading(peer: PeerIdStr): void {
489
+ startDownloading(peer: PeerSyncMeta): void {
304
490
  if (this.state.status !== BatchStatus.AwaitingDownload) {
305
491
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingDownload));
306
492
  }
307
493
 
494
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
495
+
308
496
  this.state = {
309
497
  status: BatchStatus.Downloading,
310
- peer,
498
+ peer: peer.peerId,
499
+ request,
311
500
  blocks: this.state.blocks,
312
501
  payloadEnvelopes: this.state.payloadEnvelopes,
313
502
  };
@@ -328,13 +517,27 @@ export class Batch {
328
517
  // ensure that blocks are always sorted before getting stored on the batch.state or being used to getRequests
329
518
  blocks.sort((a, b) => a.slot - b.slot);
330
519
 
331
- 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);
332
531
 
333
532
  let allComplete = true;
334
533
  const slots = new Set<number>();
335
534
  for (const block of blocks) {
336
535
  slots.add(block.slot);
337
- if (!block.hasBlockAndAllData()) {
536
+ const dataComplete = isBlockInputColumns(block)
537
+ ? // by_range needs to download all columns
538
+ block.hasBlock() && block.hasComputedAllData()
539
+ : block.hasBlockAndAllData();
540
+ if (!dataComplete) {
338
541
  allComplete = false;
339
542
  }
340
543
  }
@@ -350,11 +553,46 @@ export class Batch {
350
553
  }
351
554
  const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
352
555
 
556
+ if (allComplete && isForkPostGloas(this.forkName)) {
557
+ for (const block of blocks) {
558
+ const payloadInput = newPayloadEnvelopes?.get(block.slot);
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()) {
562
+ allComplete = false;
563
+ break;
564
+ }
565
+ }
566
+ }
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
+
353
591
  if (allComplete) {
354
592
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
355
593
  } else {
356
- this.requests = this.getRequests(blocks);
357
594
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
595
+ this.requests = this.getRequests(blocks);
358
596
  }
359
597
 
360
598
  return this.state as DownloadSuccessState;
@@ -380,10 +618,30 @@ export class Batch {
380
618
  };
381
619
  }
382
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
+
383
637
  /**
384
638
  * AwaitingProcessing -> Processing
385
639
  */
386
- startProcessing(): {blocks: IBlockInput[]; payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null} {
640
+ startProcessing(): {
641
+ blocks: IBlockInput[];
642
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
643
+ peers: PeerIdStr[];
644
+ } {
387
645
  if (this.state.status !== BatchStatus.AwaitingProcessing) {
388
646
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingProcessing));
389
647
  }
@@ -391,12 +649,12 @@ export class Batch {
391
649
  const blocks = this.state.blocks;
392
650
  const payloadEnvelopes = this.state.payloadEnvelopes;
393
651
  const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
394
- // Reset goodPeers in case another download attempt needs to be made. When Attempt is successful or not the peers
395
- // that the data came from will be handled by the Attempt that goes for processing
396
- const peers = this.goodPeers;
397
- 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();
398
656
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
399
- return {blocks, payloadEnvelopes};
657
+ return {blocks, payloadEnvelopes, peers};
400
658
  }
401
659
 
402
660
  /**
@@ -479,7 +737,7 @@ export class Batch {
479
737
 
480
738
  /** Helper to construct typed BatchError. Stack traces are correct as the error is thrown above */
481
739
  private errorType(type: BatchErrorType): BatchErrorType & BatchErrorMetadata {
482
- return {...type, ...this.getMetadata()};
740
+ return {...type, startEpoch: this.startEpoch, status: this.state.status};
483
741
  }
484
742
 
485
743
  private wrongStatusErrorType(expectedStatus: BatchStatus): BatchErrorType & BatchErrorMetadata {