@lodestar/beacon-node 1.47.0-dev.09607ce6d4 → 1.47.0-dev.101e2c290d

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 (303) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -2
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +21 -2
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/beacon/state/utils.d.ts +2 -1
  8. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  9. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  10. package/lib/api/impl/events/index.d.ts +1 -1
  11. package/lib/api/impl/events/index.d.ts.map +1 -1
  12. package/lib/api/impl/events/index.js +12 -3
  13. package/lib/api/impl/events/index.js.map +1 -1
  14. package/lib/api/impl/lodestar/index.js +1 -1
  15. package/lib/api/impl/lodestar/index.js.map +1 -1
  16. package/lib/api/rest/base.d.ts.map +1 -1
  17. package/lib/api/rest/base.js +7 -0
  18. package/lib/api/rest/base.js.map +1 -1
  19. package/lib/chain/archiveStore/archiveStore.js +1 -1
  20. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  21. package/lib/chain/blocks/importExecutionPayload.d.ts +3 -2
  22. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  23. package/lib/chain/blocks/importExecutionPayload.js +11 -9
  24. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  25. package/lib/chain/blocks/index.d.ts.map +1 -1
  26. package/lib/chain/blocks/index.js +14 -6
  27. package/lib/chain/blocks/index.js.map +1 -1
  28. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +2 -1
  29. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  30. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +6 -2
  31. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  32. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +14 -5
  33. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  34. package/lib/chain/blocks/payloadEnvelopeInput/types.js +5 -0
  35. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -1
  36. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  37. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +22 -12
  38. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  39. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  40. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  42. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  43. package/lib/chain/bls/index.d.ts +1 -1
  44. package/lib/chain/bls/index.d.ts.map +1 -1
  45. package/lib/chain/bls/interface.d.ts +5 -5
  46. package/lib/chain/bls/interface.d.ts.map +1 -1
  47. package/lib/chain/bls/multithread/index.d.ts +5 -11
  48. package/lib/chain/bls/multithread/index.d.ts.map +1 -1
  49. package/lib/chain/bls/multithread/index.js +116 -101
  50. package/lib/chain/bls/multithread/index.js.map +1 -1
  51. package/lib/chain/bls/multithread/jobItem.d.ts +5 -19
  52. package/lib/chain/bls/multithread/jobItem.d.ts.map +1 -1
  53. package/lib/chain/bls/multithread/jobItem.js +10 -65
  54. package/lib/chain/bls/multithread/jobItem.js.map +1 -1
  55. package/lib/chain/bls/multithread/types.d.ts +29 -8
  56. package/lib/chain/bls/multithread/types.d.ts.map +1 -1
  57. package/lib/chain/bls/multithread/types.js +12 -0
  58. package/lib/chain/bls/multithread/types.js.map +1 -1
  59. package/lib/chain/bls/multithread/utils.d.ts +4 -0
  60. package/lib/chain/bls/multithread/utils.d.ts.map +1 -1
  61. package/lib/chain/bls/multithread/utils.js +10 -0
  62. package/lib/chain/bls/multithread/utils.js.map +1 -1
  63. package/lib/chain/bls/multithread/worker.js +49 -23
  64. package/lib/chain/bls/multithread/worker.js.map +1 -1
  65. package/lib/chain/bls/singleThread.d.ts +4 -10
  66. package/lib/chain/bls/singleThread.d.ts.map +1 -1
  67. package/lib/chain/bls/singleThread.js +8 -45
  68. package/lib/chain/bls/singleThread.js.map +1 -1
  69. package/lib/chain/bls/utils.d.ts +4 -4
  70. package/lib/chain/bls/utils.d.ts.map +1 -1
  71. package/lib/chain/bls/utils.js +18 -19
  72. package/lib/chain/bls/utils.js.map +1 -1
  73. package/lib/chain/chain.d.ts +4 -2
  74. package/lib/chain/chain.d.ts.map +1 -1
  75. package/lib/chain/chain.js +16 -6
  76. package/lib/chain/chain.js.map +1 -1
  77. package/lib/chain/errors/blockError.d.ts +1 -0
  78. package/lib/chain/errors/blockError.d.ts.map +1 -1
  79. package/lib/chain/errors/blockError.js +6 -1
  80. package/lib/chain/errors/blockError.js.map +1 -1
  81. package/lib/chain/errors/proposerPreferences.d.ts +6 -0
  82. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  83. package/lib/chain/errors/proposerPreferences.js +1 -0
  84. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  85. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  86. package/lib/chain/forkChoice/index.js +4 -0
  87. package/lib/chain/forkChoice/index.js.map +1 -1
  88. package/lib/chain/interface.d.ts +4 -2
  89. package/lib/chain/interface.d.ts.map +1 -1
  90. package/lib/chain/interface.js.map +1 -1
  91. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  92. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  93. package/lib/chain/opPools/attestationPool.d.ts +1 -1
  94. package/lib/chain/opPools/attestationPool.d.ts.map +1 -1
  95. package/lib/chain/opPools/attestationPool.js +1 -1
  96. package/lib/chain/opPools/attestationPool.js.map +1 -1
  97. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts +16 -0
  98. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts.map +1 -0
  99. package/lib/chain/opPools/deferredVoluntaryExitPool.js +57 -0
  100. package/lib/chain/opPools/deferredVoluntaryExitPool.js.map +1 -0
  101. package/lib/chain/opPools/index.d.ts +1 -0
  102. package/lib/chain/opPools/index.d.ts.map +1 -1
  103. package/lib/chain/opPools/index.js +1 -0
  104. package/lib/chain/opPools/index.js.map +1 -1
  105. package/lib/chain/opPools/payloadAttestationPool.js +3 -3
  106. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  107. package/lib/chain/opPools/syncCommitteeMessagePool.js +1 -1
  108. package/lib/chain/opPools/syncCommitteeMessagePool.js.map +1 -1
  109. package/lib/chain/opPools/syncContributionAndProofPool.js +1 -1
  110. package/lib/chain/opPools/syncContributionAndProofPool.js.map +1 -1
  111. package/lib/chain/opPools/utils.d.ts +1 -1
  112. package/lib/chain/opPools/utils.d.ts.map +1 -1
  113. package/lib/chain/opPools/utils.js +1 -1
  114. package/lib/chain/opPools/utils.js.map +1 -1
  115. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  116. package/lib/chain/prepareNextSlot.js +23 -5
  117. package/lib/chain/prepareNextSlot.js.map +1 -1
  118. package/lib/chain/regen/interface.d.ts +4 -2
  119. package/lib/chain/regen/interface.d.ts.map +1 -1
  120. package/lib/chain/regen/interface.js +2 -0
  121. package/lib/chain/regen/interface.js.map +1 -1
  122. package/lib/chain/regen/queued.d.ts +2 -2
  123. package/lib/chain/regen/queued.d.ts.map +1 -1
  124. package/lib/chain/regen/queued.js +5 -5
  125. package/lib/chain/regen/queued.js.map +1 -1
  126. package/lib/chain/seenCache/seenAttesters.d.ts +9 -5
  127. package/lib/chain/seenCache/seenAttesters.d.ts.map +1 -1
  128. package/lib/chain/seenCache/seenAttesters.js +25 -4
  129. package/lib/chain/seenCache/seenAttesters.js.map +1 -1
  130. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +28 -1
  131. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  132. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +121 -13
  133. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  134. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +5 -1
  135. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  136. package/lib/chain/stateCache/fifoBlockStateCache.js +6 -0
  137. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  138. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +47 -5
  139. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  140. package/lib/chain/stateCache/persistentCheckpointsCache.js +117 -7
  141. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  142. package/lib/chain/stateCache/types.d.ts +3 -3
  143. package/lib/chain/stateCache/types.d.ts.map +1 -1
  144. package/lib/chain/validation/attestation.d.ts.map +1 -1
  145. package/lib/chain/validation/attestation.js +1 -4
  146. package/lib/chain/validation/attestation.js.map +1 -1
  147. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  148. package/lib/chain/validation/attesterSlashing.js +1 -1
  149. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  150. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  151. package/lib/chain/validation/executionPayloadBid.js +1 -2
  152. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  153. package/lib/chain/validation/executionPayloadEnvelope.js +1 -1
  154. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  155. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  156. package/lib/chain/validation/payloadAttestationMessage.js +5 -14
  157. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  158. package/lib/chain/validation/proposerPreferences.d.ts +1 -1
  159. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  160. package/lib/chain/validation/proposerPreferences.js +13 -4
  161. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  162. package/lib/chain/validation/proposerSlashing.js +1 -1
  163. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  164. package/lib/chain/validation/voluntaryExit.d.ts +9 -1
  165. package/lib/chain/validation/voluntaryExit.d.ts.map +1 -1
  166. package/lib/chain/validation/voluntaryExit.js +46 -1
  167. package/lib/chain/validation/voluntaryExit.js.map +1 -1
  168. package/lib/metrics/metrics/lodestar.d.ts +62 -22
  169. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  170. package/lib/metrics/metrics/lodestar.js +114 -52
  171. package/lib/metrics/metrics/lodestar.js.map +1 -1
  172. package/lib/network/gossip/topic.d.ts +13 -0
  173. package/lib/network/gossip/topic.d.ts.map +1 -1
  174. package/lib/network/gossip/topic.js +37 -0
  175. package/lib/network/gossip/topic.js.map +1 -1
  176. package/lib/network/libp2p/index.d.ts.map +1 -1
  177. package/lib/network/libp2p/index.js +10 -2
  178. package/lib/network/libp2p/index.js.map +1 -1
  179. package/lib/network/network.d.ts.map +1 -1
  180. package/lib/network/network.js +22 -43
  181. package/lib/network/network.js.map +1 -1
  182. package/lib/network/processor/constants.d.ts +7 -0
  183. package/lib/network/processor/constants.d.ts.map +1 -0
  184. package/lib/network/processor/constants.js +7 -0
  185. package/lib/network/processor/constants.js.map +1 -0
  186. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  187. package/lib/network/processor/gossipHandlers.js +5 -1
  188. package/lib/network/processor/gossipHandlers.js.map +1 -1
  189. package/lib/network/processor/index.d.ts +3 -0
  190. package/lib/network/processor/index.d.ts.map +1 -1
  191. package/lib/network/processor/index.js +36 -15
  192. package/lib/network/processor/index.js.map +1 -1
  193. package/lib/network/reqresp/score.d.ts.map +1 -1
  194. package/lib/network/reqresp/score.js +5 -11
  195. package/lib/network/reqresp/score.js.map +1 -1
  196. package/lib/network/util.d.ts +0 -1
  197. package/lib/network/util.d.ts.map +1 -1
  198. package/lib/network/util.js +0 -4
  199. package/lib/network/util.js.map +1 -1
  200. package/lib/node/deferredVoluntaryExitPublisher.d.ts +15 -0
  201. package/lib/node/deferredVoluntaryExitPublisher.d.ts.map +1 -0
  202. package/lib/node/deferredVoluntaryExitPublisher.js +38 -0
  203. package/lib/node/deferredVoluntaryExitPublisher.js.map +1 -0
  204. package/lib/node/nodejs.d.ts +2 -1
  205. package/lib/node/nodejs.d.ts.map +1 -1
  206. package/lib/node/nodejs.js +2 -0
  207. package/lib/node/nodejs.js.map +1 -1
  208. package/lib/sync/range/batch.d.ts +23 -5
  209. package/lib/sync/range/batch.d.ts.map +1 -1
  210. package/lib/sync/range/batch.js +61 -22
  211. package/lib/sync/range/batch.js.map +1 -1
  212. package/lib/sync/range/chain.d.ts +2 -1
  213. package/lib/sync/range/chain.d.ts.map +1 -1
  214. package/lib/sync/range/chain.js +23 -10
  215. package/lib/sync/range/chain.js.map +1 -1
  216. package/lib/sync/types.d.ts +32 -0
  217. package/lib/sync/types.d.ts.map +1 -1
  218. package/lib/sync/types.js +33 -0
  219. package/lib/sync/types.js.map +1 -1
  220. package/lib/sync/unknownBlock.d.ts +13 -2
  221. package/lib/sync/unknownBlock.d.ts.map +1 -1
  222. package/lib/sync/unknownBlock.js +204 -61
  223. package/lib/sync/unknownBlock.js.map +1 -1
  224. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  225. package/lib/sync/utils/downloadByRange.js +16 -11
  226. package/lib/sync/utils/downloadByRange.js.map +1 -1
  227. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  228. package/lib/sync/utils/downloadByRoot.js +3 -1
  229. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  230. package/package.json +35 -35
  231. package/src/api/impl/beacon/blocks/index.ts +3 -2
  232. package/src/api/impl/beacon/pool/index.ts +22 -2
  233. package/src/api/impl/beacon/state/utils.ts +2 -1
  234. package/src/api/impl/events/index.ts +12 -4
  235. package/src/api/impl/lodestar/index.ts +1 -1
  236. package/src/api/rest/base.ts +7 -0
  237. package/src/chain/archiveStore/archiveStore.ts +1 -1
  238. package/src/chain/blocks/importExecutionPayload.ts +12 -11
  239. package/src/chain/blocks/index.ts +18 -6
  240. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +8 -2
  241. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -4
  242. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +25 -16
  243. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +1 -9
  244. package/src/chain/bls/index.ts +1 -1
  245. package/src/chain/bls/interface.ts +7 -3
  246. package/src/chain/bls/multithread/index.ts +148 -120
  247. package/src/chain/bls/multithread/jobItem.ts +12 -82
  248. package/src/chain/bls/multithread/types.ts +33 -8
  249. package/src/chain/bls/multithread/utils.ts +11 -0
  250. package/src/chain/bls/multithread/worker.ts +85 -27
  251. package/src/chain/bls/singleThread.ts +11 -55
  252. package/src/chain/bls/utils.ts +23 -24
  253. package/src/chain/chain.ts +17 -6
  254. package/src/chain/errors/blockError.ts +8 -2
  255. package/src/chain/errors/proposerPreferences.ts +7 -0
  256. package/src/chain/forkChoice/index.ts +4 -0
  257. package/src/chain/interface.ts +4 -1
  258. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  259. package/src/chain/opPools/attestationPool.ts +1 -1
  260. package/src/chain/opPools/deferredVoluntaryExitPool.ts +65 -0
  261. package/src/chain/opPools/index.ts +1 -0
  262. package/src/chain/opPools/payloadAttestationPool.ts +3 -3
  263. package/src/chain/opPools/syncCommitteeMessagePool.ts +1 -1
  264. package/src/chain/opPools/syncContributionAndProofPool.ts +1 -1
  265. package/src/chain/opPools/utils.ts +1 -1
  266. package/src/chain/prepareNextSlot.ts +23 -5
  267. package/src/chain/regen/interface.ts +4 -2
  268. package/src/chain/regen/queued.ts +5 -5
  269. package/src/chain/seenCache/seenAttesters.ts +31 -5
  270. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +143 -14
  271. package/src/chain/stateCache/fifoBlockStateCache.ts +8 -1
  272. package/src/chain/stateCache/persistentCheckpointsCache.ts +133 -8
  273. package/src/chain/stateCache/types.ts +3 -3
  274. package/src/chain/validation/attestation.ts +1 -7
  275. package/src/chain/validation/attesterSlashing.ts +1 -8
  276. package/src/chain/validation/executionPayloadBid.ts +1 -2
  277. package/src/chain/validation/executionPayloadEnvelope.ts +0 -1
  278. package/src/chain/validation/payloadAttestationMessage.ts +6 -17
  279. package/src/chain/validation/proposerPreferences.ts +15 -5
  280. package/src/chain/validation/proposerSlashing.ts +1 -1
  281. package/src/chain/validation/voluntaryExit.ts +59 -2
  282. package/src/metrics/metrics/lodestar.ts +126 -57
  283. package/src/network/gossip/topic.ts +38 -0
  284. package/src/network/libp2p/index.ts +11 -2
  285. package/src/network/network.ts +27 -55
  286. package/src/network/processor/constants.ts +6 -0
  287. package/src/network/processor/gossipHandlers.ts +5 -1
  288. package/src/network/processor/index.ts +42 -19
  289. package/src/network/reqresp/score.ts +5 -11
  290. package/src/network/util.ts +0 -5
  291. package/src/node/deferredVoluntaryExitPublisher.ts +54 -0
  292. package/src/node/nodejs.ts +5 -1
  293. package/src/sync/range/batch.ts +83 -28
  294. package/src/sync/range/chain.ts +26 -11
  295. package/src/sync/types.ts +36 -1
  296. package/src/sync/unknownBlock.ts +240 -68
  297. package/src/sync/utils/downloadByRange.ts +21 -12
  298. package/src/sync/utils/downloadByRoot.ts +3 -1
  299. package/lib/chain/bls/maybeBatch.d.ts +0 -12
  300. package/lib/chain/bls/maybeBatch.d.ts.map +0 -1
  301. package/lib/chain/bls/maybeBatch.js +0 -35
  302. package/lib/chain/bls/maybeBatch.js.map +0 -1
  303. package/src/chain/bls/maybeBatch.ts +0 -45
