@lodestar/beacon-node 1.47.0-dev.aa6eaee53a → 1.47.0-dev.aa8303e9a2

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 (120) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +2 -1
  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 +13 -1
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/lodestar/index.js +1 -1
  8. package/lib/api/impl/lodestar/index.js.map +1 -1
  9. package/lib/api/rest/base.d.ts.map +1 -1
  10. package/lib/api/rest/base.js +7 -0
  11. package/lib/api/rest/base.js.map +1 -1
  12. package/lib/chain/archiveStore/archiveStore.js +1 -1
  13. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  14. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +2 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  16. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +6 -2
  17. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  18. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +14 -5
  19. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  20. package/lib/chain/blocks/payloadEnvelopeInput/types.js +5 -0
  21. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -1
  22. package/lib/chain/chain.d.ts +2 -1
  23. package/lib/chain/chain.d.ts.map +1 -1
  24. package/lib/chain/chain.js +10 -2
  25. package/lib/chain/chain.js.map +1 -1
  26. package/lib/chain/interface.d.ts +2 -1
  27. package/lib/chain/interface.d.ts.map +1 -1
  28. package/lib/chain/interface.js.map +1 -1
  29. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts +16 -0
  30. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts.map +1 -0
  31. package/lib/chain/opPools/deferredVoluntaryExitPool.js +57 -0
  32. package/lib/chain/opPools/deferredVoluntaryExitPool.js.map +1 -0
  33. package/lib/chain/opPools/index.d.ts +1 -0
  34. package/lib/chain/opPools/index.d.ts.map +1 -1
  35. package/lib/chain/opPools/index.js +1 -0
  36. package/lib/chain/opPools/index.js.map +1 -1
  37. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  38. package/lib/chain/prepareNextSlot.js +23 -5
  39. package/lib/chain/prepareNextSlot.js.map +1 -1
  40. package/lib/chain/regen/interface.d.ts +4 -2
  41. package/lib/chain/regen/interface.d.ts.map +1 -1
  42. package/lib/chain/regen/interface.js +2 -0
  43. package/lib/chain/regen/interface.js.map +1 -1
  44. package/lib/chain/regen/queued.d.ts +2 -2
  45. package/lib/chain/regen/queued.d.ts.map +1 -1
  46. package/lib/chain/regen/queued.js +5 -5
  47. package/lib/chain/regen/queued.js.map +1 -1
  48. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +28 -1
  49. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  50. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +121 -13
  51. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  52. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +5 -1
  53. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  54. package/lib/chain/stateCache/fifoBlockStateCache.js +6 -0
  55. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  56. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +47 -5
  57. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  58. package/lib/chain/stateCache/persistentCheckpointsCache.js +117 -7
  59. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  60. package/lib/chain/stateCache/types.d.ts +3 -3
  61. package/lib/chain/stateCache/types.d.ts.map +1 -1
  62. package/lib/chain/validation/voluntaryExit.d.ts +9 -1
  63. package/lib/chain/validation/voluntaryExit.d.ts.map +1 -1
  64. package/lib/chain/validation/voluntaryExit.js +46 -1
  65. package/lib/chain/validation/voluntaryExit.js.map +1 -1
  66. package/lib/metrics/metrics/lodestar.d.ts +24 -4
  67. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  68. package/lib/metrics/metrics/lodestar.js +34 -2
  69. package/lib/metrics/metrics/lodestar.js.map +1 -1
  70. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  71. package/lib/network/processor/gossipHandlers.js +5 -1
  72. package/lib/network/processor/gossipHandlers.js.map +1 -1
  73. package/lib/node/deferredVoluntaryExitPublisher.d.ts +15 -0
  74. package/lib/node/deferredVoluntaryExitPublisher.d.ts.map +1 -0
  75. package/lib/node/deferredVoluntaryExitPublisher.js +38 -0
  76. package/lib/node/deferredVoluntaryExitPublisher.js.map +1 -0
  77. package/lib/node/nodejs.d.ts.map +1 -1
  78. package/lib/node/nodejs.js +2 -0
  79. package/lib/node/nodejs.js.map +1 -1
  80. package/lib/sync/types.d.ts +32 -0
  81. package/lib/sync/types.d.ts.map +1 -1
  82. package/lib/sync/types.js +33 -0
  83. package/lib/sync/types.js.map +1 -1
  84. package/lib/sync/unknownBlock.d.ts.map +1 -1
  85. package/lib/sync/unknownBlock.js +95 -51
  86. package/lib/sync/unknownBlock.js.map +1 -1
  87. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  88. package/lib/sync/utils/downloadByRange.js +16 -11
  89. package/lib/sync/utils/downloadByRange.js.map +1 -1
  90. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  91. package/lib/sync/utils/downloadByRoot.js +3 -1
  92. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  93. package/package.json +31 -31
  94. package/src/api/impl/beacon/blocks/index.ts +2 -1
  95. package/src/api/impl/beacon/pool/index.ts +15 -1
  96. package/src/api/impl/lodestar/index.ts +1 -1
  97. package/src/api/rest/base.ts +7 -0
  98. package/src/chain/archiveStore/archiveStore.ts +1 -1
  99. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +8 -2
  100. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -4
  101. package/src/chain/chain.ts +10 -1
  102. package/src/chain/interface.ts +2 -0
  103. package/src/chain/opPools/deferredVoluntaryExitPool.ts +65 -0
  104. package/src/chain/opPools/index.ts +1 -0
  105. package/src/chain/prepareNextSlot.ts +23 -5
  106. package/src/chain/regen/interface.ts +4 -2
  107. package/src/chain/regen/queued.ts +5 -5
  108. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +143 -14
  109. package/src/chain/stateCache/fifoBlockStateCache.ts +8 -1
  110. package/src/chain/stateCache/persistentCheckpointsCache.ts +133 -8
  111. package/src/chain/stateCache/types.ts +3 -3
  112. package/src/chain/validation/voluntaryExit.ts +59 -2
  113. package/src/metrics/metrics/lodestar.ts +41 -6
  114. package/src/network/processor/gossipHandlers.ts +5 -1
  115. package/src/node/deferredVoluntaryExitPublisher.ts +54 -0
  116. package/src/node/nodejs.ts +3 -0
  117. package/src/sync/types.ts +33 -0
  118. package/src/sync/unknownBlock.ts +105 -55
  119. package/src/sync/utils/downloadByRange.ts +21 -12
  120. package/src/sync/utils/downloadByRoot.ts +3 -1
