@lodestar/beacon-node 1.43.0-dev.b7d69444c9 → 1.43.0-dev.be5f70c0c8

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 (317) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +16 -5
  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/lodestar/index.js +1 -1
  11. package/lib/api/impl/lodestar/index.js.map +1 -1
  12. package/lib/api/impl/validator/index.d.ts.map +1 -1
  13. package/lib/api/impl/validator/index.js +68 -2
  14. package/lib/api/impl/validator/index.js.map +1 -1
  15. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  16. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  17. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  18. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  19. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  20. package/lib/chain/blocks/importBlock.js +16 -28
  21. package/lib/chain/blocks/importBlock.js.map +1 -1
  22. package/lib/chain/blocks/importExecutionPayload.d.ts +23 -6
  23. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  24. package/lib/chain/blocks/importExecutionPayload.js +57 -24
  25. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  26. package/lib/chain/blocks/index.d.ts +5 -3
  27. package/lib/chain/blocks/index.d.ts.map +1 -1
  28. package/lib/chain/blocks/index.js +58 -25
  29. package/lib/chain/blocks/index.js.map +1 -1
  30. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +12 -1
  31. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  32. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +28 -2
  33. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  34. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +17 -0
  35. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  36. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  37. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  38. package/lib/chain/blocks/types.d.ts +4 -3
  39. package/lib/chain/blocks/types.d.ts.map +1 -1
  40. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  41. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  42. package/lib/chain/blocks/utils/chainSegment.js +89 -12
  43. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  44. package/lib/chain/blocks/verifyBlock.d.ts +5 -3
  45. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  46. package/lib/chain/blocks/verifyBlock.js +50 -7
  47. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  48. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  49. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  50. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  51. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  52. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  53. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  54. package/lib/chain/blocks/verifyBlocksSanityChecks.js +25 -5
  55. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  56. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  57. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  58. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +12 -8
  59. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  60. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  61. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +8 -3
  62. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  63. package/lib/chain/chain.d.ts +3 -2
  64. package/lib/chain/chain.d.ts.map +1 -1
  65. package/lib/chain/chain.js +35 -12
  66. package/lib/chain/chain.js.map +1 -1
  67. package/lib/chain/emitter.d.ts +0 -11
  68. package/lib/chain/emitter.d.ts.map +1 -1
  69. package/lib/chain/emitter.js +0 -4
  70. package/lib/chain/emitter.js.map +1 -1
  71. package/lib/chain/errors/blockError.d.ts +8 -1
  72. package/lib/chain/errors/blockError.d.ts.map +1 -1
  73. package/lib/chain/errors/blockError.js +2 -0
  74. package/lib/chain/errors/blockError.js.map +1 -1
  75. package/lib/chain/errors/index.d.ts +1 -0
  76. package/lib/chain/errors/index.d.ts.map +1 -1
  77. package/lib/chain/errors/index.js +1 -0
  78. package/lib/chain/errors/index.js.map +1 -1
  79. package/lib/chain/errors/proposerPreferences.d.ts +40 -0
  80. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -0
  81. package/lib/chain/errors/proposerPreferences.js +14 -0
  82. package/lib/chain/errors/proposerPreferences.js.map +1 -0
  83. package/lib/chain/initState.d.ts.map +1 -1
  84. package/lib/chain/initState.js +6 -1
  85. package/lib/chain/initState.js.map +1 -1
  86. package/lib/chain/interface.d.ts +3 -2
  87. package/lib/chain/interface.d.ts.map +1 -1
  88. package/lib/chain/interface.js.map +1 -1
  89. package/lib/chain/opPools/payloadAttestationPool.d.ts +3 -2
  90. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  91. package/lib/chain/opPools/payloadAttestationPool.js +26 -4
  92. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  93. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  94. package/lib/chain/prepareNextSlot.js +16 -18
  95. package/lib/chain/prepareNextSlot.js.map +1 -1
  96. package/lib/chain/produceBlock/produceBlockBody.d.ts +12 -3
  97. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  98. package/lib/chain/produceBlock/produceBlockBody.js +35 -17
  99. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  100. package/lib/chain/regen/interface.d.ts +1 -0
  101. package/lib/chain/regen/interface.d.ts.map +1 -1
  102. package/lib/chain/regen/interface.js +1 -0
  103. package/lib/chain/regen/interface.js.map +1 -1
  104. package/lib/chain/regen/queued.d.ts.map +1 -1
  105. package/lib/chain/regen/queued.js +1 -4
  106. package/lib/chain/regen/queued.js.map +1 -1
  107. package/lib/chain/regen/regen.d.ts.map +1 -1
  108. package/lib/chain/regen/regen.js +1 -4
  109. package/lib/chain/regen/regen.js.map +1 -1
  110. package/lib/chain/seenCache/index.d.ts +1 -0
  111. package/lib/chain/seenCache/index.d.ts.map +1 -1
  112. package/lib/chain/seenCache/index.js +1 -0
  113. package/lib/chain/seenCache/index.js.map +1 -1
  114. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +21 -11
  115. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  116. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +70 -20
  117. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  118. package/lib/chain/seenCache/seenProposerPreferences.d.ts +16 -0
  119. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -0
  120. package/lib/chain/seenCache/seenProposerPreferences.js +26 -0
  121. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -0
  122. package/lib/chain/validation/block.d.ts.map +1 -1
  123. package/lib/chain/validation/block.js +1 -0
  124. package/lib/chain/validation/block.js.map +1 -1
  125. package/lib/chain/validation/executionPayloadBid.js +11 -8
  126. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  127. package/lib/chain/validation/proposerPreferences.d.ts +8 -0
  128. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -0
  129. package/lib/chain/validation/proposerPreferences.js +91 -0
  130. package/lib/chain/validation/proposerPreferences.js.map +1 -0
  131. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  132. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  133. package/lib/metrics/metrics/lodestar.js +4 -0
  134. package/lib/metrics/metrics/lodestar.js.map +1 -1
  135. package/lib/network/gossip/interface.d.ts +7 -1
  136. package/lib/network/gossip/interface.d.ts.map +1 -1
  137. package/lib/network/gossip/interface.js +1 -0
  138. package/lib/network/gossip/interface.js.map +1 -1
  139. package/lib/network/gossip/scoringParameters.d.ts.map +1 -1
  140. package/lib/network/gossip/scoringParameters.js +12 -1
  141. package/lib/network/gossip/scoringParameters.js.map +1 -1
  142. package/lib/network/gossip/topic.d.ts +29 -766
  143. package/lib/network/gossip/topic.d.ts.map +1 -1
  144. package/lib/network/gossip/topic.js +6 -0
  145. package/lib/network/gossip/topic.js.map +1 -1
  146. package/lib/network/interface.d.ts +2 -0
  147. package/lib/network/interface.d.ts.map +1 -1
  148. package/lib/network/network.d.ts +2 -0
  149. package/lib/network/network.d.ts.map +1 -1
  150. package/lib/network/network.js +8 -0
  151. package/lib/network/network.js.map +1 -1
  152. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  153. package/lib/network/processor/gossipHandlers.js +38 -16
  154. package/lib/network/processor/gossipHandlers.js.map +1 -1
  155. package/lib/network/processor/gossipQueues/index.d.ts.map +1 -1
  156. package/lib/network/processor/gossipQueues/index.js +5 -0
  157. package/lib/network/processor/gossipQueues/index.js.map +1 -1
  158. package/lib/network/processor/index.d.ts.map +1 -1
  159. package/lib/network/processor/index.js +6 -5
  160. package/lib/network/processor/index.js.map +1 -1
  161. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  162. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  163. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  164. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  165. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  166. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  167. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  168. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  169. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  170. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  171. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  172. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  173. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  174. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  175. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  176. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  177. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  178. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  179. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  180. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  181. package/lib/network/reqresp/handlers/index.js +5 -0
  182. package/lib/network/reqresp/handlers/index.js.map +1 -1
  183. package/lib/network/reqresp/interface.d.ts +1 -1
  184. package/lib/network/reqresp/interface.js +1 -1
  185. package/lib/network/reqresp/protocols.d.ts +1 -0
  186. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  187. package/lib/network/reqresp/protocols.js +5 -0
  188. package/lib/network/reqresp/protocols.js.map +1 -1
  189. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  190. package/lib/network/reqresp/rateLimit.js +4 -0
  191. package/lib/network/reqresp/rateLimit.js.map +1 -1
  192. package/lib/network/reqresp/score.d.ts.map +1 -1
  193. package/lib/network/reqresp/score.js +1 -0
  194. package/lib/network/reqresp/score.js.map +1 -1
  195. package/lib/network/reqresp/types.d.ts +3 -0
  196. package/lib/network/reqresp/types.d.ts.map +1 -1
  197. package/lib/network/reqresp/types.js +3 -0
  198. package/lib/network/reqresp/types.js.map +1 -1
  199. package/lib/node/nodejs.js +2 -2
  200. package/lib/node/nodejs.js.map +1 -1
  201. package/lib/sync/constants.d.ts +7 -1
  202. package/lib/sync/constants.d.ts.map +1 -1
  203. package/lib/sync/constants.js +7 -4
  204. package/lib/sync/constants.js.map +1 -1
  205. package/lib/sync/range/batch.d.ts +35 -5
  206. package/lib/sync/range/batch.d.ts.map +1 -1
  207. package/lib/sync/range/batch.js +240 -59
  208. package/lib/sync/range/batch.js.map +1 -1
  209. package/lib/sync/range/chain.d.ts +22 -4
  210. package/lib/sync/range/chain.d.ts.map +1 -1
  211. package/lib/sync/range/chain.js +100 -10
  212. package/lib/sync/range/chain.js.map +1 -1
  213. package/lib/sync/range/range.d.ts.map +1 -1
  214. package/lib/sync/range/range.js +31 -9
  215. package/lib/sync/range/range.js.map +1 -1
  216. package/lib/sync/sync.d.ts.map +1 -1
  217. package/lib/sync/sync.js +13 -0
  218. package/lib/sync/sync.js.map +1 -1
  219. package/lib/sync/types.d.ts +34 -0
  220. package/lib/sync/types.d.ts.map +1 -1
  221. package/lib/sync/types.js +34 -0
  222. package/lib/sync/types.js.map +1 -1
  223. package/lib/sync/unknownBlock.d.ts +29 -1
  224. package/lib/sync/unknownBlock.d.ts.map +1 -1
  225. package/lib/sync/unknownBlock.js +727 -61
  226. package/lib/sync/unknownBlock.js.map +1 -1
  227. package/lib/sync/utils/downloadByRange.d.ts +67 -10
  228. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  229. package/lib/sync/utils/downloadByRange.js +211 -26
  230. package/lib/sync/utils/downloadByRange.js.map +1 -1
  231. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  232. package/lib/sync/utils/downloadByRoot.js +16 -2
  233. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  234. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  235. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  236. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  237. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  238. package/lib/sync/utils/rateLimit.d.ts +2 -0
  239. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  240. package/lib/sync/utils/rateLimit.js +15 -0
  241. package/lib/sync/utils/rateLimit.js.map +1 -0
  242. package/lib/util/sszBytes.d.ts.map +1 -1
  243. package/lib/util/sszBytes.js +8 -6
  244. package/lib/util/sszBytes.js.map +1 -1
  245. package/package.json +17 -16
  246. package/src/api/impl/beacon/blocks/index.ts +21 -5
  247. package/src/api/impl/beacon/pool/index.ts +83 -1
  248. package/src/api/impl/debug/index.ts +0 -1
  249. package/src/api/impl/lodestar/index.ts +1 -1
  250. package/src/api/impl/validator/index.ts +82 -1
  251. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  252. package/src/chain/blocks/importBlock.ts +16 -48
  253. package/src/chain/blocks/importExecutionPayload.ts +76 -30
  254. package/src/chain/blocks/index.ts +71 -22
  255. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +37 -3
  256. package/src/chain/blocks/payloadEnvelopeInput/types.ts +18 -0
  257. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  258. package/src/chain/blocks/types.ts +4 -3
  259. package/src/chain/blocks/utils/chainSegment.ts +114 -17
  260. package/src/chain/blocks/verifyBlock.ts +70 -9
  261. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  262. package/src/chain/blocks/verifyBlocksSanityChecks.ts +26 -7
  263. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +16 -8
  264. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +7 -4
  265. package/src/chain/chain.ts +43 -10
  266. package/src/chain/emitter.ts +0 -11
  267. package/src/chain/errors/blockError.ts +4 -1
  268. package/src/chain/errors/index.ts +1 -0
  269. package/src/chain/errors/proposerPreferences.ts +47 -0
  270. package/src/chain/initState.ts +9 -1
  271. package/src/chain/interface.ts +7 -1
  272. package/src/chain/opPools/payloadAttestationPool.ts +29 -8
  273. package/src/chain/prepareNextSlot.ts +21 -29
  274. package/src/chain/produceBlock/produceBlockBody.ts +46 -22
  275. package/src/chain/regen/interface.ts +1 -0
  276. package/src/chain/regen/queued.ts +2 -7
  277. package/src/chain/regen/regen.ts +2 -7
  278. package/src/chain/seenCache/index.ts +1 -0
  279. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +90 -24
  280. package/src/chain/seenCache/seenProposerPreferences.ts +32 -0
  281. package/src/chain/validation/block.ts +1 -0
  282. package/src/chain/validation/executionPayloadBid.ts +11 -8
  283. package/src/chain/validation/proposerPreferences.ts +110 -0
  284. package/src/metrics/metrics/lodestar.ts +4 -0
  285. package/src/network/gossip/interface.ts +6 -0
  286. package/src/network/gossip/scoringParameters.ts +14 -1
  287. package/src/network/gossip/topic.ts +6 -0
  288. package/src/network/interface.ts +2 -0
  289. package/src/network/network.ts +23 -0
  290. package/src/network/processor/gossipHandlers.ts +53 -17
  291. package/src/network/processor/gossipQueues/index.ts +5 -0
  292. package/src/network/processor/index.ts +6 -5
  293. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  294. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  295. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  296. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  297. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  298. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  299. package/src/network/reqresp/handlers/index.ts +5 -0
  300. package/src/network/reqresp/interface.ts +1 -1
  301. package/src/network/reqresp/protocols.ts +6 -0
  302. package/src/network/reqresp/rateLimit.ts +4 -0
  303. package/src/network/reqresp/score.ts +1 -0
  304. package/src/network/reqresp/types.ts +5 -0
  305. package/src/node/nodejs.ts +2 -2
  306. package/src/sync/constants.ts +8 -4
  307. package/src/sync/range/batch.ts +320 -67
  308. package/src/sync/range/chain.ts +128 -13
  309. package/src/sync/range/range.ts +34 -9
  310. package/src/sync/sync.ts +13 -1
  311. package/src/sync/types.ts +72 -0
  312. package/src/sync/unknownBlock.ts +915 -65
  313. package/src/sync/utils/downloadByRange.ts +378 -39
  314. package/src/sync/utils/downloadByRoot.ts +24 -2
  315. package/src/sync/utils/pendingBlocksTree.ts +0 -15
  316. package/src/sync/utils/rateLimit.ts +16 -0
  317. package/src/util/sszBytes.ts +8 -6