@@ -1,6 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
- import {ForkSeq, isForkPostGloas} from "@lodestar/params";
3
+ import {CheckpointWithHex} from "@lodestar/fork-choice";
4
+ import {ForkSeq, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
4
5
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
5
6
  import {computeTimeAtSlot} from "@lodestar/state-transition";
6
7
  import {RootHex, Slot, gloas} from "@lodestar/types";
@@ -16,6 +17,7 @@ import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/execut
16
17
  import {Metrics} from "../metrics/index.js";
17
18
  import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
18
19
  import {PeerSyncMeta} from "../network/peers/peersData.js";
20
+ import {MAX_PEERS_PER_ROOT} from "../network/processor/constants.js";
19
21
  import {PeerIdStr} from "../util/peerId.js";
20
22
  import {shuffle} from "../util/shuffle.js";
21
23
  import {sortBy} from "../util/sortBy.js";
@@ -24,6 +26,7 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
24
26
  import {SyncOptions} from "./options.js";
25
27
  import {
26
28
  BlockInputSyncCacheItem,
29
+ DroppedItemReason,
27
30
  PayloadSyncCacheItem,
28
31
  PendingBlockInput,
29
32
  PendingBlockInputStatus,
@@ -47,6 +50,10 @@ import {getRateLimitedUntilMs} from "./utils/rateLimit.js";
47
50
  const MAX_ATTEMPTS_PER_BLOCK = 5;
48
51
  const MAX_KNOWN_BAD_BLOCKS = 500;
49
52
  const MAX_PENDING_BLOCKS = 100;
53
+ /**
54
+ * A pending payload without slot can only stay at most 2 epochs.
55
+ */
56
+ const PRUNE_UNRESOLVED_SLOT_EPOCHS = 2;
50
57
 
51
58
  type AdvancePendingBlockResult =
52
59
  | "ready"
@@ -154,6 +161,7 @@ export class BlockInputSync {
154
161
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
155
162
  this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
156
163
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
164
+ this.chain.emitter.on(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
157
165
  this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
158
166
  this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
159
167
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -170,6 +178,7 @@ export class BlockInputSync {
170
178
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
171
179
  this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
172
180
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
181
+ this.chain.emitter.off(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
173
182
  this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
174
183
  this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
175
184
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -190,10 +199,12 @@ export class BlockInputSync {
190
199
  */
191
200
  private onUnknownBlockRoot = (data: ChainEventData[ChainEvent.unknownBlockRoot]): void => {
192
201
  try {
193
- this.addByRootHex(data.rootHex, data.peer);
194
- this.triggerUnknownBlockSearch();
195
- this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_BLOCK_ROOT});
196
- this.metrics?.blockInputSync.source.inc({source: data.source});
202
+ const isNewRoot = this.addByRootHex(data.rootHex, data.peer);
203
+ if (isNewRoot) {
204
+ this.triggerUnknownBlockSearch();
205
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_BLOCK_ROOT});
206
+ this.metrics?.blockInputSync.source.inc({source: data.source});
207
+ }
197
208
  } catch (e) {
198
209
  this.logger.debug("Error handling unknownBlockRoot event", {}, e as Error);
199
210
  }
@@ -215,10 +226,12 @@ export class BlockInputSync {
215
226
 
216
227
  private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => {
217
228
  try {
218
- this.addByPayloadRootHex(data.rootHex, data.peer, data.slot);
219
- this.triggerUnknownBlockSearch();
220
- this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT});
221
- this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
229
+ const isNewRoot = this.addByPayloadRootHex(data.rootHex, data.peer, this.resolvePayloadSlot(data.rootHex));
230
+ if (isNewRoot) {
231
+ this.triggerUnknownBlockSearch();
232
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT});
233
+ this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
234
+ }
222
235
  } catch (e) {
223
236
  this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e as Error);
224
237
  }
@@ -252,13 +265,13 @@ export class BlockInputSync {
252
265
  parentRoot: missingDependency.parentRootHex,
253
266
  parentBlockHash: missingDependency.parentBlockHashHex,
254
267
  });
