@lodestar/beacon-node 1.43.0-dev.bc569affb9 → 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 +16 -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,19 +1,25 @@
1
+ import { routes } from "@lodestar/api";
1
2
  import { ForkSeq } from "@lodestar/params";
2
3
  import { RequestError, RequestErrorCode } from "@lodestar/reqresp";
3
4
  import { computeTimeAtSlot } from "@lodestar/state-transition";
4
- import { prettyPrintIndices, pruneSetToMax, sleep } from "@lodestar/utils";
5
+ import { fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex } from "@lodestar/utils";
5
6
  import { isBlockInputBlobs, isBlockInputColumns } from "../chain/blocks/blockInput/blockInput.js";
6
7
  import { BlockInputSource } from "../chain/blocks/blockInput/types.js";
8
+ import { PayloadError, PayloadErrorCode } from "../chain/blocks/importExecutionPayload.js";
9
+ import { PayloadEnvelopeInputSource } from "../chain/blocks/payloadEnvelopeInput/index.js";
7
10
  import { BlockError, BlockErrorCode } from "../chain/errors/index.js";
8
11
  import { ChainEvent } from "../chain/index.js";
12
+ import { validateGloasBlockDataColumnSidecars } from "../chain/validation/dataColumnSidecar.js";
13
+ import { validateGossipExecutionPayloadEnvelope } from "../chain/validation/executionPayloadEnvelope.js";
9
14
  import { NetworkEvent, prettyPrintPeerIdStr } from "../network/index.js";
10
15
  import { shuffle } from "../util/shuffle.js";
11
16
  import { sortBy } from "../util/sortBy.js";
12
17
  import { wrapError } from "../util/wrapError.js";
13
18
  import { MAX_CONCURRENT_REQUESTS } from "./constants.js";
14
- import { PendingBlockInputStatus, PendingBlockType, getBlockInputSyncCacheItemRootHex, getBlockInputSyncCacheItemSlot, isPendingBlockInput, } from "./types.js";
19
+ import { PendingBlockInputStatus, PendingBlockType, PendingPayloadInputStatus, getBlockInputSyncCacheItemRootHex, getBlockInputSyncCacheItemSlot, getPayloadSyncCacheItemRootHex, getPayloadSyncCacheItemSlot, isPendingBlockInput, isPendingPayloadEnvelope, isPendingPayloadInput, } from "./types.js";
15
20
  import { DownloadByRootError, downloadByRoot } from "./utils/downloadByRoot.js";
16
- import { getAllDescendantBlocks, getDescendantBlocks, getUnknownAndAncestorBlocks } from "./utils/pendingBlocksTree.js";
21
+ import { getAllDescendantBlocks, getUnknownAndAncestorBlocks } from "./utils/pendingBlocksTree.js";
22
+ import { getRateLimitedUntilMs } from "./utils/rateLimit.js";
17
23
  const MAX_ATTEMPTS_PER_BLOCK = 5;
18
24
  const MAX_KNOWN_BAD_BLOCKS = 500;
19
25
  const MAX_PENDING_BLOCKS = 100;
@@ -25,6 +31,12 @@ var FetchResult;
25
31
  FetchResult["FailureTriedAllPeers"] = "failure_tried_all_peers";
26
32
  FetchResult["FailureMaxAttempts"] = "failure_max_attempts";
27
33
  })(FetchResult || (FetchResult = {}));
34
+ class UnknownBlockRateLimitedError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "UnknownBlockRateLimitedError";
38
+ }
39
+ }
28
40
  /**
29
41
  * BlockInputSync is a class that handles ReqResp to find blocks and data related to a specific blockRoot. The
30
42
  * blockRoot may have been found via object gossip, or the API. Gossip objects that can trigger a search are block,
@@ -68,10 +80,13 @@ export class BlockInputSync {
68
80
  * block RootHex -> PendingBlock. To avoid finding same root at the same time
69
81
  */
70
82
  pendingBlocks = new Map();
83
+ // Payload sync is keyed by beacon block root as well, so block and payload queues can unblock each other.
84
+ pendingPayloads = new Map();
71
85
  knownBadBlocks = new Set();
72
86
  maxPendingBlocks;
73
87
  subscribedToNetworkEvents = false;
74
88
  peerBalancer;