@@ -4,7 +4,10 @@ import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
6
  import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
7
- import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
7
+ import {
8
+ PayloadEnvelopeInputPruneReason,
9
+ PayloadEnvelopeInputSource,
10
+ } from "../../chain/blocks/payloadEnvelopeInput/index.js";
8
11
  import {JobQueueItemType} from "../../chain/bls/index.js";
9
12
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
10
13
  import {
@@ -21,7 +24,7 @@ import {ExecutionPayloadStatus} from "../../execution/index.js";
21
24
  import {GossipType} from "../../network/index.js";
22
25
  import {CannotAcceptWorkReason, ReprocessRejectReason} from "../../network/processor/index.js";
23
26
  import {BackfillSyncMethod} from "../../sync/backfill/backfill.js";
24
- import {PendingBlockType} from "../../sync/types.js";
27
+ import {DroppedItemReason, PendingBlockType} from "../../sync/types.js";
25
28
  import {PeerSyncType, RangeSyncType} from "../../sync/utils/remoteSyncType.js";
26
29
  import {AllocSource} from "../../util/bufferPool.js";
27
30
  import {DataColumnReconstructionCode} from "../../util/dataColumns.js";
@@ -677,9 +680,15 @@ export function createLodestarMetrics(
677
680
  name: "lodestar_sync_unknown_block_downloaded_blocks_error_total",
678
681
  help: "Total number of downloaded blocks errors in UnknownBlockSync",
679
682
  }),