255
- this.removeAndDownScoreAllDescendants(pendingBlock);
268
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidParentPayload);
256
269
  }
257
270
  return;
258
271
  }
259
272
 
260
273
  if (missingDependency.kind === "parentPayload") {
261
- this.addByPayloadRootHex(missingDependency.rootHex, data.peer);
274
+ this.addByPayloadRootHex(missingDependency.rootHex, data.peer, missingDependency.slot);
262
275
  } else if (missingDependency.kind === "parentBlock") {
263
276
  this.addByRootHex(missingDependency.rootHex, data.peer);
264
277
  }
@@ -284,6 +297,80 @@ export class BlockInputSync {
284
297
  this.triggerUnknownBlockSearch();
285
298
  };
286
299
 
300
+ /**
301
+ * Drop pending payloads and blocks that fell below the finalized slot.
302
+ * See https://github.com/ChainSafe/lodestar/issues/9800
303
+ */
304
+ private pruneFinalized = (checkpoint: CheckpointWithHex): void => {
305
+ // Use the actual finalized block slot, not computeStartSlotAtEpoch(checkpoint.epoch)
306
+ // because the payload at the finalized block is not finalized
307
+ const finalizedSlot = this.chain.forkChoice.getFinalizedBlock().slot;
308
+ const nowSec = Date.now() / 1000;
309
+ const maxSecInCache = (PRUNE_UNRESOLVED_SLOT_EPOCHS * SLOTS_PER_EPOCH * this.config.SLOT_DURATION_MS) / 1000;
310
+
311
+ let belowFinalizedPayloads = 0;
312
+ let agedOutPayloads = 0;
313
+ let unresolvedSlotPayloads = 0;
314
+
315
+ for (const [rootHex, payload] of this.pendingPayloads) {
316
+ // Last-effort resolve for a slot-less PendingPayloadRootHex, and store it back for later passes.
317
+ if (!isPendingPayloadInput(payload) && !isPendingPayloadEnvelope(payload) && payload.slot === undefined) {
318
+ payload.slot = this.resolvePayloadSlot(rootHex);
319
+ if (payload.slot === undefined) {
320
+ // Still unresolvable: age it out so the MAX_PENDING_BLOCKS cap cannot stay saturated.
321
+ if (nowSec - payload.timeAddedSec > maxSecInCache) {
322
+ this.pendingPayloads.delete(rootHex);
323
+ agedOutPayloads++;
324
+ } else {
325
+ unresolvedSlotPayloads++;
326
+ this.logger.debug("BlockInputSync.pruneFinalized: pending payload with unresolved slot", {
327
+ root: rootHex,
328
+ finalizedSlot,
329
+ });
330
+ }
331
+ continue;
332
+ }
333
+ }
334
+
335
+ const slot = getPayloadSyncCacheItemSlot(payload);
336
+ if (typeof slot === "number" && slot < finalizedSlot) {
337
+ this.pendingPayloads.delete(rootHex);
338
+ belowFinalizedPayloads++;
339
+ }
340
+ }
341
+
342
+ let deletedBlocks = 0;
343
+ for (const [rootHex, block] of this.pendingBlocks) {
344
+ const slot = getBlockInputSyncCacheItemSlot(block);
345
+ if (typeof slot === "number" && slot < finalizedSlot) {
346
+ this.pendingBlocks.delete(rootHex);
347
+ deletedBlocks++;
348
+ }
349
+ }
350
+
351
+ if (belowFinalizedPayloads > 0) {
352
+ this.metrics?.blockInputSync.removedPayloads.inc(
353
+ {reason: DroppedItemReason.belowFinalized},
354
+ belowFinalizedPayloads
355
+ );
356
+ }
357
+ if (agedOutPayloads > 0) {
358
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.agedOut}, agedOutPayloads);
359
+ }
360
+ if (deletedBlocks > 0) {
361
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.belowFinalized}, deletedBlocks);
362
+ }
363
+
364
+ this.logger.debug("BlockInputSync.pruneFinalized dropped pre-finalized pending items", {
365
+ finalizedSlot,
366
+ finalizedRoot: checkpoint.rootHex,
367
+ belowFinalizedPayloads,
368
+ agedOutPayloads,
369
+ deletedBlocks,
370
+ unresolvedSlotPayloads,
371
+ });
372
+ };
373
+
287
374
  private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
