@lodestar/beacon-node 1.50.0-dev.cc4428df4b → 1.50.0-dev.cddd01a6c6

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 (169) 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 -1
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.js +4 -4
  5. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  6. package/lib/chain/archiveStore/archiveStore.d.ts +1 -0
  7. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  8. package/lib/chain/archiveStore/archiveStore.js +6 -2
  9. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  10. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +4 -1
  11. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  12. package/lib/chain/archiveStore/utils/pruneHistory.js +4 -3
  13. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  14. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  15. package/lib/chain/blocks/importBlock.js +4 -0
  16. package/lib/chain/blocks/importBlock.js.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
  18. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  19. package/lib/chain/chain.d.ts +5 -4
  20. package/lib/chain/chain.d.ts.map +1 -1
  21. package/lib/chain/chain.js +14 -16
  22. package/lib/chain/chain.js.map +1 -1
  23. package/lib/chain/errors/envelopeReconstructionError.d.ts +5 -0
  24. package/lib/chain/errors/envelopeReconstructionError.d.ts.map +1 -1
  25. package/lib/chain/errors/envelopeReconstructionError.js +2 -0
  26. package/lib/chain/errors/envelopeReconstructionError.js.map +1 -1
  27. package/lib/chain/errors/gossipValidation.d.ts +3 -1
  28. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  29. package/lib/chain/errors/gossipValidation.js +2 -0
  30. package/lib/chain/errors/gossipValidation.js.map +1 -1
  31. package/lib/chain/initState.d.ts +2 -9
  32. package/lib/chain/initState.d.ts.map +1 -1
  33. package/lib/chain/initState.js +5 -38
  34. package/lib/chain/initState.js.map +1 -1
  35. package/lib/chain/interface.d.ts +2 -2
  36. package/lib/chain/interface.d.ts.map +1 -1
  37. package/lib/chain/opPools/opPool.d.ts +5 -4
  38. package/lib/chain/opPools/opPool.d.ts.map +1 -1
  39. package/lib/chain/opPools/opPool.js +62 -18
  40. package/lib/chain/opPools/opPool.js.map +1 -1
  41. package/lib/chain/opPools/utils.d.ts +4 -1
  42. package/lib/chain/opPools/utils.d.ts.map +1 -1
  43. package/lib/chain/opPools/utils.js +11 -1
  44. package/lib/chain/opPools/utils.js.map +1 -1
  45. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  46. package/lib/chain/prepareNextSlot.js +5 -2
  47. package/lib/chain/prepareNextSlot.js.map +1 -1
  48. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  49. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  50. package/lib/chain/produceBlock/produceBlockBody.js +6 -2
  51. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  52. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +9 -2
  53. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  54. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +23 -13
  55. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  56. package/lib/chain/validation/attesterSlashing.d.ts +4 -4
  57. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  58. package/lib/chain/validation/attesterSlashing.js +3 -0
  59. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  60. package/lib/chain/validation/proposerSlashing.d.ts +3 -3
  61. package/lib/chain/validation/proposerSlashing.d.ts.map +1 -1
  62. package/lib/chain/validation/proposerSlashing.js +3 -0
  63. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  64. package/lib/db/beacon.d.ts.map +1 -1
  65. package/lib/db/beacon.js +2 -0
  66. package/lib/db/beacon.js.map +1 -1
  67. package/lib/db/repositories/proposerSlashing.d.ts +1 -0
  68. package/lib/db/repositories/proposerSlashing.d.ts.map +1 -1
  69. package/lib/db/repositories/proposerSlashing.js +5 -1
  70. package/lib/db/repositories/proposerSlashing.js.map +1 -1
  71. package/lib/execution/engine/disabled.d.ts +0 -2
  72. package/lib/execution/engine/disabled.d.ts.map +1 -1
  73. package/lib/execution/engine/disabled.js +0 -6
  74. package/lib/execution/engine/disabled.js.map +1 -1
  75. package/lib/execution/engine/http.d.ts +1 -3
  76. package/lib/execution/engine/http.d.ts.map +1 -1
  77. package/lib/execution/engine/http.js +7 -21
  78. package/lib/execution/engine/http.js.map +1 -1
  79. package/lib/execution/engine/interface.d.ts +2 -4
  80. package/lib/execution/engine/interface.d.ts.map +1 -1
  81. package/lib/execution/engine/mock.d.ts +0 -2
  82. package/lib/execution/engine/mock.d.ts.map +1 -1
  83. package/lib/execution/engine/mock.js +0 -8
  84. package/lib/execution/engine/mock.js.map +1 -1
  85. package/lib/execution/engine/types.d.ts +0 -8
  86. package/lib/execution/engine/types.d.ts.map +1 -1
  87. package/lib/execution/engine/types.js.map +1 -1
  88. package/lib/index.d.ts +1 -1
  89. package/lib/index.d.ts.map +1 -1
  90. package/lib/index.js +1 -1
  91. package/lib/index.js.map +1 -1
  92. package/lib/metrics/metrics/beacon.d.ts +1 -1
  93. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  94. package/lib/metrics/metrics/beacon.js.map +1 -1
  95. package/lib/network/metadata.d.ts +1 -1
  96. package/lib/network/metadata.d.ts.map +1 -1
  97. package/lib/network/metadata.js +17 -17
  98. package/lib/network/metadata.js.map +1 -1
  99. package/lib/network/peers/datastore.d.ts +9 -9
  100. package/lib/network/peers/datastore.d.ts.map +1 -1
  101. package/lib/network/peers/datastore.js +36 -36
  102. package/lib/network/peers/datastore.js.map +1 -1
  103. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  104. package/lib/network/processor/gossipHandlers.js +7 -4
  105. package/lib/network/processor/gossipHandlers.js.map +1 -1
  106. package/lib/network/processor/gossipValidatorFn.d.ts +7 -3
  107. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  108. package/lib/network/processor/gossipValidatorFn.js +7 -3
  109. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  110. package/lib/network/processor/index.d.ts +9 -0
  111. package/lib/network/processor/index.d.ts.map +1 -1
  112. package/lib/network/processor/index.js +77 -10
  113. package/lib/network/processor/index.js.map +1 -1
  114. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  115. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +41 -26
  116. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  117. package/lib/sync/range/batch.d.ts +6 -0
  118. package/lib/sync/range/batch.d.ts.map +1 -1
  119. package/lib/sync/range/batch.js +10 -0
  120. package/lib/sync/range/batch.js.map +1 -1
  121. package/lib/sync/range/range.d.ts.map +1 -1
  122. package/lib/sync/range/range.js +50 -18
  123. package/lib/sync/range/range.js.map +1 -1
  124. package/lib/sync/unknownBlock.d.ts +9 -5
  125. package/lib/sync/unknownBlock.d.ts.map +1 -1
  126. package/lib/sync/unknownBlock.js +88 -46
  127. package/lib/sync/unknownBlock.js.map +1 -1
  128. package/lib/util/execution.d.ts +11 -11
  129. package/lib/util/execution.d.ts.map +1 -1
  130. package/lib/util/execution.js +11 -7
  131. package/lib/util/execution.js.map +1 -1
  132. package/package.json +15 -15
  133. package/src/api/impl/beacon/blocks/index.ts +7 -4
  134. package/src/api/impl/beacon/pool/index.ts +4 -4
  135. package/src/chain/archiveStore/archiveStore.ts +9 -3
  136. package/src/chain/archiveStore/utils/pruneHistory.ts +6 -4
  137. package/src/chain/blocks/importBlock.ts +4 -0
  138. package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
  139. package/src/chain/chain.ts +17 -19
  140. package/src/chain/errors/envelopeReconstructionError.ts +3 -0
  141. package/src/chain/errors/gossipValidation.ts +2 -0
  142. package/src/chain/initState.ts +8 -60
  143. package/src/chain/interface.ts +2 -2
  144. package/src/chain/opPools/opPool.ts +91 -18
  145. package/src/chain/opPools/utils.ts +31 -3
  146. package/src/chain/prepareNextSlot.ts +6 -3
  147. package/src/chain/produceBlock/produceBlockBody.ts +8 -1
  148. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +31 -13
  149. package/src/chain/validation/attesterSlashing.ts +7 -4
  150. package/src/chain/validation/proposerSlashing.ts +7 -4
  151. package/src/db/beacon.ts +2 -0
  152. package/src/db/repositories/proposerSlashing.ts +6 -1
  153. package/src/execution/engine/disabled.ts +0 -8
  154. package/src/execution/engine/http.ts +7 -34
  155. package/src/execution/engine/interface.ts +2 -6
  156. package/src/execution/engine/mock.ts +1 -19
  157. package/src/execution/engine/types.ts +0 -10
  158. package/src/index.ts +1 -1
  159. package/src/metrics/metrics/beacon.ts +1 -1
  160. package/src/network/metadata.ts +17 -17
  161. package/src/network/peers/datastore.ts +36 -36
  162. package/src/network/processor/gossipHandlers.ts +7 -4
  163. package/src/network/processor/gossipValidatorFn.ts +13 -3
  164. package/src/network/processor/index.ts +89 -10
  165. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +42 -28
  166. package/src/sync/range/batch.ts +11 -0
  167. package/src/sync/range/range.ts +50 -18
  168. package/src/sync/unknownBlock.ts +99 -50
  169. package/src/util/execution.ts +26 -14
