@lodestar/beacon-node 1.48.0-dev.b228e400d1 → 1.48.0-dev.b4a608e68f

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 (57) hide show
  1. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/state/utils.js +14 -0
  3. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  4. package/lib/api/impl/config/index.d.ts.map +1 -1
  5. package/lib/api/impl/config/index.js +4 -1
  6. package/lib/api/impl/config/index.js.map +1 -1
  7. package/lib/chain/blocks/importBlock.d.ts +5 -1
  8. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  9. package/lib/chain/blocks/importBlock.js +9 -3
  10. package/lib/chain/blocks/importBlock.js.map +1 -1
  11. package/lib/chain/blocks/index.d.ts +8 -4
  12. package/lib/chain/blocks/index.d.ts.map +1 -1
  13. package/lib/chain/blocks/index.js +80 -15
  14. package/lib/chain/blocks/index.js.map +1 -1
  15. package/lib/chain/blocks/types.d.ts +8 -0
  16. package/lib/chain/blocks/types.d.ts.map +1 -1
  17. package/lib/chain/blocks/types.js.map +1 -1
  18. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  19. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  20. package/lib/chain/blocks/utils/chainSegment.js +10 -1
  21. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  22. package/lib/chain/chain.d.ts +2 -2
  23. package/lib/chain/chain.d.ts.map +1 -1
  24. package/lib/chain/chain.js +2 -2
  25. package/lib/chain/chain.js.map +1 -1
  26. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  27. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  28. package/lib/chain/errors/gossipValidation.js +1 -0
  29. package/lib/chain/errors/gossipValidation.js.map +1 -1
  30. package/lib/chain/interface.d.ts +2 -2
  31. package/lib/chain/interface.d.ts.map +1 -1
  32. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  33. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  34. package/lib/chain/validatorMonitor.d.ts +7 -1
  35. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  36. package/lib/chain/validatorMonitor.js +22 -5
  37. package/lib/chain/validatorMonitor.js.map +1 -1
  38. package/lib/network/processor/index.d.ts.map +1 -1
  39. package/lib/network/processor/index.js +11 -0
  40. package/lib/network/processor/index.js.map +1 -1
  41. package/lib/sync/range/range.d.ts.map +1 -1
  42. package/lib/sync/range/range.js +24 -1
  43. package/lib/sync/range/range.js.map +1 -1
  44. package/package.json +14 -14
  45. package/src/api/impl/beacon/state/utils.ts +15 -0
  46. package/src/api/impl/config/index.ts +4 -1
  47. package/src/chain/blocks/importBlock.ts +21 -5
  48. package/src/chain/blocks/index.ts +107 -21
  49. package/src/chain/blocks/types.ts +9 -0
  50. package/src/chain/blocks/utils/chainSegment.ts +10 -1
  51. package/src/chain/chain.ts +4 -4
  52. package/src/chain/errors/gossipValidation.ts +1 -0
  53. package/src/chain/interface.ts +2 -2
  54. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  55. package/src/chain/validatorMonitor.ts +24 -7
  56. package/src/network/processor/index.ts +13 -0
  57. package/src/sync/range/range.ts +23 -1
@@ -25,7 +25,17 @@ import {
25
25
  isStatePostAltair,
26
26
  isStatePostBellatrix,
27
27
  } from "@lodestar/state-transition";
28
- import {Attestation, BeaconBlock, altair, capella, electra, isGloasBeaconBlock, phase0, ssz} from "@lodestar/types";
28
+ import {
29
+ Attestation,
30
+ BeaconBlock,
31
+ Epoch,
32
+ altair,
33
+ capella,
34
+ electra,
35
+ isGloasBeaconBlock,
36
+ phase0,
37
+ ssz,
38
+ } from "@lodestar/types";
29
39
  import {isErrorAborted, toRootHex} from "@lodestar/utils";
30
40
  import {GENESIS_SLOT, ZERO_HASH_HEX} from "../../constants/index.js";
31
41
  import {callInNextEventLoop} from "../../util/eventLoop.js";