288
375
  let pendingBlock = this.pendingBlocks.get(rootHex);
289
376
  let added = false;
@@ -303,7 +390,9 @@ export class BlockInputSync {
303
390
  });
304
391
  }
305
392
 
306
- if (peerIdStr) {
393
+ // bound peers tracked per root: propagationSource is recorded pre-validation, so cap the set (#9923).
394
+ // Once at the cap, ignore further peers rather than evicting already-tracked ones.
395
+ if (peerIdStr && pendingBlock.peerIdStrings.size < MAX_PEERS_PER_ROOT) {
307
396
  pendingBlock.peerIdStrings.add(peerIdStr);
308
397
  }
309
398
 
@@ -311,6 +400,7 @@ export class BlockInputSync {
311
400
  // Limit pending blocks to prevent DOS attacks that cause OOM
312
401
  const prunedItemCount = pruneSetToMax(this.pendingBlocks, this.maxPendingBlocks);
313
402
  if (prunedItemCount > 0) {
403
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
314
404
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
315
405
  }
316
406
  return added;
@@ -337,7 +427,9 @@ export class BlockInputSync {
337
427
  });
338
428
  }
339
429
 
340
- if (peerIdStr) {
430
+ // bound peers tracked per root: propagationSource is recorded pre-validation, so cap the set (#9923).
431
+ // Once at the cap, ignore further peers rather than evicting already-tracked ones.
432
+ if (peerIdStr && pendingBlock.peerIdStrings.size < MAX_PEERS_PER_ROOT) {
341
433
  pendingBlock.peerIdStrings.add(peerIdStr);
342
434
  }
343
435
 
@@ -345,6 +437,7 @@ export class BlockInputSync {
345
437
  // Limit pending blocks to prevent DOS attacks that cause OOM
346
438
  const prunedItemCount = pruneSetToMax(this.pendingBlocks, this.maxPendingBlocks);
347
439
  if (prunedItemCount > 0) {
440
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
348
441
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
349
442
  }
350
443
  };
@@ -369,19 +462,50 @@ export class BlockInputSync {
369
462
  root: rootHex,
370
463
  peerIdStr: peerIdStr ?? "unknown peer",
371
464
  });