680
- removedBlocks: register.gauge({
683
+ removedBlocks: register.gauge<{reason: DroppedItemReason}>({
681
684
  name: "lodestar_sync_unknown_block_removed_blocks_total",
682
- help: "Total number of removed bad blocks in UnknownBlockSync",
685
+ help: "Total pending blocks dropped from BlockInputSync without completing, by reason",
686
+ labelNames: ["reason"],
687
+ }),
688
+ removedPayloads: register.gauge<{reason: DroppedItemReason}>({
689
+ name: "lodestar_sync_unknown_block_removed_payloads_total",
690
+ help: "Total pending payloads dropped from BlockInputSync without completing, by reason",
691
+ labelNames: ["reason"],
683
692
  }),
684
693
  elapsedTimeTillReceived: register.histogram({
685
694
  name: "lodestar_sync_unknown_block_elapsed_time_till_received",
@@ -1225,6 +1234,12 @@ export function createLodestarMetrics(
1225
1234
  name: "lodestar_oppool_attester_slashing_pool_size",
1226
1235
  help: "Current size of the AttesterSlashingPool",
1227
1236
  }),
1237
+ deferredVoluntaryExitPool: {
1238
+ size: register.gauge({
1239
+ name: "lodestar_oppool_deferred_voluntary_exit_pool_size",
1240
+ help: "Current size of the DeferredVoluntaryExitPool",
1241
+ }),
1242
+ },
1228
1243
  proposerSlashingPoolSize: register.gauge({
1229
1244
  name: "lodestar_oppool_proposer_slashing_pool_size",
1230
1245
  help: "Current size of the ProposerSlashingPool",
@@ -1429,6 +1444,16 @@ export function createLodestarMetrics(
1429
1444
  help: "Checkpoint state cache size",
1430
1445
  labelNames: ["type"],
1431
1446
  }),
1447
+ persistentTierEpochs: register.gauge<{tier: number}>({
1448
+ name: "lodestar_cp_state_cache_persistent_tier_epochs",
1449
+ help: "Number of epoch keys retained in each on-disk checkpoint state retention tier",
1450
+ labelNames: ["tier"],
1451
+ }),
1452
+ persistentTierStates: register.gauge<{tier: number}>({
1453
+ name: "lodestar_cp_state_cache_persistent_tier_states",
1454
+ help: "Number of persisted checkpoint states retained in each on-disk retention tier",
1455
+ labelNames: ["tier"],
1456
+ }),
1432
1457
  reads: register.avgMinMax({
1433
1458
  name: "lodestar_cp_state_epoch_reads",
1434
1459
  help: "Avg min max of all state cache items total read count",
@@ -1643,9 +1668,15 @@ export function createLodestarMetrics(
1643
1668
  name: "lodestar_seen_payload_envelope_input_cache_serialized_object_refs",
1644
1669
  help: "Number of serialized-cache object refs retained by cached PayloadEnvelopeInputs",
1645
1670
  }),
1646
- created: register.counter({
1671
+ created: register.counter<{source: PayloadEnvelopeInputSource}>({
1647
1672
  name: "lodestar_seen_payload_envelope_input_cache_items_created_total",
1648
- help: "Number of PayloadEnvelopeInputs created",
1673
+ help: "Number of PayloadEnvelopeInputs created by source",
1674
+ labelNames: ["source"],
1675
+ }),
1676
+ pruned: register.counter<{reason: PayloadEnvelopeInputPruneReason}>({
1677
+ name: "lodestar_seen_payload_envelope_input_cache_items_pruned_total",
1678
+ help: "Number of PayloadEnvelopeInputs evicted by reason",
1679
+ labelNames: ["reason"],
1649
1680
  }),
1650
1681
  },
1651
1682
  },
@@ -1755,6 +1786,10 @@ export function createLodestarMetrics(
1755
1786
  name: "lodestar_precompute_next_epoch_transition_waste_total",
1756
1787
  help: "Total number of precomputing next epoch transition wasted",
1757
1788
  }),
1789
+ predictedReorg: register.counter({
1790
+ name: "lodestar_precompute_next_epoch_transition_predicted_reorg_total",
1791
+ help: "Predicted epoch-boundary reorgs where the strong parent was dialed instead of the weak head",
1792
+ }),
1758
1793
  duration: register.histogram({
1759
1794
  name: "lodestar_precompute_next_epoch_transition_duration_seconds",
1760
1795
  help: "Duration of precomputeNextEpochTransition, including epoch transition and hashTreeRoot",
@@ -196,7 +196,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
196
196
  forkName: fork,
197
197
  sampledColumns: chain.custodyConfig.sampledColumns,
198
198
  custodyColumns: chain.custodyConfig.custodyColumns,
199
- timeCreatedSec: seenTimestampSec,
199
+ seenTimestampSec,
200
+ source: PayloadEnvelopeInputSource.gossip,
200
201
  });
201
202
  }
202
203
 
@@ -640,6 +641,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
640
641
  metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
641
642
 
642
643
  if (isForkPostGloas(blockInput.forkName)) {
644
+ // we should have the payloadInput in the seen cache so no need getOrReload() here
643
645
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
644
646
  // This payloadInput should have been created just after gossip validation
645
647
  if (!payloadInput) {
@@ -1189,6 +1191,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1189
1191
  chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope);
1190
1192
 
1191
1193
  const blockRootHex = toRootHex(envelope.beaconBlockRoot);
1194
+ // a gossip payload cannot be `MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE` slots after the block
1195
+ // otherwise it'll get to UnknownBlockInput flow
1192
1196
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
1193
1197
 
1194
1198
  if (!payloadInput) {
@@ -0,0 +1,54 @@
1
+ import {routes} from "@lodestar/api";
2
+ import {Logger} from "@lodestar/utils";
3
+ import {IBeaconChain} from "../chain/index.js";
4
+ import {INetwork} from "../network/index.js";
5
+ import {ClockEvent} from "../util/clock.js";
6
+
7
+ /**
8
+ * On every epoch, drain the deferred voluntary exit pool and publish the exits that have become
9
+ * processable. Lives at the node layer rather than inside `BeaconChain` because publishing needs
10
+ * `network`, which the chain does not (and should not) depend on.
11
+ */
12
+ export function startDeferredVoluntaryExitPublisher({
13
+ chain,
14
+ network,
15
+ logger,
16
+ signal,
17
+ }: {
18
+ chain: IBeaconChain;
19
+ network: INetwork;
20
+ logger: Logger;
21
+ signal: AbortSignal;
22
+ }): void {
23
+ const onEpoch = async (): Promise<void> => {
24
+ try {
25
+ // Use the cached head state directly rather than getHeadStateAtCurrentEpoch, which would regen
26
+ // the head forward to the current epoch. At init from a stale db-finalized state that regen can
27
+ // be many epoch transitions. The head state is fine here: this is a best-effort drain that
28
+ // re-runs every epoch, so a just-processable exit is republished next tick.
29
+ const state = chain.getHeadState();
30
+ const exits = chain.deferredVoluntaryExitPool.drainProcessableExits(state);
31
+ for (const exit of exits) {
32
+ try {
33
+ chain.opPool.insertVoluntaryExit(exit);
34
+ chain.emitter.emit(routes.events.EventType.voluntaryExit, exit);
35
+ await network.publishVoluntaryExit(exit);
36
+ logger.info("Voluntary exit successfully published for validator", {
37
+ validatorIndex: exit.message.validatorIndex,
38
+ });
39
+ } catch (e) {
40
+ logger.warn(
41
+ "Failed to publish deferred voluntary exit",
42
+ {validatorIndex: exit.message.validatorIndex},
43
+ e as Error
44
+ );
45
+ }
46
+ }
47
+ } catch (e) {
48
+ logger.warn("Failed to drain deferred voluntary exit pool", {}, e as Error);
49
+ }
50
+ };
51
+
52
+ chain.clock.addListener(ClockEvent.epoch, onEpoch);
53
+ signal.addEventListener("abort", () => chain.clock.removeListener(ClockEvent.epoch, onEpoch), {once: true});
54
+ }
@@ -22,6 +22,7 @@ import {Network, getReqRespHandlers} from "../network/index.js";
22
22
  import {BackfillSync} from "../sync/backfill/index.js";
23
23
  import {BeaconSync, IBeaconSync} from "../sync/index.js";
24
24
  import {Clock} from "../util/clock.js";
25
+ import {startDeferredVoluntaryExitPublisher} from "./deferredVoluntaryExitPublisher.js";
25
26
  import {runNodeNotifier} from "./notifier.js";
26
27
  import {IBeaconNodeOptions} from "./options.js";
27
28
 
@@ -335,6 +336,8 @@ export class BeaconNode {
335
336
 
336
337
  void runNodeNotifier({network, chain, sync, config, logger, signal});
337
338
 
339
+ startDeferredVoluntaryExitPublisher({chain, network, logger, signal});
340
+
338
341
  return new BeaconNode({
339
342
  opts,
340
343
  config,
package/src/sync/types.ts CHANGED
@@ -36,6 +36,39 @@ export enum PendingBlockInputStatus {
36
36
  processing = "processing",
37
37
  }
38
38
 
39
+ /**
40
+ * Why a pending item was dropped from BlockInputSync WITHOUT completing.
41
+ * The happy path — a successfully imported/processed block or payload — is removed by the import
42
+ * flow (onPayloadImported, processReadyBlock/processPayload success) and is deliberately NOT tracked
43
+ * here
44
+ */
45
+ export enum DroppedItemReason {
46
+ /** prune: item's own slot is below the finalized slot (routine cleanup), or a downloaded block landed at/below finality */
47
+ belowFinalized = "below_finalized",
48
+ /** prune: slot-less, unresolvable payload evicted after PRUNE_UNRESOLVED_SLOT_EPOCHS */
49
+ agedOut = "aged_out",
50
+ /** pruneSetToMax evicted the oldest entry because the cache hit its capacity (DoS guard) */
51
+ capacity = "capacity",
52
+ /** descendant removed because an ancestor was invalid/removed */
53
+ invalidParent = "invalid_parent",
54
+ /** transient execution-engine error removal (EXECUTION_ENGINE_ERROR) */
55
+ elError = "el_error",
56
+ /** the execution engine declared the item invalid (EXECUTION_ENGINE_INVALID) */
57
+ elInvalid = "el_invalid",
58
+ /** a block failed our own checks (not correct w.r.t. our chain) */
59
+ invalidBlock = "invalid_block",
60
+ /** a block declares a parent payload hash that conflicts with the parent's actual payload (gloas) */
61
+ invalidParentPayload = "invalid_parent_payload",
62
+ /** a payload envelope failed gossip verification (ENVELOPE_VERIFICATION_ERROR) */
63
+ invalidEnvelope = "invalid_envelope",
64
+ /** a payload had an invalid signature (INVALID_SIGNATURE) */
65
+ invalidSignature = "invalid_signature",
66
+ /** a block root was given up on after exhausting download attempts (data could not be fetched) */
67
+ unavailable = "unavailable",
68
+ /** an unexpected / unhandled error code led to removal */
69
+ unknown = "unknown",
70
+ }
71
+
39
72
  export enum PendingPayloadInputStatus {
40
73
  pending = "pending",
41
74
  fetching = "fetching",
@@ -1,7 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {CheckpointWithHex} from "@lodestar/fork-choice";
4
- import {ForkSeq, isForkPostGloas} from "@lodestar/params";
4
+ import {ForkSeq, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
5
5
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
6
6
  import {computeTimeAtSlot} from "@lodestar/state-transition";
7
7
  import {RootHex, Slot, gloas} from "@lodestar/types";
@@ -25,6 +25,7 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
25
25
  import {SyncOptions} from "./options.js";
26
26
  import {
27
27
  BlockInputSyncCacheItem,
28
+ DroppedItemReason,
28
29
  PayloadSyncCacheItem,
29
30
  PendingBlockInput,
30
31
  PendingBlockInputStatus,
@@ -48,6 +49,10 @@ import {getRateLimitedUntilMs} from "./utils/rateLimit.js";
48
49
  const MAX_ATTEMPTS_PER_BLOCK = 5;
49
50
  const MAX_KNOWN_BAD_BLOCKS = 500;
50
51
  const MAX_PENDING_BLOCKS = 100;
52
+ /**
53
+ * A pending payload without slot can only stay at most 2 epochs.
54
+ */
55
+ const PRUNE_UNRESOLVED_SLOT_EPOCHS = 2;
51
56
 
52
57
  type AdvancePendingBlockResult =
53
58
  | "ready"
@@ -255,7 +260,7 @@ export class BlockInputSync {
255
260
  parentRoot: missingDependency.parentRootHex,
256
261
  parentBlockHash: missingDependency.parentBlockHashHex,
257
262
  });
258
- this.removeAndDownScoreAllDescendants(pendingBlock);
263
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidParentPayload);
259
264
  }
260
265
  return;
261
266
  }
@@ -295,28 +300,37 @@ export class BlockInputSync {
295
300
  // Use the actual finalized block slot, not computeStartSlotAtEpoch(checkpoint.epoch)
296
301
  // because the payload at the finalized block is not finalized
297
302
  const finalizedSlot = this.chain.forkChoice.getFinalizedBlock().slot;
303
+ const nowSec = Date.now() / 1000;
304
+ const maxSecInCache = (PRUNE_UNRESOLVED_SLOT_EPOCHS * SLOTS_PER_EPOCH * this.config.SLOT_DURATION_MS) / 1000;
298
305
 
299
- let deletedPayloads = 0;
306
+ let belowFinalizedPayloads = 0;
307
+ let agedOutPayloads = 0;
300
308
  let unresolvedSlotPayloads = 0;
309
+
301
310
  for (const [rootHex, payload] of this.pendingPayloads) {
302
311
  // Last-effort resolve for a slot-less PendingPayloadRootHex, and store it back for later passes.
303
312
  if (!isPendingPayloadInput(payload) && !isPendingPayloadEnvelope(payload) && payload.slot === undefined) {
304
313
  payload.slot = this.resolvePayloadSlot(rootHex);
314
+ if (payload.slot === undefined) {
315
+ // Still unresolvable: age it out so the MAX_PENDING_BLOCKS cap cannot stay saturated.
316
+ if (nowSec - payload.timeAddedSec > maxSecInCache) {
317
+ this.pendingPayloads.delete(rootHex);
318
+ agedOutPayloads++;
319
+ } else {
320
+ unresolvedSlotPayloads++;
321
+ this.logger.debug("BlockInputSync.pruneFinalized: pending payload with unresolved slot", {
322
+ root: rootHex,
323
+ finalizedSlot,
324
+ });
325
+ }
326
+ continue;
327
+ }
305
328
  }
306
329
 
307
330
  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) {
331
+ if (typeof slot === "number" && slot < finalizedSlot) {
318
332
  this.pendingPayloads.delete(rootHex);
319
- deletedPayloads++;
333
+ belowFinalizedPayloads++;
320
334
  }
321
335
  }
322
336
 
@@ -329,14 +343,24 @@ export class BlockInputSync {
329
343
  }
330
344
  }
331
345
 
332
- if (deletedPayloads > 0 || deletedBlocks > 0) {
333
- this.metrics?.blockInputSync.removedBlocks.inc(deletedPayloads + deletedBlocks);
346
+ if (belowFinalizedPayloads > 0) {
347
+ this.metrics?.blockInputSync.removedPayloads.inc(
348
+ {reason: DroppedItemReason.belowFinalized},
349
+ belowFinalizedPayloads
350
+ );
351
+ }
352
+ if (agedOutPayloads > 0) {
353
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.agedOut}, agedOutPayloads);
354
+ }
355
+ if (deletedBlocks > 0) {
356
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.belowFinalized}, deletedBlocks);
334
357
  }
335
358
 
336
359
  this.logger.debug("BlockInputSync.pruneFinalized dropped pre-finalized pending items", {
337
360
  finalizedSlot,
338
361
  finalizedRoot: checkpoint.rootHex,
339
- deletedPayloads,
362
+ belowFinalizedPayloads,
363
+ agedOutPayloads,
340
364
  deletedBlocks,
341
365
  unresolvedSlotPayloads,
342
366
  });
@@ -369,6 +393,7 @@ export class BlockInputSync {
369
393
  // Limit pending blocks to prevent DOS attacks that cause OOM
370
394
  const prunedItemCount = pruneSetToMax(this.pendingBlocks, this.maxPendingBlocks);
371
395
  if (prunedItemCount > 0) {
396
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
372
397
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
373
398
  }
374
399
  return added;
@@ -403,6 +428,7 @@ export class BlockInputSync {
403
428
  // Limit pending blocks to prevent DOS attacks that cause OOM
404
429
  const prunedItemCount = pruneSetToMax(this.pendingBlocks, this.maxPendingBlocks);
405
430
  if (prunedItemCount > 0) {
431
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
406
432
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingBlocks`);
407
433
  }
408
434
  };
@@ -427,6 +453,14 @@ export class BlockInputSync {
427
453
  root: rootHex,
428
454
  peerIdStr: peerIdStr ?? "unknown peer",
429
455
  });
456
+ } else if (
457
+ // update a previously slot-less entry once a slot becomes available
458
+ slot !== undefined &&
459
+ !isPendingPayloadInput(pendingPayload) &&
460
+ !isPendingPayloadEnvelope(pendingPayload) &&
461
+ pendingPayload.slot === undefined
462
+ ) {
463
+ pendingPayload.slot = slot;
430
464
  }
431
465
 
432
466
  if (peerIdStr) {
@@ -435,6 +469,7 @@ export class BlockInputSync {
435
469
 
436
470
  const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
437
471
  if (prunedItemCount > 0) {
472
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
438
473
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
439
474
  }
440
475
  return added;
@@ -488,6 +523,7 @@ export class BlockInputSync {
488
523
 
489
524
  const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
490
525
  if (prunedItemCount > 0) {
526
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
491
527
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
492
528
  }
493
529
  };
@@ -544,15 +580,7 @@ export class BlockInputSync {
544
580
  return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
545
581
  }
546
582
 
547
- const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);
548
- if (parentPayloadInput) {
549
- if (parentPayloadInput.getBlockHashHex() === parentBlockHashHex) {
550
- return {kind: "parentPayload", rootHex: parentRootHex, slot: parentBlock.slot};
551
- }
552
-
553
- return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
554
- }
555
-
583
+ // Parent is in fork choice but its payload `parentBlockHashHex` isn't revealed yet
556
584
  return {kind: "parentPayload", rootHex: parentRootHex, slot: parentBlock.slot};
557
585
  }
558
586
 
@@ -590,7 +618,7 @@ export class BlockInputSync {
590
618
  parentRoot: missingDependency.parentRootHex,
591
619
  parentBlockHash: missingDependency.parentBlockHashHex,
592
620
  });
593
- this.removeAndDownScoreAllDescendants(pendingBlock);
621
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidParentPayload);
594
622
  return "removed";
595
623
  }
596
624
  }
@@ -813,7 +841,7 @@ export class BlockInputSync {
813
841
  ...logCtx2,
814
842
  finalizedSlot,
815
843
  });
816
- this.removeAndDownScoreAllDescendants(block);
844
+ this.removeAndDownScoreAllDescendants(block, DroppedItemReason.belowFinalized);
817
845
  } else {
818
846
  this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
819
847
  }
@@ -830,7 +858,7 @@ export class BlockInputSync {
830
858
 
831
859
  this.metrics?.blockInputSync.downloadedBlocksError.inc();
832
860
  this.logger.debug("Ignoring unknown block root after many failed downloads", logCtx, res.err);
833
- this.removeAndDownScoreAllDescendants(block);
861
+ this.removeAndDownScoreAllDescendants(block, DroppedItemReason.unavailable);
834
862
  }
835
863
  }
836
864
 
@@ -894,9 +922,9 @@ export class BlockInputSync {
894
922
  this.pendingBlocks.delete(blockRootHex);
895
923
 
896
924
  if (isForkPostGloas(fork)) {
897
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
925
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(blockRootHex);
898
926
  if (!payloadInput) {
899
- this.logger.warn("PayloadEnvelopeInput not seeded during unknown sync processReadyBlock()", logCtx);
927
+ this.logger.debug("PayloadEnvelopeInput unavailable during unknown sync processReadyBlock()", logCtx);
900
928
  } else {
901
929
  // Similar to the gossip path: immediately attempt fetch of data columns from the execution engine.
902
930
  // The bid already carries the kzg commitments, so there is no reason to wait for the payload to arrive.
@@ -938,19 +966,19 @@ export class BlockInputSync {
938
966
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
939
967
  // Removing the block(s) without penalizing the peers, hoping for EL to
940
968
  // recover on a latter download + verify attempt.
941
- this.removeAllDescendants(pendingBlock);
969
+ this.removeAllDescendants(pendingBlock, DroppedItemReason.elError, DroppedItemReason.elError);
942
970
  break;
943
971
 
944
972
  case BlockErrorCode.EXECUTION_ENGINE_INVALID:
945
973
  // the peer served a bad block
946
974
  this.logger.debug("Execution engine rejected block from unknown parent sync", errorData, res.err);
947
- this.removeAndDownScoreAllDescendants(pendingBlock);
975
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
948
976
  break;
949
977
 
950
978
  default:
951
979
  // Block is not correct with respect to our chain. Log error loudly
952
980
  this.logger.debug("Error processing block from unknown parent sync", errorData, res.err);
953
- this.removeAndDownScoreAllDescendants(pendingBlock);
981
+ this.removeAndDownScoreAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
954
982
  }
955
983
  }
956
984
 
@@ -969,12 +997,12 @@ export class BlockInputSync {
969
997
  */
970
998
  private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise<void> {
971
999
  const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
1000
+
972
1001
  if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) {
973
1002
  this.pendingPayloads.delete(rootHex);
974
1003
  return;
975
1004
  }
976
1005
 
977
- const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
978
1006
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
979
1007
  // Block not in fork choice yet. payloadInput may be seeded from the block body during download, so a
980
1008
  // non-null payloadInput does not imply the block is imported; defer regardless and pull the block first.
@@ -990,8 +1018,9 @@ export class BlockInputSync {
990
1018
  return;
991
1019
  }
992
1020
 
1021
+ const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
993
1022
  if (!payloadInput) {
994
- this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
1023
+ this.logger.debug("PayloadEnvelopeInput not yet reloadable for imported block, will retry", {
995
1024
  root: rootHex,
996
1025
  });
997
1026
  return;
@@ -1153,17 +1182,27 @@ export class BlockInputSync {
1153
1182
  break;
1154
1183
 
1155
1184
  case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1185
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1186
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.elInvalid);
1187
+ break;
1188
+
1156
1189
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1190
+ this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1191
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidEnvelope);
1192
+ break;
1193
+
1157
1194
  case PayloadErrorCode.INVALID_SIGNATURE:
1158
1195
  // TODO GLOAS: Decide how invalid payload inputs should eventually leave memory without
1159
1196
  // reintroducing envelope replacement / recreation flows.
1160
1197
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1161
- this.removePendingPayloadAndDescendants(rootHex);
1198
+ this.removePendingPayloadAndDescendants(rootHex, DroppedItemReason.invalidSignature);
1162
1199
  break;
1163
1200
 
1164
1201
  default:
1165
1202
  this.logger.debug("Error processing payload from unknown sync", logCtx, res.err);
1166
- this.pendingPayloads.delete(rootHex);
1203
+ if (this.pendingPayloads.delete(rootHex)) {
1204
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.unknown}, 1);
1205
+ }
1167
1206
  }
1168
1207
  return;
1169
1208
  }
@@ -1187,7 +1226,7 @@ export class BlockInputSync {
1187
1226
  let slot = getPayloadSyncCacheItemSlot(cacheItem);
1188
1227
  let payloadInput = isPendingPayloadInput(cacheItem)
1189
1228
  ? cacheItem.payloadInput
1190
- : this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1229
+ : await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1191
1230
  let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
1192
1231
 
1193
1232
  let i = 0;
@@ -1220,7 +1259,7 @@ export class BlockInputSync {
1220
1259
  slot = envelope.message.payload.slotNumber;
1221
1260
  }
1222
1261
 
1223
- payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1262
+ payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
1224
1263
  if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
1225
1264
  // Block not in fork choice yet. Validating now would throw BLOCK_ROOT_UNKNOWN, so keep the downloaded
1226
1265
  // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
@@ -1553,9 +1592,9 @@ export class BlockInputSync {
1553
1592
  * Downscore all peers that have referenced any of this bad blocks. May report peers multiple times if they have
1554
1593
  * referenced more than one bad block.
1555
1594
  */
1556
- private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem): void {
1595
+ private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
1557
1596
  // Get all blocks that are a descendant of this one
1558
- const badPendingBlocks = this.removeAllDescendants(block);
1597
+ const badPendingBlocks = this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent);
1559
1598
  // just console log and do not penalize on pending/bad blocks for debugging
1560
1599
  // console.log("removeAndDownscoreAllDescendants", {block});
1561
1600
 
@@ -1582,18 +1621,22 @@ export class BlockInputSync {
1582
1621
  }
1583
1622
 
1584
1623
  // Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
1585
- private removePendingPayloadAndDescendants(rootHex: RootHex): void {
1624
+ private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
1586
1625
  // Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
1587
1626
  // later validation/finalization logic decides when it can leave memory.
1588
- this.pendingPayloads.delete(rootHex);
1627
+ if (this.pendingPayloads.delete(rootHex)) {
1628
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: headReason}, 1);
1629
+ }
1589
1630
 
1590
1631
  const badPendingBlocks = getAllDescendantBlocks(rootHex, this.pendingBlocks);
1591
- this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1592
-
1593
1632
  for (const block of badPendingBlocks) {
1594
1633
  const descendantRootHex = getBlockInputSyncCacheItemRootHex(block);
1595
- this.pendingBlocks.delete(descendantRootHex);
1596
- this.pendingPayloads.delete(descendantRootHex);
1634
+ if (this.pendingBlocks.delete(descendantRootHex)) {
1635
+ this.metrics?.blockInputSync.removedBlocks.inc({reason: DroppedItemReason.invalidParent}, 1);
1636
+ }
1637
+ if (this.pendingPayloads.delete(descendantRootHex)) {
1638
+ this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.invalidParent}, 1);
1639
+ }
1597
1640
  this.chain.seenBlockInputCache.prune(descendantRootHex);
1598
1641
  this.logger.debug("Removing pending descendant after invalid parent payload", {
1599
1642
  slot: getBlockInputSyncCacheItemSlot(block),
@@ -1603,18 +1646,25 @@ export class BlockInputSync {
1603
1646
  }
1604
1647
  }
1605
1648
 
1606
- private removeAllDescendants(block: BlockInputSyncCacheItem): BlockInputSyncCacheItem[] {
1649
+ private removeAllDescendants(
1650
+ block: BlockInputSyncCacheItem,
1651
+ headReason: DroppedItemReason,
1652
+ descendantReason: DroppedItemReason = DroppedItemReason.invalidParent
1653
+ ): BlockInputSyncCacheItem[] {
1607
1654
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1608
1655
  const slot = getBlockInputSyncCacheItemSlot(block);
1609
- // Get all blocks that are a descendant of this one
1656
+ // Get all blocks that are a descendant of this one (index 0 is the head item itself)
1610
1657
  const badPendingBlocks = [block, ...getAllDescendantBlocks(rootHex, this.pendingBlocks)];
1611
1658
 
1612
- this.metrics?.blockInputSync.removedBlocks.inc(badPendingBlocks.length);
1613
-
1614
- for (const block of badPendingBlocks) {
1659
+ for (const [i, block] of badPendingBlocks.entries()) {
1615
1660
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
1616
- this.pendingBlocks.delete(rootHex);
1617
- this.pendingPayloads.delete(rootHex);
1661
+ const reason = i === 0 ? headReason : descendantReason;
1662
+ if (this.pendingBlocks.delete(rootHex)) {
1663
+ this.metrics?.blockInputSync.removedBlocks.inc({reason}, 1);
1664
+ }
1665
+ if (this.pendingPayloads.delete(rootHex)) {
1666
+ this.metrics?.blockInputSync.removedPayloads.inc({reason}, 1);
1667
+ }
1618
1668
  this.chain.seenBlockInputCache.prune(rootHex);
1619
1669
  // Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
1620
1670
  // importBlock()-owned lifecycle.