@@ -6,6 +6,7 @@ import {toRootHex} from "@lodestar/utils";
6
6
  import {EnvelopeReconstructionError} from "../../../chain/errors/index.js";
7
7
  import {IBeaconChain} from "../../../chain/index.js";
8
8
  import {IBeaconDb} from "../../../db/index.js";
9
+ import {MAX_BODIES_PER_REQUEST} from "../../../util/execution.js";
9
10
  import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js";
10
11
  import {prettyPrintPeerId} from "../../util.js";
11
12
 
@@ -19,10 +20,15 @@ export async function* onExecutionPayloadEnvelopesByRoot(
19
20
  // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
20
21
  // Archival nodes may still serve older retained payloads to allow genesis sync.
21
22
 
22
- // Resolve slots first so archived header envelopes are rebuilt in one EL batch, not one call per root
23
+ // Resolve slots first so archived envelopes can be reconstructed in EL batches.
24
+ // Duplicate roots are served once, since each archived envelope costs an EL fetch and a rebuild.
23
25
  const requests: {blockSlot: Slot; blockRootHex: RootHex}[] = [];
26
+ const seenRoots = new Set<RootHex>();
24
27
  for (const root of requestBody) {
25
28
  const rootHex = toRootHex(root);
29
+ if (seenRoots.has(rootHex)) continue;
30
+ seenRoots.add(rootHex);
31
+
26
32
  const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex);
27
33
  // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive
28
34
  const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root);