465
+ } else if (
466
+ // update a previously slot-less entry once a slot becomes available
467
+ slot !== undefined &&
468
+ !isPendingPayloadInput(pendingPayload) &&
469
+ !isPendingPayloadEnvelope(pendingPayload) &&
470
+ pendingPayload.slot === undefined
471
+ ) {
472
+ pendingPayload.slot = slot;
372
473
  }
373
-
374
- if (peerIdStr) {
474
+ if (peerIdStr && pendingPayload.peerIdStrings.size < MAX_PEERS_PER_ROOT) {
375
475
  pendingPayload.peerIdStrings.add(peerIdStr);
376
476
  }
377
477
 
378
478
  const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
379
479
  if (prunedItemCount > 0) {
480
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
380
481
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
381
482
  }
382
483
  return added;
383
484
  };
384
485
 
486
+ /**
487
+ * Resolve the block slot for a payload root.
488
+ */
489
+ private resolvePayloadSlot(rootHex: RootHex): Slot | undefined {
490
+ // a pending block of the same root
491
+ const pendingBlock = this.pendingBlocks.get(rootHex);
492
+ if (pendingBlock) {
493
+ const pendingBlockSlot = getBlockInputSyncCacheItemSlot(pendingBlock);
494
+ if (typeof pendingBlockSlot === "number") {
495
+ return pendingBlockSlot;
496
+ }
497
+ }
498
+
499
+ // the block is in fork choice
500
+ const forkChoiceSlot = this.chain.forkChoice.getBlockHexDefaultStatus(rootHex)?.slot;
501
+ if (forkChoiceSlot !== undefined) {
502
+ return forkChoiceSlot;
503
+ }
504
+
505
+ // a seen payload envelope input
506
+ return this.chain.seenPayloadEnvelopeInputCache.get(rootHex)?.slot;
507
+ }
508
+
385
509
  private addByPayloadInput = (
386
510
  payloadInput: PayloadEnvelopeInput,
387
511
  peerIdStr?: PeerIdStr,
@@ -393,7 +517,7 @@ export class BlockInputSync {
393
517
  envelope
394
518
  );
395
519
 
396
- if (peerIdStr) {
520
+ if (peerIdStr && pendingPayload.peerIdStrings.size < MAX_PEERS_PER_ROOT) {
397
521
  pendingPayload.peerIdStrings.add(peerIdStr);
398
522
  }
399
523
 
@@ -407,6 +531,7 @@ export class BlockInputSync {
407
531
 
408
532
  const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
409
533
  if (prunedItemCount > 0) {
534
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
410
535
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
411
536
  }
412
537
  };
@@ -436,10 +561,12 @@ export class BlockInputSync {
436
561
  blockInput: IBlockInput
437
562
  ):
438
563
  | {kind: "ready"}
439
- | {kind: "block" | "parentBlock" | "parentPayload"; rootHex: RootHex}
564
+ | {kind: "block" | "parentBlock"; rootHex: RootHex}
565
+ | {kind: "parentPayload"; rootHex: RootHex; slot: Slot}
440
566
  | {kind: "invalidParentPayload"; parentRootHex: RootHex; parentBlockHashHex: RootHex} {
441
567
  const parentRootHex = blockInput.parentRootHex;
442
- if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) {
568
+ const parentBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentRootHex);
569
+ if (!parentBlock) {
443
570
  return {kind: "parentBlock", rootHex: parentRootHex};
444
571
  }
445
572
 
@@ -461,16 +588,8 @@ export class BlockInputSync {
461
588
  return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
462
589
  }
463
590
 
464
- const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);
465
- if (parentPayloadInput) {
466
- if (parentPayloadInput.getBlockHashHex() === parentBlockHashHex) {
467
- return {kind: "parentPayload", rootHex: parentRootHex};
468
- }
469
-
470
- return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
471
- }
472
-
473
- return {kind: "parentPayload", rootHex: parentRootHex};
591
+ // Parent is in fork choice but its payload `parentBlockHashHex` isn't revealed yet
592
+ return {kind: "parentPayload", rootHex: parentRootHex, slot: parentBlock.slot};
474
593
  }
