@lodestar/beacon-node 1.43.0-dev.ca1fc40294 → 1.43.0-dev.dfb984e779

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 (170) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -2
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/lodestar/index.js +1 -1
  5. package/lib/api/impl/lodestar/index.js.map +1 -1
  6. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  7. package/lib/chain/blocks/importBlock.js +6 -3
  8. package/lib/chain/blocks/importBlock.js.map +1 -1
  9. package/lib/chain/blocks/importExecutionPayload.d.ts +26 -14
  10. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  11. package/lib/chain/blocks/importExecutionPayload.js +73 -77
  12. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  13. package/lib/chain/blocks/index.d.ts +5 -3
  14. package/lib/chain/blocks/index.d.ts.map +1 -1
  15. package/lib/chain/blocks/index.js +28 -10
  16. package/lib/chain/blocks/index.js.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  18. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  19. package/lib/chain/blocks/types.d.ts +14 -20
  20. package/lib/chain/blocks/types.d.ts.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +81 -12
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/blocks/verifyBlock.d.ts +3 -2
  26. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  27. package/lib/chain/blocks/verifyBlock.js +30 -5
  28. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  29. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  30. package/lib/chain/blocks/verifyBlocksSanityChecks.js +15 -4
  31. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  32. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  33. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  34. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  35. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  36. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  37. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -1
  38. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +2 -11
  39. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -1
  40. package/lib/chain/chain.d.ts +3 -2
  41. package/lib/chain/chain.d.ts.map +1 -1
  42. package/lib/chain/chain.js +14 -3
  43. package/lib/chain/chain.js.map +1 -1
  44. package/lib/chain/errors/blockError.d.ts +8 -1
  45. package/lib/chain/errors/blockError.d.ts.map +1 -1
  46. package/lib/chain/errors/blockError.js +2 -0
  47. package/lib/chain/errors/blockError.js.map +1 -1
  48. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  49. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  50. package/lib/chain/errors/executionPayloadBid.js +1 -0
  51. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  52. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  53. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  54. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  55. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  56. package/lib/chain/forkChoice/index.js +2 -2
  57. package/lib/chain/forkChoice/index.js.map +1 -1
  58. package/lib/chain/interface.d.ts +3 -2
  59. package/lib/chain/interface.d.ts.map +1 -1
  60. package/lib/chain/interface.js.map +1 -1
  61. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  62. package/lib/chain/prepareNextSlot.js +30 -10
  63. package/lib/chain/prepareNextSlot.js.map +1 -1
  64. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  65. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  66. package/lib/chain/produceBlock/produceBlockBody.js +34 -13
  67. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  68. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +11 -4
  69. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  70. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +20 -18
  71. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  72. package/lib/chain/validation/block.d.ts.map +1 -1
  73. package/lib/chain/validation/block.js +1 -0
  74. package/lib/chain/validation/block.js.map +1 -1
  75. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  76. package/lib/chain/validation/executionPayloadBid.js +13 -1
  77. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  78. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  79. package/lib/chain/validation/executionPayloadEnvelope.js +11 -1
  80. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  81. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  82. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  83. package/lib/metrics/metrics/lodestar.js +4 -0
  84. package/lib/metrics/metrics/lodestar.js.map +1 -1
  85. package/lib/network/processor/gossipHandlers.js +4 -6
  86. package/lib/network/processor/gossipHandlers.js.map +1 -1
  87. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  88. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  89. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  90. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  91. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  92. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  93. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  94. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  95. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  97. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  98. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  99. package/lib/node/notifier.js +7 -1
  100. package/lib/node/notifier.js.map +1 -1
  101. package/lib/sync/range/batch.d.ts +12 -2
  102. package/lib/sync/range/batch.d.ts.map +1 -1
  103. package/lib/sync/range/batch.js +56 -30
  104. package/lib/sync/range/batch.js.map +1 -1
  105. package/lib/sync/range/chain.d.ts +6 -2
  106. package/lib/sync/range/chain.d.ts.map +1 -1
  107. package/lib/sync/range/chain.js +4 -3
  108. package/lib/sync/range/chain.js.map +1 -1
  109. package/lib/sync/range/range.d.ts.map +1 -1
  110. package/lib/sync/range/range.js +17 -6
  111. package/lib/sync/range/range.js.map +1 -1
  112. package/lib/sync/types.d.ts +34 -0
  113. package/lib/sync/types.d.ts.map +1 -1
  114. package/lib/sync/types.js +34 -0
  115. package/lib/sync/types.js.map +1 -1
  116. package/lib/sync/unknownBlock.d.ts +24 -1
  117. package/lib/sync/unknownBlock.d.ts.map +1 -1
  118. package/lib/sync/unknownBlock.js +649 -53
  119. package/lib/sync/unknownBlock.js.map +1 -1
  120. package/lib/sync/utils/downloadByRange.d.ts +46 -10
  121. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  122. package/lib/sync/utils/downloadByRange.js +147 -24
  123. package/lib/sync/utils/downloadByRange.js.map +1 -1
  124. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  125. package/lib/sync/utils/downloadByRoot.js +6 -2
  126. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  127. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  128. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  129. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  130. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  131. package/package.json +16 -15
  132. package/src/api/impl/beacon/blocks/index.ts +5 -2
  133. package/src/api/impl/lodestar/index.ts +1 -1
  134. package/src/chain/blocks/importBlock.ts +4 -2
  135. package/src/chain/blocks/importExecutionPayload.ts +92 -97
  136. package/src/chain/blocks/index.ts +44 -13
  137. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  138. package/src/chain/blocks/types.ts +14 -25
  139. package/src/chain/blocks/utils/chainSegment.ts +106 -17
  140. package/src/chain/blocks/verifyBlock.ts +35 -6
  141. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -7
  142. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  143. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +9 -18
  144. package/src/chain/chain.ts +23 -3
  145. package/src/chain/errors/blockError.ts +4 -1
  146. package/src/chain/errors/executionPayloadBid.ts +6 -0
  147. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  148. package/src/chain/forkChoice/index.ts +2 -2
  149. package/src/chain/interface.ts +7 -1
  150. package/src/chain/prepareNextSlot.ts +42 -12
  151. package/src/chain/produceBlock/produceBlockBody.ts +37 -11
  152. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +22 -20
  153. package/src/chain/validation/block.ts +1 -0
  154. package/src/chain/validation/executionPayloadBid.ts +14 -0
  155. package/src/chain/validation/executionPayloadEnvelope.ts +12 -2
  156. package/src/metrics/metrics/lodestar.ts +4 -0
  157. package/src/network/processor/gossipHandlers.ts +6 -6
  158. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  159. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  160. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  161. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  162. package/src/node/notifier.ts +8 -1
  163. package/src/sync/range/batch.ts +90 -35
  164. package/src/sync/range/chain.ts +13 -5
  165. package/src/sync/range/range.ts +18 -6
  166. package/src/sync/types.ts +72 -0
  167. package/src/sync/unknownBlock.ts +810 -57
  168. package/src/sync/utils/downloadByRange.ts +256 -39
  169. package/src/sync/utils/downloadByRoot.ts +12 -2
  170. package/src/sync/utils/pendingBlocksTree.ts +0 -15
