@lodestar/beacon-node 1.47.0-dev.7a8fd842fc → 1.47.0-dev.7aa8c9c93a

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 (265) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +6 -8
  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 +12 -1
  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/validator/index.js +1 -1
  15. package/lib/api/impl/validator/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/historicalState/metrics.d.ts.map +1 -1
  20. package/lib/chain/archiveStore/historicalState/metrics.js +21 -0
  21. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  22. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  23. package/lib/chain/blocks/importBlock.js +2 -7
  24. package/lib/chain/blocks/importBlock.js.map +1 -1
  25. package/lib/chain/blocks/importExecutionPayload.d.ts +3 -2
  26. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  27. package/lib/chain/blocks/importExecutionPayload.js +11 -9
  28. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  29. package/lib/chain/blocks/index.d.ts.map +1 -1
  30. package/lib/chain/blocks/index.js +14 -6
  31. package/lib/chain/blocks/index.js.map +1 -1
  32. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +2 -1
  33. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  34. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +6 -2
  35. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  36. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +14 -5
  37. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  38. package/lib/chain/blocks/payloadEnvelopeInput/types.js +5 -0
  39. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -1
  40. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +22 -12
  42. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  43. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  44. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  45. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  46. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  47. package/lib/chain/bls/index.d.ts +1 -1
  48. package/lib/chain/bls/index.d.ts.map +1 -1
  49. package/lib/chain/bls/interface.d.ts +5 -5
  50. package/lib/chain/bls/interface.d.ts.map +1 -1
  51. package/lib/chain/bls/multithread/index.d.ts +5 -11
  52. package/lib/chain/bls/multithread/index.d.ts.map +1 -1
  53. package/lib/chain/bls/multithread/index.js +116 -101
  54. package/lib/chain/bls/multithread/index.js.map +1 -1
  55. package/lib/chain/bls/multithread/jobItem.d.ts +5 -19
  56. package/lib/chain/bls/multithread/jobItem.d.ts.map +1 -1
  57. package/lib/chain/bls/multithread/jobItem.js +10 -65
  58. package/lib/chain/bls/multithread/jobItem.js.map +1 -1
  59. package/lib/chain/bls/multithread/types.d.ts +29 -8
  60. package/lib/chain/bls/multithread/types.d.ts.map +1 -1
  61. package/lib/chain/bls/multithread/types.js +12 -0
  62. package/lib/chain/bls/multithread/types.js.map +1 -1
  63. package/lib/chain/bls/multithread/utils.d.ts +4 -0
  64. package/lib/chain/bls/multithread/utils.d.ts.map +1 -1
  65. package/lib/chain/bls/multithread/utils.js +10 -0
  66. package/lib/chain/bls/multithread/utils.js.map +1 -1
  67. package/lib/chain/bls/multithread/worker.js +49 -23
  68. package/lib/chain/bls/multithread/worker.js.map +1 -1
  69. package/lib/chain/bls/singleThread.d.ts +4 -10
  70. package/lib/chain/bls/singleThread.d.ts.map +1 -1
  71. package/lib/chain/bls/singleThread.js +8 -45
  72. package/lib/chain/bls/singleThread.js.map +1 -1
  73. package/lib/chain/bls/utils.d.ts +4 -4
  74. package/lib/chain/bls/utils.d.ts.map +1 -1
  75. package/lib/chain/bls/utils.js +18 -19
  76. package/lib/chain/bls/utils.js.map +1 -1
  77. package/lib/chain/builderCircuitBreaker.d.ts +6 -5
  78. package/lib/chain/builderCircuitBreaker.d.ts.map +1 -1
  79. package/lib/chain/builderCircuitBreaker.js +28 -15
  80. package/lib/chain/builderCircuitBreaker.js.map +1 -1
  81. package/lib/chain/chain.d.ts +2 -1
  82. package/lib/chain/chain.d.ts.map +1 -1
  83. package/lib/chain/chain.js +12 -5
  84. package/lib/chain/chain.js.map +1 -1
  85. package/lib/chain/errors/blockError.d.ts +1 -0
  86. package/lib/chain/errors/blockError.d.ts.map +1 -1
  87. package/lib/chain/errors/blockError.js +6 -1
  88. package/lib/chain/errors/blockError.js.map +1 -1
  89. package/lib/chain/errors/proposerPreferences.d.ts +6 -0
  90. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  91. package/lib/chain/errors/proposerPreferences.js +1 -0
  92. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  93. package/lib/chain/interface.d.ts +2 -1
  94. package/lib/chain/interface.d.ts.map +1 -1
  95. package/lib/chain/interface.js.map +1 -1
  96. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  97. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  98. package/lib/chain/opPools/attestationPool.d.ts +1 -1
  99. package/lib/chain/opPools/attestationPool.d.ts.map +1 -1
  100. package/lib/chain/opPools/attestationPool.js +1 -1
  101. package/lib/chain/opPools/attestationPool.js.map +1 -1
  102. package/lib/chain/opPools/payloadAttestationPool.js +3 -3
  103. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  104. package/lib/chain/opPools/syncCommitteeMessagePool.js +1 -1
  105. package/lib/chain/opPools/syncCommitteeMessagePool.js.map +1 -1
  106. package/lib/chain/opPools/syncContributionAndProofPool.js +1 -1
  107. package/lib/chain/opPools/syncContributionAndProofPool.js.map +1 -1
  108. package/lib/chain/opPools/utils.d.ts +1 -1
  109. package/lib/chain/opPools/utils.d.ts.map +1 -1
  110. package/lib/chain/opPools/utils.js +1 -1
  111. package/lib/chain/opPools/utils.js.map +1 -1
  112. package/lib/chain/options.d.ts +1 -1
  113. package/lib/chain/options.d.ts.map +1 -1
  114. package/lib/chain/prepareNextSlot.js +1 -1
  115. package/lib/chain/prepareNextSlot.js.map +1 -1
  116. package/lib/chain/seenCache/seenAttesters.d.ts +9 -5
  117. package/lib/chain/seenCache/seenAttesters.d.ts.map +1 -1
  118. package/lib/chain/seenCache/seenAttesters.js +25 -4
  119. package/lib/chain/seenCache/seenAttesters.js.map +1 -1
  120. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +30 -1
  121. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  122. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +123 -13
  123. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  124. package/lib/chain/validation/attestation.d.ts.map +1 -1
  125. package/lib/chain/validation/attestation.js +1 -4
  126. package/lib/chain/validation/attestation.js.map +1 -1
  127. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  128. package/lib/chain/validation/attesterSlashing.js +1 -1
  129. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  130. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  131. package/lib/chain/validation/executionPayloadBid.js +1 -2
  132. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  133. package/lib/chain/validation/executionPayloadEnvelope.js +1 -1
  134. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  135. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  136. package/lib/chain/validation/payloadAttestationMessage.js +5 -14
  137. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  138. package/lib/chain/validation/proposerPreferences.d.ts +1 -1
  139. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  140. package/lib/chain/validation/proposerPreferences.js +13 -4
  141. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  142. package/lib/chain/validation/proposerSlashing.js +1 -1
  143. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  144. package/lib/execution/builder/http.d.ts +10 -7
  145. package/lib/execution/builder/http.d.ts.map +1 -1
  146. package/lib/execution/builder/http.js +11 -8
  147. package/lib/execution/builder/http.js.map +1 -1
  148. package/lib/metrics/metrics/beacon.d.ts +3 -3
  149. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  150. package/lib/metrics/metrics/beacon.js +9 -9
  151. package/lib/metrics/metrics/beacon.js.map +1 -1
  152. package/lib/metrics/metrics/lodestar.d.ts +55 -20
  153. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  154. package/lib/metrics/metrics/lodestar.js +103 -51
  155. package/lib/metrics/metrics/lodestar.js.map +1 -1
  156. package/lib/network/gossip/topic.d.ts +13 -0
  157. package/lib/network/gossip/topic.d.ts.map +1 -1
  158. package/lib/network/gossip/topic.js +37 -0
  159. package/lib/network/gossip/topic.js.map +1 -1
  160. package/lib/network/libp2p/index.d.ts.map +1 -1
  161. package/lib/network/libp2p/index.js +10 -2
  162. package/lib/network/libp2p/index.js.map +1 -1
  163. package/lib/network/network.d.ts.map +1 -1
  164. package/lib/network/network.js +22 -43
  165. package/lib/network/network.js.map +1 -1
  166. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  167. package/lib/network/processor/gossipHandlers.js +12 -3
  168. package/lib/network/processor/gossipHandlers.js.map +1 -1
  169. package/lib/network/reqresp/score.d.ts.map +1 -1
  170. package/lib/network/reqresp/score.js +5 -11
  171. package/lib/network/reqresp/score.js.map +1 -1
  172. package/lib/network/util.d.ts +0 -1
  173. package/lib/network/util.d.ts.map +1 -1
  174. package/lib/network/util.js +0 -4
  175. package/lib/network/util.js.map +1 -1
  176. package/lib/node/nodejs.d.ts +2 -1
  177. package/lib/node/nodejs.d.ts.map +1 -1
  178. package/lib/node/nodejs.js.map +1 -1
  179. package/lib/sync/range/batch.d.ts +23 -5
  180. package/lib/sync/range/batch.d.ts.map +1 -1
  181. package/lib/sync/range/batch.js +61 -22
  182. package/lib/sync/range/batch.js.map +1 -1
  183. package/lib/sync/range/chain.d.ts +2 -1
  184. package/lib/sync/range/chain.d.ts.map +1 -1
  185. package/lib/sync/range/chain.js +23 -10
  186. package/lib/sync/range/chain.js.map +1 -1
  187. package/lib/sync/types.d.ts.map +1 -1
  188. package/lib/sync/types.js.map +1 -1
  189. package/lib/sync/unknownBlock.d.ts +9 -0
  190. package/lib/sync/unknownBlock.d.ts.map +1 -1
  191. package/lib/sync/unknownBlock.js +99 -21
  192. package/lib/sync/unknownBlock.js.map +1 -1
  193. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  194. package/lib/sync/utils/downloadByRange.js +16 -11
  195. package/lib/sync/utils/downloadByRange.js.map +1 -1
  196. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  197. package/lib/sync/utils/downloadByRoot.js +3 -1
  198. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  199. package/package.json +35 -35
  200. package/src/api/impl/beacon/blocks/index.ts +7 -8
  201. package/src/api/impl/beacon/pool/index.ts +12 -1
  202. package/src/api/impl/beacon/state/utils.ts +2 -1
  203. package/src/api/impl/events/index.ts +12 -4
  204. package/src/api/impl/validator/index.ts +1 -1
  205. package/src/api/rest/base.ts +7 -0
  206. package/src/chain/archiveStore/historicalState/metrics.ts +22 -0
  207. package/src/chain/blocks/importBlock.ts +1 -6
  208. package/src/chain/blocks/importExecutionPayload.ts +12 -11
  209. package/src/chain/blocks/index.ts +18 -6
  210. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +8 -2
  211. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -4
  212. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +25 -16
  213. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +1 -9
  214. package/src/chain/bls/index.ts +1 -1
  215. package/src/chain/bls/interface.ts +7 -3
  216. package/src/chain/bls/multithread/index.ts +148 -120
  217. package/src/chain/bls/multithread/jobItem.ts +12 -82
  218. package/src/chain/bls/multithread/types.ts +33 -8
  219. package/src/chain/bls/multithread/utils.ts +11 -0
  220. package/src/chain/bls/multithread/worker.ts +85 -27
  221. package/src/chain/bls/singleThread.ts +11 -55
  222. package/src/chain/bls/utils.ts +23 -24
  223. package/src/chain/builderCircuitBreaker.ts +31 -17
  224. package/src/chain/chain.ts +13 -6
  225. package/src/chain/errors/blockError.ts +8 -2
  226. package/src/chain/errors/proposerPreferences.ts +7 -0
  227. package/src/chain/interface.ts +2 -1
  228. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  229. package/src/chain/opPools/attestationPool.ts +1 -1
  230. package/src/chain/opPools/payloadAttestationPool.ts +3 -3
  231. package/src/chain/opPools/syncCommitteeMessagePool.ts +1 -1
  232. package/src/chain/opPools/syncContributionAndProofPool.ts +1 -1
  233. package/src/chain/opPools/utils.ts +1 -1
  234. package/src/chain/options.ts +1 -1
  235. package/src/chain/prepareNextSlot.ts +1 -1
  236. package/src/chain/seenCache/seenAttesters.ts +31 -5
  237. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +145 -14
  238. package/src/chain/validation/attestation.ts +1 -7
  239. package/src/chain/validation/attesterSlashing.ts +1 -8
  240. package/src/chain/validation/executionPayloadBid.ts +1 -2
  241. package/src/chain/validation/executionPayloadEnvelope.ts +0 -1
  242. package/src/chain/validation/payloadAttestationMessage.ts +6 -17
  243. package/src/chain/validation/proposerPreferences.ts +15 -5
  244. package/src/chain/validation/proposerSlashing.ts +1 -1
  245. package/src/execution/builder/http.ts +11 -11
  246. package/src/metrics/metrics/beacon.ts +9 -9
  247. package/src/metrics/metrics/lodestar.ts +113 -54
  248. package/src/network/gossip/topic.ts +38 -0
  249. package/src/network/libp2p/index.ts +11 -2
  250. package/src/network/network.ts +27 -55
  251. package/src/network/processor/gossipHandlers.ts +14 -1
  252. package/src/network/reqresp/score.ts +5 -11
  253. package/src/network/util.ts +0 -5
  254. package/src/node/nodejs.ts +2 -1
  255. package/src/sync/range/batch.ts +83 -28
  256. package/src/sync/range/chain.ts +26 -11
  257. package/src/sync/types.ts +3 -1
  258. package/src/sync/unknownBlock.ts +115 -26
  259. package/src/sync/utils/downloadByRange.ts +21 -12
  260. package/src/sync/utils/downloadByRoot.ts +3 -1
  261. package/lib/chain/bls/maybeBatch.d.ts +0 -12
  262. package/lib/chain/bls/maybeBatch.d.ts.map +0 -1
  263. package/lib/chain/bls/maybeBatch.js +0 -35
  264. package/lib/chain/bls/maybeBatch.js.map +0 -1
  265. package/src/chain/bls/maybeBatch.ts +0 -45