475
594
 
476
595
  private advancePendingBlock(pendingBlock: PendingBlockInput): AdvancePendingBlockResult {
@@ -493,9 +612,9 @@ export class BlockInputSync {
493
612
  }
494
613
 
495
614
  case "parentPayload": {
496
- let added = this.addByPayloadRootHex(missingDependency.rootHex);
615
+ let added = this.addByPayloadRootHex(missingDependency.rootHex, undefined, missingDependency.slot);
497
616
  for (const peerIdStr of pendingBlock.peerIdStrings) {
498
- added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr) || added;
617
+ added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr, missingDependency.slot) || added;
499
618
  }
500
619
  return added ? "queued_parent_payload" : "blocked";
501
620
  }
@@ -507,7 +626,7 @@ export class BlockInputSync {
507
626
  parentRoot: missingDependency.parentRootHex,
508
627
  parentBlockHash: missingDependency.parentBlockHashHex,
509
628
  });
510
- this.removeAndDownScoreAllDescendants(pendingBlock);
629
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidParentPayload);
511
630
  return "removed";
512
631
  }
513
632
  }
@@ -730,7 +849,7 @@ export class BlockInputSync {
730
849
  ...logCtx2,
731
850
  finalizedSlot,
732
851
  });
733
- this.removeAndDownScoreAllDescendants(block);
852
+ this.removeAndDownScoreAllDescendants(block, DroppedItemReason.belowFinalized);
734
853
  } else {
735
854
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
736
855
  }
@@ -747,7 +866,7 @@ export class BlockInputSync {
747
866
 
748
867
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
749
868
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
750
- this.removeAndDownScoreAllDescendants(block);
869
+ this.removeAndDownScoreAllDescendants(block, DroppedItemReason.unavailable);
751
870
  }
752
871
  }
753
872
 