@@ -41,35 +47,43 @@ export async function* onExecutionPayloadEnvelopesByRoot(
41
47
  requests.push({blockSlot: slot, blockRootHex: rootHex});
42
48
  }
43
49
 
44
- let envelopesBytes: (Uint8Array | null)[];
45
- try {
46
- // by-root allows omission, so a mismatched envelope is left out rather than failing the response
47
- envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(requests, "omit");
48
- } catch (e) {
49
- if (e instanceof EnvelopeReconstructionError) {
50
- throw new ResponseError(
51
- RespStatus.RESOURCE_UNAVAILABLE,
52
- `Failed to reconstruct archived envelopes: ${e.message}`
53
- );
50
+ // Yield between EL batches so the first response does not wait for the entire request.
51
+ let yielded = 0;
52
+ for (let i = 0; i < requests.length; i += MAX_BODIES_PER_REQUEST) {
53
+ const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
54
+ let envelopesBytes: (Uint8Array | null)[];
55
+ try {
56
+ // By-root allows omission, so an envelope that cannot be rebuilt is left out rather than failing the response.
57
+ envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
58
+ } catch (e) {
59
+ if (e instanceof EnvelopeReconstructionError) {
60
+ // By-root permits partial responses, so an EL failure can end the response here.
61
+ if (yielded > 0) return;
62
+ throw new ResponseError(
63
+ RespStatus.RESOURCE_UNAVAILABLE,
64
+ `Failed to reconstruct archived envelopes: ${e.message}`
65
+ );
66
+ }
67
+ throw e;
54
68
  }
55
- throw e;
56
- }
57
69
 
58
- for (let i = 0; i < requests.length; i++) {
59
- const {blockSlot, blockRootHex} = requests[i];
60
- const envelopeBytes = envelopesBytes[i];
61
- if (envelopeBytes) {
62
- yield {
63
- data: envelopeBytes,
64
- boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
65
- };
66
- } else {
67
- chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
68
- slot: blockSlot,
69
- root: blockRootHex,
70
- peer: prettyPrintPeerId(peerId),
71
- client: peerClient,
72
- });
70
+ for (let j = 0; j < batch.length; j++) {
71
+ const {blockSlot, blockRootHex} = batch[j];
72
+ const envelopeBytes = envelopesBytes[j];
73
+ if (envelopeBytes) {
74
+ yielded++;
75
+ yield {
76
+ data: envelopeBytes,
77
+ boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
78
+ };
79
+ } else {
80
+ chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
81
+ slot: blockSlot,
82
+ root: blockRootHex,
83
+ peer: prettyPrintPeerId(peerId),
84
+ client: peerClient,
85
+ });
86
+ }
73
87
  }
74
88
  }
75
89
  }
@@ -773,6 +773,7 @@ export class Batch {
773
773
  // remove any downloaded blocks and re-attempt
774
774
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
775
775
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
776
+ this.resetRequests();
776
777
  }
777
778
 