@@ -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,38 @@ 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";
45
+ import {getRateLimitedUntilMs} from "./utils/rateLimit.js";
31
46
 
32
47
  const MAX_ATTEMPTS_PER_BLOCK = 5;
33
48
  const MAX_KNOWN_BAD_BLOCKS = 500;
34
49
  const MAX_PENDING_BLOCKS = 100;
35
50
 
51
+ type AdvancePendingBlockResult =
52
+ | "ready"
53
+ | "queued_block"
54
+ | "queued_parent_block"
55
+ | "queued_parent_payload"
56
+ | "blocked"
57
+ | "removed";
58
+
36
59
  enum FetchResult {
37
60
  SuccessResolved = "success_resolved",
38
61
  SuccessMissingParent = "success_missing_parent",
@@ -41,6 +64,13 @@ enum FetchResult {
41
64
  FailureMaxAttempts = "failure_max_attempts",
42
65
  }
43
66
 
67
+ class UnknownBlockRateLimitedError extends Error {
68
+ constructor(message: string) {
69
+ super(message);
70
+ this.name = "UnknownBlockRateLimitedError";
71
+ }
72
+ }
73
+
44
74
  /**
45
75
  * BlockInputSync is a class that handles ReqResp to find blocks and data related to a specific blockRoot. The
46
76
  * blockRoot may have been found via object gossip, or the API. Gossip objects that can trigger a search are block,
@@ -78,10 +108,13 @@ export class BlockInputSync {
78
108
  * block RootHex -> PendingBlock. To avoid finding same root at the same time
79
109
  */
80
110
  private readonly pendingBlocks = new Map<RootHex, BlockInputSyncCacheItem>();
111
+ // Payload sync is keyed by beacon block root as well, so block and payload queues can unblock each other.
112
+ private readonly pendingPayloads = new Map<RootHex, PayloadSyncCacheItem>();
81
113
  private readonly knownBadBlocks = new Set<RootHex>();
82
114
  private readonly maxPendingBlocks;
83
115
  private subscribedToNetworkEvents = false;
84
116
  private peerBalancer: UnknownBlockPeerBalancer;
117
+ private rateLimitBackoffTimeout: NodeJS.Timeout | undefined;
85
118
 
86
119
  constructor(
87
120
  private readonly config: ChainForkConfig,
@@ -98,6 +131,9 @@ export class BlockInputSync {
98
131
  metrics.blockInputSync.pendingBlocks.addCollect(() =>
99
132
  metrics.blockInputSync.pendingBlocks.set(this.pendingBlocks.size)
100
133
  );
134
+ metrics.blockInputSync.pendingPayloads.addCollect(() =>
135
+ metrics.blockInputSync.pendingPayloads.set(this.pendingPayloads.size)
136
+ );
101
137
  metrics.blockInputSync.knownBadBlocks.addCollect(() =>
102
138
  metrics.blockInputSync.knownBadBlocks.set(this.knownBadBlocks.size)
103
139
  );
@@ -114,8 +150,12 @@ export class BlockInputSync {
114
150
  if (!this.subscribedToNetworkEvents) {
115
151
  this.logger.verbose("BlockInputSync enabled.");
116
152
  this.chain.emitter.on(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
153
+ this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
117
154
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
155
+ this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
118
156
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
157
+ this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
158
+ this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
119
159
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
120
160
  this.network.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
121
161
  this.subscribedToNetworkEvents = true;
@@ -124,9 +164,14 @@ export class BlockInputSync {
124
164
 
125
165
  unsubscribeFromNetwork(): void {
126
166
  this.logger.verbose("BlockInputSync disabled.");
167
+ this.clearRateLimitBackoffTimer();
127
168
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
169
+ this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
128
170
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
171
+ this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
129
172
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
173
+ this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
174
+ this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
130
175
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
131
176
  this.network.events.off(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
132
177
  this.subscribedToNetworkEvents = false;
@@ -168,12 +213,55 @@ export class BlockInputSync {
168
213
  }
169
214
  };
170
215
 
216
+ private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => {
217
+ try {
218
+ this.addByPayloadRootHex(data.rootHex, data.peer);
219
+ this.triggerUnknownBlockSearch();
220
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
221
+ this.metrics?.blockInputSync.source.inc({source: data.source});
222
+ } catch (e) {
223
+ this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e as Error);
224
+ }
225
+ };
226
+
227
+ private onIncompletePayloadEnvelope = (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]): void => {
228
+ try {
229
+ this.addByPayloadInput(data.payloadInput, data.peer);
230
+ this.triggerUnknownBlockSearch();
231
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
232
+ this.metrics?.blockInputSync.source.inc({source: data.source});
233
+ } catch (e) {
234
+ this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e as Error);
235
+ }
236
+ };
237
+
171
238
  /**
172
239
  * Process an unknownBlockParent event and register the block in `pendingBlocks` Map.
173
240
  */
174
241
  private onUnknownParent = (data: ChainEventData[ChainEvent.blockUnknownParent]): void => {
175
242
  try {
176
- this.addByRootHex(data.blockInput.parentRootHex, data.peer);
243
+ const missingDependency = this.getMissingBlockDependency(data.blockInput);
244
+ if (missingDependency.kind === "invalidParentPayload") {
245
+ this.addByBlockInput(data.blockInput, data.peer);
246
+
247
+ const pendingBlock = this.pendingBlocks.get(data.blockInput.blockRootHex);
248
+ if (pendingBlock && isPendingBlockInput(pendingBlock)) {
249
+ this.logger.debug("Ignoring block with conflicting parent payload hash", {
250
+ slot: pendingBlock.blockInput.slot,
251
+ root: pendingBlock.blockInput.blockRootHex,
252
+ parentRoot: missingDependency.parentRootHex,
253
+ parentBlockHash: missingDependency.parentBlockHashHex,
254
+ });
255
+ this.removeAndDownScoreAllDescendants(pendingBlock);
256
+ }
257
+ return;
258
+ }
259
+
260
+ if (missingDependency.kind === "parentPayload") {
261
+ this.addByPayloadRootHex(missingDependency.rootHex, data.peer);
262
+ } else if (missingDependency.kind === "parentBlock") {
263
+ this.addByRootHex(missingDependency.rootHex, data.peer);
264
+ }
177
265
  this.addByBlockInput(data.blockInput, data.peer);
178
266
  this.triggerUnknownBlockSearch();
179
267
  this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PARENT});
@@ -183,8 +271,22 @@ export class BlockInputSync {
183
271
  }
184
272
  };
185
273
 
186
- private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): void => {
274
+ private onBlockImported = (): void => {
275
+ if (this.pendingPayloads.size > 0) {
276
+ this.triggerUnknownBlockSearch();
277
+ }
278
+ };
279
+
280
+ private onPayloadImported = ({
281
+ blockRoot,
282
+ }: routes.events.EventData[routes.events.EventType.executionPayload]): void => {
283
+ this.pendingPayloads.delete(blockRoot);
284
+ this.triggerUnknownBlockSearch();
285
+ };
286
+
287
+ private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
187
288
  let pendingBlock = this.pendingBlocks.get(rootHex);
289
+ let added = false;
188
290
  if (!pendingBlock) {
189
291
  pendingBlock = {
190
292
  status: PendingBlockInputStatus.pending,
@@ -193,6 +295,7 @@ export class BlockInputSync {
193
295
  timeAddedSec: Date.now() / 1000,
194
296
  };
195
297
  this.pendingBlocks.set(rootHex, pendingBlock);
298
+ added = true;
196
299
 
197
300
  this.logger.verbose("Added new rootHex to BlockInputSync.pendingBlocks", {
198
301
  root: pendingBlock.rootHex,
@@ -210,6 +313,7 @@ export class BlockInputSync {
210
313
  if (prunedItemCount > 0) {
211
314
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
212
315
  }
316
+ return added;
213
317
  };
214
318
 
215
319
  private addByBlockInput = (blockInput: IBlockInput, peerIdStr?: string): void => {
@@ -242,6 +346,58 @@ export class BlockInputSync {
242
346
  }
243
347
  };
244
348
 
349
+ private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
350
+ let pendingPayload = this.pendingPayloads.get(rootHex);
351
+ let added = false;
352
+ if (!pendingPayload) {
353
+ pendingPayload = {
354
+ status: PendingPayloadInputStatus.pending,
355
+ rootHex,
356
+ peerIdStrings: new Set(),
357
+ timeAddedSec: Date.now() / 1000,
358
+ };
359
+ this.pendingPayloads.set(rootHex, pendingPayload);
360
+ added = true;
361
+
362
+ this.logger.verbose("Added new payload rootHex to BlockInputSync.pendingPayloads", {
363
+ root: rootHex,
364
+ peerIdStr: peerIdStr ?? "unknown peer",
365
+ });
366
+ }
367
+
368
+ if (peerIdStr) {
369
+ pendingPayload.peerIdStrings.add(peerIdStr);
370
+ }
371
+
372
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
373
+ if (prunedItemCount > 0) {
374
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
375
+ }
376
+ return added;
377
+ };
378
+
379
+ private addByPayloadInput = (
380
+ payloadInput: PayloadEnvelopeInput,
381
+ peerIdStr?: PeerIdStr,
382
+ envelope?: gloas.SignedExecutionPayloadEnvelope
383
+ ): void => {
384
+ const pendingPayload = this.toPendingPayloadInput(
385
+ payloadInput,
386
+ this.pendingPayloads.get(payloadInput.blockRootHex),
387
+ envelope
388
+ );
389
+
390
+ if (peerIdStr) {
391
+ pendingPayload.peerIdStrings.add(peerIdStr);
392
+ }
393
+
394
+ this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
395
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
396
+ if (prunedItemCount > 0) {
397
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
398
+ }
399
+ };
400
+
245
401
  private onPeerConnected = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
246
402
  try {
247
403
  const peerId = data.peer;
@@ -256,56 +412,242 @@ export class BlockInputSync {
256
412
  private onPeerDisconnected = (data: NetworkEventData[NetworkEvent.peerDisconnected]): void => {
257
413
  const peerId = data.peer;
258
414
  this.peerBalancer.onPeerDisconnected(peerId);
415
+ this.scheduleRateLimitBackoffRetry();
259
416
  };
260
417
 
418
+ /**
419
+ * Post-gloas, a locally complete block can still be blocked on its parent's execution payload lineage.
420
+ * Distinguish which dependency is missing so the scheduler can enqueue the right follow-up work.
421
+ */
422
+ private getMissingBlockDependency(
423
+ blockInput: IBlockInput
424
+ ):
425
+ | {kind: "ready"}
426
+ | {kind: "block" | "parentBlock" | "parentPayload"; rootHex: RootHex}
427
+ | {kind: "invalidParentPayload"; parentRootHex: RootHex; parentBlockHashHex: RootHex} {
428
+ const parentRootHex = blockInput.parentRootHex;
429
+ if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) {
430
+ return {kind: "parentBlock", rootHex: parentRootHex};
431
+ }
432
+
433
+ if (!blockInput.hasBlock()) {
434
+ return {kind: "block", rootHex: blockInput.blockRootHex};
435
+ }
436
+
437
+ if (this.config.getForkSeq(blockInput.slot) < ForkSeq.gloas) {
438
+ return {kind: "ready"};
439
+ }
440
+
441
+ const block = blockInput.getBlock() as gloas.SignedBeaconBlock;
442
+ const parentBlockHashHex = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
443
+ if (this.chain.forkChoice.getBlockHexAndBlockHash(parentRootHex, parentBlockHashHex) !== null) {
444
+ return {kind: "ready"};
445
+ }
446
+
447
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
448
+ return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
449
+ }
450
+
451
+ const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);
452
+ if (parentPayloadInput) {
453
+ if (parentPayloadInput.getBlockHashHex() === parentBlockHashHex) {
454
+ return {kind: "parentPayload", rootHex: parentRootHex};
455
+ }
456
+
457
+ return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
458
+ }
459
+
460
+ return {kind: "parentPayload", rootHex: parentRootHex};
461
+ }
462
+
463
+ private advancePendingBlock(pendingBlock: PendingBlockInput): AdvancePendingBlockResult {
464
+ const missingDependency = this.getMissingBlockDependency(pendingBlock.blockInput);
465
+
466
+ switch (missingDependency.kind) {
467
+ case "ready":
468
+ return "ready";
469
+
470
+ case "block":
471
+ pendingBlock.status = PendingBlockInputStatus.pending;
472
+ return "queued_block";
473
+
474
+ case "parentBlock": {
475
+ let added = this.addByRootHex(missingDependency.rootHex);
476
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
477
+ added = this.addByRootHex(missingDependency.rootHex, peerIdStr) || added;
478
+ }
479
+ return added ? "queued_parent_block" : "blocked";
480
+ }
481
+
482
+ case "parentPayload": {
483
+ let added = this.addByPayloadRootHex(missingDependency.rootHex);
484
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
485
+ added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr) || added;
486
+ }
487
+ return added ? "queued_parent_payload" : "blocked";
488
+ }
489
+
490
+ case "invalidParentPayload":
491
+ this.logger.debug("Removing block with conflicting parent payload hash", {
492
+ slot: pendingBlock.blockInput.slot,
493
+ root: pendingBlock.blockInput.blockRootHex,
494
+ parentRoot: missingDependency.parentRootHex,
495
+ parentBlockHash: missingDependency.parentBlockHashHex,
496
+ });
497
+ this.removeAndDownScoreAllDescendants(pendingBlock);
498
+ return "removed";
499
+ }
500
+ }
501
+
502
+ private toPendingPayloadInput(
503
+ payloadInput: PayloadEnvelopeInput,
504
+ previous?: PayloadSyncCacheItem,
505
+ envelope?: gloas.SignedExecutionPayloadEnvelope
506
+ ): PendingPayloadInput {
507
+ // Normalize every payload queueing path into the same cache shape while preserving first-seen
508
+ // timing and peer provenance from any earlier by-root or envelope-only entry.
509
+ const queuedEnvelope = envelope ?? (previous && isPendingPayloadEnvelope(previous) ? previous.envelope : undefined);
510
+
511
+ if (queuedEnvelope && !payloadInput.hasPayloadEnvelope()) {
512
+ payloadInput.addPayloadEnvelope({
513
+ envelope: queuedEnvelope,
514
+ source: PayloadEnvelopeInputSource.byRoot,
515
+ seenTimestampSec: Date.now() / 1000,
516
+ });
517
+ }
518
+
519
+ return {
520
+ status: payloadInput.isComplete() ? PendingPayloadInputStatus.downloaded : PendingPayloadInputStatus.pending,
521
+ payloadInput,
522
+ timeAddedSec: previous?.timeAddedSec ?? Date.now() / 1000,
523
+ timeSyncedSec: payloadInput.isComplete() ? Date.now() / 1000 : undefined,
524
+ peerIdStrings: new Set(previous?.peerIdStrings ?? []),
525
+ };
526
+ }
527
+
261
528
  /**
262
529
  * Gather tip parent blocks with unknown parent and do a search for all of them
263
530
  */
264
531
  private triggerUnknownBlockSearch = (): void => {
265
532
  // Cheap early stop to prevent calling the network.getConnectedPeers()
266
- if (this.pendingBlocks.size === 0) {
533
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
267
534
  return;
268
535
  }
269
536
 
270
- // If the node loses all peers with pending unknown blocks, the sync will stall
537
+ // If the node loses all peers with pending unknown blocks or payloads, the sync will stall
271
538
  const connectedPeers = this.network.getConnectedPeers();
272
- if (connectedPeers.length === 0) {
273
- this.logger.debug("No connected peers, skipping unknown block search.");
274
- return;
275
- }
539
+ const hasConnectedPeers = connectedPeers.length > 0;
276
540
 
277
541
  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)) {
542
+ let processedBlocks = 0;
543
+ let shouldRerunBlockSearch = false;
544
+
545
+ for (const block of ancestors) {
546
+ const advanceResult = this.advancePendingBlock(block);
547
+ switch (advanceResult) {
548
+ case "ready":
286
549
  processedBlocks++;
287
- this.processBlock(block).catch((e) => {
550
+ this.processReadyBlock(block).catch((e) => {
288
551
  this.logger.debug("Unexpected error - process old downloaded block", {}, e);
289
552
  });
290
- }
553
+ break;
554
+
555
+ case "queued_block":
556
+ case "queued_parent_block":
557
+ shouldRerunBlockSearch = true;
558
+ break;
559
+
560
+ case "queued_parent_payload":
561
+ case "blocked":
562
+ case "removed":
563
+ break;
291
564
  }
565
+ }
292
566
 
567
+ if (unknowns.length > 0) {
568
+ if (!hasConnectedPeers) {
569
+ this.logger.debug("No connected peers, skipping unknown block download.");
570
+ } else {
571
+ // Most of the time there is exactly 1 unknown block
572
+ for (const block of unknowns) {
573
+ this.downloadBlock(block).catch((e) => {
574
+ this.logger.debug("Unexpected error - downloadBlock", {root: getBlockInputSyncCacheItemRootHex(block)}, e);
575
+ });
576
+ }
577
+ }
578
+ } else if (ancestors.length > 0) {
579
+ // It's rare when there is no unknown block
580
+ // see https://github.com/ChainSafe/lodestar/issues/5649#issuecomment-1594213550
293
581
  this.logger.verbose("No unknown block, process ancestor downloaded blocks", {
294
582
  pendingBlocks: this.pendingBlocks.size,
295
583
  ancestorBlocks: ancestors.length,
296
584
  processedBlocks,
297
585
  });
298
- return;
299
586
  }
300
587
 
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);
588
+ // Blocks can unblock payloads and payloads can unblock blocks, so every scheduler pass services both queues.
589
+ for (const payload of Array.from(this.pendingPayloads.values())) {
590
+ if (isPendingPayloadInput(payload) && payload.status === PendingPayloadInputStatus.downloaded) {
591
+ this.processPayload(payload).catch((e) => {
592
+ this.logger.debug("Unexpected error - process downloaded payload", {}, e);
593
+ });
594
+ continue;
595
+ }
596
+
597
+ if (isPendingPayloadEnvelope(payload)) {
598
+ this.reconcilePayloadEnvelope(payload).catch((e) => {
599
+ this.logger.debug("Unexpected error - reconcile pending payload envelope", {}, e);
600
+ });
601
+ continue;
602
+ }
603
+
604
+ if (!hasConnectedPeers) {
605
+ this.logger.debug("No connected peers, skipping unknown payload download.", {
606
+ root: getPayloadSyncCacheItemRootHex(payload),
607
+ });
608
+ continue;
609
+ }
610
+
611
+ this.downloadPayload(payload).catch((e) => {
612
+ this.logger.debug("Unexpected error - downloadPayload", {root: getPayloadSyncCacheItemRootHex(payload)}, e);
305
613
  });
306
614
  }
615
+
616
+ if (shouldRerunBlockSearch) {
617
+ this.triggerUnknownBlockSearch();
618
+ }
307
619
  };
308
620
 
621
+ private scheduleRateLimitBackoffRetry(): void {
622
+ this.clearRateLimitBackoffTimer();
623
+
624
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
625
+ return;
626
+ }
627
+
628
+ const now = Date.now();
629
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
630
+ if (retryAt === null) {
631
+ return;
632
+ }
633
+
634
+ this.rateLimitBackoffTimeout = setTimeout(
635
+ () => {
636
+ this.rateLimitBackoffTimeout = undefined;
637
+ this.triggerUnknownBlockSearch();
638
+ this.scheduleRateLimitBackoffRetry();
639
+ },
640
+ Math.max(0, retryAt - now)
641
+ );
642
+ }
643
+
644
+ private clearRateLimitBackoffTimer(): void {
645
+ if (this.rateLimitBackoffTimeout !== undefined) {
646
+ clearTimeout(this.rateLimitBackoffTimeout);
647
+ this.rateLimitBackoffTimeout = undefined;
648
+ }
649
+ }
650
+
309
651
  private async downloadBlock(block: BlockInputSyncCacheItem): Promise<void> {
310
652
  if (block.status !== PendingBlockInputStatus.pending) {
311
653
  return;
@@ -342,10 +684,26 @@ export class BlockInputSync {
342
684
  this.logger.verbose("Downloaded unknown block", logCtx2);
343
685
 
344
686
  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
- });
687
+ // If the direct parent is already in fork choice, let the block state machine decide if
688
+ // the next step is block import, parent payload download, or branch removal.
689
+ const advanceResult = this.advancePendingBlock(pending);
690
+ switch (advanceResult) {
691
+ case "ready":
692
+ this.processReadyBlock(pending).catch((e) => {
693
+ this.logger.debug("Unexpected error - process newly downloaded block", logCtx2, e);
694
+ });
695
+ break;
696
+
697
+ case "queued_block":
698
+ case "queued_parent_block":
699
+ case "queued_parent_payload":
700
+ this.triggerUnknownBlockSearch();
701
+ break;
702
+
703
+ case "blocked":
704
+ case "removed":
705
+ break;
706
+ }
349
707
  } else if (blockSlot <= finalizedSlot) {
350
708
  // the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
351
709
  // we should found it through forkchoice. If not, we should penalize all peers sending us this block chain
@@ -361,6 +719,16 @@ export class BlockInputSync {
361
719
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
362
720
  }
363
721
  } else {
722
+ if (res.err instanceof UnknownBlockRateLimitedError) {
723
+ const pendingBlock = this.pendingBlocks.get(rootHex);
724
+ if (pendingBlock) {
725
+ pendingBlock.status = PendingBlockInputStatus.pending;
726
+ }
727
+ this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
728
+ this.scheduleRateLimitBackoffRetry();
729
+ return;
730
+ }
731
+
364
732
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
365
733
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
366
734
  this.removeAndDownScoreAllDescendants(block);
@@ -368,26 +736,11 @@ export class BlockInputSync {
368
736
  }
369
737
 
370
738
  /**
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
739
+ * Import a block that has already passed the local dependency checks in BlockInputSync.
740
+ * On error, remove and downscore descendants as appropriate for the failure type.
374
741
  */
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
742
+ private async processReadyBlock(pendingBlock: PendingBlockInput): Promise<void> {
378
743
  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
744
  return;
392
745
  }
393
746
 
@@ -432,15 +785,9 @@ export class BlockInputSync {
432
785
  if (!res.err) {
433
786
  // no need to update status to "processed", delete anyway
434
787
  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
- }
788
+ // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
789
+ // are advanced through the same dependency checks as every other pending item.
790
+ this.triggerUnknownBlockSearch();
444
791
  } else {
445
792
  const errorData = {slot: pendingBlock.blockInput.slot, root: pendingBlock.blockInput.blockRootHex};
446
793
  if (res.err instanceof BlockError) {
@@ -456,6 +803,19 @@ export class BlockInputSync {
456
803
  pendingBlock.status = PendingBlockInputStatus.downloaded;
457
804
  break;
458
805
 
806
+ case BlockErrorCode.PARENT_PAYLOAD_UNKNOWN:
807
+ this.logger.error(
808
+ "processReadyBlock() hit unexpected parent payload dependency after readiness checks",
809
+ {
810
+ ...errorData,
811
+ parentRoot: pendingBlock.blockInput.parentRootHex,
812
+ parentBlockHash: res.err.type.parentBlockHash,
813
+ },
814
+ res.err
815
+ );
816
+ pendingBlock.status = PendingBlockInputStatus.downloaded;
817
+ break;
818
+
459
819
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
460
820
  // Removing the block(s) without penalizing the peers, hoping for EL to
461
821
  // recover on a latter download + verify attempt
@@ -477,6 +837,393 @@ export class BlockInputSync {
477
837
  }
478
838
  }
479
839
 
840
+ /**
841
+ * Reconcile an envelope-first payload entry once the block import path has seeded its
842
+ * PayloadEnvelopeInput. This may queue block download, validate the speculative envelope, or
843
+ * downgrade back to by-root fetching when the cached envelope does not match the imported block.
844
+ */
845
+ private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise<void> {
846
+ const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
847
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
848
+ this.pendingPayloads.delete(rootHex);
849
+ return;
850
+ }
851
+
852
+ const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
853
+ if (!payloadInput) {
854
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
855
+ // Column commitments live on the block body, so an envelope-only entry has to pull the block first.
856
+ if (!this.pendingBlocks.has(rootHex)) {
857
+ this.addByRootHex(rootHex);
858
+ }
859
+
860
+ const pendingBlock = this.pendingBlocks.get(rootHex);
861
+ if (pendingBlock && this.network.getConnectedPeers().length > 0) {
862
+ await this.downloadBlock(pendingBlock);
863
+ }
864
+ } else {
865
+ this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
866
+ root: rootHex,
867
+ });
868
+ }
869
+ return;
870
+ }
871
+
872
+ if (!payloadInput.hasPayloadEnvelope()) {
873
+ const validationResult = await wrapError(
874
+ validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope)
875
+ );
876
+ if (validationResult.err) {
877
+ this.logger.debug(
878
+ "Pending payload envelope failed validation after block import, refetching by root",
879
+ {slot: pendingPayload.envelope.message.payload.slotNumber, root: rootHex},
880
+ validationResult.err
881
+ );
882
+
883
+ const pendingPayloadByRoot: PendingPayloadRootHex = {
884
+ status: PendingPayloadInputStatus.pending,
885
+ rootHex,
886
+ timeAddedSec: pendingPayload.timeAddedSec,
887
+ peerIdStrings: new Set(pendingPayload.peerIdStrings),
888
+ };
889
+ this.pendingPayloads.set(rootHex, pendingPayloadByRoot);
890
+
891
+ if (this.network.getConnectedPeers().length > 0) {
892
+ await this.downloadPayload(pendingPayloadByRoot);
893
+ }
894
+ return;
895
+ }
896
+ }
897
+
898
+ const upgradedPayload = this.toPendingPayloadInput(payloadInput, pendingPayload, pendingPayload.envelope);
899
+ this.pendingPayloads.set(rootHex, upgradedPayload);
900
+
901
+ if (upgradedPayload.status === PendingPayloadInputStatus.downloaded) {
902
+ await this.processPayload(upgradedPayload);
903
+ return;
904
+ }
905
+
906
+ await this.downloadPayload(upgradedPayload);
907
+ }
908
+
909
+ private async downloadPayload(payload: PayloadSyncCacheItem): Promise<void> {
910
+ if (isPendingPayloadEnvelope(payload)) {
911
+ await this.reconcilePayloadEnvelope(payload);
912
+ return;
913
+ }
914
+
915
+ const rootHex = getPayloadSyncCacheItemRootHex(payload);
916
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
917
+ this.pendingPayloads.delete(rootHex);
918
+ return;
919
+ }
920
+
921
+ if (payload.status !== PendingPayloadInputStatus.pending) {
922
+ return;
923
+ }
924
+
925
+ const logCtx = {
926
+ slot: getPayloadSyncCacheItemSlot(payload),
927
+ root: rootHex,
928
+ pendingPayloads: this.pendingPayloads.size,
929
+ };
930
+
931
+ this.logger.verbose("BlockInputSync.downloadPayload()", logCtx);
932
+
933
+ payload.status = PendingPayloadInputStatus.fetching;
934
+
935
+ const res = await wrapError(this.fetchPayloadInput(payload));
936
+ if (!res.err) {
937
+ const pendingPayload = res.result;
938
+ this.pendingPayloads.set(getPayloadSyncCacheItemRootHex(pendingPayload), pendingPayload);
939
+
940
+ if (isPendingPayloadEnvelope(pendingPayload)) {
941
+ await this.reconcilePayloadEnvelope(pendingPayload);
942
+ } else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
943
+ await this.processPayload(pendingPayload);
944
+ }
945
+ return;
946
+ }
947
+
948
+ this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
949
+ if (!isPendingPayloadEnvelope(payload)) {
950
+ payload.status = PendingPayloadInputStatus.pending;
951
+ }
952
+ }
953
+
954
+ private async processPayload(pendingPayload: PendingPayloadInput): Promise<void> {
955
+ const rootHex = pendingPayload.payloadInput.blockRootHex;
956
+ const logCtx = {slot: pendingPayload.payloadInput.slot, root: rootHex};
957
+
958
+ if (pendingPayload.status !== PendingPayloadInputStatus.downloaded) {
959
+ this.logger.debug("Skipping payload processing before payload input is downloaded", {
960
+ ...logCtx,
961
+ status: pendingPayload.status,
962
+ });
963
+ return;
964
+ }
965
+
966
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
967
+ this.logger.debug("Payload already imported while processing unknown payload", logCtx);
968
+ this.pendingPayloads.delete(rootHex);
969
+ return;
970
+ }
971
+
972
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
973
+ this.logger.debug("Payload input is ready before its block is in fork choice", logCtx);
974
+ const added = this.addByRootHex(rootHex);
975
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
976
+ if (added) {
977
+ this.triggerUnknownBlockSearch();
978
+ }
979
+ return;
980
+ }
981
+
982
+ pendingPayload.status = PendingPayloadInputStatus.processing;
983
+
984
+ const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput));
985
+ if (!res.err) {
986
+ this.logger.debug("Processed payload from unknown sync", logCtx);
987
+ this.pendingPayloads.delete(rootHex);
988
+ this.triggerUnknownBlockSearch();
989
+ return;
990
+ }
991
+
992
+ if (res.err instanceof PayloadError) {
993
+ switch (res.err.type.code) {
994
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
995
+ // Payload sync discovered the block dependency before the block queue did. Re-enqueue the
996
+ // block and keep the payload ready so the scheduler can retry once the block reaches fork choice.
997
+ if (this.addByRootHex(rootHex)) {
998
+ this.triggerUnknownBlockSearch();
999
+ }
1000
+ // Keep the payload out of any synchronous requeue pass; a later scheduler pass will retry it.
1001
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1002
+ break;
1003
+
1004
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1005
+ this.logger.debug("Execution engine error while processing payload from unknown sync", logCtx, res.err);
1006
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1007
+ break;
1008
+
1009
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1010
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1011
+ case PayloadErrorCode.INVALID_SIGNATURE:
1012
+ // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1013
+ // reintroducing envelope replacement / recreation flows.
1014
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1015
+ this.removePendingPayloadAndDescendants(rootHex);
1016
+ break;
1017
+
1018
+ default:
1019
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1020
+ this.pendingPayloads.delete(rootHex);
1021
+ }
1022
+ return;
1023
+ }
1024
+
1025
+ this.logger.debug("Unknown error processing payload from unknown sync", logCtx, res.err);
1026
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
1027
+ }
1028
+
1029
+ /**
1030
+ * Download payload material keyed by beacon block root. Unlike block download, payload sync may
1031
+ * already have a locally cached envelope or partial columns, so each attempt starts from local state
1032
+ * and only asks peers for the remaining pieces.
1033
+ */
1034
+ private async fetchPayloadInput(
1035
+ cacheItem: PendingPayloadInput | PendingPayloadRootHex
1036
+ ): Promise<PendingPayloadInput | PendingPayloadEnvelope> {
1037
+ const rootHex = getPayloadSyncCacheItemRootHex(cacheItem);
1038
+ const blockRoot = fromHex(rootHex);
1039
+ const excludedPeers = new Set<PeerIdStr>();
1040
+
1041
+ let slot = getPayloadSyncCacheItemSlot(cacheItem);
1042
+ let payloadInput = isPendingPayloadInput(cacheItem)
1043
+ ? cacheItem.payloadInput
1044
+ : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1045
+ let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1046
+
1047
+ let i = 0;
1048
+ let deferredByRateLimit = false;
1049
+ while (i++ < this.getMaxDownloadAttempts()) {
1050
+ const pendingColumns = payloadInput?.hasAllData()
1051
+ ? new Set<number>()
1052
+ : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1053
+ const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1054
+ if (peerMeta === null) {
1055
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1056
+ throw new UnknownBlockRateLimitedError(
1057
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1058
+ );
1059
+ }
1060
+
1061
+ throw Error(
1062
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1063
+ );
1064
+ }
1065
+
1066
+ const {peerId, client: peerClient} = peerMeta;
1067
+ cacheItem.peerIdStrings.add(peerId);
1068
+
1069
+ try {
1070
+ if (!envelope) {
1071
+ envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1072
+ slot = envelope.message.payload.slotNumber;
1073
+ }
1074
+
1075
+ payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1076
+ if (!payloadInput) {
1077
+ if (this.chain.forkChoice.hasBlockHex(rootHex)) {
1078
+ throw new Error(`Missing PayloadEnvelopeInput for known block ${rootHex}`);
1079
+ }
1080
+ // Keep the validated envelope around, but wait for the block body before turning it into a full payload input.
1081
+ return {
1082
+ status: PendingPayloadInputStatus.waitingForBlock,
1083
+ envelope,
1084
+ timeAddedSec: cacheItem.timeAddedSec,
1085
+ peerIdStrings: cacheItem.peerIdStrings,
1086
+ };
1087
+ }
1088
+
1089
+ if (!payloadInput.hasPayloadEnvelope()) {
1090
+ await validateGossipExecutionPayloadEnvelope(this.chain, envelope);
1091
+ }
1092
+
1093
+ let pendingPayload = this.toPendingPayloadInput(payloadInput, cacheItem, envelope);
1094
+ if (!pendingPayload.payloadInput.hasAllData()) {
1095
+ const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
1096
+ if (missing.length > 0) {
1097
+ const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
1098
+ const seenTimestampSec = Date.now() / 1000;
1099
+ for (const columnSidecar of columnSidecars) {
1100
+ if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
1101
+ continue;
1102
+ }
1103
+
1104
+ pendingPayload.payloadInput.addColumn({
1105
+ columnSidecar,
1106
+ source: PayloadEnvelopeInputSource.byRoot,
1107
+ seenTimestampSec,
1108
+ peerIdStr: peerId,
1109
+ });
1110
+ }
1111
+ pendingPayload = this.toPendingPayloadInput(pendingPayload.payloadInput, pendingPayload);
1112
+ }
1113
+ }
1114
+
1115
+ this.logger.verbose("BlockInputSync.fetchPayloadInput: successful download", {
1116
+ slot,
1117
+ rootHex,
1118
+ peerId,
1119
+ peerClient,
1120
+ hasPayload: pendingPayload.payloadInput.hasPayloadEnvelope(),
1121
+ hasAllData: pendingPayload.payloadInput.hasAllData(),
1122
+ });
1123
+
1124
+ if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
1125
+ return pendingPayload;
1126
+ }
1127
+
1128
+ cacheItem = pendingPayload;
1129
+ payloadInput = pendingPayload.payloadInput;
1130
+ } catch (e) {
1131
+ this.logger.debug(
1132
+ "Error downloading payload in BlockInputSync.fetchPayloadInput",
1133
+ {slot, rootHex, attempt: i, peer: peerId, peerClient},
1134
+ e as Error
1135
+ );
1136
+
1137
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1138
+ if (rateLimitedUntilMs !== null) {
1139
+ deferredByRateLimit = true;
1140
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1141
+ this.scheduleRateLimitBackoffRetry();
1142
+ } else if (e instanceof RequestError) {
1143
+ switch (e.type.code) {
1144
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1145
+ case RequestErrorCode.REQUEST_TIMEOUT:
1146
+ break;
1147
+ default:
1148
+ excludedPeers.add(peerId);
1149
+ break;
1150
+ }
1151
+ } else {
1152
+ excludedPeers.add(peerId);
1153
+ }
1154
+ } finally {
1155
+ this.peerBalancer.onRequestCompleted(peerId);
1156
+ }
1157
+ }
1158
+
1159
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1160
+ throw new UnknownBlockRateLimitedError(
1161
+ `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1162
+ );
1163
+ }
1164
+
1165
+ throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
1166
+ }
1167
+
1168
+ private async fetchExecutionPayloadEnvelope(
1169
+ peerIdStr: PeerIdStr,
1170
+ blockRoot: Uint8Array,
1171
+ rootHex: RootHex
1172
+ ): Promise<gloas.SignedExecutionPayloadEnvelope> {
1173
+ const response = await this.network.sendExecutionPayloadEnvelopesByRoot(peerIdStr, [blockRoot]);
1174
+ const envelope = response.at(0);
1175
+ if (!envelope) {
1176
+ throw new Error(`Missing execution payload envelope for root=${rootHex}`);
1177
+ }
1178
+
1179
+ const receivedRootHex = toRootHex(envelope.message.beaconBlockRoot);
1180
+ if (receivedRootHex !== rootHex) {
1181
+ throw new Error(`Execution payload envelope root mismatch requested=${rootHex} received=${receivedRootHex}`);
1182
+ }
1183
+
1184
+ return envelope;
1185
+ }
1186
+
1187
+ private async fetchPayloadColumns(
1188
+ peerMeta: PeerSyncMeta,
1189
+ payloadInput: PayloadEnvelopeInput,
1190
+ missing: number[]
1191
+ ): Promise<gloas.DataColumnSidecar[]> {
1192
+ const {peerId: peerIdStr} = peerMeta;
1193
+ const peerColumns = new Set(peerMeta.custodyColumns ?? []);
1194
+ const requestedColumns = missing.filter((columnIndex) => peerColumns.has(columnIndex));
1195
+ if (requestedColumns.length === 0) {
1196
+ return [];
1197
+ }
1198
+
1199
+ const columnSidecars = (await this.network.sendDataColumnSidecarsByRoot(peerIdStr, [
1200
+ {blockRoot: fromHex(payloadInput.blockRootHex), columns: requestedColumns},
1201
+ ])) as gloas.DataColumnSidecar[];
1202
+
1203
+ if (columnSidecars.length === 0) {
1204
+ throw new Error(`No data column sidecars returned for payload root=${payloadInput.blockRootHex}`);
1205
+ }
1206
+
1207
+ const requestedColumnsSet = new Set(requestedColumns);
1208
+ const extraColumns = columnSidecars.filter((columnSidecar) => !requestedColumnsSet.has(columnSidecar.index));
1209
+ if (extraColumns.length > 0) {
1210
+ throw new Error(
1211
+ `Received unexpected payload data columns indices=${prettyPrintIndices(extraColumns.map((column) => column.index))}`
1212
+ );
1213
+ }
1214
+
1215
+ // PayloadEnvelopeInput already carries the block slot, root, and commitments, so reuse the
1216
+ // block-based Gloas validator rather than maintaining a second payload-specific variant.
1217
+ await validateGloasBlockDataColumnSidecars(
1218
+ payloadInput.slot,
1219
+ fromHex(payloadInput.blockRootHex),
1220
+ payloadInput.getBlobKzgCommitments(),
1221
+ columnSidecars,
1222
+ this.chain.metrics?.peerDas
1223
+ );
1224
+ return columnSidecars;
1225
+ }
1226
+
480
1227
  /**
481
1228
  * From a set of shuffled peers:
482
1229
  * - fetch the block
@@ -498,6 +1245,7 @@ export class BlockInputSync {
498
1245
  }
499
1246
 
500
1247
  let i = 0;
1248
+ let deferredByRateLimit = false;
501
1249
  while (i++ < this.getMaxDownloadAttempts()) {
502
1250
  const pendingColumns =
503
1251
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
@@ -505,6 +1253,12 @@ export class BlockInputSync {
505
1253
  : defaultPendingColumns;
506
1254
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
507
1255
  if (peerMeta === null) {
1256
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1257
+ throw new UnknownBlockRateLimitedError(
1258
+ `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1259
+ );
1260
+ }
1261
+
508
1262
  // no more peer with needed columns to try, throw error