@@ -1,13 +1,18 @@
1
+ import {routes} from "@lodestar/api";
1
2
  import {ChainForkConfig} from "@lodestar/config";
2
3
  import {ForkSeq} from "@lodestar/params";
3
4
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
4
5
  import {computeTimeAtSlot} from "@lodestar/state-transition";
5
- import {RootHex} from "@lodestar/types";
6
- import {Logger, prettyPrintIndices, pruneSetToMax, sleep} from "@lodestar/utils";
6
+ import {RootHex, gloas} from "@lodestar/types";
7
+ import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils";
7
8
  import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js";
8
9
  import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js";
10
+ import {PayloadError, PayloadErrorCode} from "../chain/blocks/importExecutionPayload.js";
11
+ import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../chain/blocks/payloadEnvelopeInput/index.js";
9
12
  import {BlockError, BlockErrorCode} from "../chain/errors/index.js";
10
13
  import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js";
14
+ import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js";
15
+ import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/executionPayloadEnvelope.js";
11
16
  import {Metrics} from "../metrics/index.js";
12
17
  import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
13
18
  import {PeerSyncMeta} from "../network/peers/peersData.js";
@@ -19,20 +24,37 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
19
24
  import {SyncOptions} from "./options.js";
20
25
  import {
21
26
  BlockInputSyncCacheItem,
27
+ PayloadSyncCacheItem,
22
28
  PendingBlockInput,
23
29
  PendingBlockInputStatus,
24
30
  PendingBlockType,
31
+ PendingPayloadEnvelope,
32
+ PendingPayloadInput,
33
+ PendingPayloadInputStatus,
34
+ PendingPayloadRootHex,
25
35
  getBlockInputSyncCacheItemRootHex,
26
36
  getBlockInputSyncCacheItemSlot,
37
+ getPayloadSyncCacheItemRootHex,
38
+ getPayloadSyncCacheItemSlot,
27
39
  isPendingBlockInput,
40
+ isPendingPayloadEnvelope,
41
+ isPendingPayloadInput,
28
42
  } from "./types.js";
29
43
  import {DownloadByRootError, downloadByRoot} from "./utils/downloadByRoot.js";
30
- import {getAllDescendantBlocks, getDescendantBlocks, getUnknownAndAncestorBlocks} from "./utils/pendingBlocksTree.js";
44
+ import {getAllDescendantBlocks, getUnknownAndAncestorBlocks} from "./utils/pendingBlocksTree.js";
31
45
 
32
46
  const MAX_ATTEMPTS_PER_BLOCK = 5;
33
47
  const MAX_KNOWN_BAD_BLOCKS = 500;
34
48
  const MAX_PENDING_BLOCKS = 100;
35
49
 