@@ -44,6 +54,15 @@ import {getCheckpointFromState} from "./utils/checkpoint.js";
44
54
  * Fork-choice allows to import attestations from current (0) or past (1) epoch.
45
55
  */
46
56
  const FORK_CHOICE_ATT_EPOCH_LIMIT = 1;
57
+ /**
58
+ * Whether the attestations of a block at `blockEpoch` are imported into fork choice
59
+ */
60
+ export function importsBlockAttestations(opts: ImportBlockOpts, blockEpoch: Epoch, currentEpoch: Epoch): boolean {
61
+ return (
62
+ opts.importAttestations === AttestationImportOpt.Force ||
63
+ (opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
64
+ );
65
+ }
47
66
  /**
48
67
  * Emit eventstream events for block contents events only for blocks that are recent enough to clock
49
68
  */
@@ -149,10 +168,7 @@ export async function importBlock(
149
168
  // Only process attestations of blocks with relevant attestations for the fork-choice:
150
169
  // If current epoch is N, and block is epoch X, block may include attestations for epoch X or X - 1.
151
170
  // The latest block that is useful is at epoch N - 1 which may include attestations for epoch N - 1 or N - 2.
152
- if (
153
- opts.importAttestations === AttestationImportOpt.Force ||
154
- (opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
155
- ) {
171
+ if (importsBlockAttestations(opts, blockEpoch, currentEpoch)) {
156
172
  const attestations = block.message.body.attestations;
157
173
  const rootCache = new RootCache(postState);
158
174
  const invalidAttestationErrorsByCode = new Map<string, {error: Error; count: number}>();
@@ -1,3 +1,5 @@
1
+ import {PayloadStatus} from "@lodestar/fork-choice";
2
+ import {computeEpochAtSlot} from "@lodestar/state-transition";
1
3
  import {SignedBeaconBlock, Slot} from "@lodestar/types";
2
4
  import {isErrorAborted, toRootHex} from "@lodestar/utils";
3
5
  import {Metrics} from "../../metrics/metrics.js";
@@ -5,15 +7,17 @@ import {nextEventLoop} from "../../util/eventLoop.js";
5
7
  import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js";
6
8
  import type {BeaconChain} from "../chain.js";
7
9
  import {BlockError, BlockErrorCode, isBlockErrorAborted} from "../errors/index.js";
10
+ import {ForkchoiceCaller} from "../forkChoice/index.js";
8
11
  import {BlockProcessOpts} from "../options.js";
9
12
  import {IBlockInput} from "./blockInput/types.js";
10
- import {importBlock} from "./importBlock.js";
13
+ import {importBlock, importsBlockAttestations} from "./importBlock.js";
11
14
  import {PayloadError, importExecutionPayload} from "./importExecutionPayload.js";
12
15
  import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
13
- import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
14
- import {assertLinearChainSegment} from "./utils/chainSegment.js";
16
+ import {FullyVerifiedBlock, ImportBlockOpts, ProcessBlocksResult} from "./types.js";
17
+ import {OrphanedPayloadEnvelope, assertLinearChainSegment} from "./utils/chainSegment.js";
15
18
  import {verifyBlocksInEpoch} from "./verifyBlock.js";
16
19
  import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js";
20
+ import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
17
21
 
18
22
  export {AttestationImportOpt, type ImportBlockOpts} from "./types.js";
19
23
 
@@ -23,10 +27,16 @@ const QUEUE_MAX_LENGTH = 256;
23
27
  * BlockProcessor processes block jobs in a queued fashion, one after the other.
24
28
  */
25
29
  export class BlockProcessor {
26
- readonly jobQueue: JobItemQueue<[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts], void>;
30
+ readonly jobQueue: JobItemQueue<
31
+ [IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts],
32
+ ProcessBlocksResult
33
+ >;
27
34
 
28
35
  constructor(chain: BeaconChain, metrics: Metrics | null, opts: BlockProcessOpts, signal: AbortSignal) {
29
- this.jobQueue = new JobItemQueue<[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts], void>(
36
+ this.jobQueue = new JobItemQueue<
37
+ [IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts],
38
+ ProcessBlocksResult
39
+ >(
30
40
  (job, payloadEnvelopes, importOpts) => {
31
41
  return processBlocks.call(chain, job, payloadEnvelopes, {...opts, ...importOpts});
32
42
  },
@@ -39,8 +49,8 @@ export class BlockProcessor {
39
49
  job: IBlockInput[],
40
50
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
41
51
  opts: ImportBlockOpts = {}
42
- ): Promise<void> {
43
- await this.jobQueue.push(job, payloadEnvelopes, opts);
52
+ ): Promise<ProcessBlocksResult> {
53
+ return this.jobQueue.push(job, payloadEnvelopes, opts);
44
54
  }
45
55
  }
46
56
 
@@ -59,9 +69,10 @@ export async function processBlocks(
59
69
  blocks: IBlockInput[],
60
70
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
61
71
  opts: BlockProcessOpts & ImportBlockOpts
62
- ): Promise<void> {
72
+ ): Promise<ProcessBlocksResult> {
73
+ const emptyResult: ProcessBlocksResult = {orphaned: [], skipped: false};
63
74
  if (blocks.length === 0) {
64
- return; // TODO: or throw?
75
+ return emptyResult; // TODO: or throw?
65
76
  }
66
77
 
67
78
  try {
@@ -70,7 +81,7 @@ export async function processBlocks(
70
81
  // No relevant blocks, skip verifyBlocksInEpoch()
71
82
  if (relevantBlocks.length === 0 || parentBlock === null) {
72
83
  // parentBlock can only be null if relevantBlocks are empty
73
- return;
84
+ return await importPayloadEnvelopesOfKnownBlocks.call(this, payloadEnvelopes, opts);
74
85
  }
75
86
 
76
87
  const {warnings: orphanedPayloads} = assertLinearChainSegment(
@@ -79,12 +90,21 @@ export async function processBlocks(
79
90
  payloadEnvelopes,
80
91
  parentBlock
81
92
  );
82
- if (orphanedPayloads != null) {
83
- for (const orphaned of orphanedPayloads) {
84
- this.logger.debug("Orphaned payload envelope in chain segment", {
85
- slot: orphaned.slot,
86
- blockRoot: orphaned.payloadEnvelopeInput.blockRootHex,
87
- });
93
+ // Orphaned payloads of recent blocks are still imported, a reorg of the child could make the payload canonical
94
+ // again and later blocks build on it. Older ones are skipped: without the block attestations the orphaned FULL
95
+ // variant has no descendants and ties at zero weight with the EMPTY variant carrying the chain, the payload
96
+ // status tiebreaker then picks FULL and parks the head there.
97
+ const blockEpoch = computeEpochAtSlot(relevantBlocks[0].getBlock().message.slot);
98
+ const importsAttestations = importsBlockAttestations(opts, blockEpoch, this.clock.currentEpoch);
99
+ const skipped = orphanedPayloads != null && payloadEnvelopes !== null && !importsAttestations;
100
+ let payloadEnvelopesToImport = payloadEnvelopes;
101
+ if (skipped) {
102
+ payloadEnvelopesToImport = new Map(payloadEnvelopes);
103
+ for (const orphaned of orphanedPayloads ?? []) {
104
+ payloadEnvelopesToImport.delete(orphaned.slot);
105
+ // never validated, drop it from the shared cache so it is not served to peers, by-root sync reloads the
106
+ // entry from db if the payload is ever needed
107
+ this.seenPayloadEnvelopeInputCache.prune(orphaned.payloadEnvelopeInput.blockRootHex);
88
108
  }
89
109
  }
90
110
 
@@ -97,7 +117,7 @@ export async function processBlocks(
97
117
  proposerBalanceDeltas,
98
118
  segmentExecStatus,
99
119
  indexedAttestationsByBlock,
100
- } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopes, opts);
120
+ } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopesToImport, opts);
101
121
 
102
122
  // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid
103
123
  // and we need to further propagate
@@ -132,8 +152,8 @@ export async function processBlocks(
132
152
  }
133
153
 
134
154
  const slotSet = new Set<Slot>(blocks.map((b) => b.getBlock().message.slot));
135
- if (payloadEnvelopes) {
136
- for (const slot of payloadEnvelopes.keys()) slotSet.add(slot);
155
+ if (payloadEnvelopesToImport) {
156
+ for (const slot of payloadEnvelopesToImport.keys()) slotSet.add(slot);
137
157
  }
138
158
  const slots = Array.from(slotSet).sort((a, b) => a - b);
139
159
  for (const slot of slots) {
@@ -146,7 +166,7 @@ export async function processBlocks(
146
166
  // PayloadEnvelopeInput is shared and may receive an envelope after the DA snapshot was taken.
147
167
  const payloadDA = payloadDAStatuses.get(slot);
148
168
  if (payloadDA !== undefined) {
149
- const payloadInput = payloadEnvelopes?.get(slot);
169
+ const payloadInput = payloadEnvelopesToImport?.get(slot);
150
170
  if (payloadInput === undefined) {
151
171
  throw new Error(`Missing payload input for slot ${slot} after DA verification`);
152
172
  }
@@ -159,9 +179,11 @@ export async function processBlocks(
159
179
 
160
180
  await nextEventLoop();
161
181
  }
182
+
183
+ return {orphaned: orphanedPayloads ?? [], skipped};
162
184
  } catch (e) {
163
185
  if (isErrorAborted(e) || isQueueErrorAborted(e) || isBlockErrorAborted(e)) {
164
- return; // Ignore
186
+ return emptyResult; // Ignore
165
187
  }
166
188
 
167
189
  // above functions should only throw BlockError, or PayloadError from the gloas payload import
@@ -209,6 +231,70 @@ export async function processBlocks(
209
231
  }
210
232
  }
211
233
 
234
+ /**
235
+ * A peer can serve a block without its envelope, the block is then imported with a PENDING payload. Later batches
236
+ * carry the envelope but all their blocks are known, without importing it here every child building on the FULL
237
+ * variant fails with PARENT_PAYLOAD_UNKNOWN and range sync never progresses. If our head already descends from the
238
+ * block's EMPTY variant the chain did not build on the payload, importing it would only add a dead FULL leaf. As for
239
+ * orphaned payloads in a segment this only applies while the block attestations are not imported, a recent payload
240
+ * is imported anyway so a reorg building on it does not have to fetch it through unknown block sync.
241
+ */
242
+ async function importPayloadEnvelopesOfKnownBlocks(
243
+ this: BeaconChain,
244
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
245
+ opts: ImportBlockOpts
246
+ ): Promise<ProcessBlocksResult> {
247
+ if (payloadEnvelopes === null) {
248
+ return {orphaned: [], skipped: false};
249
+ }
250
+
251
+ const head = this.forkChoice.getHead();
252
+ const currentEpoch = this.clock.currentEpoch;
253
+ const payloadInputs: PayloadEnvelopeInput[] = [];
254
+ const orphaned: OrphanedPayloadEnvelope[] = [];
255
+ for (const payloadInput of payloadEnvelopes.values()) {
256
+ const {blockRootHex, slot} = payloadInput;
257
+ if (
258
+ !payloadInput.hasPayloadEnvelope() ||
259
+ !this.forkChoice.hasBlockHex(blockRootHex) ||
260
+ this.forkChoice.getBlockHexAndBlockHash(blockRootHex, payloadInput.getBlockHashHex()) !== null
261
+ ) {
262
+ continue;
263
+ }
264
+ if (
265
+ head.blockRoot !== blockRootHex &&
266
+ !importsBlockAttestations(opts, computeEpochAtSlot(slot), currentEpoch) &&
267
+ this.forkChoice.isDescendant(blockRootHex, PayloadStatus.EMPTY, head.blockRoot, head.payloadStatus)
268
+ ) {
269
+ // never validated, see processBlocks()
270
+ this.seenPayloadEnvelopeInputCache.prune(blockRootHex);
271
+ orphaned.push({slot, payloadEnvelopeInput: payloadInput});
272
+ continue;
273
+ }
274
+ payloadInputs.push(payloadInput);
275
+ }
276
+
277
+ // orphaned envelopes on this path are always skipped, range sync logs them with peer/client context
278
+ const result: ProcessBlocksResult = {orphaned, skipped: orphaned.length > 0};
279
+
280
+ if (payloadInputs.length === 0) {
281
+ return result;
282
+ }
283
+
284
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(payloadInputs, new AbortController().signal);
285
+ for (let i = 0; i < payloadInputs.length; i++) {
286
+ this.logger.debug("Importing payload envelope of known block", {
287
+ slot: payloadInputs[i].slot,
288
+ blockRoot: payloadInputs[i].blockRootHex,
289
+ });
290
+ await importExecutionPayload.call(this, payloadInputs[i], dataAvailabilityStatuses[i], {validSignature: false});
291
+ }
292
+ // the new FULL variants are not seen by the head cached from the last block import
293
+ this.recomputeForkChoiceHead(ForkchoiceCaller.importBlock);
294
+
295
+ return result;
296
+ }
297
+
212
298
  function getBlockOrPayloadError(e: unknown, block: SignedBeaconBlock): BlockError | PayloadError {
213
299
  if (e instanceof BlockError) {
214
300
  return e;
@@ -4,6 +4,7 @@ import {ForkSeq} from "@lodestar/params";
4
4
  import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
5
5
  import type {IndexedAttestation, Slot, fulu} from "@lodestar/types";
6
6
  import {IBlockInput} from "./blockInput/types.js";
7
+ import {OrphanedPayloadEnvelope} from "./utils/chainSegment.js";
7
8
 
8
9
  export enum GossipedInputType {
9
10
  block = "block",
@@ -110,3 +111,11 @@ export type FullyVerifiedBlock = {
110
111
  /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */
111
112
  executionStatus: BlockExecutionStatus | PayloadExecutionStatus;
112
113
  };
114
+
115
+ /**
116
+ * Result of processing a chain segment.
117
+ */
118
+ export type ProcessBlocksResult = {
119
+ orphaned: OrphanedPayloadEnvelope[];
120
+ skipped: boolean;
121
+ };
@@ -22,7 +22,7 @@ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
22
22
  * - Verifies parent root + slot linearity
23
23
  * - For gloas: verifies bid.parentBlockHash matches the tracked execution hash; if not, the
24
24
  * previous FULL envelope is treated as orphaned (segment continues as if previous slot was EMPTY)
25
- * - If an envelope exists for this slot: verifies it references this block's root
25
+ * - If an envelope exists for this slot (or the parent's slot): verifies it references this block's root
26
26
  * - Advances the tracked execution hash (FULL if envelope present, EMPTY if not)
27
27
  */
28
28
  export function assertLinearChainSegment(
@@ -48,6 +48,15 @@ export function assertLinearChainSegment(
48
48
  if (parentBlock !== null) {
49
49
  const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
50
50
  if (parentPayloadInput?.hasPayloadEnvelope()) {
51
+ // Envelopes are served by slot, the one at the parent's slot must reference the parent block. Without this
52
+ // check an envelope of a sibling block is taken as orphaned and only fails downstream in the payload import
53
+ if (parentPayloadInput.blockRootHex !== parentBlock.blockRoot) {
54
+ throw new BlockError(blocks[0].getBlock(), {
55
+ code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
56
+ envelopeBlockRoot: parentPayloadInput.blockRootHex,
57
+ blockRoot: parentBlock.blockRoot,
58
+ });
59
+ }
51
60
  const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
52
61
  if (currentExecHash === parentPayloadBlockHash) {
53
62
  // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
@@ -82,7 +82,7 @@ import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/bloc
82
82
  import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js";
83
83
  import {PayloadEnvelopeInputSource} from "./blocks/payloadEnvelopeInput/index.js";
84
84
  import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js";
85
- import {ImportPayloadOpts} from "./blocks/types.js";
85
+ import {ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
86
86
  import {persistBlockInput} from "./blocks/writeBlockInputToDb.js";
87
87
  import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js";
88
88
  import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js";
@@ -1167,15 +1167,15 @@ export class BeaconChain implements IBeaconChain {
1167
1167
  }
1168
1168
 
1169
1169
  async processBlock(block: IBlockInput, opts?: ImportBlockOpts): Promise<void> {
1170
- return this.blockProcessor.processBlocksJob([block], null, opts);
1170
+ await this.blockProcessor.processBlocksJob([block], null, opts);
1171
1171
  }
1172
1172
 
1173
1173
  async processChainSegment(
1174
1174
  blocks: IBlockInput[],
1175
1175
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
1176
1176
  opts?: ImportBlockOpts
1177
- ): Promise<void> {
1178
- await this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
1177
+ ): Promise<ProcessBlocksResult> {
1178
+ return this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
1179
1179
  }
1180
1180
 
1181
1181
  async processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void> {
@@ -8,6 +8,7 @@ export enum GossipAction {
8
8
  export enum GossipErrorCode {
9
9
  INVALID_SERIALIZED_BYTES_ERROR_CODE = "GOSSIP_ERROR_INVALID_SERIALIZED_BYTES",
10
10
  PAST_SLOT = "GOSSIP_ERROR_PAST_SLOT",
11
+ FUTURE_SLOT = "GOSSIP_ERROR_FUTURE_SLOT",
11
12
  }
12
13
 
13
14
  export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
@@ -35,7 +35,7 @@ import {IArchiveStore} from "./archiveStore/interface.js";
35
35
  import {CheckpointBalancesCache} from "./balancesCache.js";
36
36
  import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache.js";
37
37
  import {IBlockInput} from "./blocks/blockInput/index.js";
38
- import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js";
38
+ import {ImportBlockOpts, ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
39
39
  import {IBlsVerifier} from "./bls/index.js";
40
40
  import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js";
41
41
  import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js";
@@ -262,7 +262,7 @@ export interface IBeaconChain {
262
262
  blocks: IBlockInput[],
263
263
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
264
264
  opts?: ImportBlockOpts
265
- ): Promise<void>;
265
+ ): Promise<ProcessBlocksResult>;
266
266
 
267
267
  /** Process execution payload envelope: verify, import to fork choice, and persist to DB */
268
268
  processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void>;
@@ -363,7 +363,7 @@ export class AggregatedAttestationPool {
363
363
  stateEpoch,
364
364
  rootCache,
365
365
  gloasState?.executionPayloadAvailability ?? null,
366
- gloasState?.latestExecutionPayloadBid.slot ?? null
366
+ gloasState?.latestBlockHeader.slot ?? null
367
367
  );
368
368
 
369
369
  const weight =
@@ -1,5 +1,5 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
3
3
  import {
4
4
  IBeaconStateView,
5
5
  ParticipationFlags,
@@ -1117,7 +1117,11 @@ function isCanonical(rootCache: RootHexCache, block: AttestationBlockInclusion):
1117
1117
 
1118
1118
  /** Returns true if root at slot is the same at slot - 1 == there was no new block at slot */
1119
1119
  function isMissedSlot(rootCache: RootHexCache, slot: Slot): boolean {
1120
- return slot > 0 && rootCache.getBlockRootAtSlot(slot) === rootCache.getBlockRootAtSlot(slot - 1);
1120
+ if (slot <= 0) {
1121
+ return false;
1122
+ }
1123
+ const root = rootCache.getBlockRootAtSlot(slot);
1124
+ return root !== null && root === rootCache.getBlockRootAtSlot(slot - 1);
1121
1125
  }
1122
1126
 
1123
1127
  function renderBlockProposalSummary(
@@ -1131,7 +1135,11 @@ function renderBlockProposalSummary(
1131
1135
  return "not_submitted";
1132
1136
  }
1133
1137
 
1134
- if (rootCache.getBlockRootAtSlot(proposalSlot) === proposal.blockRoot) {
1138
+ const canonicalRoot = rootCache.getBlockRootAtSlot(proposalSlot);
1139
+ if (canonicalRoot === null) {
1140
+ return "unknown";
1141
+ }
1142
+ if (canonicalRoot === proposal.blockRoot) {
1135
1143
  // Canonical state includes our block
1136
1144
  return "canonical";
1137
1145
  }
@@ -1157,14 +1165,23 @@ function renderBlockProposalSummary(
1157
1165
  * In normal network conditions the same root is read multiple times, specially the target.
1158
1166
  */
1159
1167
  export class RootHexCache {
1160
- private readonly blockRootSlotCache = new Map<Slot, RootHex>();
1168
+ private readonly blockRootSlotCache = new Map<Slot, RootHex | null>();
1161
1169
 
1162
1170
  constructor(private readonly state: IBeaconStateView) {}
1163
1171
 
1164
- getBlockRootAtSlot(slot: Slot): RootHex {
1172
+ /**
1173
+ * Block root at `slot`, null if the state does not cover it: slots at or after the state's own slot (an inclusion
1174
+ * in a block the head state does not know yet, e.g. an orphaned block, or the slot after the last attestation of
1175
+ * the epoch) or slots older than `SLOTS_PER_HISTORICAL_ROOT`. Callers treat null as unknown instead of throwing
1176
+ * and aborting the summaries of all remaining validators.
1177
+ */
1178
+ getBlockRootAtSlot(slot: Slot): RootHex | null {
1165
1179
  let root = this.blockRootSlotCache.get(slot);
1166
- if (!root) {
1167
- root = toRootHex(this.state.getBlockRootAtSlot(slot));
1180
+ if (root === undefined) {
1181
+ root =
1182
+ slot < this.state.slot && slot >= this.state.slot - SLOTS_PER_HISTORICAL_ROOT
1183
+ ? toRootHex(this.state.getBlockRootAtSlot(slot))
1184
+ : null;
1168
1185
  this.blockRootSlotCache.set(slot, root);
1169
1186
  }
1170
1187
  return root;
@@ -360,6 +360,19 @@ export class NetworkProcessor {
360
360
  return;
361
361
  }
362
362
 
363
+ // DOS protection: drop far-future messages before allocating any unknown-root tracking entry.
364
+ // Bound each topic to what its gossip validator accepts; only execution_payload_bid accepts the next slot.
365
+ const latestPermissableSlot =
366
+ this.chain.clock.currentSlotWithGossipDisparity + (topicType === GossipType.execution_payload_bid ? 1 : 0);
367
+ if (slot > latestPermissableSlot) {
368
+ // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
369
+ this.metrics?.networkProcessor.gossipValidationError.inc({
370
+ topic: topicType,
371
+ error: GossipErrorCode.FUTURE_SLOT,
372
+ });
373
+ return;
374
+ }
375
+
363
376
  message.msgSlot = slot;
364
377
 
365
378
  // this determines whether this message needs to wait for a Block or Envelope
@@ -217,7 +217,29 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
217
217
  }
218
218
  }
219
219
  } else {
220
- await this.chain.processChainSegment(blocks, payloadEnvelopes, flags);
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();
226
+ let client = "unknown";
227
+ if (peerIdStr !== undefined) {
228
+ try {
229
+ client = this.getConnectedPeerSyncMeta(peerIdStr).client;
230
+ } catch {
231
+ // peer disconnected since serving the envelope, keep "unknown"
232
+ }
233
+ }
234
+ this.logger.debug("Orphaned payload envelope in range sync batch", {
235
+ slot,
236
+ root: payloadEnvelopeInput.blockRootHex,
237
+ peer: peerIdStr ?? "unknown",
238
+ source,
239
+ client,
240
+ skipped,
241
+ });
242
+ }
221
243
  }
222
244
  };
223
245