509
1263
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
510
1264
  this.metrics?.blockInputSync.fetchTimeSec.observe(
@@ -560,14 +1314,23 @@ export class BlockInputSync {
560
1314
  } else if (e instanceof RequestError) {
561
1315
  // should look into req_resp metrics in this case
562
1316
  downloadByRootMetrics?.error.inc({code: "req_resp", client: peerClient});
563
- switch (e.type.code) {
564
- case RequestErrorCode.REQUEST_RATE_LIMITED:
565
- case RequestErrorCode.REQUEST_TIMEOUT:
566
- // do not exclude peer for these errors
567
- break;
568
- default:
569
- excludedPeers.add(peerId);
570
- break;
1317
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1318
+ if (rateLimitedUntilMs !== null) {
1319
+ deferredByRateLimit = true;
1320
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1321
+ this.scheduleRateLimitBackoffRetry();
1322
+ } else {
1323
+ switch (e.type.code) {
1324
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1325
+ case RequestErrorCode.RESP_RATE_LIMITED:
1326
+ case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
1327
+ case RequestErrorCode.REQUEST_TIMEOUT:
1328
+ // do not exclude peer for these errors
1329
+ break;
1330
+ default:
1331
+ excludedPeers.add(peerId);
1332
+ break;
1333
+ }
571
1334
  }
572
1335
  } else {
573
1336
  // investigate if this happens
@@ -595,6 +1358,10 @@ export class BlockInputSync {
595
1358
 
596
1359
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
597
1360
 
1361
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1362
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1363
+ }
1364
+
598
1365
  if (!isPendingBlockInput(cacheItem)) {
599
1366
  throw Error(`${message} No block and no data was found.`);
600
1367
  }
@@ -660,6 +1427,28 @@ export class BlockInputSync {
660
1427
  pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
661
1428
  }
662
1429
 
1430
+ // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1431
+ private removePendingPayloadAndDescendants(rootHex: RootHex): void {
1432
+ // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1433
+ // later validation/finalization logic decides when it can leave memory.
1434
+ this.pendingPayloads.delete(rootHex);
1435
+
1436
+ const badPendingBlocks = getAllDescendantBlocks(rootHex, this.pendingBlocks);
1437
+ this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1438
+
1439
+ for (const block of badPendingBlocks) {
1440
+ const descendantRootHex = getBlockInputSyncCacheItemRootHex(block);
1441
+ this.pendingBlocks.delete(descendantRootHex);
1442
+ this.pendingPayloads.delete(descendantRootHex);
1443
+ this.chain.seenBlockInputCache.prune(descendantRootHex);
1444
+ this.logger.debug("Removing pending descendant after invalid parent payload", {
1445
+ slot: getBlockInputSyncCacheItemSlot(block),
1446
+ blockRoot: descendantRootHex,
1447
+ parentPayloadRoot: rootHex,
1448
+ });
1449
+ }
1450
+ }
1451
+
663
1452
  private removeAllDescendants(block: BlockInputSyncCacheItem): BlockInputSyncCacheItem[] {
664
1453
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
665
1454
  const slot = getBlockInputSyncCacheItemSlot(block);
@@ -671,7 +1460,10 @@ export class BlockInputSync {
671
1460
  for (const block of badPendingBlocks) {
672
1461
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
673
1462
  this.pendingBlocks.delete(rootHex);
1463
+ this.pendingPayloads.delete(rootHex);
674
1464
  this.chain.seenBlockInputCache.prune(rootHex);
1465
+ // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1466
+ // importBlock()-owned lifecycle.
675
1467
  this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
676
1468
  slot,
677
1469
  blockRoot: rootHex,
@@ -701,10 +1493,12 @@ export class BlockInputSync {
701
1493
  export class UnknownBlockPeerBalancer {
702
1494
  readonly peersMeta: Map<PeerIdStr, PeerSyncMeta>;
703
1495
  readonly activeRequests: Map<PeerIdStr, number>;
1496
+ readonly rateLimitedUntilByPeer: Map<PeerIdStr, number>;
704
1497
 
705
1498
  constructor() {
706
1499
  this.peersMeta = new Map();
707
1500
  this.activeRequests = new Map();
1501
+ this.rateLimitedUntilByPeer = new Map();
708
1502
  }
709
1503
 
710
1504
  /** Trigger on each peer re-status */
@@ -719,6 +1513,41 @@ export class UnknownBlockPeerBalancer {
719
1513
  onPeerDisconnected(peerId: PeerIdStr): void {
720
1514
  this.peersMeta.delete(peerId);
721
1515
  this.activeRequests.delete(peerId);
1516
+ this.rateLimitedUntilByPeer.delete(peerId);
1517
+ }
1518
+
1519
+ onRateLimited(peerId: PeerIdStr, rateLimitedUntilMs: number): void {
1520
+ this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
1521
+ }
1522
+
1523
+ getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
1524
+ const now = Date.now();
1525
+ let retryAt: number | null = null;
1526
+
1527
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
1528
+ if (rateLimitedUntil <= now) {
1529
+ this.rateLimitedUntilByPeer.delete(peerId);
1530
+ continue;
1531
+ }
1532
+
1533
+ if (excludedPeers?.has(peerId)) {
1534
+ continue;
1535
+ }
1536
+
1537
+ const syncMeta = this.peersMeta.get(peerId);
1538
+ if (syncMeta === undefined) {
1539
+ this.rateLimitedUntilByPeer.delete(peerId);
1540
+ continue;
1541
+ }
1542
+
1543
+ if (pendingColumns !== undefined && !this.peerHasPendingColumns(syncMeta, pendingColumns)) {
1544
+ continue;
1545
+ }
1546
+
1547
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
1548
+ }
1549
+
1550
+ return retryAt;
722
1551
  }
723
1552
 
724
1553
  /**
@@ -767,6 +1596,7 @@ export class UnknownBlockPeerBalancer {
767
1596
  }
768
1597
 
769
1598
  private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
1599
+ const now = Date.now();
770
1600
  let maxColumnCount = 0;
771
1601
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
772
1602
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -775,12 +1605,24 @@ export class UnknownBlockPeerBalancer {
775
1605
  continue;
776
1606
  }
777
1607
 
1608
+ const rateLimitedUntil = this.rateLimitedUntilByPeer.get(peerId);
1609
+ if (rateLimitedUntil !== undefined) {
1610
+ if (now < rateLimitedUntil) {
1611
+ continue;
1612
+ }
1613
+ this.rateLimitedUntilByPeer.delete(peerId);
1614
+ }
1615
+
778
1616
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
779
1617
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
780
1618
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
781
1619
  continue;
782
1620
  }
783
1621
 
1622
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1623
+ continue;
1624
+ }
1625
+
784
1626
  if (pendingDataColumns.size === 0) {
785
1627
  considerPeers.push({peerId, columnCount: 0});
786
1628
  continue;
@@ -814,4 +1656,12 @@ export class UnknownBlockPeerBalancer {
814
1656
 
815
1657
  return eligiblePeers;
816
1658
  }
1659
+
1660
+ private peerHasPendingColumns(syncMeta: PeerSyncMeta, pendingDataColumns: Set<number>): boolean {
1661
+ if (pendingDataColumns.size === 0) {
1662
+ return true;
1663
+ }
1664
+
1665
+ return syncMeta.custodyColumns.some((column) => pendingDataColumns.has(column));
1666
+ }
817
1667
  }