89
+ rateLimitBackoffTimeout;
75
90
  constructor(config, network, chain, logger, metrics, opts) {
76
91
  this.config = config;
77
92
  this.network = network;
@@ -83,6 +98,7 @@ export class BlockInputSync {
83
98
  this.peerBalancer = new UnknownBlockPeerBalancer();
84
99
  if (metrics) {
85
100
  metrics.blockInputSync.pendingBlocks.addCollect(() => metrics.blockInputSync.pendingBlocks.set(this.pendingBlocks.size));
101
+ metrics.blockInputSync.pendingPayloads.addCollect(() => metrics.blockInputSync.pendingPayloads.set(this.pendingPayloads.size));
86
102
  metrics.blockInputSync.knownBadBlocks.addCollect(() => metrics.blockInputSync.knownBadBlocks.set(this.knownBadBlocks.size));
87
103
  }
88
104
  }
@@ -95,8 +111,12 @@ export class BlockInputSync {
95
111
  if (!this.subscribedToNetworkEvents) {
96
112
  this.logger.verbose("BlockInputSync enabled.");
97
113
  this.chain.emitter.on(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
114
+ this.chain.emitter.on(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
98
115
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
116
+ this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
99
117
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
118
+ this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
119
+ this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
100
120
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
101
121
  this.network.events.on(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
102
122
  this.subscribedToNetworkEvents = true;
@@ -104,9 +124,14 @@ export class BlockInputSync {
104
124
  }
105
125
  unsubscribeFromNetwork() {
106
126
  this.logger.verbose("BlockInputSync disabled.");
127
+ this.clearRateLimitBackoffTimer();
107
128
  this.chain.emitter.off(ChainEvent.unknownBlockRoot, this.onUnknownBlockRoot);
129
+ this.chain.emitter.off(ChainEvent.unknownEnvelopeBlockRoot, this.onUnknownEnvelopeBlockRoot);
108
130
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
131
+ this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
109
132
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
133
+ this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
134
+ this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
110
135
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
111
136
  this.network.events.off(NetworkEvent.peerDisconnected, this.onPeerDisconnected);
112
137
  this.subscribedToNetworkEvents = false;
@@ -145,12 +170,54 @@ export class BlockInputSync {
145
170
  this.logger.debug("Error handling incompleteBlockInput event", {}, e);
146
171
  }
147
172
  };
173
+ onUnknownEnvelopeBlockRoot = (data) => {
174
+ try {
175
+ this.addByPayloadRootHex(data.rootHex, data.peer);
176
+ this.triggerUnknownBlockSearch();
177
+ this.metrics?.blockInputSync.requests.inc({ type: PendingBlockType.UNKNOWN_DATA });
178
+ this.metrics?.blockInputSync.source.inc({ source: data.source });
179
+ }
180
+ catch (e) {
181
+ this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e);
182
+ }
183
+ };
184
+ onIncompletePayloadEnvelope = (data) => {
185
+ try {
186
+ this.addByPayloadInput(data.payloadInput, data.peer);
187
+ this.triggerUnknownBlockSearch();
188
+ this.metrics?.blockInputSync.requests.inc({ type: PendingBlockType.UNKNOWN_DATA });
189
+ this.metrics?.blockInputSync.source.inc({ source: data.source });
190
+ }
191
+ catch (e) {
192
+ this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e);
193
+ }
194
+ };
148
195
  /**
149
196
  * Process an unknownBlockParent event and register the block in `pendingBlocks` Map.
150
197
  */
151
198
  onUnknownParent = (data) => {
152
199
  try {
153
- this.addByRootHex(data.blockInput.parentRootHex, data.peer);
200
+ const missingDependency = this.getMissingBlockDependency(data.blockInput);
201
+ if (missingDependency.kind === "invalidParentPayload") {
202
+ this.addByBlockInput(data.blockInput, data.peer);
203
+ const pendingBlock = this.pendingBlocks.get(data.blockInput.blockRootHex);
204
+ if (pendingBlock && isPendingBlockInput(pendingBlock)) {
205
+ this.logger.debug("Ignoring block with conflicting parent payload hash", {
206
+ slot: pendingBlock.blockInput.slot,
207
+ root: pendingBlock.blockInput.blockRootHex,
208
+ parentRoot: missingDependency.parentRootHex,
209
+ parentBlockHash: missingDependency.parentBlockHashHex,
210
+ });
211
+ this.removeAndDownScoreAllDescendants(pendingBlock);
212
+ }
213
+ return;
214
+ }
215
+ if (missingDependency.kind === "parentPayload") {
216
+ this.addByPayloadRootHex(missingDependency.rootHex, data.peer);
217
+ }
218
+ else if (missingDependency.kind === "parentBlock") {
219
+ this.addByRootHex(missingDependency.rootHex, data.peer);
220
+ }
154
221
  this.addByBlockInput(data.blockInput, data.peer);
155
222
  this.triggerUnknownBlockSearch();
156
223
  this.metrics?.blockInputSync.requests.inc({ type: PendingBlockType.UNKNOWN_PARENT });
@@ -160,8 +227,18 @@ export class BlockInputSync {
160
227
  this.logger.debug("Error handling unknownParent event", {}, e);
161
228
  }
162
229
  };
230
+ onBlockImported = () => {
231
+ if (this.pendingPayloads.size > 0) {
232
+ this.triggerUnknownBlockSearch();
233
+ }
234
+ };
235
+ onPayloadImported = ({ blockRoot, }) => {
236
+ this.pendingPayloads.delete(blockRoot);
237
+ this.triggerUnknownBlockSearch();
238
+ };
163
239
  addByRootHex = (rootHex, peerIdStr) => {
164
240
  let pendingBlock = this.pendingBlocks.get(rootHex);
241
+ let added = false;
165
242
  if (!pendingBlock) {
166
243
  pendingBlock = {
167
244
  status: PendingBlockInputStatus.pending,
@@ -170,6 +247,7 @@ export class BlockInputSync {
170
247
  timeAddedSec: Date.now() / 1000,
171
248
  };
172
249
  this.pendingBlocks.set(rootHex, pendingBlock);
250
+ added = true;
173
251
  this.logger.verbose("Added new rootHex to BlockInputSync.pendingBlocks", {
174
252
  root: pendingBlock.rootHex,
175
253
  peerIdStr: peerIdStr ?? "unknown peer",
@@ -184,6 +262,7 @@ export class BlockInputSync {
184
262
  if (prunedItemCount > 0) {
185
263
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
186
264
  }
265
+ return added;
187
266
  };
188
267
  addByBlockInput = (blockInput, peerIdStr) => {
189
268
  let pendingBlock = this.pendingBlocks.get(blockInput.blockRootHex);
@@ -211,6 +290,43 @@ export class BlockInputSync {
211
290
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
212
291
  }
213
292
  };
293
+ addByPayloadRootHex = (rootHex, peerIdStr) => {
294
+ let pendingPayload = this.pendingPayloads.get(rootHex);
295
+ let added = false;
296
+ if (!pendingPayload) {
297
+ pendingPayload = {
298
+ status: PendingPayloadInputStatus.pending,
299
+ rootHex,
300
+ peerIdStrings: new Set(),
301
+ timeAddedSec: Date.now() / 1000,
302
+ };
303
+ this.pendingPayloads.set(rootHex, pendingPayload);
304
+ added = true;
305
+ this.logger.verbose("Added new payload rootHex to BlockInputSync.pendingPayloads", {
306
+ root: rootHex,
307
+ peerIdStr: peerIdStr ?? "unknown peer",
308
+ });
309
+ }
310
+ if (peerIdStr) {
311
+ pendingPayload.peerIdStrings.add(peerIdStr);
312
+ }
313
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
314
+ if (prunedItemCount > 0) {
315
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
316
+ }
317
+ return added;
318
+ };
319
+ addByPayloadInput = (payloadInput, peerIdStr, envelope) => {
320
+ const pendingPayload = this.toPendingPayloadInput(payloadInput, this.pendingPayloads.get(payloadInput.blockRootHex), envelope);
321
+ if (peerIdStr) {
322
+ pendingPayload.peerIdStrings.add(peerIdStr);
323
+ }
324
+ this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
325
+ const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
326
+ if (prunedItemCount > 0) {
327
+ this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
328
+ }
329
+ };
214
330
  onPeerConnected = (data) => {
215
331
  try {
216
332
  const peerId = data.peer;
@@ -225,49 +341,197 @@ export class BlockInputSync {
225
341
  onPeerDisconnected = (data) => {
226
342
  const peerId = data.peer;
227
343
  this.peerBalancer.onPeerDisconnected(peerId);
344
+ this.scheduleRateLimitBackoffRetry();
228
345
  };
346
+ /**
347
+ * Post-gloas, a locally complete block can still be blocked on its parent's execution payload lineage.
348
+ * Distinguish which dependency is missing so the scheduler can enqueue the right follow-up work.
349
+ */
350
+ getMissingBlockDependency(blockInput) {
351
+ const parentRootHex = blockInput.parentRootHex;
352
+ if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) {
353
+ return { kind: "parentBlock", rootHex: parentRootHex };
354
+ }
355
+ if (!blockInput.hasBlock()) {
356
+ return { kind: "block", rootHex: blockInput.blockRootHex };
357
+ }
358
+ if (this.config.getForkSeq(blockInput.slot) < ForkSeq.gloas) {
359
+ return { kind: "ready" };
360
+ }
361
+ const block = blockInput.getBlock();
362
+ const parentBlockHashHex = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
363
+ if (this.chain.forkChoice.getBlockHexAndBlockHash(parentRootHex, parentBlockHashHex) !== null) {
364
+ return { kind: "ready" };
365
+ }
366
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
367
+ return { kind: "invalidParentPayload", parentRootHex, parentBlockHashHex };
368
+ }
369
+ const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);
370
+ if (parentPayloadInput) {
371
+ if (parentPayloadInput.getBlockHashHex() === parentBlockHashHex) {
372
+ return { kind: "parentPayload", rootHex: parentRootHex };
373
+ }
374
+ return { kind: "invalidParentPayload", parentRootHex, parentBlockHashHex };
375
+ }
376
+ return { kind: "parentPayload", rootHex: parentRootHex };
377
+ }
378
+ advancePendingBlock(pendingBlock) {
379
+ const missingDependency = this.getMissingBlockDependency(pendingBlock.blockInput);
380
+ switch (missingDependency.kind) {
381
+ case "ready":
382
+ return "ready";
383
+ case "block":
384
+ pendingBlock.status = PendingBlockInputStatus.pending;
385
+ return "queued_block";
386
+ case "parentBlock": {
387
+ let added = this.addByRootHex(missingDependency.rootHex);
388
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
389
+ added = this.addByRootHex(missingDependency.rootHex, peerIdStr) || added;
390
+ }
391
+ return added ? "queued_parent_block" : "blocked";
392
+ }
393
+ case "parentPayload": {
394
+ let added = this.addByPayloadRootHex(missingDependency.rootHex);
395
+ for (const peerIdStr of pendingBlock.peerIdStrings) {
396
+ added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr) || added;
397
+ }
398
+ return added ? "queued_parent_payload" : "blocked";
399
+ }
400
+ case "invalidParentPayload":
401
+ this.logger.debug("Removing block with conflicting parent payload hash", {
402
+ slot: pendingBlock.blockInput.slot,
403
+ root: pendingBlock.blockInput.blockRootHex,
404
+ parentRoot: missingDependency.parentRootHex,
405
+ parentBlockHash: missingDependency.parentBlockHashHex,
406
+ });
407
+ this.removeAndDownScoreAllDescendants(pendingBlock);
408
+ return "removed";
409
+ }
410
+ }
411
+ toPendingPayloadInput(payloadInput, previous, envelope) {
412
+ // Normalize every payload queueing path into the same cache shape while preserving first-seen
413
+ // timing and peer provenance from any earlier by-root or envelope-only entry.
414
+ const queuedEnvelope = envelope ?? (previous && isPendingPayloadEnvelope(previous) ? previous.envelope : undefined);
415
+ if (queuedEnvelope && !payloadInput.hasPayloadEnvelope()) {
416
+ payloadInput.addPayloadEnvelope({
417
+ envelope: queuedEnvelope,
418
+ source: PayloadEnvelopeInputSource.byRoot,
419
+ seenTimestampSec: Date.now() / 1000,
420
+ });
421
+ }
422
+ return {
423
+ status: payloadInput.isComplete() ? PendingPayloadInputStatus.downloaded : PendingPayloadInputStatus.pending,
424
+ payloadInput,
425
+ timeAddedSec: previous?.timeAddedSec ?? Date.now() / 1000,
426
+ timeSyncedSec: payloadInput.isComplete() ? Date.now() / 1000 : undefined,
427
+ peerIdStrings: new Set(previous?.peerIdStrings ?? []),
428
+ };
429
+ }
229
430
  /**
230
431
  * Gather tip parent blocks with unknown parent and do a search for all of them
231
432
  */
232
433
  triggerUnknownBlockSearch = () => {
233
434
  // Cheap early stop to prevent calling the network.getConnectedPeers()
234
- if (this.pendingBlocks.size === 0) {
435
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
235
436
  return;
236
437
  }
237
- // If the node loses all peers with pending unknown blocks, the sync will stall
438
+ // If the node loses all peers with pending unknown blocks or payloads, the sync will stall
238
439
  const connectedPeers = this.network.getConnectedPeers();
239
- if (connectedPeers.length === 0) {
240
- this.logger.debug("No connected peers, skipping unknown block search.");
241
- return;
242
- }
440
+ const hasConnectedPeers = connectedPeers.length > 0;
243
441
  const { unknowns, ancestors } = getUnknownAndAncestorBlocks(this.pendingBlocks);
244
- // it's rare when there is no unknown block
245
- // see https://github.com/ChainSafe/lodestar/issues/5649#issuecomment-1594213550
246
- if (unknowns.length === 0) {
247
- let processedBlocks = 0;
248
- for (const block of ancestors) {
249
- // when this happens, it's likely the block and parent block are processed by head sync
250
- if (this.chain.forkChoice.hasBlockHex(block.blockInput.parentRootHex)) {
442
+ let processedBlocks = 0;
443
+ let shouldRerunBlockSearch = false;
444
+ for (const block of ancestors) {
445
+ const advanceResult = this.advancePendingBlock(block);
446
+ switch (advanceResult) {
447
+ case "ready":
251
448
  processedBlocks++;
252
- this.processBlock(block).catch((e) => {
449
+ this.processReadyBlock(block).catch((e) => {
253
450
  this.logger.debug("Unexpected error - process old downloaded block", {}, e);
254
451
  });
452
+ break;
453
+ case "queued_block":
454
+ case "queued_parent_block":
455
+ shouldRerunBlockSearch = true;
456
+ break;
457
+ case "queued_parent_payload":
458
+ case "blocked":
459
+ case "removed":
460
+ break;
461
+ }
462
+ }
463
+ if (unknowns.length > 0) {
464
+ if (!hasConnectedPeers) {
465
+ this.logger.debug("No connected peers, skipping unknown block download.");
466
+ }
467
+ else {
468
+ // Most of the time there is exactly 1 unknown block
469
+ for (const block of unknowns) {
470
+ this.downloadBlock(block).catch((e) => {
471
+ this.logger.debug("Unexpected error - downloadBlock", { root: getBlockInputSyncCacheItemRootHex(block) }, e);
472
+ });
255
473
  }
256
474
  }
475
+ }
476
+ else if (ancestors.length > 0) {
477
+ // It's rare when there is no unknown block
478
+ // see https://github.com/ChainSafe/lodestar/issues/5649#issuecomment-1594213550
257
479
  this.logger.verbose("No unknown block, process ancestor downloaded blocks", {
258
480
  pendingBlocks: this.pendingBlocks.size,
259
481
  ancestorBlocks: ancestors.length,
260
482
  processedBlocks,
261
483
  });
262
- return;
263
484
  }
264
- // most of the time there is exactly 1 unknown block
265
- for (const block of unknowns) {
266
- this.downloadBlock(block).catch((e) => {
267
- this.logger.debug("Unexpected error - downloadBlock", { root: getBlockInputSyncCacheItemRootHex(block) }, e);
485
+ // Blocks can unblock payloads and payloads can unblock blocks, so every scheduler pass services both queues.
486
+ for (const payload of Array.from(this.pendingPayloads.values())) {
487
+ if (isPendingPayloadInput(payload) && payload.status === PendingPayloadInputStatus.downloaded) {
488
+ this.processPayload(payload).catch((e) => {
489
+ this.logger.debug("Unexpected error - process downloaded payload", {}, e);
490
+ });
491
+ continue;
492
+ }
493
+ if (isPendingPayloadEnvelope(payload)) {
494
+ this.reconcilePayloadEnvelope(payload).catch((e) => {
495
+ this.logger.debug("Unexpected error - reconcile pending payload envelope", {}, e);
496
+ });
497
+ continue;
498
+ }
499
+ if (!hasConnectedPeers) {
500
+ this.logger.debug("No connected peers, skipping unknown payload download.", {
501
+ root: getPayloadSyncCacheItemRootHex(payload),
502
+ });
503
+ continue;
504
+ }
505
+ this.downloadPayload(payload).catch((e) => {
506
+ this.logger.debug("Unexpected error - downloadPayload", { root: getPayloadSyncCacheItemRootHex(payload) }, e);
268
507
  });
269
508
  }
509
+ if (shouldRerunBlockSearch) {
510
+ this.triggerUnknownBlockSearch();
511
+ }
270
512
  };
513
+ scheduleRateLimitBackoffRetry() {
514
+ this.clearRateLimitBackoffTimer();
515
+ if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
516
+ return;
517
+ }
518
+ const now = Date.now();
519
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
520
+ if (retryAt === null) {
521
+ return;
522
+ }
523
+ this.rateLimitBackoffTimeout = setTimeout(() => {
524
+ this.rateLimitBackoffTimeout = undefined;
525
+ this.triggerUnknownBlockSearch();
526
+ this.scheduleRateLimitBackoffRetry();
527
+ }, Math.max(0, retryAt - now));
528
+ }
529
+ clearRateLimitBackoffTimer() {
530
+ if (this.rateLimitBackoffTimeout !== undefined) {
531
+ clearTimeout(this.rateLimitBackoffTimeout);
532
+ this.rateLimitBackoffTimeout = undefined;
533
+ }
534
+ }
271
535
  async downloadBlock(block) {
272
536
  if (block.status !== PendingBlockInputStatus.pending) {
273
537
  return;
@@ -297,10 +561,24 @@ export class BlockInputSync {
297
561
  };
298
562
  this.logger.verbose("Downloaded unknown block", logCtx2);
299
563
  if (parentInForkChoice) {
300
- // Bingo! Process block. Add to pending blocks anyway for recycle the cache that prevents duplicate processing
301
- this.processBlock(pending).catch((e) => {
302
- this.logger.debug("Unexpected error - process newly downloaded block", logCtx2, e);
303
- });
564
+ // If the direct parent is already in fork choice, let the block state machine decide if
565
+ // the next step is block import, parent payload download, or branch removal.
566
+ const advanceResult = this.advancePendingBlock(pending);
567
+ switch (advanceResult) {
568
+ case "ready":
569
+ this.processReadyBlock(pending).catch((e) => {
570
+ this.logger.debug("Unexpected error - process newly downloaded block", logCtx2, e);
571
+ });
572
+ break;
573
+ case "queued_block":
574
+ case "queued_parent_block":
575
+ case "queued_parent_payload":
576
+ this.triggerUnknownBlockSearch();
577
+ break;
578
+ case "blocked":
579
+ case "removed":
580
+ break;
581
+ }
304
582
  }
305
583
  else if (blockSlot <= finalizedSlot) {
306
584
  // the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
@@ -319,32 +597,26 @@ export class BlockInputSync {
319
597
  }
320
598
  }
321
599
  else {
600
+ if (res.err instanceof UnknownBlockRateLimitedError) {
601
+ const pendingBlock = this.pendingBlocks.get(rootHex);
602
+ if (pendingBlock) {
603
+ pendingBlock.status = PendingBlockInputStatus.pending;
604
+ }
605
+ this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
606
+ this.scheduleRateLimitBackoffRetry();
607
+ return;
608
+ }
322
609
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
323
610
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
324
611
  this.removeAndDownScoreAllDescendants(block);
325
612
  }
326
613
  }
327
614
  /**
328
- * Send block to the processor awaiting completition. If processed successfully, send all children to the processor.
329
- * On error, remove and downscore all descendants.
330
- * This function could run recursively for all descendant blocks
615
+ * Import a block that has already passed the local dependency checks in BlockInputSync.
616
+ * On error, remove and downscore descendants as appropriate for the failure type.
331
617
  */
332
- async processBlock(pendingBlock) {
333
- // pending block status is `downloaded` right after `downloadBlock`
334
- // but could be `pending` if added by `onUnknownBlockParent` event and this function is called recursively
618
+ async processReadyBlock(pendingBlock) {
335
619
  if (pendingBlock.status !== PendingBlockInputStatus.downloaded) {
336
- if (pendingBlock.status === PendingBlockInputStatus.pending) {
337
- const connectedPeers = this.network.getConnectedPeers();
338
- if (connectedPeers.length === 0) {
339
- this.logger.debug("No connected peers, skipping download block", {
340
- slot: pendingBlock.blockInput.slot,
341
- blockRoot: pendingBlock.blockInput.blockRootHex,
342
- });
343
- return;
344
- }
345
- // if the download is a success we'll call `processBlock()` for this block
346
- await this.downloadBlock(pendingBlock);
347
- }
348
620
  return;
349
621
  }
350
622
  pendingBlock.status = PendingBlockInputStatus.processing;
@@ -384,14 +656,9 @@ export class BlockInputSync {
384
656
  if (!res.err) {
385
657
  // no need to update status to "processed", delete anyway
386
658
  this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex);
387
- // Send child blocks to the processor
388
- for (const descendantBlock of getDescendantBlocks(pendingBlock.blockInput.blockRootHex, this.pendingBlocks)) {
389
- if (isPendingBlockInput(descendantBlock)) {
390
- this.processBlock(descendantBlock).catch((e) => {
391
- this.logger.debug("Unexpected error - process descendant block", {}, e);
392
- });
393
- }
394
- }
659
+ // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
660
+ // are advanced through the same dependency checks as every other pending item.
661
+ this.triggerUnknownBlockSearch();
395
662
  }
396
663
  else {
397
664
  const errorData = { slot: pendingBlock.blockInput.slot, root: pendingBlock.blockInput.blockRootHex };
@@ -406,6 +673,14 @@ export class BlockInputSync {
406
673
  this.logger.debug("Attempted to process block but its parent was still unknown", errorData, res.err);
407
674
  pendingBlock.status = PendingBlockInputStatus.downloaded;
408
675
  break;
676
+ case BlockErrorCode.PARENT_PAYLOAD_UNKNOWN:
677
+ this.logger.error("processReadyBlock() hit unexpected parent payload dependency after readiness checks", {
678
+ ...errorData,
679
+ parentRoot: pendingBlock.blockInput.parentRootHex,
680
+ parentBlockHash: res.err.type.parentBlockHash,
681
+ }, res.err);
682
+ pendingBlock.status = PendingBlockInputStatus.downloaded;
683
+ break;
409
684
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
410
685
  // Removing the block(s) without penalizing the peers, hoping for EL to
411
686
  // recover on a latter download + verify attempt
@@ -424,6 +699,312 @@ export class BlockInputSync {
424
699
  }
425
700
  }
426
701
  }
702
+ /**
703
+ * Reconcile an envelope-first payload entry once the block import path has seeded its
704
+ * PayloadEnvelopeInput. This may queue block download, validate the speculative envelope, or
705
+ * downgrade back to by-root fetching when the cached envelope does not match the imported block.
706
+ */
707
+ async reconcilePayloadEnvelope(pendingPayload) {
708
+ const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
709
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
710
+ this.pendingPayloads.delete(rootHex);
711
+ return;
712
+ }
713
+ const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
714
+ if (!payloadInput) {
715
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
716
+ // Column commitments live on the block body, so an envelope-only entry has to pull the block first.
717
+ if (!this.pendingBlocks.has(rootHex)) {
718
+ this.addByRootHex(rootHex);
719
+ }
720
+ const pendingBlock = this.pendingBlocks.get(rootHex);
721
+ if (pendingBlock && this.network.getConnectedPeers().length > 0) {
722
+ await this.downloadBlock(pendingBlock);
723
+ }
724
+ }
725
+ else {
726
+ this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
727
+ root: rootHex,
728
+ });
729
+ }
730
+ return;
731
+ }
732
+ if (!payloadInput.hasPayloadEnvelope()) {
733
+ const validationResult = await wrapError(validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope));
734
+ if (validationResult.err) {
735
+ this.logger.debug("Pending payload envelope failed validation after block import, refetching by root", { slot: pendingPayload.envelope.message.payload.slotNumber, root: rootHex }, validationResult.err);
736
+ const pendingPayloadByRoot = {
737
+ status: PendingPayloadInputStatus.pending,
738
+ rootHex,
739
+ timeAddedSec: pendingPayload.timeAddedSec,
740
+ peerIdStrings: new Set(pendingPayload.peerIdStrings),
741
+ };
742
+ this.pendingPayloads.set(rootHex, pendingPayloadByRoot);
743
+ if (this.network.getConnectedPeers().length > 0) {
744
+ await this.downloadPayload(pendingPayloadByRoot);
745
+ }
746
+ return;
747
+ }
748
+ }
749
+ const upgradedPayload = this.toPendingPayloadInput(payloadInput, pendingPayload, pendingPayload.envelope);
750
+ this.pendingPayloads.set(rootHex, upgradedPayload);
751
+ if (upgradedPayload.status === PendingPayloadInputStatus.downloaded) {
752
+ await this.processPayload(upgradedPayload);
753
+ return;
754
+ }
755
+ await this.downloadPayload(upgradedPayload);
756
+ }
757
+ async downloadPayload(payload) {
758
+ if (isPendingPayloadEnvelope(payload)) {
759
+ await this.reconcilePayloadEnvelope(payload);
760
+ return;
761
+ }
762
+ const rootHex = getPayloadSyncCacheItemRootHex(payload);
763
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
764
+ this.pendingPayloads.delete(rootHex);
765
+ return;
766
+ }
767
+ if (payload.status !== PendingPayloadInputStatus.pending) {
768
+ return;
769
+ }
770
+ const logCtx = {
771
+ slot: getPayloadSyncCacheItemSlot(payload),
772
+ root: rootHex,
773
+ pendingPayloads: this.pendingPayloads.size,
774
+ };
775
+ this.logger.verbose("BlockInputSync.downloadPayload()", logCtx);
776
+ payload.status = PendingPayloadInputStatus.fetching;
777
+ const res = await wrapError(this.fetchPayloadInput(payload));
778
+ if (!res.err) {
779
+ const pendingPayload = res.result;
780
+ this.pendingPayloads.set(getPayloadSyncCacheItemRootHex(pendingPayload), pendingPayload);
781
+ if (isPendingPayloadEnvelope(pendingPayload)) {
782
+ await this.reconcilePayloadEnvelope(pendingPayload);
783
+ }
784
+ else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
785
+ await this.processPayload(pendingPayload);
786
+ }
787
+ return;
788
+ }
789
+ this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
790
+ if (!isPendingPayloadEnvelope(payload)) {
791
+ payload.status = PendingPayloadInputStatus.pending;
792
+ }
793
+ }
794
+ async processPayload(pendingPayload) {
795
+ const rootHex = pendingPayload.payloadInput.blockRootHex;
796
+ const logCtx = { slot: pendingPayload.payloadInput.slot, root: rootHex };
797
+ if (pendingPayload.status !== PendingPayloadInputStatus.downloaded) {
798
+ this.logger.debug("Skipping payload processing before payload input is downloaded", {
799
+ ...logCtx,
800
+ status: pendingPayload.status,
801
+ });
802
+ return;
803
+ }
804
+ if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
805
+ this.logger.debug("Payload already imported while processing unknown payload", logCtx);
806
+ this.pendingPayloads.delete(rootHex);
807
+ return;
808
+ }
809
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
810
+ this.logger.debug("Payload input is ready before its block is in fork choice", logCtx);
811
+ const added = this.addByRootHex(rootHex);
812
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
813
+ if (added) {
814
+ this.triggerUnknownBlockSearch();
815
+ }
816
+ return;
817
+ }
818
+ pendingPayload.status = PendingPayloadInputStatus.processing;
819
+ const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput));
820
+ if (!res.err) {
821
+ this.logger.debug("Processed payload from unknown sync", logCtx);
822
+ this.pendingPayloads.delete(rootHex);
823
+ this.triggerUnknownBlockSearch();
824
+ return;
825
+ }
826
+ if (res.err instanceof PayloadError) {
827
+ switch (res.err.type.code) {
828
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
829
+ // Payload sync discovered the block dependency before the block queue did. Re-enqueue the
830
+ // block and keep the payload ready so the scheduler can retry once the block reaches fork choice.
831
+ if (this.addByRootHex(rootHex)) {
832
+ this.triggerUnknownBlockSearch();
833
+ }
834
+ // Keep the payload out of any synchronous requeue pass; a later scheduler pass will retry it.
835
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
836
+ break;
837
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
838
+ this.logger.debug("Execution engine error while processing payload from unknown sync", logCtx, res.err);
839
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
840
+ break;
841
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
842
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
843
+ case PayloadErrorCode.INVALID_SIGNATURE:
844
+ // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
845
+ // reintroducing envelope replacement / recreation flows.
846
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
847
+ this.removePendingPayloadAndDescendants(rootHex);
848
+ break;
849
+ default:
850
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
851
+ this.pendingPayloads.delete(rootHex);
852
+ }
853
+ return;
854
+ }
855
+ this.logger.debug("Unknown error processing payload from unknown sync", logCtx, res.err);
856
+ pendingPayload.status = PendingPayloadInputStatus.downloaded;
857
+ }
858
+ /**
859
+ * Download payload material keyed by beacon block root. Unlike block download, payload sync may
860
+ * already have a locally cached envelope or partial columns, so each attempt starts from local state
861
+ * and only asks peers for the remaining pieces.
862
+ */
863
+ async fetchPayloadInput(cacheItem) {
864
+ const rootHex = getPayloadSyncCacheItemRootHex(cacheItem);
865
+ const blockRoot = fromHex(rootHex);
866
+ const excludedPeers = new Set();
867
+ let slot = getPayloadSyncCacheItemSlot(cacheItem);
868
+ let payloadInput = isPendingPayloadInput(cacheItem)
869
+ ? cacheItem.payloadInput
870
+ : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
871
+ let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
872
+ let i = 0;
873
+ let deferredByRateLimit = false;
874
+ while (i++ < this.getMaxDownloadAttempts()) {
875
+ const pendingColumns = payloadInput?.hasAllData()
876
+ ? new Set()
877
+ : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
878
+ const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
879
+ if (peerMeta === null) {
880
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
881
+ throw new UnknownBlockRateLimitedError(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`);
882
+ }
883
+ throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`);
884
+ }
885
+ const { peerId, client: peerClient } = peerMeta;
886
+ cacheItem.peerIdStrings.add(peerId);
887
+ try {
888
+ if (!envelope) {
889
+ envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
890
+ slot = envelope.message.payload.slotNumber;
891
+ }
892
+ payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
893
+ if (!payloadInput) {
894
+ if (this.chain.forkChoice.hasBlockHex(rootHex)) {
895
+ throw new Error(`Missing PayloadEnvelopeInput for known block ${rootHex}`);
896
+ }
897
+ // Keep the validated envelope around, but wait for the block body before turning it into a full payload input.
898
+ return {
899
+ status: PendingPayloadInputStatus.waitingForBlock,
900
+ envelope,
901
+ timeAddedSec: cacheItem.timeAddedSec,
902
+ peerIdStrings: cacheItem.peerIdStrings,
903
+ };
904
+ }
905
+ if (!payloadInput.hasPayloadEnvelope()) {
906
+ await validateGossipExecutionPayloadEnvelope(this.chain, envelope);
907
+ }
908
+ let pendingPayload = this.toPendingPayloadInput(payloadInput, cacheItem, envelope);
909
+ if (!pendingPayload.payloadInput.hasAllData()) {
910
+ const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
911
+ if (missing.length > 0) {
912
+ const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
913
+ const seenTimestampSec = Date.now() / 1000;
914
+ for (const columnSidecar of columnSidecars) {
915
+ if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
916
+ continue;
917
+ }
918
+ pendingPayload.payloadInput.addColumn({
919
+ columnSidecar,
920
+ source: PayloadEnvelopeInputSource.byRoot,
921
+ seenTimestampSec,
922
+ peerIdStr: peerId,
923
+ });
924
+ }
925
+ pendingPayload = this.toPendingPayloadInput(pendingPayload.payloadInput, pendingPayload);
926
+ }
927
+ }
928
+ this.logger.verbose("BlockInputSync.fetchPayloadInput: successful download", {
929
+ slot,
930
+ rootHex,
931
+ peerId,
932
+ peerClient,
933
+ hasPayload: pendingPayload.payloadInput.hasPayloadEnvelope(),
934
+ hasAllData: pendingPayload.payloadInput.hasAllData(),
935
+ });
936
+ if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
937
+ return pendingPayload;
938
+ }
939
+ cacheItem = pendingPayload;
940
+ payloadInput = pendingPayload.payloadInput;
941
+ }
942
+ catch (e) {
943
+ this.logger.debug("Error downloading payload in BlockInputSync.fetchPayloadInput", { slot, rootHex, attempt: i, peer: peerId, peerClient }, e);
944
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
945
+ if (rateLimitedUntilMs !== null) {
946
+ deferredByRateLimit = true;
947
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
948
+ this.scheduleRateLimitBackoffRetry();
949
+ }
950
+ else if (e instanceof RequestError) {
951
+ switch (e.type.code) {
952
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
953
+ case RequestErrorCode.REQUEST_TIMEOUT:
954
+ break;
955
+ default:
956
+ excludedPeers.add(peerId);
957
+ break;
958
+ }
959
+ }
960
+ else {
961
+ excludedPeers.add(peerId);
962
+ }
963
+ }
964
+ finally {
965
+ this.peerBalancer.onRequestCompleted(peerId);
966
+ }
967
+ }
968
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
969
+ throw new UnknownBlockRateLimitedError(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`);
970
+ }
971
+ throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
972
+ }
973
+ async fetchExecutionPayloadEnvelope(peerIdStr, blockRoot, rootHex) {
974
+ const response = await this.network.sendExecutionPayloadEnvelopesByRoot(peerIdStr, [blockRoot]);
975
+ const envelope = response.at(0);
976
+ if (!envelope) {
977
+ throw new Error(`Missing execution payload envelope for root=${rootHex}`);
978
+ }
979
+ const receivedRootHex = toRootHex(envelope.message.beaconBlockRoot);
980
+ if (receivedRootHex !== rootHex) {
981
+ throw new Error(`Execution payload envelope root mismatch requested=${rootHex} received=${receivedRootHex}`);
982
+ }
983
+ return envelope;
984
+ }
985
+ async fetchPayloadColumns(peerMeta, payloadInput, missing) {
986
+ const { peerId: peerIdStr } = peerMeta;
987
+ const peerColumns = new Set(peerMeta.custodyColumns ?? []);
988
+ const requestedColumns = missing.filter((columnIndex) => peerColumns.has(columnIndex));
989
+ if (requestedColumns.length === 0) {
990
+ return [];
991
+ }
992
+ const columnSidecars = (await this.network.sendDataColumnSidecarsByRoot(peerIdStr, [
993
+ { blockRoot: fromHex(payloadInput.blockRootHex), columns: requestedColumns },
994
+ ]));
995
+ if (columnSidecars.length === 0) {
996
+ throw new Error(`No data column sidecars returned for payload root=${payloadInput.blockRootHex}`);
997
+ }
998
+ const requestedColumnsSet = new Set(requestedColumns);
999
+ const extraColumns = columnSidecars.filter((columnSidecar) => !requestedColumnsSet.has(columnSidecar.index));
1000
+ if (extraColumns.length > 0) {
1001
+ throw new Error(`Received unexpected payload data columns indices=${prettyPrintIndices(extraColumns.map((column) => column.index))}`);
1002
+ }
1003
+ // PayloadEnvelopeInput already carries the block slot, root, and commitments, so reuse the
1004
+ // block-based Gloas validator rather than maintaining a second payload-specific variant.
1005
+ await validateGloasBlockDataColumnSidecars(payloadInput.slot, fromHex(payloadInput.blockRootHex), payloadInput.getBlobKzgCommitments(), columnSidecars, this.chain.metrics?.peerDas);
1006
+ return columnSidecars;
1007
+ }
427
1008
  /**
428
1009
  * From a set of shuffled peers:
429
1010
  * - fetch the block
@@ -443,12 +1024,16 @@ export class BlockInputSync {
443
1024
  this.metrics?.blockInputSync.fetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
444
1025
  }
445
1026
  let i = 0;
1027
+ let deferredByRateLimit = false;
446
1028
  while (i++ < this.getMaxDownloadAttempts()) {
447
1029
  const pendingColumns = isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
448
1030
  ? new Set(cacheItem.blockInput.getMissingSampledColumnMeta().missing)
449
1031
  : defaultPendingColumns;
450
1032
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
451
1033
  if (peerMeta === null) {
1034
+ if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1035
+ throw new UnknownBlockRateLimitedError(`Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`);
1036
+ }
452
1037
  // no more peer with needed columns to try, throw error
453
1038
  const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
454
1039
  this.metrics?.blockInputSync.fetchTimeSec.observe({ result: FetchResult.FailureTriedAllPeers }, Date.now() / 1000 - fetchStartSec);
@@ -496,14 +1081,24 @@ export class BlockInputSync {
496
1081
  else if (e instanceof RequestError) {
497
1082
  // should look into req_resp metrics in this case
498
1083
  downloadByRootMetrics?.error.inc({ code: "req_resp", client: peerClient });
499
- switch (e.type.code) {
500
- case RequestErrorCode.REQUEST_RATE_LIMITED:
501
- case RequestErrorCode.REQUEST_TIMEOUT:
502
- // do not exclude peer for these errors
503
- break;
504
- default:
505
- excludedPeers.add(peerId);
506
- break;
1084
+ const rateLimitedUntilMs = getRateLimitedUntilMs(e);
1085
+ if (rateLimitedUntilMs !== null) {
1086
+ deferredByRateLimit = true;
1087
+ this.peerBalancer.onRateLimited(peerId, rateLimitedUntilMs);
1088
+ this.scheduleRateLimitBackoffRetry();
1089
+ }
1090
+ else {
1091
+ switch (e.type.code) {
1092
+ case RequestErrorCode.REQUEST_RATE_LIMITED:
1093
+ case RequestErrorCode.RESP_RATE_LIMITED:
1094
+ case RequestErrorCode.REQUEST_SELF_RATE_LIMITED:
1095
+ case RequestErrorCode.REQUEST_TIMEOUT:
1096
+ // do not exclude peer for these errors
1097
+ break;
1098
+ default:
1099
+ excludedPeers.add(peerId);
1100
+ break;
1101
+ }
507
1102
  }
508
1103
  }
509
1104
  else {
@@ -529,6 +1124,9 @@ export class BlockInputSync {
529
1124
  }
530
1125
  } // end while loop over peers
531
1126
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1127
+ if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1128
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1129
+ }
532
1130
  if (!isPendingBlockInput(cacheItem)) {
533
1131
  throw Error(`${message} No block and no data was found.`);
534
1132
  }
@@ -582,6 +1180,25 @@ export class BlockInputSync {
582
1180
  // Prune knownBadBlocks
583
1181
  pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
584
1182
  }
1183
+ // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1184
+ removePendingPayloadAndDescendants(rootHex) {
1185
+ // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1186
+ // later validation/finalization logic decides when it can leave memory.
1187
+ this.pendingPayloads.delete(rootHex);
1188
+ const badPendingBlocks = getAllDescendantBlocks(rootHex, this.pendingBlocks);
1189
+ this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1190
+ for (const block of badPendingBlocks) {
1191
+ const descendantRootHex = getBlockInputSyncCacheItemRootHex(block);
1192
+ this.pendingBlocks.delete(descendantRootHex);
1193
+ this.pendingPayloads.delete(descendantRootHex);
1194
+ this.chain.seenBlockInputCache.prune(descendantRootHex);
1195
+ this.logger.debug("Removing pending descendant after invalid parent payload", {
1196
+ slot: getBlockInputSyncCacheItemSlot(block),
1197
+ blockRoot: descendantRootHex,
1198
+ parentPayloadRoot: rootHex,
1199
+ });
1200
+ }
1201
+ }
585
1202
  removeAllDescendants(block) {
586
1203
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
587
1204
  const slot = getBlockInputSyncCacheItemSlot(block);
@@ -591,7 +1208,10 @@ export class BlockInputSync {
591
1208
  for (const block of badPendingBlocks) {
592
1209
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
593
1210
  this.pendingBlocks.delete(rootHex);
1211
+ this.pendingPayloads.delete(rootHex);
594
1212
  this.chain.seenBlockInputCache.prune(rootHex);
1213
+ // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1214
+ // importBlock()-owned lifecycle.
595
1215
  this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
596
1216
  slot,
597
1217
  blockRoot: rootHex,
@@ -614,9 +1234,11 @@ export class BlockInputSync {
614
1234
  export class UnknownBlockPeerBalancer {
615
1235
  peersMeta;
616
1236
  activeRequests;
1237
+ rateLimitedUntilByPeer;
617
1238
  constructor() {
618
1239
  this.peersMeta = new Map();
619
1240
  this.activeRequests = new Map();
1241
+ this.rateLimitedUntilByPeer = new Map();
620
1242
  }
621
1243
  /** Trigger on each peer re-status */
622
1244
  onPeerConnected(peerId, syncMeta) {
@@ -628,6 +1250,33 @@ export class UnknownBlockPeerBalancer {
628
1250
  onPeerDisconnected(peerId) {
629
1251
  this.peersMeta.delete(peerId);
630
1252
  this.activeRequests.delete(peerId);
1253
+ this.rateLimitedUntilByPeer.delete(peerId);
1254
+ }
1255
+ onRateLimited(peerId, rateLimitedUntilMs) {
1256
+ this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
1257
+ }
1258
+ getNextRateLimitRetryAt(pendingColumns, excludedPeers) {
1259
+ const now = Date.now();
1260
+ let retryAt = null;
1261
+ for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
1262
+ if (rateLimitedUntil <= now) {
1263
+ this.rateLimitedUntilByPeer.delete(peerId);
1264
+ continue;
1265
+ }
1266
+ if (excludedPeers?.has(peerId)) {
1267
+ continue;
1268
+ }
1269
+ const syncMeta = this.peersMeta.get(peerId);
1270
+ if (syncMeta === undefined) {
1271
+ this.rateLimitedUntilByPeer.delete(peerId);
1272
+ continue;
1273
+ }
1274
+ if (pendingColumns !== undefined && !this.peerHasPendingColumns(syncMeta, pendingColumns)) {
1275
+ continue;
1276
+ }
1277
+ retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil);
1278
+ }
1279
+ return retryAt;
631
1280
  }
632
1281
  /**
633
1282
  * called from fetchBlockInput() where we only have block root and nothing else
@@ -667,6 +1316,7 @@ export class UnknownBlockPeerBalancer {
667
1316
  return totalActiveRequests;
668
1317
  }
669
1318
  filterPeers(pendingDataColumns, excludedPeers) {
1319
+ const now = Date.now();
670
1320
  let maxColumnCount = 0;
671
1321
  const considerPeers = [];
672
1322
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -674,11 +1324,21 @@ export class UnknownBlockPeerBalancer {
674
1324
  // made request to this peer already
675
1325
  continue;
676
1326
  }
1327
+ const rateLimitedUntil = this.rateLimitedUntilByPeer.get(peerId);
1328
+ if (rateLimitedUntil !== undefined) {
1329
+ if (now < rateLimitedUntil) {
1330
+ continue;
1331
+ }
1332
+ this.rateLimitedUntilByPeer.delete(peerId);
1333
+ }
677
1334
  const activeRequests = this.activeRequests.get(peerId) ?? 0;
678
1335
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
679
1336
  // should return peer with no more than MAX_CONCURRENT_REQUESTS active requests
680
1337
  continue;
681
1338
  }
1339
+ if (!this.peerHasPendingColumns(syncMeta, pendingDataColumns)) {
1340
+ continue;
1341
+ }
682
1342
  if (pendingDataColumns.size === 0) {
683
1343
  considerPeers.push({ peerId, columnCount: 0 });
684
1344
  continue;
@@ -708,5 +1368,11 @@ export class UnknownBlockPeerBalancer {
708
1368
  }
709
1369
  return eligiblePeers;
710
1370
  }
1371
+ peerHasPendingColumns(syncMeta, pendingDataColumns) {
1372
+ if (pendingDataColumns.size === 0) {
1373
+ return true;
1374
+ }
1375
+ return syncMeta.custodyColumns.some((column) => pendingDataColumns.has(column));
1376
+ }
711
1377
  }
712
1378
  //# sourceMappingURL=unknownBlock.js.map