@@ -811,9 +930,9 @@ export class BlockInputSync {
811
930
  this.pendingBlocks.delete(blockRootHex);
812
931
 
813
932
  if (isForkPostGloas(fork)) {
814
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
933
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(blockRootHex);
815
934
  if (!payloadInput) {
816
- this.logger.warn("PayloadEnvelopeInput not seeded during unknown sync processReadyBlock()", logCtx);
935
+ this.logger.debug("PayloadEnvelopeInput unavailable during unknown sync processReadyBlock()", logCtx);
817
936
  } else {
818
937
  // Similar to the gossip path: immediately attempt fetch of data columns from the execution engine.
819
938
  // The bid already carries the kzg commitments, so there is no reason to wait for the payload to arrive.
@@ -855,19 +974,19 @@ export class BlockInputSync {
855
974
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
856
975
  // Removing the block(s) without penalizing the peers, hoping for EL to
857
976
  // recover on a latter download + verify attempt.
858
- this.removeAllDescendants(pendingBlock);
977
+ this.removeAllDescendants(pendingBlock, DroppedItemReason.elError, DroppedItemReason.elError);
859
978
  break;
860
979
 
861
980
  case BlockErrorCode.EXECUTION_ENGINE_INVALID:
862
981
  // the peer served a bad block
863
982
  this.logger.debug("Execution engine rejected block from unknown parent sync", errorData, res.err);
864
- this.removeAndDownScoreAllDescendants(pendingBlock);
983
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
865
984
  break;
866
985
 
867
986
  default:
868
987
  // Block is not correct with respect to our chain. Log error loudly
869
988
  this.logger.debug("Error processing block from unknown parent sync", errorData, res.err);
870
- this.removeAndDownScoreAllDescendants(pendingBlock);
989
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
871
990
  }
872
991
  }
873
992
 
@@ -886,12 +1005,12 @@ export class BlockInputSync {
886
1005
  */
887
1006
  private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise<void> {
888
1007
  const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
1008
+
889
1009
  if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
890
1010
  this.pendingPayloads.delete(rootHex);
891
1011
  return;
892
1012
  }
893
1013
 
894
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
895
1014
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
896
1015
  // Block not in fork choice yet. payloadInput may be seeded from the block body during download, so a
897
1016
  // non-null payloadInput does not imply the block is imported; defer regardless and pull the block first.
@@ -907,8 +1026,9 @@ export class BlockInputSync {
907
1026
  return;
908
1027
  }
909
1028
 
1029
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
910
1030
  if (!payloadInput) {
911
- this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
1031
+ this.logger.debug("PayloadEnvelopeInput not yet reloadable for imported block, will retry", {
912
1032
  root: rootHex,
913
1033
  });
914
1034
  return;
@@ -928,6 +1048,7 @@ export class BlockInputSync {
928
1048
  const pendingPayloadByRoot: PendingPayloadRootHex = {
929
1049
  status: PendingPayloadInputStatus.pending,
930
1050
  rootHex,
1051
+ slot: pendingPayload.envelope.message.payload.slotNumber,
931
1052
  timeAddedSec: pendingPayload.timeAddedSec,
932
1053
  peerIdStrings: new Set(pendingPayload.peerIdStrings),
933
1054
  };
@@ -967,6 +1088,10 @@ export class BlockInputSync {
967
1088
  return;
968
1089
  }
969
1090
 
1091
+ if (!isPendingPayloadInput(payload) && payload.slot === undefined) {
1092
+ payload.slot = this.resolvePayloadSlot(rootHex);
1093
+ }
1094
+
970
1095
  const payloadSlot = getPayloadSyncCacheItemSlot(payload);
971
1096
  const logCtx = {
972
1097
  slot: payloadSlot,
@@ -982,7 +1107,13 @@ export class BlockInputSync {
982
1107
  const res = await wrapError(this.fetchPayloadInput(payload));
983
1108
  if (!res.err) {
984
1109
  const pendingPayload = res.result;
985
- this.pendingPayloads.set(getPayloadSyncCacheItemRootHex(pendingPayload), pendingPayload);
1110
+ const resultRootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
1111
+ // finalization may have deleted this entry while we were awaiting the network, do not resurrect it via the set below
1112
+ if (!this.pendingPayloads.has(resultRootHex)) {
1113
+ this.logger.verbose("Dropping downloaded payload, entry pruned during fetch", logCtx);
1114
+ return;
1115
+ }
1116
+ this.pendingPayloads.set(resultRootHex, pendingPayload);
986
1117
 
987
1118
  if (isPendingPayloadEnvelope(pendingPayload)) {
988
1119
  await this.reconcilePayloadEnvelope(pendingPayload);
@@ -1059,17 +1190,27 @@ export class BlockInputSync {
1059
1190
  break;
1060
1191
 
1061
1192
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1193
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1194
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
1195
+ break;
1196
+
1062
1197
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1198
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1199
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
1200
+ break;
1201
+
1063
1202
  case PayloadErrorCode.INVALID_SIGNATURE:
1064
1203
  // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1065
1204
  // reintroducing envelope replacement / recreation flows.
1066
1205
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1067
- this.removePendingPayloadAndDescendants(rootHex);
1206
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
1068
1207
  break;
1069
1208
 
1070
1209
  default:
1071
1210
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1072
- this.pendingPayloads.delete(rootHex);
1211
+ if (this.pendingPayloads.delete(rootHex)) {
1212
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.unknown}, 1);
1213
+ }
1073
1214
  }
1074
1215
  return;
1075
1216
  }
@@ -1093,7 +1234,7 @@ export class BlockInputSync {
1093
1234
  let slot = getPayloadSyncCacheItemSlot(cacheItem);
1094
1235
  let payloadInput = isPendingPayloadInput(cacheItem)
1095
1236
  ? cacheItem.payloadInput
1096
- : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1237
+ : await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1097
1238
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1098
1239
 
1099
1240
  let i = 0;
@@ -1102,7 +1243,12 @@ export class BlockInputSync {
1102
1243
  const pendingColumns = payloadInput?.hasAllData()
1103
1244
  ? new Set<number>()
1104
1245
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1105
- const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1246
+ // prefer peers that gossiped this payload root to us (#9923)
1247
+ const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1248
+ pendingColumns,
1249
+ excludedPeers,
1250
+ cacheItem.peerIdStrings
1251
+ );
1106
1252
  if (peerMeta === null) {
1107
1253
  if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1108
1254
  throw new UnknownBlockRateLimitedError(
@@ -1110,9 +1256,11 @@ export class BlockInputSync {
1110
1256
  );
1111
1257
  }
1112
1258
 
1113
- throw Error(
1114
- `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1115
- );
1259
+ const reason =
1260
+ pendingColumns.size > 0
1261
+ ? `cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1262
+ : "no peer available to download pending payload";
1263
+ throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: ${reason}`);
1116
1264
  }
1117
1265
 
1118
1266
  const {peerId, client: peerClient} = peerMeta;
@@ -1124,7 +1272,7 @@ export class BlockInputSync {
1124
1272
  slot = envelope.message.payload.slotNumber;
1125
1273
  }
1126
1274
 
1127
- payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1275
+ payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1128
1276
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
1129
1277
  // Block not in fork choice yet. Validating now would throw BLOCK_ROOT_UNKNOWN, so keep the downloaded
1130
1278
  // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
@@ -1309,7 +1457,12 @@ export class BlockInputSync {
1309
1457
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
1310
1458
  ? new Set(cacheItem.blockInput.getMissingSampledColumnMeta().missing)
1311
1459
  : defaultPendingColumns;
1312
- const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
1460
+ // prefer peers that gossiped this block root to us (#9923)
1461
+ const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1462
+ pendingColumns,
1463
+ excludedPeers,
1464
+ cacheItem.peerIdStrings
1465
+ );
1313
1466
  if (peerMeta === null) {
1314
1467
  if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1315
1468
  throw new UnknownBlockRateLimitedError(
@@ -1457,9 +1610,9 @@ export class BlockInputSync {
1457
1610
  * Downscore all peers that have referenced any of this bad blocks. May report peers multiple times if they have
1458
1611
  * referenced more than one bad block.
1459
1612
  */
1460
- private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem): void {
1613
+ private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1461
1614
  // Get all blocks that are a descendant of this one
1462
- const badPendingBlocks = this.removeAllDescendants(block);
1615
+ const badPendingBlocks = this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent);
1463
1616
  // just console log and do not penalize on pending/bad blocks for debugging
1464
1617
  // console.log("removeAndDownscoreAllDescendants", {block});
1465
1618
 
@@ -1486,18 +1639,22 @@ export class BlockInputSync {
1486
1639
  }
1487
1640
 
1488
1641
  // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1489
- private removePendingPayloadAndDescendants(rootHex: RootHex): void {
1642
+ private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
1490
1643
  // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1491
1644
  // later validation/finalization logic decides when it can leave memory.
1492
- this.pendingPayloads.delete(rootHex);
1645
+ if (this.pendingPayloads.delete(rootHex)) {
1646
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
1647
+ }
1493
1648
 
1494
1649
  const badPendingBlocks = getAllDescendantBlocks(rootHex, this.pendingBlocks);
1495
- this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1496
-
1497
1650
  for (const block of badPendingBlocks) {
1498
1651
  const descendantRootHex = getBlockInputSyncCacheItemRootHex(block);
1499
- this.pendingBlocks.delete(descendantRootHex);
1500
- this.pendingPayloads.delete(descendantRootHex);
1652
+ if (this.pendingBlocks.delete(descendantRootHex)) {
1653
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.invalidParent}, 1);
1654
+ }
1655
+ if (this.pendingPayloads.delete(descendantRootHex)) {
1656
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.invalidParent}, 1);
1657
+ }
1501
1658
  this.chain.seenBlockInputCache.prune(descendantRootHex);
1502
1659
  this.logger.debug("Removing pending descendant after invalid parent payload", {
1503
1660
  slot: getBlockInputSyncCacheItemSlot(block),
@@ -1507,18 +1664,25 @@ export class BlockInputSync {
1507
1664
  }
1508
1665
  }
1509
1666
 
1510
- private removeAllDescendants(block: BlockInputSyncCacheItem): BlockInputSyncCacheItem[] {
1667
+ private removeAllDescendants(
1668
+ block: BlockInputSyncCacheItem,
1669
+ headReason: DroppedItemReason,
1670
+ descendantReason: DroppedItemReason = DroppedItemReason.invalidParent
1671
+ ): BlockInputSyncCacheItem[] {
1511
1672
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1512
1673
  const slot = getBlockInputSyncCacheItemSlot(block);
1513
- // Get all blocks that are a descendant of this one
1674
+ // Get all blocks that are a descendant of this one (index 0 is the head item itself)
1514
1675
  const badPendingBlocks = [block, ...getAllDescendantBlocks(rootHex, this.pendingBlocks)];
1515
1676
 
1516
- this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1517
-
1518
- for (const block of badPendingBlocks) {
1677
+ for (const [i, block] of badPendingBlocks.entries()) {
1519
1678
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1520
- this.pendingBlocks.delete(rootHex);
1521
- this.pendingPayloads.delete(rootHex);
1679
+ const reason = i === 0 ? headReason : descendantReason;
1680
+ if (this.pendingBlocks.delete(rootHex)) {
1681
+ this.metrics?.blockInputSync.removedBlocks.inc({reason}, 1);
1682
+ }
1683
+ if (this.pendingPayloads.delete(rootHex)) {
1684
+ this.metrics?.blockInputSync.removedPayloads.inc({reason}, 1);
1685
+ }
1522
1686
  this.chain.seenBlockInputCache.prune(rootHex);
1523
1687
  // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1524
1688
  // importBlock()-owned lifecycle.
@@ -1609,11 +1773,17 @@ export class UnknownBlockPeerBalancer {
1609
1773
  }
1610
1774
 
1611
1775
  /**
1612
- * called from fetchBlockInput() where we only have block root and nothing else
1776
+ * called from fetchBlockInput()/fetchPayloadInput() with a block/payload root.
1613
1777
  * excludedPeers are the peers that we requested already so we don't want to try again
1614
1778
  * pendingColumns is empty for prefulu, or the 1st time we we download a block by root
1779
+ * preferredPeers are the peers NetworkProcessor told us gossiped this root (from
1780
+ * pendingBlock/cacheItem.peerIdStrings). They are preferred but not required.
1615
1781
  */
1616
- bestPeerForPendingColumns(pendingColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerSyncMeta | null {
1782
+ bestPeerForPendingColumns(
1783
+ pendingColumns: Set<number>,
1784
+ excludedPeers: Set<PeerIdStr>,
1785
+ preferredPeers?: Set<PeerIdStr>
1786
+ ): PeerSyncMeta | null {
1617
1787
  const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers);
1618
1788
  if (eligiblePeers.length === 0) {
1619
1789
  return null;
@@ -1621,7 +1791,9 @@ export class UnknownBlockPeerBalancer {
1621
1791
 
1622
1792
  const sortedEligiblePeers = sortBy(
1623
1793
  shuffle(eligiblePeers),
1624
- // prefer peers with least active req
1794
+ // 1st key: prefer peers we heard about this root from
1795
+ (peerId) => (preferredPeers?.has(peerId) ? 0 : 1),
1796
+ // 2nd key (tiebreak): prefer peers with least active req
1625
1797
  (peerId) => this.activeRequests.get(peerId) ?? 0
1626
1798
  );
1627
1799