50
+ type AdvancePendingBlockResult =
51
+ | "ready"
52
+ | "queued_block"
53
+ | "queued_parent_block"
54
+ | "queued_parent_payload"
55
+ | "blocked"
56
+ | "removed";
57
+
36
58
  enum FetchResult {
37
59
  SuccessResolved = "success_resolved",
38
60
  SuccessMissingParent = "success_missing_parent",
@@ -78,6 +100,8 @@ export class BlockInputSync {
78
100
  * block RootHex -> PendingBlock. To avoid finding same root at the same time
79
101
  */
80
102
  private readonly pendingBlocks = new Map<RootHex, BlockInputSyncCacheItem>();
103
+ // Payload sync is keyed by beacon block root as well, so block and payload queues can unblock each other.
104
+ private readonly pendingPayloads = new Map<RootHex, PayloadSyncCacheItem>();
81
105
  private readonly knownBadBlocks = new Set<RootHex>();
82
106
  private readonly maxPendingBlocks;
83
107
  private subscribedToNetworkEvents = false;
@@ -98,6 +122,9 @@ export class BlockInputSync {
98
122
  metrics.blockInputSync.pendingBlocks.addCollect(() =>
99
123
  metrics.blockInputSync.pendingBlocks.set(this.pendingBlocks.size)
100
124
  );
125
+ metrics.blockInputSync.pendingPayloads.addCollect(() =>
126
+ metrics.blockInputSync.pendingPayloads.set(this.pendingPayloads.size)
127
+ );
101
128
  metrics.blockInputSync.knownBadBlocks.addCollect(() =>
102
129
  metrics.blockInputSync.knownBadBlocks.set(this.knownBadBlocks.size)
103
130
  );
@@ -114,8 +141,13 @@ export class BlockInputSync {
114
141
  if (!this.subscribedToNetworkEvents) {
115
142
  this.logger.verbose("BlockInputSync enabled.");
116
143
  this.chain.emitter.on(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
144
+ this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
117
145
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
146
+ this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
118
147
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
148
+ this.chain.emitter.on(ChainEvent.envelopeUnknownBlock, this.onEnvelopeUnknownBlock);
149
+ this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
150
+ this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
119
151
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
120
152
  this.network.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
121
153
  this.subscribedToNetworkEvents = true;
@@ -125,8 +157,13 @@ export class BlockInputSync {
125
157
  unsubscribeFromNetwork(): void {
126
158
  this.logger.verbose("BlockInputSync disabled.");
127
159
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
160
+ this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
128
161
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
162
+ this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
129
163
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
164
+ this.chain.emitter.off(ChainEvent.envelopeUnknownBlock, this.onEnvelopeUnknownBlock);
165
+ this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
166
+ this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
130
167
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
131
168
  this.network.events.off(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
132
169
  this.subscribedToNetworkEvents = false;
@@ -168,12 +205,55 @@ export class BlockInputSync {
168
205
  }
169
206
  };
170
207
 
208
+ private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => {
209
+ try {
210
+ this.addByPayloadRootHex(data.rootHex, data.peer);
211
+ this.triggerUnknownBlockSearch();
212
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
213
+ this.metrics?.blockInputSync.source.inc({source: data.source});
214
+ } catch (e) {
215
+ this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e as Error);
216
+ }
217
+ };
218
+
219
+ private onIncompletePayloadEnvelope = (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]): void => {
220
+ try {
221
+ this.addByPayloadInput(data.payloadInput, data.peer);
222
+ this.triggerUnknownBlockSearch();
223
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
224
+ this.metrics?.blockInputSync.source.inc({source: data.source});
225
+ } catch (e) {
226
+ this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e as Error);
227
+ }
228
+ };
229
+
171
230
  /**
172
231
  * Process an unknownBlockParent event and register the block in `pendingBlocks` Map.
173
232
  */
174
233
  private onUnknownParent = (data: ChainEventData[ChainEvent.blockUnknownParent]): void => {
175
234
  try {
176
- this.addByRootHex(data.blockInput.parentRootHex, data.peer);
235
+ const missingDependency = this.getMissingBlockDependency(data.blockInput);
236
+ if (missingDependency.kind === "invalidParentPayload") {
237
+ this.addByBlockInput(data.blockInput, data.peer);
238
+
239
+ const pendingBlock = this.pendingBlocks.get(data.blockInput.blockRootHex);
240
+ if (pendingBlock && isPendingBlockInput(pendingBlock)) {
241
+ this.logger.debug("Ignoring block with conflicting parent payload hash", {
242
+ slot: pendingBlock.blockInput.slot,
243
+ root: pendingBlock.blockInput.blockRootHex,
244
+ parentRoot: missingDependency.parentRootHex,
245
+ parentBlockHash: missingDependency.parentBlockHashHex,
246
+ });
247
+ this.removeAndDownScoreAllDescendants(pendingBlock);
248
+ }
249
+ return;
250
+ }
251
+
252
+ if (missingDependency.kind === "parentPayload") {
253
+ this.addByPayloadRootHex(missingDependency.rootHex, data.peer);
254
+ } else if (missingDependency.kind === "parentBlock") {
255
+ this.addByRootHex(missingDependency.rootHex, data.peer);
256
+ }
177
257
  this.addByBlockInput(data.blockInput, data.peer);
178
258
  this.triggerUnknownBlockSearch();
179
259
  this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PARENT});
@@ -183,8 +263,35 @@ export class BlockInputSync {
183
263
  }
184
264
  };
185
265
 
186
- private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): void => {
266
+ private onEnvelopeUnknownBlock = (data: ChainEventData[ChainEvent.envelopeUnknownBlock]): void => {
267
+ try {
268
+ const blockRootHex = toRootHex(data.envelope.message.beaconBlockRoot);
269
+ this.addByRootHex(blockRootHex, data.peer);
270
+ this.addByPayloadEnvelope(data.envelope, data.peer);
271
+ this.triggerUnknownBlockSearch();
272
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
273
+ this.metrics?.blockInputSync.source.inc({source: data.source});
274
+ } catch (e) {
275
+ this.logger.debug("Error handling envelopeUnknownBlock event", {}, e as Error);
276
+ }
277
+ };
278
+
279
+ private onBlockImported = (): void => {
280
+ if (this.pendingPayloads.size > 0) {
281
+ this.triggerUnknownBlockSearch();
282
+ }
283
+ };
284
+
285
+ private onPayloadImported = ({
286
+ blockRoot,
287
+ }: routes.events.EventData[routes.events.EventType.executionPayload]): void => {
288
+ this.pendingPayloads.delete(blockRoot);
289
+ this.triggerUnknownBlockSearch();
290
+ };
291
+
292
+ private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
187
293
  let pendingBlock = this.pendingBlocks.get(rootHex);
294
+ let added = false;
188
295
  if (!pendingBlock) {
189
296
  pendingBlock = {
190
297
  status: PendingBlockInputStatus.pending,
@@ -193,6 +300,7 @@ export class BlockInputSync {
193
300
  timeAddedSec: Date.now() / 1000,
194
301
  };
195
302
  this.pendingBlocks.set(rootHex, pendingBlock);
303
+ added = true;
196
304
 
197
305
  this.logger.verbose("Added new rootHex to BlockInputSync.pendingBlocks", {
198
306
  root: pendingBlock.rootHex,
@@ -210,6 +318,7 @@ export class BlockInputSync {
210
318
  if (prunedItemCount > 0) {
211
319
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
212
320
  }
321
+ return added;
213
322
  };
214
323
 
215
324
  private addByBlockInput = (blockInput: IBlockInput, peerIdStr?: string): void => {
@@ -242,6 +351,93 @@ export class BlockInputSync {
242
351
  }
243
352
  };
244
353
 
354
+ private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
355
+ let pendingPayload = this.pendingPayloads.get(rootHex);
356
+ let added = false;
357
+ if (!pendingPayload) {
358
+ pendingPayload = {
359
+ status: PendingPayloadInputStatus.pending,
360
+ rootHex,
361
+ peerIdStrings: new Set(),
362
+ timeAddedSec: Date.now() / 1000,
363
+ };
364
+ this.pendingPayloads.set(rootHex, pendingPayload);
365
+ added = true;
366
+
367
+ this.logger.verbose("Added new payload rootHex to BlockInputSync.pendingPayloads", {
368
+ root: rootHex,
369
+ peerIdStr: peerIdStr ?? "unknown peer",
370
+ });
371
+ }
372
+
373
+ if (peerIdStr) {
374
+ pendingPayload.peerIdStrings.add(peerIdStr);
375
+ }
376
+
377
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
378
+ if (prunedItemCount > 0) {
379
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
380
+ }
381
+ return added;
382
+ };
383
+
384
+ private addByPayloadEnvelope = (envelope: gloas.SignedExecutionPayloadEnvelope, peerIdStr?: PeerIdStr): void => {
385
+ const rootHex = toRootHex(envelope.message.beaconBlockRoot);
386
+ const existingPendingPayload = this.pendingPayloads.get(rootHex);
387
+ let pendingPayload = this.pendingPayloads.get(rootHex);
388
+ if (!pendingPayload || !isPendingPayloadEnvelope(pendingPayload)) {
389
+ pendingPayload = {
390
+ status: PendingPayloadInputStatus.waitingForBlock,
391
+ envelope,
392
+ peerIdStrings: new Set(existingPendingPayload?.peerIdStrings ?? []),
393
+ timeAddedSec: existingPendingPayload?.timeAddedSec ?? Date.now() / 1000,
394
+ };
395
+ this.pendingPayloads.set(rootHex, pendingPayload);
396
+
397
+ this.logger.verbose("Added payload envelope to BlockInputSync.pendingPayloads", {
398
+ slot: envelope.message.payload.slotNumber,
399
+ root: rootHex,
400
+ });
401
+ } else {
402
+ this.logger.debug("Overwriting pending payload envelope for root already waiting for block", {
403
+ slot: envelope.message.payload.slotNumber,
404
+ root: rootHex,
405
+ });
406
+ pendingPayload.envelope = envelope;
407
+ }
408
+
409
+ if (peerIdStr) {
410
+ pendingPayload.peerIdStrings.add(peerIdStr);
411
+ }
412
+
413
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
414
+ if (prunedItemCount > 0) {
415
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
416
+ }
417
+ };
418
+
419
+ private addByPayloadInput = (
420
+ payloadInput: PayloadEnvelopeInput,
421
+ peerIdStr?: PeerIdStr,
422
+ envelope?: gloas.SignedExecutionPayloadEnvelope
423
+ ): void => {
424
+ const pendingPayload = this.toPendingPayloadInput(
425
+ payloadInput,
426
+ this.pendingPayloads.get(payloadInput.blockRootHex),
427
+ envelope
428
+ );
429
+
430
+ if (peerIdStr) {
431
+ pendingPayload.peerIdStrings.add(peerIdStr);
432
+ }
433
+
434
+ this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
435
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
436
+ if (prunedItemCount > 0) {
437
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
438
+ }
439
+ };
440
+
245
441
  private onPeerConnected = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
246
442
  try {
247
443
  const peerId = data.peer;
@@ -258,52 +454,207 @@ export class BlockInputSync {
258
454
  this.peerBalancer.onPeerDisconnected(peerId);
259
455
  };
260
456
 
457
+ /**
458
+ * Post-gloas, a locally complete block can still be blocked on its parent's execution payload lineage.
459
+ * Distinguish which dependency is missing so the scheduler can enqueue the right follow-up work.
460
+ */
461
+ private getMissingBlockDependency(
462
+ blockInput: IBlockInput
463
+ ):
464
+ | {kind: "ready"}
465
+ | {kind: "block" | "parentBlock" | "parentPayload"; rootHex: RootHex}
466
+ | {kind: "invalidParentPayload"; parentRootHex: RootHex; parentBlockHashHex: RootHex} {
467
+ const parentRootHex = blockInput.parentRootHex;
468
+ if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) {
469
+ return {kind: "parentBlock", rootHex: parentRootHex};
470
+ }
471
+
472
+ if (!blockInput.hasBlock()) {
473
+ return {kind: "block", rootHex: blockInput.blockRootHex};
474
+ }
475
+
476
+ if (this.config.getForkSeq(blockInput.slot) < ForkSeq.gloas) {
477
+ return {kind: "ready"};
478
+ }
479
+
480
+ const block = blockInput.getBlock() as gloas.SignedBeaconBlock;
481
+ const parentBlockHashHex = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
482
+ if (this.chain.forkChoice.getBlockHexAndBlockHash(parentRootHex, parentBlockHashHex) !== null) {
483
+ return {kind: "ready"};
484
+ }
485
+
486
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
487
+ return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
488
+ }
489
+
490
+ const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);
491
+ if (parentPayloadInput) {
492
+ if (parentPayloadInput.getBlockHashHex() === parentBlockHashHex) {
493
+ return {kind: "parentPayload", rootHex: parentRootHex};
494
+ }
495
+
496
+ return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
497
+ }
498
+
499
+ return {kind: "parentPayload", rootHex: parentRootHex};
500
+ }
501
+
502
+ private advancePendingBlock(pendingBlock: PendingBlockInput): AdvancePendingBlockResult {
503
+ const missingDependency = this.getMissingBlockDependency(pendingBlock.blockInput);
504
+
505
+ switch (missingDependency.kind) {
506
+ case "ready":
507
+ return "ready";
508
+
509
+ case "block":
510
+ pendingBlock.status = PendingBlockInputStatus.pending;
511
+ return "queued_block";
512
+
513
+ case "parentBlock": {
514
+ let added = this.addByRootHex(missingDependency.rootHex);
515
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
516
+ added = this.addByRootHex(missingDependency.rootHex, peerIdStr) || added;
517
+ }
518
+ return added ? "queued_parent_block" : "blocked";
519
+ }
520
+
521
+ case "parentPayload": {
522
+ let added = this.addByPayloadRootHex(missingDependency.rootHex);
523
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
524
+ added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr) || added;
525
+ }
526
+ return added ? "queued_parent_payload" : "blocked";
527
+ }
528
+
529
+ case "invalidParentPayload":
530
+ this.logger.debug("Removing block with conflicting parent payload hash", {
531
+ slot: pendingBlock.blockInput.slot,
532
+ root: pendingBlock.blockInput.blockRootHex,
533
+ parentRoot: missingDependency.parentRootHex,
534
+ parentBlockHash: missingDependency.parentBlockHashHex,
535
+ });
536
+ this.removeAndDownScoreAllDescendants(pendingBlock);
537
+ return "removed";
538
+ }
539
+ }
540
+
541
+ private toPendingPayloadInput(
542
+ payloadInput: PayloadEnvelopeInput,
543
+ previous?: PayloadSyncCacheItem,
544
+ envelope?: gloas.SignedExecutionPayloadEnvelope
545
+ ): PendingPayloadInput {
546
+ // Normalize every payload queueing path into the same cache shape while preserving first-seen
547
+ // timing and peer provenance from any earlier by-root or envelope-only entry.
548
+ const queuedEnvelope = envelope ?? (previous && isPendingPayloadEnvelope(previous) ? previous.envelope : undefined);
549
+
550
+ if (queuedEnvelope && !payloadInput.hasPayloadEnvelope()) {
551
+ payloadInput.addPayloadEnvelope({
552
+ envelope: queuedEnvelope,
553
+ source: PayloadEnvelopeInputSource.byRoot,
554
+ seenTimestampSec: Date.now() / 1000,
555
+ });
556
+ }
557
+
558
+ return {
559
+ status: payloadInput.isComplete() ? PendingPayloadInputStatus.downloaded : PendingPayloadInputStatus.pending,
560
+ payloadInput,
561
+ timeAddedSec: previous?.timeAddedSec ?? Date.now() / 1000,
562
+ timeSyncedSec: payloadInput.isComplete() ? Date.now() / 1000 : undefined,
563
+ peerIdStrings: new Set(previous?.peerIdStrings ?? []),
564
+ };
565
+ }
566
+
261
567
  /**
262
568
  * Gather tip parent blocks with unknown parent and do a search for all of them
263
569
  */
264
570
  private triggerUnknownBlockSearch = (): void => {
265
571
  // Cheap early stop to prevent calling the network.getConnectedPeers()
266
- if (this.pendingBlocks.size === 0) {
572
+ if (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0) {
267
573
  return;
268
574
  }
269
575
 
270
- // If the node loses all peers with pending unknown blocks, the sync will stall
576
+ // If the node loses all peers with pending unknown blocks or payloads, the sync will stall
271
577
  const connectedPeers = this.network.getConnectedPeers();
272
- if (connectedPeers.length === 0) {
273
- this.logger.debug("No connected peers, skipping unknown block search.");
274
- return;
275
- }
578
+ const hasConnectedPeers = connectedPeers.length > 0;
276
579
 
277
580
  const {unknowns, ancestors} = getUnknownAndAncestorBlocks(this.pendingBlocks);
278
- // it's rare when there is no unknown block
279
- // see https://github.com/ChainSafe/lodestar/issues/5649#issuecomment-1594213550
280
- if (unknowns.length === 0) {
281
- let processedBlocks = 0;
282
-
283
- for (const block of ancestors) {
284
- // when this happens, it's likely the block and parent block are processed by head sync
285
- if (this.chain.forkChoice.hasBlockHex(block.blockInput.parentRootHex)) {
581
+ let processedBlocks = 0;
582
+ let shouldRerunBlockSearch = false;
583
+
584
+ for (const block of ancestors) {
585
+ const advanceResult = this.advancePendingBlock(block);
586
+ switch (advanceResult) {
587
+ case "ready":
286
588
  processedBlocks++;
287
- this.processBlock(block).catch((e) => {
589
+ this.processReadyBlock(block).catch((e) => {
288
590
  this.logger.debug("Unexpected error - process old downloaded block", {}, e);
289
591
  });
290
- }
592
+ break;
593
+
594
+ case "queued_block":
595
+ case "queued_parent_block":
596
+ shouldRerunBlockSearch = true;
597
+ break;
598
+
599
+ case "queued_parent_payload":
600
+ case "blocked":
601
+ case "removed":
602
+ break;
291
603
  }
604
+ }
292
605
 
606
+ if (unknowns.length > 0) {
607
+ if (!hasConnectedPeers) {
608
+ this.logger.debug("No connected peers, skipping unknown block download.");
609
+ } else {
610
+ // Most of the time there is exactly 1 unknown block
611
+ for (const block of unknowns) {
612
+ this.downloadBlock(block).catch((e) => {
613
+ this.logger.debug("Unexpected error - downloadBlock", {root: getBlockInputSyncCacheItemRootHex(block)}, e);
614
+ });
615
+ }
616
+ }
617
+ } else if (ancestors.length > 0) {
618
+ // It's rare when there is no unknown block
619
+ // see https://github.com/ChainSafe/lodestar/issues/5649#issuecomment-1594213550
293
620
  this.logger.verbose("No unknown block, process ancestor downloaded blocks", {
294
621
  pendingBlocks: this.pendingBlocks.size,
295
622
  ancestorBlocks: ancestors.length,
296
623
  processedBlocks,
297
624
  });
298
- return;
299
625
  }
300
626
 
301
- // most of the time there is exactly 1 unknown block
302
- for (const block of unknowns) {
303
- this.downloadBlock(block).catch((e) => {
304
- this.logger.debug("Unexpected error - downloadBlock", {root: getBlockInputSyncCacheItemRootHex(block)}, e);
627
+ // Blocks can unblock payloads and payloads can unblock blocks, so every scheduler pass services both queues.
628
+ for (const payload of Array.from(this.pendingPayloads.values())) {
629
+ if (isPendingPayloadInput(payload) && payload.status === PendingPayloadInputStatus.downloaded) {
630
+ this.processPayload(payload).catch((e) => {
631
+ this.logger.debug("Unexpected error - process downloaded payload", {}, e);
632
+ });
633
+ continue;
634
+ }
635
+
636
+ if (isPendingPayloadEnvelope(payload)) {
637
+ this.reconcilePayloadEnvelope(payload).catch((e) => {
638
+ this.logger.debug("Unexpected error - reconcile pending payload envelope", {}, e);
639
+ });
640
+ continue;
641
+ }
642
+
643
+ if (!hasConnectedPeers) {
644
+ this.logger.debug("No connected peers, skipping unknown payload download.", {
645
+ root: getPayloadSyncCacheItemRootHex(payload),
646
+ });
647
+ continue;
648
+ }
649
+
650
+ this.downloadPayload(payload).catch((e) => {
651
+ this.logger.debug("Unexpected error - downloadPayload", {root: getPayloadSyncCacheItemRootHex(payload)}, e);
305
652
  });
306
653
  }
654
+
655
+ if (shouldRerunBlockSearch) {
656
+ this.triggerUnknownBlockSearch();
657
+ }
307
658
  };
308
659
 
309
660
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
@@ -342,10 +693,26 @@ export class BlockInputSync {
342
693
  this.logger.verbose("Downloaded unknown block", logCtx2);
343
694
 
344
695
  if (parentInForkChoice) {
345
- // Bingo! Process block. Add to pending blocks anyway for recycle the cache that prevents duplicate processing
346
- this.processBlock(pending).catch((e) => {
347
- this.logger.debug("Unexpected error - process newly downloaded block", logCtx2, e);
348
- });
696
+ // If the direct parent is already in fork choice, let the block state machine decide if
697
+ // the next step is block import, parent payload download, or branch removal.
698
+ const advanceResult = this.advancePendingBlock(pending);
699
+ switch (advanceResult) {
700
+ case "ready":
701
+ this.processReadyBlock(pending).catch((e) => {
702
+ this.logger.debug("Unexpected error - process newly downloaded block", logCtx2, e);
703
+ });
704
+ break;
705
+
706
+ case "queued_block":
707
+ case "queued_parent_block":
708
+ case "queued_parent_payload":
709
+ this.triggerUnknownBlockSearch();
710
+ break;
711
+
712
+ case "blocked":
713
+ case "removed":
714
+ break;
715
+ }
349
716
  } else if (blockSlot <= finalizedSlot) {
350
717
  // the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
351
718
  // we should found it through forkchoice. If not, we should penalize all peers sending us this block chain
@@ -368,26 +735,11 @@ export class BlockInputSync {
368
735
  }
369
736
 
370
737
  /**
371
- * Send block to the processor awaiting completition. If processed successfully, send all children to the processor.
372
- * On error, remove and downscore all descendants.
373
- * This function could run recursively for all descendant blocks
738
+ * Import a block that has already passed the local dependency checks in BlockInputSync.
739
+ * On error, remove and downscore descendants as appropriate for the failure type.
374
740
  */
375
- private async processBlock(pendingBlock: PendingBlockInput): Promise<void> {
376
- // pending block status is `downloaded` right after `downloadBlock`
377
- // but could be `pending` if added by `onUnknownBlockParent` event and this function is called recursively
741
+ private async processReadyBlock(pendingBlock: PendingBlockInput): Promise<void> {
378
742
  if (pendingBlock.status !== PendingBlockInputStatus.downloaded) {
379
- if (pendingBlock.status === PendingBlockInputStatus.pending) {
380
- const connectedPeers = this.network.getConnectedPeers();
381
- if (connectedPeers.length === 0) {
382
- this.logger.debug("No connected peers, skipping download block", {
383
- slot: pendingBlock.blockInput.slot,
384
- blockRoot: pendingBlock.blockInput.blockRootHex,
385
- });
386
- return;
387
- }
388
- // if the download is a success we'll call `processBlock()` for this block
389
- await this.downloadBlock(pendingBlock);
390
- }
391
743
  return;
392
744
  }
393
745
 
@@ -432,15 +784,9 @@ export class BlockInputSync {
432
784
  if (!res.err) {
433
785
  // no need to update status to "processed", delete anyway
434
786
  this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex);
435
-
436
- // Send child blocks to the processor
437
- for (const descendantBlock of getDescendantBlocks(pendingBlock.blockInput.blockRootHex, this.pendingBlocks)) {
438
- if (isPendingBlockInput(descendantBlock)) {
439
- this.processBlock(descendantBlock).catch((e) => {
440
- this.logger.debug("Unexpected error - process descendant block", {}, e);
441
- });
442
- }
443
- }
787
+ // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
788
+ // are advanced through the same dependency checks as every other pending item.
789
+ this.triggerUnknownBlockSearch();
444
790
  } else {
445
791
  const errorData = {slot: pendingBlock.blockInput.slot, root: pendingBlock.blockInput.blockRootHex};
446
792
  if (res.err instanceof BlockError) {
@@ -456,6 +802,19 @@ export class BlockInputSync {
456
802
  pendingBlock.status = PendingBlockInputStatus.downloaded;
457
803
  break;
458
804
 
805
+ case BlockErrorCode.PARENT_PAYLOAD_UNKNOWN:
806
+ this.logger.error(
807
+ "processReadyBlock() hit unexpected parent payload dependency after readiness checks",
808
+ {
809
+ ...errorData,
810
+ parentRoot: pendingBlock.blockInput.parentRootHex,
811
+ parentBlockHash: res.err.type.parentBlockHash,
812
+ },
813
+ res.err
814
+ );
815
+ pendingBlock.status = PendingBlockInputStatus.downloaded;
816
+ break;
817
+
459
818
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
460
819
  // Removing the block(s) without penalizing the peers, hoping for EL to
461
820
  // recover on a latter download + verify attempt
@@ -477,6 +836,375 @@ export class BlockInputSync {
477
836
  }
478
837
  }
479
838
 
839
+ /**
840
+ * Reconcile an envelope-first payload entry once the block import path has seeded its
841
+ * PayloadEnvelopeInput. This may queue block download, validate the speculative envelope, or
842
+ * downgrade back to by-root fetching when the cached envelope does not match the imported block.
843
+ */
844
+ private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise<void> {
845
+ const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
846
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
847
+ this.pendingPayloads.delete(rootHex);
848
+ return;
849
+ }
850
+
851
+ const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
852
+ if (!payloadInput) {
853
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
854
+ // Column commitments live on the block body, so an envelope-only entry has to pull the block first.
855
+ if (!this.pendingBlocks.has(rootHex)) {
856
+ this.addByRootHex(rootHex);
857
+ }
858
+
859
+ const pendingBlock = this.pendingBlocks.get(rootHex);
860
+ if (pendingBlock && this.network.getConnectedPeers().length > 0) {
861
+ await this.downloadBlock(pendingBlock);
862
+ }
863
+ } else {
864
+ this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
865
+ root: rootHex,
866
+ });
867
+ }
868
+ return;
869
+ }
870
+
871
+ if (!payloadInput.hasPayloadEnvelope()) {
872
+ const validationResult = await wrapError(
873
+ validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope)
874
+ );
875
+ if (validationResult.err) {
876
+ this.logger.debug(
877
+ "Pending payload envelope failed validation after block import, refetching by root",
878
+ {slot: pendingPayload.envelope.message.payload.slotNumber, root: rootHex},
879
+ validationResult.err
880
+ );
881
+
882
+ const pendingPayloadByRoot: PendingPayloadRootHex = {
883
+ status: PendingPayloadInputStatus.pending,
884
+ rootHex,
885
+ timeAddedSec: pendingPayload.timeAddedSec,
886
+ peerIdStrings: new Set(pendingPayload.peerIdStrings),
887
+ };
888
+ this.pendingPayloads.set(rootHex, pendingPayloadByRoot);
889
+
890
+ if (this.network.getConnectedPeers().length > 0) {
891
+ await this.downloadPayload(pendingPayloadByRoot);
892
+ }
893
+ return;
894
+ }
895
+ }
896
+
897
+ const upgradedPayload = this.toPendingPayloadInput(payloadInput, pendingPayload, pendingPayload.envelope);
898
+ this.pendingPayloads.set(rootHex, upgradedPayload);
899
+
900
+ if (upgradedPayload.status === PendingPayloadInputStatus.downloaded) {
901
+ await this.processPayload(upgradedPayload);
902
+ return;
903
+ }
904
+
905
+ await this.downloadPayload(upgradedPayload);
906
+ }
907
+
908
+ private async downloadPayload(payload: PayloadSyncCacheItem): Promise<void> {
909
+ if (isPendingPayloadEnvelope(payload)) {
910
+ await this.reconcilePayloadEnvelope(payload);
911
+ return;
912
+ }
913
+
914
+ const rootHex = getPayloadSyncCacheItemRootHex(payload);
915
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
916
+ this.pendingPayloads.delete(rootHex);
917
+ return;
918
+ }
919
+
920
+ if (payload.status !== PendingPayloadInputStatus.pending) {
921
+ return;
922
+ }
923
+
924
+ const logCtx = {
925
+ slot: getPayloadSyncCacheItemSlot(payload),
926
+ root: rootHex,
927
+ pendingPayloads: this.pendingPayloads.size,
928
+ };
929
+
930
+ this.logger.verbose("BlockInputSync.downloadPayload()", logCtx);
931
+
932
+ payload.status = PendingPayloadInputStatus.fetching;
933
+
934
+ const res = await wrapError(this.fetchPayloadInput(payload));
935
+ if (!res.err) {
936
+ const pendingPayload = res.result;
937
+ this.pendingPayloads.set(getPayloadSyncCacheItemRootHex(pendingPayload), pendingPayload);
938
+
939
+ if (isPendingPayloadEnvelope(pendingPayload)) {
940
+ await this.reconcilePayloadEnvelope(pendingPayload);
941
+ } else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
942
+ await this.processPayload(pendingPayload);
943
+ }
944
+ return;
945
+ }
946
+
947
+ this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
948
+ if (!isPendingPayloadEnvelope(payload)) {
949
+ payload.status = PendingPayloadInputStatus.pending;
950
+ }
951
+ }
952
+
953
+ private async processPayload(pendingPayload: PendingPayloadInput): Promise<void> {
954
+ const rootHex = pendingPayload.payloadInput.blockRootHex;
955
+ const logCtx = {slot: pendingPayload.payloadInput.slot, root: rootHex};
956
+
957
+ if (pendingPayload.status !== PendingPayloadInputStatus.downloaded) {
958
+ this.logger.debug("Skipping payload processing before payload input is downloaded", {
959
+ ...logCtx,
960
+ status: pendingPayload.status,
961
+ });
962
+ return;
963
+ }
964
+
965
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
966
+ this.logger.debug("Payload already imported while processing unknown payload", logCtx);
967
+ this.pendingPayloads.delete(rootHex);
968
+ return;
969
+ }
970
+
971
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
972
+ this.logger.debug("Payload input is ready before its block is in fork choice", logCtx);
973
+ const added = this.addByRootHex(rootHex);
974
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
975
+ if (added) {
976
+ this.triggerUnknownBlockSearch();
977
+ }
978
+ return;
979
+ }
980
+
981
+ pendingPayload.status = PendingPayloadInputStatus.processing;
982
+
983
+ const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput));
984
+ if (!res.err) {
985
+ this.logger.debug("Processed payload from unknown sync", logCtx);
986
+ this.pendingPayloads.delete(rootHex);
987
+ this.triggerUnknownBlockSearch();
988
+ return;
989
+ }
990
+
991
+ if (res.err instanceof PayloadError) {
992
+ switch (res.err.type.code) {
993
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
994
+ // Payload sync discovered the block dependency before the block queue did. Re-enqueue the
995
+ // block and keep the payload ready so the scheduler can retry once the block reaches fork choice.
996
+ if (this.addByRootHex(rootHex)) {
997
+ this.triggerUnknownBlockSearch();
998
+ }
999
+ // Keep the payload out of any synchronous requeue pass; a later scheduler pass will retry it.
1000
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1001
+ break;
1002
+
1003
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1004
+ this.logger.debug("Execution engine error while processing payload from unknown sync", logCtx, res.err);
1005
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1006
+ break;
1007
+
1008
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1009
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1010
+ case PayloadErrorCode.INVALID_SIGNATURE:
1011
+ // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1012
+ // reintroducing envelope replacement / recreation flows.
1013
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1014
+ this.removePendingPayloadAndDescendants(rootHex);
1015
+ break;
1016
+
1017
+ default:
1018
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1019
+ this.pendingPayloads.delete(rootHex);
1020
+ }
1021
+ return;
1022
+ }
1023
+
1024
+ this.logger.debug("Unknown error processing payload from unknown sync", logCtx, res.err);
1025
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1026
+ }
1027
+
1028
+ /**
1029
+ * Download payload material keyed by beacon block root. Unlike block download, payload sync may
1030
+ * already have a locally cached envelope or partial columns, so each attempt starts from local state
1031
+ * and only asks peers for the remaining pieces.
1032
+ */
1033
+ private async fetchPayloadInput(
1034
+ cacheItem: PendingPayloadInput | PendingPayloadRootHex
1035
+ ): Promise<PendingPayloadInput | PendingPayloadEnvelope> {
1036
+ const rootHex = getPayloadSyncCacheItemRootHex(cacheItem);
1037
+ const blockRoot = fromHex(rootHex);
1038
+ const excludedPeers = new Set<PeerIdStr>();
1039
+
1040
+ let slot = getPayloadSyncCacheItemSlot(cacheItem);
1041
+ let payloadInput = isPendingPayloadInput(cacheItem)
1042
+ ? cacheItem.payloadInput
1043
+ : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1044
+ let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1045
+
1046
+ let i = 0;
1047
+ while (i++ < this.getMaxDownloadAttempts()) {
1048
+ const pendingColumns = payloadInput?.hasAllData()
1049
+ ? new Set<number>()
1050
+ : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1051
+ const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1052
+ if (peerMeta === null) {
1053
+ throw Error(
1054
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1055
+ );
1056
+ }
1057
+
1058
+ const {peerId, client: peerClient} = peerMeta;
1059
+ cacheItem.peerIdStrings.add(peerId);
1060
+
1061
+ try {
1062
+ if (!envelope) {
1063
+ envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1064
+ slot = envelope.message.payload.slotNumber;
1065
+ }
1066
+
1067
+ payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1068
+ if (!payloadInput) {
1069
+ if (this.chain.forkChoice.hasBlockHex(rootHex)) {
1070
+ throw new Error(`Missing PayloadEnvelopeInput for known block ${rootHex}`);
1071
+ }
1072
+ // Keep the validated envelope around, but wait for the block body before turning it into a full payload input.
1073
+ return {
1074
+ status: PendingPayloadInputStatus.waitingForBlock,
1075
+ envelope,
1076
+ timeAddedSec: cacheItem.timeAddedSec,
1077
+ peerIdStrings: cacheItem.peerIdStrings,
1078
+ };
1079
+ }
1080
+
1081
+ if (!payloadInput.hasPayloadEnvelope()) {
1082
+ await validateGossipExecutionPayloadEnvelope(this.chain, envelope);
1083
+ }
1084
+
1085
+ let pendingPayload = this.toPendingPayloadInput(payloadInput, cacheItem, envelope);
1086
+ if (!pendingPayload.payloadInput.hasAllData()) {
1087
+ const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
1088
+ if (missing.length > 0) {
1089
+ const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
1090
+ const seenTimestampSec = Date.now() / 1000;
1091
+ for (const columnSidecar of columnSidecars) {
1092
+ if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
1093
+ continue;
1094
+ }
1095
+
1096
+ pendingPayload.payloadInput.addColumn({
1097
+ columnSidecar,
1098
+ source: PayloadEnvelopeInputSource.byRoot,
1099
+ seenTimestampSec,
1100
+ peerIdStr: peerId,
1101
+ });
1102
+ }
1103
+ pendingPayload = this.toPendingPayloadInput(pendingPayload.payloadInput, pendingPayload);
1104
+ }
1105
+ }
1106
+
1107
+ this.logger.verbose("BlockInputSync.fetchPayloadInput: successful download", {
1108
+ slot,
1109
+ rootHex,
1110
+ peerId,
1111
+ peerClient,
1112
+ hasPayload: pendingPayload.payloadInput.hasPayloadEnvelope(),
1113
+ hasAllData: pendingPayload.payloadInput.hasAllData(),
1114
+ });
1115
+
1116
+ if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1117
+ return pendingPayload;
1118
+ }
1119
+
1120
+ cacheItem = pendingPayload;
1121
+ payloadInput = pendingPayload.payloadInput;
1122
+ } catch (e) {
1123
+ this.logger.debug(
1124
+ "Error downloading payload in BlockInputSync.fetchPayloadInput",
1125
+ {slot, rootHex, attempt: i, peer: peerId, peerClient},
1126
+ e as Error
1127
+ );
1128
+
1129
+ if (e instanceof RequestError) {
1130
+ switch (e.type.code) {
1131
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1132
+ case RequestErrorCode.REQUEST_TIMEOUT:
1133
+ break;
1134
+ default:
1135
+ excludedPeers.add(peerId);
1136
+ break;
1137
+ }
1138
+ } else {
1139
+ excludedPeers.add(peerId);
1140
+ }
1141
+ } finally {
1142
+ this.peerBalancer.onRequestCompleted(peerId);
1143
+ }
1144
+ }
1145
+
1146
+ throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1147
+ }
1148
+
1149
+ private async fetchExecutionPayloadEnvelope(
1150
+ peerIdStr: PeerIdStr,
1151
+ blockRoot: Uint8Array,
1152
+ rootHex: RootHex
1153
+ ): Promise<gloas.SignedExecutionPayloadEnvelope> {
1154
+ const response = await this.network.sendExecutionPayloadEnvelopesByRoot(peerIdStr, [blockRoot]);
1155
+ const envelope = response.at(0);
1156
+ if (!envelope) {
1157
+ throw new Error(`Missing execution payload envelope for root=${rootHex}`);
1158
+ }
1159
+
1160
+ const receivedRootHex = toRootHex(envelope.message.beaconBlockRoot);
1161
+ if (receivedRootHex !== rootHex) {
1162
+ throw new Error(`Execution payload envelope root mismatch requested=${rootHex} received=${receivedRootHex}`);
1163
+ }
1164
+
1165
+ return envelope;
1166
+ }
1167
+
1168
+ private async fetchPayloadColumns(
1169
+ peerMeta: PeerSyncMeta,
1170
+ payloadInput: PayloadEnvelopeInput,
1171
+ missing: number[]
1172
+ ): Promise<gloas.DataColumnSidecar[]> {
1173
+ const {peerId: peerIdStr} = peerMeta;
1174
+ const peerColumns = new Set(peerMeta.custodyColumns ?? []);
1175
+ const requestedColumns = missing.filter((columnIndex) => peerColumns.has(columnIndex));
1176
+ if (requestedColumns.length === 0) {
1177
+ return [];
1178
+ }
1179
+
1180
+ const columnSidecars = (await this.network.sendDataColumnSidecarsByRoot(peerIdStr, [
1181
+ {blockRoot: fromHex(payloadInput.blockRootHex), columns: requestedColumns},
1182
+ ])) as gloas.DataColumnSidecar[];
1183
+
1184
+ if (columnSidecars.length === 0) {
1185
+ throw new Error(`No data column sidecars returned for payload root=${payloadInput.blockRootHex}`);
1186
+ }
1187
+
1188
+ const requestedColumnsSet = new Set(requestedColumns);
1189
+ const extraColumns = columnSidecars.filter((columnSidecar) => !requestedColumnsSet.has(columnSidecar.index));
1190
+ if (extraColumns.length > 0) {
1191
+ throw new Error(
1192
+ `Received unexpected payload data columns indices=${prettyPrintIndices(extraColumns.map((column) => column.index))}`
1193
+ );
1194
+ }
1195
+
1196
+ // PayloadEnvelopeInput already carries the block slot, root, and commitments, so reuse the
1197
+ // block-based Gloas validator rather than maintaining a second payload-specific variant.
1198
+ await validateGloasBlockDataColumnSidecars(
1199
+ payloadInput.slot,
1200
+ fromHex(payloadInput.blockRootHex),
1201
+ payloadInput.getBlobKzgCommitments(),
1202
+ columnSidecars,
1203
+ this.chain.metrics?.peerDas
1204
+ );
1205
+ return columnSidecars;
1206
+ }
1207
+
480
1208
  /**
481
1209
  * From a set of shuffled peers:
482
1210
  * - fetch the block
@@ -660,6 +1388,28 @@ export class BlockInputSync {
660
1388
  pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
661
1389
  }
662
1390
 
1391
+ // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1392
+ private removePendingPayloadAndDescendants(rootHex: RootHex): void {
1393
+ // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1394
+ // later validation/finalization logic decides when it can leave memory.
1395
+ this.pendingPayloads.delete(rootHex);
1396
+
1397
+ const badPendingBlocks = getAllDescendantBlocks(rootHex, this.pendingBlocks);
1398
+ this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1399
+
1400
+ for (const block of badPendingBlocks) {
1401
+ const descendantRootHex = getBlockInputSyncCacheItemRootHex(block);
1402
+ this.pendingBlocks.delete(descendantRootHex);
1403
+ this.pendingPayloads.delete(descendantRootHex);
1404
+ this.chain.seenBlockInputCache.prune(descendantRootHex);
1405
+ this.logger.debug("Removing pending descendant after invalid parent payload", {
1406
+ slot: getBlockInputSyncCacheItemSlot(block),
1407
+ blockRoot: descendantRootHex,
1408
+ parentPayloadRoot: rootHex,
1409
+ });
1410
+ }
1411
+ }
1412
+
663
1413
  private removeAllDescendants(block: BlockInputSyncCacheItem): BlockInputSyncCacheItem[] {
664
1414
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
665
1415
  const slot = getBlockInputSyncCacheItemSlot(block);
@@ -671,7 +1421,10 @@ export class BlockInputSync {
671
1421
  for (const block of badPendingBlocks) {
672
1422
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
673
1423
  this.pendingBlocks.delete(rootHex);
1424
+ this.pendingPayloads.delete(rootHex);
674
1425
  this.chain.seenBlockInputCache.prune(rootHex);
1426
+ // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1427
+ // importBlock()-owned lifecycle.
675
1428
  this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
676
1429
  slot,
677
1430
  blockRoot: rootHex,