@@ -1,12 +1,13 @@
1
1
  import {setMaxListeners} from "node:events";
2
2
  import {PrivateKey} from "@libp2p/interface";
3
3
  import {Registry} from "prom-client";
4
+ import {type PubkeyCache} from "@chainsafe/lodestar-z/pubkeys";
4
5
  import {hasher} from "@chainsafe/persistent-merkle-tree";
5
6
  import {BeaconApiMethods} from "@lodestar/api/beacon/server";
6
7
  import {BeaconConfig} from "@lodestar/config";
7
8
  import type {LoggerNode} from "@lodestar/logger/node";
8
9
  import {ZERO_HASH_HEX} from "@lodestar/params";
9
- import {IBeaconStateView, PubkeyCache, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
10
+ import {IBeaconStateView, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
10
11
  import {phase0} from "@lodestar/types";
11
12
  import {sleep, toRootHex} from "@lodestar/utils";
12
13
  import {ProcessShutdownCallback} from "@lodestar/validator";
@@ -39,6 +39,7 @@ export enum BatchStatus {
39
39
  AwaitingValidation = "AwaitingValidation",
40
40
  }
41
41
 
42
+ /** Identity of one download attempt*/
42
43
  export type Attempt = {
43
44
  /** The peer that made the attempt */
44
45
  peers: PeerIdStr[];
@@ -46,6 +47,16 @@ export type Attempt = {
46
47
  hash: RootHex;
47
48
  };
48
49
 
50
+ /**
51
+ * A failed attempt that has been classified for blame.
52
+ */
53
+ export type FailedAttempt = Attempt & {
54
+ /**
55
+ * True when the failure is evidence the peers served bad data, so they may be downscored..
56
+ */
57
+ peerAttributable: boolean;
58
+ };
59
+
49
60
  type TrackedRequest = {
50
61
  /** only happen for the 1st batch in checkpoint sync */
51
62
  parentPayload: boolean;
@@ -108,10 +119,12 @@ export type BatchMetadata = {
108
119
  // Retry counters
109
120
  downloadAttempts: number;
110
121
  processingAttempts: number;
122
+ executionErrorAttempts: number;
111
123
 
112
124
  // Cumulative peer attribution for failed attempts (only present when non-empty)
113
125
  failedDownloadPeers?: string;
114
126
  failedProcessingPeers?: string;
127
+ executionErrorPeers?: string;
115
128
  };
116
129
 
117
130
  function formatRangeReq(req: {startSlot: Slot; count: number}): string {
@@ -152,10 +165,10 @@ export class Batch {
152
165
  state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
153
166
  /** Peers that provided good data, with column coverage for by_range requests */
154
167
  private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
155
- /** The `Attempts` that have been made and failed to send us this batch. */
156
- readonly failedProcessingAttempts: Attempt[] = [];
157
- /** The `Attempts` that have been made and failed because of execution malfunction. */
158
- readonly executionErrorAttempts: Attempt[] = [];
168
+ /** The attempts that have been made and failed to send us this batch. */
169
+ readonly failedProcessingAttempts: FailedAttempt[] = [];
170
+ /** The attempts that have been made and failed because of execution malfunction. */
171
+ readonly executionErrorAttempts: FailedAttempt[] = [];
159
172
  /** The number of download retries this batch has undergone due to a failed request. */
160
173
  private readonly failedDownloadAttempts: PeerIdStr[] = [];
161
174
  private readonly config: ChainForkConfig;
@@ -419,10 +432,18 @@ export class Batch {
419
432
  }
420
433
 
421
434
  /**
422
- * Gives a list of peers from which this batch has had a failed download or processing attempt.
435
+ * Gives a list of peers from which this batch has had a failed download attempt, or a
436
+ * peer-attributable failed processing/execution attempt.
423
437
  */
424
438
  getFailedPeers(): PeerIdStr[] {
425
- return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
439
+ return [...this.failedDownloadAttempts, ...this.getPeerAttributableAttempts().flatMap((a) => a.peers)];
440
+ }
441
+
442
+ /**
443
+ * Attempts whose failure is peer-attributable, across both the processing and execution error
444
+ */
445
+ getPeerAttributableAttempts(): FailedAttempt[] {
446
+ return [...this.failedProcessingAttempts, ...this.executionErrorAttempts].filter((a) => a.peerAttributable);
426
447
  }
427
448
 
428
449
  /**
@@ -457,6 +478,7 @@ export class Batch {
457
478
  getMetadata(): BatchMetadata {
458
479
  const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
459
480
  const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
481
+ const executionErrorPeerList = this.executionErrorAttempts.flatMap((a) => a.peers);
460
482
  return {
461
483
  startEpoch: this.startEpoch,
462
484
  startSlot: this.startSlot,
@@ -468,12 +490,16 @@ export class Batch {
468
490
  ...(envelopesRequest && {envelopesReq: formatRangeReq(envelopesRequest)}),
469
491
  downloadAttempts: this.failedDownloadAttempts.length,
470
492
  processingAttempts: this.failedProcessingAttempts.length,
493
+ executionErrorAttempts: this.executionErrorAttempts.length,
471
494
  ...(this.failedDownloadAttempts.length > 0 && {
472
495
  failedDownloadPeers: this.failedDownloadAttempts.join(","),
473
496
  }),
474
497
  ...(failedProcessingPeerList.length > 0 && {
475
498
  failedProcessingPeers: failedProcessingPeerList.join(","),
476
499
  }),
500
+ ...(executionErrorPeerList.length > 0 && {
501
+ executionErrorPeers: executionErrorPeerList.join(","),
502
+ }),
477
503
  };
478
504
  }
479
505
 
@@ -703,11 +729,7 @@ export class Batch {
703
729
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.Processing));
704
730
  }
705
731
 
706
- if (isExecutionEngineError(err)) {
707
- this.onExecutionEngineError(this.state.attempt);
708
- } else {
709
- this.onProcessingError(this.state.attempt);
710
- }
732
+ this.routeProcessingFailure(err, this.state.attempt);
711
733
  }
712
734
 
713
735
  /**
@@ -718,10 +740,17 @@ export class Batch {
718
740
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingValidation));
719
741
  }
720
742
 
721
- if (isExecutionEngineError(err)) {
722
- this.onExecutionEngineError(this.state.attempt);
743
+ this.routeProcessingFailure(err, this.state.attempt);
744
+ }
745
+
746
+ private routeProcessingFailure(err: Error, attempt: Attempt): void {
747
+ const code = err instanceof BlockError || err instanceof PayloadError ? err.type.code : null;
748
+ const failedAttempt: FailedAttempt = {...attempt, peerAttributable: isPeerAttributableFailure(code)};
749
+
750
+ if (isExecutionEngineFailure(code)) {
751
+ this.onExecutionEngineError(failedAttempt);
723
752
  } else {
724
- this.onProcessingError(this.state.attempt);
753
+ this.onProcessingError(failedAttempt);
725
754
  }
726
755
  }
727
756
 
@@ -735,7 +764,7 @@ export class Batch {
735
764
  return this.state.attempt;
736
765
  }
737
766
 
738
- private onExecutionEngineError(attempt: Attempt): void {
767
+ private onExecutionEngineError(attempt: FailedAttempt): void {
739
768
  this.executionErrorAttempts.push(attempt);
740
769
  if (this.executionErrorAttempts.length > MAX_BATCH_PROCESSING_ATTEMPTS) {
741
770
  throw new BatchError(this.errorType({code: BatchErrorCode.MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS}));
@@ -746,7 +775,7 @@ export class Batch {
746
775
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
747
776
  }
748
777
 
749
- private onProcessingError(attempt: Attempt): void {
778
+ private onProcessingError(attempt: FailedAttempt): void {
750
779
  this.failedProcessingAttempts.push(attempt);
751
780
  if (this.failedProcessingAttempts.length > MAX_BATCH_PROCESSING_ATTEMPTS) {
752
781
  throw new BatchError(this.errorType({code: BatchErrorCode.MAX_PROCESSING_ATTEMPTS}));
@@ -789,18 +818,44 @@ type BatchErrorMetadata = {
789
818
 
790
819
  export class BatchError extends LodestarError<BatchErrorType & BatchErrorMetadata> {}
791
820
 
792
- function isExecutionEngineError(err: Error): boolean {
793
- if (!(err instanceof BlockError)) {
794
- return false;
795
- }
796
-
797
- if (err.type.code === BlockErrorCode.EXECUTION_ENGINE_ERROR) {
798
- return true;
799
- }
800
-
821
+ /**
822
+ * Whether a processing failure is an execution-engine failure. Pre-gloas, all EL errors stay in
823
+ * BlockErrorCode, post-gloas, they stay in PayloadErrorCode.
824
+ */
825
+ function isExecutionEngineFailure(code: BlockErrorCode | PayloadErrorCode | null): boolean {
801
826
  return (
802
- err.type.code === BlockErrorCode.BEACON_CHAIN_ERROR &&
803
- err.type.error instanceof PayloadError &&
804
- err.type.error.type.code === PayloadErrorCode.EXECUTION_ENGINE_ERROR
827
+ code === BlockErrorCode.EXECUTION_ENGINE_INVALID ||
828
+ code === BlockErrorCode.EXECUTION_ENGINE_ERROR ||
829
+ code === PayloadErrorCode.EXECUTION_ENGINE_INVALID ||
830
+ code === PayloadErrorCode.EXECUTION_ENGINE_ERROR
805
831
  );
806
832
  }
833
+
834
+ /**
835
+ * Based on an error code, return if peer is attributable for FailedAttempt.
836
+ */
837
+ function isPeerAttributableFailure(code: BlockErrorCode | PayloadErrorCode | null): boolean {
838
+ switch (code) {
839
+ // EL returned a definitive INVALID verdict on the payload
840
+ case BlockErrorCode.EXECUTION_ENGINE_INVALID:
841
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
842
+ // a block or envelope signature is invalid
843
+ case BlockErrorCode.INVALID_SIGNATURE:
844
+ case PayloadErrorCode.INVALID_SIGNATURE:
845
+ // the block's state transition produced an invalid state root, or failed per_slot/per_block processing
846
+ case BlockErrorCode.INVALID_STATE_ROOT:
847
+ case BlockErrorCode.PER_BLOCK_PROCESSING_ERROR:
848
+ // the segment this peer served is internally inconsistent (in-segment link breaks, see PR #9684)
849
+ case BlockErrorCode.NON_LINEAR_SLOTS:
850
+ case BlockErrorCode.NON_LINEAR_PARENT_ROOTS:
851
+ case BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS:
852
+ // the envelope references a mismatched block root, or fails field verification
853
+ case BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH:
854
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
855
+ // the peer served a block we have explicitly blacklisted
856
+ case BlockErrorCode.BLACKLISTED_BLOCK:
857
+ return true;
858
+ default:
859
+ return false;
860
+ }
861
+ }
@@ -358,9 +358,10 @@ export class SyncChain {
358
358
  // If a batch exceeds it's retry limit, maybe downscore peers.
359
359
  // shouldDownscoreOnBatchError() functions enforces that all BatchErrorCode values are covered
360
360
  if (e instanceof BatchError) {
361
- const shouldReportPeer = shouldReportPeerOnBatchError(e.type.code);
361
+ const shouldReportPeer = shouldReportPeerOnBatchError(e.type.code, this.batches.get(e.type.startEpoch));
362
362
  if (shouldReportPeer) {
363
- for (const peer of this.peerset.keys()) {
363
+ // Only the peers actually at fault are reported, never the whole peerset.
364
+ for (const peer of shouldReportPeer.peers) {
364
365
  this.reportPeer(peer, shouldReportPeer.action, shouldReportPeer.reason);
365
366
  }
366
367
  }
@@ -768,8 +769,11 @@ export class SyncChain {
768
769
 
769
770
  // The last batch attempt is right, all others are wrong. Penalize other peers
770
771
  const attemptOk = batch.validationSuccess();
771
- for (const attempt of batch.failedProcessingAttempts) {
772
+ for (const attempt of batch.getPeerAttributableAttempts()) {
772
773
  if (attempt.hash !== attemptOk.hash) {
774
+ // attempt.peers contains block/column/envelope peers.
775
+ // And if there is one bad envelope we will downscore block and column peers to
776
+ // TODO: resolve this
773
777
  for (const badAttemptPeer of attempt.peers) {
774
778
  if (attemptOk.peers.find((goodPeer) => goodPeer === badAttemptPeer)) {
775
779
  // The same peer corrected its previous attempt
@@ -830,21 +834,32 @@ export class SyncChain {
830
834
  * If peer should not be downscored, returns null.
831
835
  */
832
836
  export function shouldReportPeerOnBatchError(
833
- code: BatchErrorCode
834
- ): {action: PeerAction.LowToleranceError; reason: string} | null {
837
+ code: BatchErrorCode,
838
+ batch: Batch | undefined
839
+ ): {action: PeerAction.LowToleranceError; reason: string; peers: PeerIdStr[]} | null {
835
840
  switch (code) {
836
- // A batch could not be processed after max retry limit. It's likely that all peers
837
- // in this chain are sending invalid batches repeatedly so are either malicious or faulty.
838
- // We drop the chain and report all peers.
839
- // There are some edge cases with forks that could cause this situation, but it's unlikely.
841
+ // A batch could not be processed after max retry limit. Report
842
+ // only the peers that served a peer-attributable failed attempt.
840
843
  case BatchErrorCode.MAX_PROCESSING_ATTEMPTS:
841
- return {action: PeerAction.LowToleranceError, reason: "SyncChainMaxProcessingAttempts"};
844
+ case BatchErrorCode.MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS: {
845
+ // penalize both the processing-error and execution-error peers
846
+ const attributablePeers = [
847
+ ...new Set(batch?.getPeerAttributableAttempts().flatMap((attempt) => attempt.peers) ?? []),
848
+ ];
849
+ if (attributablePeers.length === 0) {
850
+ return null;
851
+ }
852
+ const reason =
853
+ code === BatchErrorCode.MAX_PROCESSING_ATTEMPTS
854
+ ? "SyncChainMaxProcessingAttempts"
855
+ : "SyncChainMaxExecutionEngineErrorAttempts";
856
+ return {action: PeerAction.LowToleranceError, reason, peers: attributablePeers};
857
+ }
842
858
 
843
859
  // TODO: Should peers be reported for MAX_DOWNLOAD_ATTEMPTS?
844
860
  case BatchErrorCode.MAX_DOWNLOAD_ATTEMPTS:
845
861
  case BatchErrorCode.INVALID_COUNT:
846
862
  case BatchErrorCode.WRONG_STATUS:
847
- case BatchErrorCode.MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS:
848
863
  return null;
849
864
  }
850
865
  }
package/src/sync/types.ts CHANGED
@@ -77,7 +77,9 @@ export type PendingPayloadInput = {
77
77
  export type PendingPayloadRootHex = {
78
78
  status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching;
79
79
  rootHex: RootHex;
80
- // message slot hint, may be missing when resolving a parent payload
80
+ // Trusted slot only (fork choice / validated data), may be missing until resolved. NOT the gossip
81
+ // message slot from ChainEvent.unknownEnvelopeBlockRoot, which is untrusted and not necessarily the
82
+ // payload/block slot. See BlockInputSync.resolvePayloadSlot.
81
83
  slot?: Slot;
82
84
  timeAddedSec: number;
83
85
  timeSyncedSec?: number;
@@ -1,5 +1,6 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
+ import {CheckpointWithHex} from "@lodestar/fork-choice";
3
4
  import {ForkSeq, isForkPostGloas} from "@lodestar/params";
4
5
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
5
6
  import {computeTimeAtSlot} from "@lodestar/state-transition";
@@ -154,6 +155,7 @@ export class BlockInputSync {
154
155
  this.chain.emitter.on(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
155
156
  this.chain.emitter.on(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
156
157
  this.chain.emitter.on(ChainEvent.blockUnknownParent, this.onUnknownParent);
158
+ this.chain.emitter.on(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
157
159
  this.chain.emitter.on(routes.events.EventType.block, this.onBlockImported);
158
160
  this.chain.emitter.on(routes.events.EventType.executionPayload, this.onPayloadImported);
159
161
  this.network.events.on(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -170,6 +172,7 @@ export class BlockInputSync {
170
172
  this.chain.emitter.off(ChainEvent.incompleteBlockInput, this.onIncompleteBlockInput);
171
173
  this.chain.emitter.off(ChainEvent.incompletePayloadEnvelope, this.onIncompletePayloadEnvelope);
172
174
  this.chain.emitter.off(ChainEvent.blockUnknownParent, this.onUnknownParent);
175
+ this.chain.emitter.off(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
173
176
  this.chain.emitter.off(routes.events.EventType.block, this.onBlockImported);
174
177
  this.chain.emitter.off(routes.events.EventType.executionPayload, this.onPayloadImported);
175
178
  this.network.events.off(NetworkEvent.peerConnected, this.onPeerConnected);
@@ -215,7 +218,7 @@ export class BlockInputSync {
215
218
 
216
219
  private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => {
217
220
  try {
218
- this.addByPayloadRootHex(data.rootHex, data.peer, data.slot);
221
+ this.addByPayloadRootHex(data.rootHex, data.peer, this.resolvePayloadSlot(data.rootHex));
219
222
  this.triggerUnknownBlockSearch();
220
223
  this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT});
221
224
  this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
@@ -258,7 +261,7 @@ export class BlockInputSync {
258
261
  }
259
262
 
260
263
  if (missingDependency.kind === "parentPayload") {
261
- this.addByPayloadRootHex(missingDependency.rootHex, data.peer);
264
+ this.addByPayloadRootHex(missingDependency.rootHex, data.peer, missingDependency.slot);
262
265
  } else if (missingDependency.kind === "parentBlock") {
263
266
  this.addByRootHex(missingDependency.rootHex, data.peer);
264
267
  }
@@ -284,6 +287,61 @@ export class BlockInputSync {
284
287
  this.triggerUnknownBlockSearch();
285
288
  };
286
289
 
290
+ /**
291
+ * Drop pending payloads and blocks that fell below the finalized slot.
292
+ * See https://github.com/ChainSafe/lodestar/issues/9800
293
+ */
294
+ private pruneFinalized = (checkpoint: CheckpointWithHex): void => {
295
+ // Use the actual finalized block slot, not computeStartSlotAtEpoch(checkpoint.epoch)
296
+ // because the payload at the finalized block is not finalized
297
+ const finalizedSlot = this.chain.forkChoice.getFinalizedBlock().slot;
298
+
299
+ let deletedPayloads = 0;
300
+ let unresolvedSlotPayloads = 0;
301
+ for (const [rootHex, payload] of this.pendingPayloads) {
302
+ // Last-effort resolve for a slot-less PendingPayloadRootHex, and store it back for later passes.
303
+ if (!isPendingPayloadInput(payload) && !isPendingPayloadEnvelope(payload) && payload.slot === undefined) {
304
+ payload.slot = this.resolvePayloadSlot(rootHex);
305
+ }
306
+
307
+ const slot = getPayloadSyncCacheItemSlot(payload);
308
+ if (typeof slot !== "number") {
309
+ unresolvedSlotPayloads++;
310
+ this.logger.debug("BlockInputSync.pruneFinalized: pending payload with unresolved slot", {
311
+ root: rootHex,
312
+ finalizedSlot,
313
+ });
314
+ continue;
315
+ }
316
+
317
+ if (slot < finalizedSlot) {
318
+ this.pendingPayloads.delete(rootHex);
319
+ deletedPayloads++;
320
+ }
321
+ }
322
+
323
+ let deletedBlocks = 0;
324
+ for (const [rootHex, block] of this.pendingBlocks) {
325
+ const slot = getBlockInputSyncCacheItemSlot(block);
326
+ if (typeof slot === "number" && slot < finalizedSlot) {
327
+ this.pendingBlocks.delete(rootHex);
328
+ deletedBlocks++;
329
+ }
330
+ }
331
+
332
+ if (deletedPayloads > 0 || deletedBlocks > 0) {
333
+ this.metrics?.blockInputSync.removedBlocks.inc(deletedPayloads + deletedBlocks);
334
+ }
335
+
336
+ this.logger.debug("BlockInputSync.pruneFinalized dropped pre-finalized pending items", {
337
+ finalizedSlot,
338
+ finalizedRoot: checkpoint.rootHex,
339
+ deletedPayloads,
340
+ deletedBlocks,
341
+ unresolvedSlotPayloads,
342
+ });
343
+ };
344
+
287
345
  private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
288
346
  let pendingBlock = this.pendingBlocks.get(rootHex);
289
347
  let added = false;
@@ -382,6 +440,29 @@ export class BlockInputSync {
382
440
  return added;
383
441
  };
384
442
 
443
+ /**
444
+ * Resolve the block slot for a payload root.
445
+ */
446
+ private resolvePayloadSlot(rootHex: RootHex): Slot | undefined {
447
+ // a pending block of the same root
448
+ const pendingBlock = this.pendingBlocks.get(rootHex);
449
+ if (pendingBlock) {
450
+ const pendingBlockSlot = getBlockInputSyncCacheItemSlot(pendingBlock);
451
+ if (typeof pendingBlockSlot === "number") {
452
+ return pendingBlockSlot;
453
+ }
454
+ }
455
+
456
+ // the block is in fork choice
457
+ const forkChoiceSlot = this.chain.forkChoice.getBlockHexDefaultStatus(rootHex)?.slot;
458
+ if (forkChoiceSlot !== undefined) {
459
+ return forkChoiceSlot;
460
+ }
461
+
462
+ // a seen payload envelope input
463
+ return this.chain.seenPayloadEnvelopeInputCache.get(rootHex)?.slot;
464
+ }
465
+
385
466
  private addByPayloadInput = (
386
467
  payloadInput: PayloadEnvelopeInput,
387
468
  peerIdStr?: PeerIdStr,
@@ -436,10 +517,12 @@ export class BlockInputSync {
436
517
  blockInput: IBlockInput
437
518
  ):
438
519
  | {kind: "ready"}
439
- | {kind: "block" | "parentBlock" | "parentPayload"; rootHex: RootHex}
520
+ | {kind: "block" | "parentBlock"; rootHex: RootHex}
521
+ | {kind: "parentPayload"; rootHex: RootHex; slot: Slot}
440
522
  | {kind: "invalidParentPayload"; parentRootHex: RootHex; parentBlockHashHex: RootHex} {
441
523
  const parentRootHex = blockInput.parentRootHex;
442
- if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) {
524
+ const parentBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentRootHex);
525
+ if (!parentBlock) {
443
526
  return {kind: "parentBlock", rootHex: parentRootHex};
444
527
  }
445
528
 
@@ -461,16 +544,8 @@ export class BlockInputSync {
461
544
  return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
462
545
  }
463
546
 
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};
547
+ // Parent is in fork choice but its payload `parentBlockHashHex` isn't revealed yet
548
+ return {kind: "parentPayload", rootHex: parentRootHex, slot: parentBlock.slot};
474
549
  }
475
550
 
476
551
  private advancePendingBlock(pendingBlock: PendingBlockInput): AdvancePendingBlockResult {
@@ -493,9 +568,9 @@ export class BlockInputSync {
493
568
  }
494
569
 
495
570
  case "parentPayload": {
496
- let added = this.addByPayloadRootHex(missingDependency.rootHex);
571
+ let added = this.addByPayloadRootHex(missingDependency.rootHex, undefined, missingDependency.slot);
497
572
  for (const peerIdStr of pendingBlock.peerIdStrings) {
498
- added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr) || added;
573
+ added = this.addByPayloadRootHex(missingDependency.rootHex, peerIdStr, missingDependency.slot) || added;
499
574
  }
500
575
  return added ? "queued_parent_payload" : "blocked";
501
576
  }
@@ -811,9 +886,9 @@ export class BlockInputSync {
811
886
  this.pendingBlocks.delete(blockRootHex);
812
887
 
813
888
  if (isForkPostGloas(fork)) {
814
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
889
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(blockRootHex);
815
890
  if (!payloadInput) {
816
- this.logger.warn("PayloadEnvelopeInput not seeded during unknown sync processReadyBlock()", logCtx);
891
+ this.logger.debug("PayloadEnvelopeInput unavailable during unknown sync processReadyBlock()", logCtx);
817
892
  } else {
818
893
  // Similar to the gossip path: immediately attempt fetch of data columns from the execution engine.
819
894
  // The bid already carries the kzg commitments, so there is no reason to wait for the payload to arrive.
@@ -886,12 +961,12 @@ export class BlockInputSync {
886
961
  */
887
962
  private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise<void> {
888
963
  const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
964
+
889
965
  if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
890
966
  this.pendingPayloads.delete(rootHex);
891
967
  return;
892
968
  }
893
969
 
894
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
895
970
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
896
971
  // Block not in fork choice yet. payloadInput may be seeded from the block body during download, so a
897
972
  // non-null payloadInput does not imply the block is imported; defer regardless and pull the block first.
@@ -907,8 +982,9 @@ export class BlockInputSync {
907
982
  return;
908
983
  }
909
984
 
985
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
910
986
  if (!payloadInput) {
911
- this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
987
+ this.logger.debug("PayloadEnvelopeInput not yet reloadable for imported block, will retry", {
912
988
  root: rootHex,
913
989
  });
914
990
  return;
@@ -928,6 +1004,7 @@ export class BlockInputSync {
928
1004
  const pendingPayloadByRoot: PendingPayloadRootHex = {
929
1005
  status: PendingPayloadInputStatus.pending,
930
1006
  rootHex,
1007
+ slot: pendingPayload.envelope.message.payload.slotNumber,
931
1008
  timeAddedSec: pendingPayload.timeAddedSec,
932
1009
  peerIdStrings: new Set(pendingPayload.peerIdStrings),
933
1010
  };
@@ -967,6 +1044,10 @@ export class BlockInputSync {
967
1044
  return;
968
1045
  }
969
1046
 
1047
+ if (!isPendingPayloadInput(payload) && payload.slot === undefined) {
1048
+ payload.slot = this.resolvePayloadSlot(rootHex);
1049
+ }
1050
+
970
1051
  const payloadSlot = getPayloadSyncCacheItemSlot(payload);
971
1052
  const logCtx = {
972
1053
  slot: payloadSlot,
@@ -982,7 +1063,13 @@ export class BlockInputSync {
982
1063
  const res = await wrapError(this.fetchPayloadInput(payload));
983
1064
  if (!res.err) {
984
1065
  const pendingPayload = res.result;
985
- this.pendingPayloads.set(getPayloadSyncCacheItemRootHex(pendingPayload), pendingPayload);
1066
+ const resultRootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
1067
+ // finalization may have deleted this entry while we were awaiting the network, do not resurrect it via the set below
1068
+ if (!this.pendingPayloads.has(resultRootHex)) {
1069
+ this.logger.verbose("Dropping downloaded payload, entry pruned during fetch", logCtx);
1070
+ return;
1071
+ }
1072
+ this.pendingPayloads.set(resultRootHex, pendingPayload);
986
1073
 
987
1074
  if (isPendingPayloadEnvelope(pendingPayload)) {
988
1075
  await this.reconcilePayloadEnvelope(pendingPayload);
@@ -1093,7 +1180,7 @@ export class BlockInputSync {
1093
1180
  let slot = getPayloadSyncCacheItemSlot(cacheItem);
1094
1181
  let payloadInput = isPendingPayloadInput(cacheItem)
1095
1182
  ? cacheItem.payloadInput
1096
- : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1183
+ : await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1097
1184
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1098
1185
 
1099
1186
  let i = 0;
@@ -1110,9 +1197,11 @@ export class BlockInputSync {
1110
1197
  );
1111
1198
  }
1112
1199
 
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
- );
1200
+ const reason =
1201
+ pendingColumns.size > 0
1202
+ ? `cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
1203
+ : "no peer available to download pending payload";
1204
+ throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: ${reason}`);
1116
1205
  }
1117
1206
 
1118
1207
  const {peerId, client: peerClient} = peerMeta;
@@ -1124,7 +1213,7 @@ export class BlockInputSync {
1124
1213
  slot = envelope.message.payload.slotNumber;
1125
1214
  }
1126
1215
 
1127
- payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1216
+ payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1128
1217
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
1129
1218
  // Block not in fork choice yet. Validating now would throw BLOCK_ROOT_UNKNOWN, so keep the downloaded
1130
1219
  // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
@@ -136,6 +136,8 @@ export function cacheByRangeResponses({
136
136
  const source = BlockInputSource.byRange;
137
137
  const updatedBatchBlocks = new Map<Slot, IBlockInput>(batchBlocks.map((block) => [block.slot, block]));
138
138
 
139
+ const payloadEnvelopes = new Map<Slot, PayloadEnvelopeInput>(existingPayloadEnvelopes);
140
+
139
141
  const blocks = responses.validatedBlocks ?? [];
140
142
  for (let i = 0; i < blocks.length; i++) {
141
143
  const {block, blockRoot} = blocks[i];
@@ -172,15 +174,17 @@ export function cacheByRangeResponses({
172
174
  // later step) can throw and abort the batch — otherwise a gloas block would sit in
173
175
  // seenBlockInputCache but unseeded here, and payload-by-root sync would later throw "Missing
174
176
  // PayloadEnvelopeInput for known block" (see issue #9306). add() is idempotent.
175
- if (isForkPostGloas(blockInput.forkName)) {
176
- seenPayloadEnvelopeInputCache.add({
177
+ if (isForkPostGloas(blockInput.forkName) && !payloadEnvelopes.has(blockInput.slot)) {
178
+ const payloadInput = seenPayloadEnvelopeInputCache.add({
177
179
  blockRootHex: blockInput.blockRootHex,
178
180
  block: blockInput.getBlock() as SignedBeaconBlock<ForkPostGloas>,
179
181
  forkName: blockInput.forkName,
180
182
  sampledColumns: custodyConfig.sampledColumns,
181
183
  custodyColumns: custodyConfig.custodyColumns,
182
- timeCreatedSec: seenTimestampSec,
184
+ seenTimestampSec,
185
+ source: PayloadEnvelopeInputSource.byRange,
183
186
  });
187
+ payloadEnvelopes.set(blockInput.slot, payloadInput);
184
188
  }
185
189
  }
186
190
 
@@ -222,17 +226,22 @@ export function cacheByRangeResponses({
222
226
  }
223
227
  }
224
228
 
225
- let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null =
226
- existingPayloadEnvelopes !== null ? new Map(existingPayloadEnvelopes) : null;
227
229
  if (downloadedPayloadEnvelopes !== null) {
228
- payloadEnvelopes ??= new Map();
229
230
  for (const [slot, envelope] of downloadedPayloadEnvelopes) {
230
- const envelopeBlockRootHex = toRootHex(envelope.message.beaconBlockRoot);
231
- const payloadInput = seenPayloadEnvelopeInputCache.get(envelopeBlockRootHex);
231
+ // the only PayloadEnvelopeInput miss is the dangling parent, which we can get from the seen cache
232
+ let payloadInput = payloadEnvelopes.get(slot);
232
233
  if (payloadInput === undefined) {
233
- // Unreachable given the validatedBlocks loop above seeded an entry for every gloas block in
234
- // the batch. for the parent block, it's populated at BeaconChain init
235
- throw new Error(`Missing PayloadEnvelopeInput for block ${envelopeBlockRootHex}`);
234
+ if (updatedBatchBlocks.has(slot)) {
235
+ throw new Error(
236
+ `Missing PayloadEnvelopeInput for in-batch slot ${slot} root ${toRootHex(envelope.message.beaconBlockRoot)}`
237
+ );
238
+ }
239
+ payloadInput = seenPayloadEnvelopeInputCache.get(toRootHex(envelope.message.beaconBlockRoot));
240
+ }
241
+ if (payloadInput === undefined) {
242
+ throw new Error(
243
+ `Missing PayloadEnvelopeInput for slot ${slot} root ${toRootHex(envelope.message.beaconBlockRoot)}`
244
+ );
236
245
  }
237
246
 
238
247
  if (!payloadInput.hasPayloadEnvelope()) {
@@ -262,7 +271,7 @@ export function cacheByRangeResponses({
262
271
  // Gloas columns are attached to the matching PayloadEnvelopeInput, NOT to IBlockInput.
263
272
  // Gloas DataColumnSidecar has `slot` directly (no signedBlockHeader).
264
273
  const dataSlot = firstColumn.slot;
265
- const payloadInput = payloadEnvelopes?.get(dataSlot);
274
+ const payloadInput = payloadEnvelopes.get(dataSlot);
266
275
  if (!payloadInput) {
267
276
  // Should not happen: we built payloadInputs for all gloas blocks above
268
277
  continue;
@@ -13,6 +13,7 @@ import {BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lod
13
13
  import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils";
14
14
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
15
15
  import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js";
16
+ import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
16
17
  import {ChainEventEmitter} from "../../chain/emitter.js";
17
18
  import {IBeaconChain} from "../../chain/interface.js";
18
19
  import {validateBlockBlobSidecars} from "../../chain/validation/blobSidecar.js";
@@ -122,7 +123,8 @@ export async function downloadByRoot({
122
123
  forkName: blockInput.forkName,
123
124
  sampledColumns: chain.custodyConfig.sampledColumns,
124
125
  custodyColumns: chain.custodyConfig.custodyColumns,
125
- timeCreatedSec: Date.now() / 1000,
126
+ seenTimestampSec: Date.now() / 1000,
127
+ source: PayloadEnvelopeInputSource.byRoot,
126
128
  });
127
129
  }
128
130