778
779
  private onProcessingError(attempt: FailedAttempt): void {
@@ -784,6 +785,16 @@ export class Batch {
784
785
  // remove any downloaded blocks and re-attempt
785
786
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
786
787
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
788
+ this.resetRequests();
789
+ }
790
+
791
+ /**
792
+ * After a partial download `requests` only cover the data still missing from the downloaded blocks,
793
+ * e.g. a single column. Once those blocks are dropped, request the whole batch again, otherwise
794
+ * every peer fails the retry for lack of blocks to validate the data against.
795
+ */
796
+ private resetRequests(): void {
797
+ this.requests = this.getRequests([]);
787
798
  }
788
799
 
789
800
  /** Helper to construct typed BatchError. Stack traces are correct as the error is thrown above */
@@ -5,6 +5,7 @@ import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
6
  import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
8
+ import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
8
9
  import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
9
10
  import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
10
11
  import {BlockError} from "../../chain/errors/index.js";
@@ -207,22 +208,50 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
207
208
  blsVerifyOnMainThread: false,
208
209
  };
209
210
 
210
- if (this.opts?.disableProcessAsChainSegment) {
211
- // Should only be used for debugging or testing
212
- for (const block of blocks) {
213
- await this.chain.processBlock(block, flags);
214
- const payloadEnvelope = payloadEnvelopes?.get(block.slot);
215
- if (payloadEnvelope?.hasPayloadEnvelope()) {
216
- await this.chain.processExecutionPayload(payloadEnvelope);
211
+ try {
212
+ if (this.opts?.disableProcessAsChainSegment) {
213
+ // Should only be used for debugging or testing
214
+ for (const block of blocks) {
215
+ await this.chain.processBlock(block, flags);
216
+ const payloadEnvelope = payloadEnvelopes?.get(block.slot);
217
+ if (payloadEnvelope?.hasPayloadEnvelope()) {
218
+ await this.chain.processExecutionPayload(payloadEnvelope);
219
+ }
220
+ }
221
+ } else {
222
+ const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
223
+ // We log orphaned payloads to work with different clients
224
+ // in the future, consider applying penalties in certain conditions
225
+ // make sure the payload source was from range sync in that case
226
+ for (const {slot, payloadEnvelopeInput} of orphaned) {
227
+ const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
228
+ let client = "unknown";
229
+ if (peerIdStr !== undefined) {
230
+ try {
231
+ client = this.getConnectedPeerSyncMeta(peerIdStr).client;
232
+ } catch {
233
+ // peer disconnected since serving the envelope, keep "unknown"
234
+ }
235
+ }
236
+ this.logger.debug("Orphaned payload envelope in range sync batch", {
237
+ slot,
238
+ root: payloadEnvelopeInput.blockRootHex,
239
+ peer: peerIdStr ?? "unknown",
240
+ source,
241
+ client,
242
+ skipped,
243
+ });
217
244
  }
218
245
  }
219
- } else {
220
- const {orphaned, skipped} = await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
221
- // We log orphaned payloads to work with different clients
222
- // in the future, consider applying penalties in certain conditions
223
- // make sure the payload source was from range sync in that case
224
- for (const {slot, payloadEnvelopeInput} of orphaned) {
225
- const {peerIdStr, source} = payloadEnvelopeInput.getPayloadEnvelopeSource();
246
+ } catch (e) {
247
+ if (
248
+ e instanceof PayloadError &&
249
+ (e.type.code === PayloadErrorCode.INVALID_SIGNATURE ||
250
+ e.type.code === PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR ||
251
+ e.type.code === PayloadErrorCode.EXECUTION_ENGINE_INVALID)
252
+ ) {
253
+ const {payloadInput} = e;
254
+ const {peerIdStr, source} = payloadInput.getPayloadEnvelopeSource();
226
255
  let client = "unknown";
227
256
  if (peerIdStr !== undefined) {
228
257
  try {
@@ -231,15 +260,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
231
260
  // peer disconnected since serving the envelope, keep "unknown"
232
261
  }
233
262
  }
234
- this.logger.debug("Orphaned payload envelope in range sync batch", {
235
- slot,
236
- root: payloadEnvelopeInput.blockRootHex,
263
+ this.logger.debug("Removing invalid payload envelope in range sync batch", {
264
+ slot: payloadInput.slot,
265
+ root: payloadInput.blockRootHex,
237
266
  peer: peerIdStr ?? "unknown",
238
267
  source,
239
268
  client,
240
- skipped,
269
+ code: e.type.code,
241
270
  });
271
+ // The batch is downloaded again, it would otherwise reuse the cached envelope and fail the same way
272
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(payloadInput);
242
273
  }
274
+ throw e;
243
275
  }
244
276
  };
245
277
 
@@ -15,7 +15,7 @@ import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js";
15
15
  import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js";
16
16
  import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/executionPayloadEnvelope.js";
17
17
  import {Metrics} from "../metrics/index.js";
18
- import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
18
+ import {INetwork, NetworkEvent, NetworkEventData, PeerAction, prettyPrintPeerIdStr} from "../network/index.js";
19
19
  import {PeerSyncMeta} from "../network/peers/peersData.js";
20
20
  import {MAX_PEERS_PER_ROOT} from "../network/processor/constants.js";
21
21
  import {ClockEvent} from "../util/clock.js";
@@ -442,6 +442,11 @@ export class BlockInputSync {
442
442
  };
443
443
 
444
444
  private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
445
+ if (this.knownBadBlocks.has(rootHex)) {
446
+ this.logger.debug("Ignoring known bad block root", {root: rootHex, peerIdStr: peerIdStr ?? "unknown peer"});
447
+ return false;
448
+ }
449
+
445
450
  let pendingBlock = this.pendingBlocks.get(rootHex);
446
451
  let added = false;
447
452
  if (!pendingBlock) {
@@ -477,6 +482,11 @@ export class BlockInputSync {
477
482
  };
478
483
 
479
484
  private addByBlockInput = (blockInput: IBlockInput, peerIdStr?: string): void => {
485
+ if (this.knownBadBlocks.has(blockInput.blockRootHex)) {
486
+ this.logger.debug("Ignoring known bad block", {slot: blockInput.slot, root: blockInput.blockRootHex, peerIdStr});
487
+ return;
488
+ }
489
+
480
490
  let pendingBlock = this.pendingBlocks.get(blockInput.blockRootHex);
481
491
  // if entry is missing or was added via rootHex and now we have more complete information overwrite
482
492
  // the existing information with the more complete cache entry
@@ -513,6 +523,15 @@ export class BlockInputSync {
513
523
  };
514
524
 
515
525
  private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr, slot?: Slot): boolean => {
526
+ if (this.knownBadBlocks.has(rootHex)) {
527
+ this.logger.debug("Ignoring payload for known bad block root", {
528
+ slot: slot ?? "unknown",
529
+ root: rootHex,
530
+ peerIdStr,
531
+ });
532
+ return false;
533
+ }
534
+
516
535
  let pendingPayload = this.pendingPayloads.get(rootHex);
517
536
  let added = false;
518
537
  if (!pendingPayload) {
@@ -581,6 +600,15 @@ export class BlockInputSync {
581
600
  peerIdStr?: PeerIdStr,
582
601
  envelope?: gloas.SignedExecutionPayloadEnvelope
583
602
  ): void => {
603
+ if (this.knownBadBlocks.has(payloadInput.blockRootHex)) {
604
+ this.logger.debug("Ignoring payload input for known bad block root", {
605
+ slot: payloadInput.slot,
606
+ root: payloadInput.blockRootHex,
607
+ peerIdStr,
608
+ });
609
+ return;
610
+ }
611
+
584
612
  const pendingPayload = this.toPendingPayloadInput(
585
613
  payloadInput,
586
614
  this.pendingPayloads.get(payloadInput.blockRootHex),
@@ -654,6 +682,7 @@ export class BlockInputSync {
654
682
  return {kind: "ready"};
655
683
  }
656
684
 
685
+ // The parent's payload is known but does not match `parentBlockHashHex`
657
686
  if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
658
687
  return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
659
688
  }
@@ -1032,7 +1061,8 @@ export class BlockInputSync {
1032
1061
  }
1033
1062
  } else if (blockSlot <= finalizedSlot) {
1034
1063
  // the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
1035
- // we should found it through forkchoice. If not, we should penalize all peers sending us this block chain
1064
+ // we should found it through forkchoice. If not, drop the chain without penalizing peers because pending entries are
1065
+ // block retained on failed download, so finalization can pass the block while it is still pending
1036
1066
  // 0 - 1 - ... - n - finalizedSlot
1037
1067
  // \
1038
1068
  // parent 1 - parent 2 - ... - unknownParent block
@@ -1040,7 +1070,7 @@ export class BlockInputSync {
1040
1070
  ...logCtx2,
1041
1071
  finalizedSlot,
1042
1072
  });
1043
- this.removeAndDownScoreAllDescendants(block, DroppedItemReason.belowFinalized);
1073
+ this.removeAllDescendants(block, DroppedItemReason.belowFinalized);
1044
1074
  } else {
1045
1075
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
1046
1076
  }
@@ -1172,15 +1202,18 @@ export class BlockInputSync {
1172
1202
  break;
1173
1203
 
1174
1204
  case BlockErrorCode.EXECUTION_ENGINE_INVALID:
1175
- // the peer served a bad block
1205
+ // Ban but do not downscore: optimistic-sync peers may forward and serve blocks whose payload is later
1206
+ // rejected by the execution engine. Per the spec, an INVALID execution payload SHOULD NOT cause a peer
1207
+ // to be down-scored or disconnected, only consensus-layer invalidity MAY.
1208
+ // https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
1176
1209
  this.logger.debug("Execution engine rejected block from unknown parent sync", errorData, res.err);
1177
- this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
1210
+ this.removeAndBanAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
1178
1211
  break;
1179
1212
 
1180
1213
  default:
1181
1214
  // Block is not correct with respect to our chain. Log error loudly
1182
1215
  this.logger.debug("Error processing block from unknown parent sync", errorData, res.err);
1183
- this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
1216
+ this.removeAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
1184
1217
  }
1185
1218
  }
1186
1219
 
@@ -1393,20 +1426,23 @@ export class BlockInputSync {
1393
1426
  pendingPayload.status = PendingPayloadInputStatus.downloaded;
1394
1427
  break;
1395
1428
 
1429
+ // The invalid envelope is evicted from the seen cache in the cases below, otherwise the next
1430
+ // attempt would reuse it instead of fetching another envelope from peers
1396
1431
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1397
1432
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1433
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1398
1434
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
1399
1435
  break;
1400
1436
 
1401
1437
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1402
1438
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1439
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1403
1440
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
1404
1441
  break;
1405
1442
 
1406
1443
  case PayloadErrorCode.INVALID_SIGNATURE:
1407
- // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1408
- // reintroducing envelope replacement / recreation flows.
1409
1444
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1445
+ this.chain.seenPayloadEnvelopeInputCache.removeInvalid(pendingPayload.payloadInput);
1410
1446
  this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
1411
1447
  break;
1412
1448
 
@@ -1478,11 +1514,11 @@ export class BlockInputSync {
1478
1514
  }
1479
1515
 
1480
1516
  const {peerId, client: peerClient} = peerMeta;
1481
- cacheItem.peerIdStrings.add(peerId);
1482
1517
 
1483
1518
  try {
1484
1519
  if (!envelope) {
1485
1520
  envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
1521
+ cacheItem.peerIdStrings.add(peerId);
1486
1522
  const slotWasUnknown = typeof slot !== "number";
1487
1523
  slot = envelope.message.payload.slotNumber;
1488
1524
  if (slotWasUnknown) {
@@ -1524,6 +1560,8 @@ export class BlockInputSync {
1524
1560
  const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
1525
1561
  if (missing.length > 0) {
1526
1562
  const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
1563
+ // toPendingPayloadInput copied cacheItem's set, so add to the object that is carried forward
1564
+ pendingPayload.peerIdStrings.add(peerId);
1527
1565
  const seenTimestampSec = Date.now() / 1000;
1528
1566
  for (const columnSidecar of columnSidecars) {
1529
1567
  if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
@@ -1716,8 +1754,6 @@ export class BlockInputSync {
1716
1754
  }
1717
1755
  const {peerId, client: peerClient} = peerMeta;
1718
1756
 
1719
- cacheItem.peerIdStrings.add(peerId);
1720
-
1721
1757
  try {
1722
1758
  const downloadResult = await downloadByRoot({
1723
1759
  config: this.config,
@@ -1728,6 +1764,7 @@ export class BlockInputSync {
1728
1764
  cacheItem,
1729
1765
  });
1730
1766
  cacheItem = downloadResult.result;
1767
+ cacheItem.peerIdStrings.add(peerId);
1731
1768
  if (slot === undefined) {
1732
1769
  slot = cacheItem.blockInput.slot;
1733
1770
  // we were not able to observe the time into slot when starting the fetch, do it now
@@ -1843,44 +1880,8 @@ export class BlockInputSync {
1843
1880
  throw Error(message);
1844
1881
  }
1845
1882
 
1846
- /**
1847
- * Gets all descendant blocks of `block` recursively from `pendingBlocks`.
1848
- * Assumes that if a parent block does not exist or is not processable, all descendant blocks are bad too.
1849
- * Downscore all peers that have referenced any of this bad blocks. May report peers multiple times if they have
1850
- * referenced more than one bad block.
1851
- */
1852
- private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1853
- // Get all blocks that are a descendant of this one
1854
- const badPendingBlocks = this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent);
1855
- // just console log and do not penalize on pending/bad blocks for debugging
1856
- // console.log("removeAndDownscoreAllDescendants", {block});
1857
-
1858
- for (const block of badPendingBlocks) {
1859
- //
1860
- // TODO(fulu): why is this commented out here?
1861
- //
1862
- // this.knownBadBlocks.add(block.blockRootHex);
1863
- // for (const peerIdStr of block.peerIdStrings) {
1864
- // // TODO: Refactor peerRpcScores to work with peerIdStr only
1865
- // this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadBlockByRoot");
1866
- // }
1867
- this.logger.debug("ignored Banning unknown block", {
1868
- slot: getBlockInputSyncCacheItemSlot(block),
1869
- root: getBlockInputSyncCacheItemRootHex(block),
1870
- peerIdStrings: Array.from(block.peerIdStrings)
1871
- .map((id) => prettyPrintPeerIdStr(id))
1872
- .join(","),
1873
- });
1874
- }
1875
-
1876
- // Prune knownBadBlocks
1877
- pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
1878
- }
1879
-
1880
1883
  // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1881
1884
  private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
1882
- // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1883
- // later validation/finalization logic decides when it can leave memory.
1884
1885
  if (this.pendingPayloads.delete(rootHex)) {
1885
1886
  this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
1886
1887
  }
@@ -1903,13 +1904,61 @@ export class BlockInputSync {
1903
1904
  }
1904
1905
  }
1905
1906
 
1907
+ /**
1908
+ * Same as `removeAndBanAllDescendants`, and downscores every peer that referenced or served a banned item, once per
1909
+ * peer. Only call for consensus-layer invalidity, never for an INVALID execution payload:
1910
+ * https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
1911
+ */
1912
+ private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1913
+ const peersToReport = new Set<PeerIdStr>();
1914
+ for (const bannedBlock of this.removeAndBanAllDescendants(block, headReason)) {
1915
+ for (const peerIdStr of bannedBlock.peerIdStrings) {
1916
+ peersToReport.add(peerIdStr);
1917
+ }
1918
+ }
1919
+
1920
+ for (const peerIdStr of peersToReport) {
1921
+ this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, headReason);
1922
+ }
1923
+ }
1924
+
1925
+ /**
1926
+ * Removes `block` and all its pending descendants and bans their roots in `knownBadBlocks`. Only call when the chain
1927
+ * is provably bad: a banned root is never searched for again. Returns the banned items.
1928
+ */
1929
+ private removeAndBanAllDescendants(
1930
+ block: BlockInputSyncCacheItem,
1931
+ headReason: DroppedItemReason
1932
+ ): BlockInputSyncCacheItem[] {
1933
+ const bannedBlocks: BlockInputSyncCacheItem[] = [];
1934
+ for (const badBlock of this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent)) {
1935
+ const rootHex = getBlockInputSyncCacheItemRootHex(badBlock);
1936
+ // a gossip import may race a failing sync path, never ban a root fork choice has imported
1937
+ if (this.chain.forkChoice.hasBlockHex(rootHex)) {
1938
+ continue;
1939
+ }
1940
+ this.knownBadBlocks.add(rootHex);
1941
+ bannedBlocks.push(badBlock);
1942
+ this.logger.debug("Banning unknown block", {
1943
+ slot: getBlockInputSyncCacheItemSlot(badBlock),
1944
+ root: rootHex,
1945
+ reason: headReason,
1946
+ peerIdStrings: Array.from(badBlock.peerIdStrings)
1947
+ .map((id) => prettyPrintPeerIdStr(id))
1948
+ .join(","),
1949
+ });
1950
+ }
1951
+
1952
+ pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
1953
+ return bannedBlocks;
1954
+ }
1955
+
1906
1956
  private removeAllDescendants(
1907
1957
  block: BlockInputSyncCacheItem,
1908
1958
  headReason: DroppedItemReason,
1909
1959
  descendantReason: DroppedItemReason = DroppedItemReason.invalidParent
1910
1960
  ): BlockInputSyncCacheItem[] {
1911
1961
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1912
- const slot = getBlockInputSyncCacheItemSlot(block);
1913
1962
  // Get all blocks that are a descendant of this one (index 0 is the head item itself)
1914
1963
  const badPendingBlocks = [block, ...getAllDescendantBlocks(rootHex, this.pendingBlocks)];
1915
1964
 
@@ -1926,8 +1975,8 @@ export class BlockInputSync {
1926
1975
  // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1927
1976
  // importBlock()-owned lifecycle.
1928
1977
  this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
1929
- slot,
1930
- blockRoot: rootHex,
1978
+ slot: getBlockInputSyncCacheItemSlot(block),
1979
+ root: rootHex,
1931
1980
  });
1932
1981
  }
1933
1982
 
@@ -259,21 +259,22 @@ export async function getDataColumnSidecarsFromExecution(
259
259
  return DataColumnEngineResult.SuccessResolved;
260
260
  }
261
261
 
262
- /** engine_getPayloadBodiesByHashV2: ELs MUST support at least 32 hashes per request. */
263
- const MAX_BODIES_REQUEST = 32;
262
+ /** engine_getPayloadBodiesByHashV2: every EL must accept requests of up to 32 hashes, larger ones may fail with -38004 */
263
+ export const MAX_BODIES_PER_REQUEST = 32;
264
264
 
265
265
  type SlotEnvelopeBytes = {slot: Slot; envelopeBytes: Uint8Array};
266
266
 
267
267
  type RangeEntry = ArchivedEnvelope & {slot: Slot};
268
268
 
269
- /** What a body root mismatch means on a given serving path */
270
- export type ReconstructMismatchPolicy = "throw" | "omit";
269
+ /** What an envelope that cannot be rebuilt (EL-unavailable body or body root mismatch) means on a given serving path */
270
+ export type ReconstructMissPolicy = "throw" | "omit";
271
271
 
272
- export type RebuildMiss =
273
- /** EL does not have the block, or has pruned its block access list */
274
- | {slot: Slot; reason: "unavailable"}
275
- /** An EL body does not hash to its stored root (local inconsistency) */
276
- | {slot: Slot; reason: "mismatch"; error: EnvelopeReconstructionError};
272
+ /**
273
+ * An archived envelope that could not be rebuilt. `"unavailable"`: the EL does not have the block or its
274
+ * block access list (BODY_UNAVAILABLE). `"mismatch"`: an EL body does not hash to its stored root, a local
275
+ * inconsistency (BODY_ROOT_MISMATCH). `error` carries the matching code for paths that surface it.
276
+ */
277
+ export type RebuildMiss = {slot: Slot; reason: "unavailable" | "mismatch"; error: EnvelopeReconstructionError};
277
278
 
278
279
  /**
279
280
  * Stream finalized envelopes over [startSlot, endSlot) as serialized bytes, rebuilding header
@@ -299,7 +300,7 @@ export async function* reconstructExecutionPayloadEnvelopesByRange(
299
300
 
300
301
  for await (const {key, value: bytes} of archive.binaryEntriesStream({gte: startSlot, lt: endSlot})) {
301
302
  batch.push({slot: archive.decodeKey(key), ...decodeArchivedEnvelope(bytes)});
302
- if (batch.length === MAX_BODIES_REQUEST) {
303
+ if (batch.length === MAX_BODIES_PER_REQUEST) {
303
304
  yield* reconstructBatch(executionEngine, logger, metrics, batch);
304
305
  batch = [];
305
306
  }
@@ -359,9 +360,13 @@ export async function reconstructExecutionPayloadEnvelopes(
359
360
  headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[]
360
361
  ): Promise<(gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[]> {
361
362
  const out: (gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[] = [];
362
- for (let i = 0; i < headerEnvelopes.length; i += MAX_BODIES_REQUEST) {
363
+ for (let i = 0; i < headerEnvelopes.length; i += MAX_BODIES_PER_REQUEST) {
363
364
  out.push(
364
- ...(await reconstructEnvelopesBatch(executionEngine, metrics, headerEnvelopes.slice(i, i + MAX_BODIES_REQUEST)))
365
+ ...(await reconstructEnvelopesBatch(
366
+ executionEngine,
367
+ metrics,
368
+ headerEnvelopes.slice(i, i + MAX_BODIES_PER_REQUEST)
369
+ ))
365
370
  );
366
371
  }
367
372
  return out;
@@ -401,7 +406,14 @@ async function reconstructEnvelopesBatch(
401
406
  // A zero-length block access list cannot be valid, RLP encodes an empty list as 0xc0
402
407
  if (body == null || body.withdrawals == null || body.blockAccessList == null || body.blockAccessList.length === 0) {
403
408
  metrics?.payloadEnvelopeReconstruction.envelopes.inc({result: "unavailable"});
404
- return {slot, reason: "unavailable"};
409
+ return {
410
+ slot,
411
+ reason: "unavailable",
412
+ error: new EnvelopeReconstructionError(
413
+ {code: EnvelopeReconstructionErrorCode.BODY_UNAVAILABLE, slot},
414
+ `execution client cannot serve the payload body or block access list for archived envelope slot=${slot}`
415
+ ),
416
+ };
405
417
  }
406
418
  try {
407
419
  const envelope = signedHeaderEnvelopeToFull(headerEnvelope, {
@@ -409,7 +421,7 @@ async function reconstructEnvelopesBatch(
409
421
  withdrawals: body.withdrawals,
410
422
  blockAccessList: body.blockAccessList,
411
423
  });
412
- metrics?.payloadEnvelopeReconstruction.envelopes.inc({result: "ok"});
424
+ metrics?.payloadEnvelopeReconstruction.envelopes.inc({result: "success"});
413
425
  return envelope;
414
426
  } catch (e) {
415
427
  if (