@lodestar/beacon-node 1.48.0-dev.b24dce51bf → 1.48.0-dev.baa28132d2

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 (90) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +0 -7
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/utils.js +14 -0
  6. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  7. package/lib/api/impl/config/index.d.ts.map +1 -1
  8. package/lib/api/impl/config/index.js +4 -1
  9. package/lib/api/impl/config/index.js.map +1 -1
  10. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.js +4 -1
  12. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  13. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +9 -1
  14. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/utils/archiveBlocks.js +37 -4
  16. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  17. package/lib/chain/blocks/importBlock.d.ts +5 -1
  18. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  19. package/lib/chain/blocks/importBlock.js +12 -5
  20. package/lib/chain/blocks/importBlock.js.map +1 -1
  21. package/lib/chain/blocks/index.d.ts +8 -4
  22. package/lib/chain/blocks/index.d.ts.map +1 -1
  23. package/lib/chain/blocks/index.js +80 -15
  24. package/lib/chain/blocks/index.js.map +1 -1
  25. package/lib/chain/blocks/types.d.ts +8 -0
  26. package/lib/chain/blocks/types.d.ts.map +1 -1
  27. package/lib/chain/blocks/types.js.map +1 -1
  28. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  29. package/lib/chain/blocks/utils/chainSegment.js +16 -10
  30. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  31. package/lib/chain/chain.d.ts +2 -2
  32. package/lib/chain/chain.d.ts.map +1 -1
  33. package/lib/chain/chain.js +2 -2
  34. package/lib/chain/chain.js.map +1 -1
  35. package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
  36. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  37. package/lib/chain/errors/executionPayloadBid.js +1 -0
  38. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  39. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  40. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  41. package/lib/chain/errors/gossipValidation.js +1 -0
  42. package/lib/chain/errors/gossipValidation.js.map +1 -1
  43. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  44. package/lib/chain/forkChoice/index.js +2 -0
  45. package/lib/chain/forkChoice/index.js.map +1 -1
  46. package/lib/chain/interface.d.ts +2 -2
  47. package/lib/chain/interface.d.ts.map +1 -1
  48. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  49. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  50. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  51. package/lib/chain/validation/executionPayloadBid.js +16 -0
  52. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  53. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  54. package/lib/execution/builder/validateBid.js +3 -0
  55. package/lib/execution/builder/validateBid.js.map +1 -1
  56. package/lib/metrics/metrics/lodestar.d.ts +4 -3
  57. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  58. package/lib/metrics/metrics/lodestar.js +5 -5
  59. package/lib/metrics/metrics/lodestar.js.map +1 -1
  60. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  61. package/lib/network/processor/gossipHandlers.js +378 -278
  62. package/lib/network/processor/gossipHandlers.js.map +1 -1
  63. package/lib/network/processor/index.d.ts.map +1 -1
  64. package/lib/network/processor/index.js +22 -16
  65. package/lib/network/processor/index.js.map +1 -1
  66. package/lib/sync/range/range.d.ts.map +1 -1
  67. package/lib/sync/range/range.js +24 -1
  68. package/lib/sync/range/range.js.map +1 -1
  69. package/package.json +14 -14
  70. package/src/api/impl/beacon/blocks/index.ts +0 -10
  71. package/src/api/impl/beacon/state/utils.ts +15 -0
  72. package/src/api/impl/config/index.ts +4 -1
  73. package/src/chain/archiveStore/archiveStore.ts +6 -0
  74. package/src/chain/archiveStore/utils/archiveBlocks.ts +39 -3
  75. package/src/chain/blocks/importBlock.ts +25 -7
  76. package/src/chain/blocks/index.ts +107 -21
  77. package/src/chain/blocks/types.ts +9 -0
  78. package/src/chain/blocks/utils/chainSegment.ts +16 -11
  79. package/src/chain/chain.ts +4 -4
  80. package/src/chain/errors/executionPayloadBid.ts +2 -0
  81. package/src/chain/errors/gossipValidation.ts +1 -0
  82. package/src/chain/forkChoice/index.ts +2 -0
  83. package/src/chain/interface.ts +2 -2
  84. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  85. package/src/chain/validation/executionPayloadBid.ts +18 -0
  86. package/src/execution/builder/validateBid.ts +4 -0
  87. package/src/metrics/metrics/lodestar.ts +6 -5
  88. package/src/network/processor/gossipHandlers.ts +487 -348
  89. package/src/network/processor/index.ts +24 -17
  90. package/src/sync/range/range.ts +23 -1
@@ -20,11 +20,14 @@ export function renderJsonSpec(config: ChainConfig): routes.config.Spec {
20
20
  const presetJson = presetToJson(activePreset);
21
21
  const constantsJson = specValuesToJson(specConstants);
22
22
 
23
- // TODO Fulu: remove this check once interop issues are resolved
23
+ // TODO Fulu/Gloas: remove these checks once interop issues are resolved
24
24
  // see https://github.com/attestantio/go-eth2-client/issues/230
25
25
  if (config.FULU_FORK_EPOCH === Infinity) {
26
26
  delete configJson.BLOB_SCHEDULE;
27
27
  }
28
+ if (config.GLOAS_FORK_EPOCH === Infinity) {
29
+ delete configJson.GAS_LIMIT_SCHEDULE;
30
+ }
28
31
 
29
32
  return {...configJson, ...presetJson, ...constantsJson};
30
33
  }
@@ -1,5 +1,6 @@
1
1
  import {CheckpointWithHex} from "@lodestar/fork-choice";
2
2
  import {LoggerNode} from "@lodestar/logger/node";
3
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
3
4
  import {Checkpoint} from "@lodestar/types/phase0";
4
5
  import {callFnWhenAwait} from "@lodestar/utils";
5
6
  import {IBeaconDb} from "../../db/index.js";
@@ -208,6 +209,9 @@ export class ArchiveStore {
208
209
  const finalizedEpoch = finalized.epoch;
209
210
  this.logger.verbose("Start processing finalized checkpoint", {epoch: finalizedEpoch, rootHex: finalized.rootHex});
210
211
 
212
+ // we want to track late imported canonical blocks, but it's not nice to do it at syncig time
213
+ const isNodeSynced = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot <= SLOTS_PER_EPOCH;
214
+
211
215
  let timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
212
216
  await archiveBlocks(
213
217
  this.chain.config,
@@ -217,6 +221,8 @@ export class ArchiveStore {
217
221
  this.logger,
218
222
  finalized,
219
223
  this.chain.clock.currentEpoch,
224
+ this.metrics,
225
+ isNodeSynced,
220
226
  this.archiveDataEpochs,
221
227
  this.chain.opts.persistOrphanedBlocks,
222
228
  this.chain.opts.persistOrphanedBlocksDir
@@ -8,6 +8,7 @@ import {Epoch, Slot} from "@lodestar/types";
8
8
  import {Logger, fromAsync, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
9
9
  import {IBeaconDb} from "../../../db/index.js";
10
10
  import {BlockArchiveBatchPutBinaryItem} from "../../../db/repositories/index.js";
11
+ import {Metrics} from "../../../metrics/metrics.js";
11
12
  import {ensureDir, writeIfNotExist} from "../../../util/file.js";
12
13
  import {BlockRootHex} from "../../../util/sszBytes.js";
13
14
  import {LightClientServer} from "../../lightClient/index.js";
@@ -20,6 +21,16 @@ const BLOB_SIDECAR_BATCH_SIZE = 32;
20
21
 
21
22
  type BlockRootSlot = {slot: Slot; root: Uint8Array};
22
23
 
24
+ /**
25
+ * Why a finalized-canonical block was imported after the attestation cutoff.
26
+ */
27
+ export enum LateCanonicalBlockReason {
28
+ // received on time, but finished importing after the attestation cutoff
29
+ SlowImport = "slow_import",
30
+ // block reached this node late on the network thread
31
+ LateReceive = "late_receive",
32
+ }
33
+
23
34
  /**
24
35
  * Persist orphaned block to disk
25
36
  */
@@ -54,6 +65,8 @@ export async function archiveBlocks(
54
65
  logger: Logger,
55
66
  finalizedCheckpoint: CheckpointWithHex,
56
67
  currentEpoch: Epoch,
68
+ metrics: Metrics | null,
69
+ isNodeSynced: boolean,
57
70
  archiveDataEpochs?: number,
58
71
  persistOrphanedBlocks?: boolean,
59
72
  persistOrphanedBlocksDir?: string
@@ -76,6 +89,29 @@ export async function archiveBlocks(
76
89
 
77
90
  const logCtx = {currentEpoch, finalizedEpoch: finalizedCheckpoint.epoch, finalizedRoot: finalizedCheckpoint.rootHex};
78
91
 
92
+ // `finalizedCanonicalBlocks` is newest -> oldest; its LAST element is the previous-finalized
93
+ // boundary block, already audited on the prior run, so exclude it to avoid double counting.
94
+ if (isNodeSynced && finalizedCanonicalBlocks.length > 1) {
95
+ const lateBlocks = finalizedCanonicalBlocks.slice(0, -1).filter((block) => !block.importedTimely);
96
+ if (lateBlocks.length > 0) {
97
+ let slowImport = 0;
98
+ for (const block of lateBlocks) {
99
+ // received late => late_receive; otherwise we had it on time but were slow to import => slow_import
100
+ const reason = block.timeliness ? LateCanonicalBlockReason.SlowImport : LateCanonicalBlockReason.LateReceive;
101
+ if (block.timeliness) slowImport++;
102
+ metrics?.importBlock.lateCanonicalBlock.inc({reason});
103
+ }
104
+ const lateSlots = lateBlocks.map((block) => block.slot).sort((a, b) => a - b);
105
+ logger.verbose("Late imported canonical blocks", {
106
+ ...logCtx,
107
+ count: lateBlocks.length,
108
+ slowImport,
109
+ lateReceive: lateBlocks.length - slowImport,
110
+ slotRange: prettyPrintIndices(lateSlots),
111
+ });
112
+ }
113
+ }
114
+
79
115
  if (finalizedCanonicalBlockRoots.length > 0) {
80
116
  const migratedSlots = await migrateBlocksFromHotToColdDb(db, logger, finalizedCanonicalBlockRoots);
81
117
  logger.verbose("Migrated blocks from hot DB to cold DB", {
@@ -298,7 +334,7 @@ async function migrateBlocksFromHotToColdDb(db: IBeaconDb, logger: Logger, block
298
334
  ]);
299
335
  for (const entry of canonicalBlockEntries) migratedSlots.push(entry.slot);
300
336
  }
301
- // Ancestor walk is newest → oldest; sort ascending so `prettyPrintIndices` renders cleanly.
337
+ // Ancestor walk is newest to oldest; sort ascending so `prettyPrintIndices` renders cleanly.
302
338
  return migratedSlots.sort((a, b) => a - b);
303
339
  }
304
340
 
@@ -437,7 +473,7 @@ async function migrateDataColumnSidecarsFromHotToColdDb(
437
473
  await Promise.all(promises);
438
474
  }
439
475
 
440
- // Ancestor walk is newest → oldest; sort ascending so `prettyPrintIndices` renders cleanly.
476
+ // Ancestor walk is newest to oldest; sort ascending so `prettyPrintIndices` renders cleanly.
441
477
  return migratedSlots.sort((a, b) => a - b);
442
478
  }
443
479
 
@@ -485,7 +521,7 @@ async function migrateExecutionPayloadEnvelopesFromHotToColdDb(
485
521
  ]);
486
522
 
487
523
  // Slots are ascending in hot-db key order — sort to guarantee `prettyPrintIndices` output is clean
488
- // regardless of ancestor-walk order (newest → oldest).
524
+ // regardless of ancestor-walk order (newest to oldest).
489
525
  return envelopeEntries.map((entry) => entry.key).sort((a, b) => a - b);
490
526
  }
491
527
 
@@ -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
  */
@@ -85,7 +104,7 @@ export async function importBlock(
85
104
  const currentEpoch = computeEpochAtSlot(currentSlot);
86
105
  const blockEpoch = computeEpochAtSlot(blockSlot);
87
106
  const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
88
- const blockDelaySec =
107
+ const receiveDelaySec =
89
108
  fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime);
90
109
  const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
91
110
  const fork = this.config.getForkSeq(blockSlot);
@@ -122,10 +141,12 @@ export async function importBlock(
122
141
  executionStatus = parentBlock.executionStatus;
123
142
  }
124
143
 
144
+ const importDelaySec = this.clock.secFromSlot(blockSlot);
125
145
  const blockSummary = this.forkChoice.onBlock(
126
146
  block.message,
127
147
  postState,
128
- blockDelaySec,
148
+ receiveDelaySec,
149
+ importDelaySec,
129
150
  currentSlot,
130
151
  executionStatus,
131
152
  dataAvailabilityStatus
@@ -147,10 +168,7 @@ export async function importBlock(
147
168
  // Only process attestations of blocks with relevant attestations for the fork-choice:
148
169
  // If current epoch is N, and block is epoch X, block may include attestations for epoch X or X - 1.
149
170
  // The latest block that is useful is at epoch N - 1 which may include attestations for epoch N - 1 or N - 2.
150
- if (
151
- opts.importAttestations === AttestationImportOpt.Force ||
152
- (opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
153
- ) {
171
+ if (importsBlockAttestations(opts, blockEpoch, currentEpoch)) {
154
172
  const attestations = block.message.body.attestations;
155
173
  const rootCache = new RootCache(postState);
156
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
+ };
@@ -37,17 +37,6 @@ export function assertLinearChainSegment(
37
37
 
38
38
  const warnings: OrphanedPayloadEnvelope[] = [];
39
39
 
40
- let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
41
- if (parentBlock !== null) {
42
- const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
43
- if (parentPayloadInput?.hasPayloadEnvelope()) {
44
- // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
45
- // hash, not its own payload. Its payload envelope is in this batch, so use that blockHash as
46
- // the real parent EL head.
47
- parentExecHash = parentPayloadInput.getBlockHashHex();
48
- }
49
- }
50
-
51
40
  // Track the expected execution payload block hash through the segment, seeded from the FIRST
52
41
  // block's own bid
53
42
  const firstBlockMessage = blocks[0].getBlock().message;
@@ -55,6 +44,22 @@ export function assertLinearChainSegment(
55
44
  ? toRootHex(firstBlockMessage.body.signedExecutionPayloadBid.message.parentBlockHash)
56
45
  : null;
57
46
 
47
+ let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
48
+ if (parentBlock !== null) {
49
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
50
+ if (parentPayloadInput?.hasPayloadEnvelope()) {
51
+ const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
52
+ if (currentExecHash === parentPayloadBlockHash) {
53
+ // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
54
+ // hash, its own payload is in this batch and is the real parent EL head
55
+ parentExecHash = parentPayloadBlockHash;
56
+ } else {
57
+ // First block builds on the parent's EMPTY variant, the parent's payload in this batch is orphaned
58
+ warnings.push({slot: parentBlock.slot, payloadEnvelopeInput: parentPayloadInput});
59
+ }
60
+ }
61
+ }
62
+
58
63
  // First block must build on the parent's EL head. PARENT_PAYLOAD_UNKNOWN (vs the in-segment
59
64
  // NON_LINEAR_PAYLOAD_ROOTS below) tells range sync this is a parent-link problem, not an in-batch one.
60
65
  if (parentExecHash !== null && currentExecHash !== null && currentExecHash !== parentExecHash) {
@@ -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> {
@@ -5,6 +5,7 @@ export enum ExecutionPayloadBidErrorCode {
5
5
  BUILDER_NOT_ELIGIBLE = "EXECUTION_PAYLOAD_BID_ERROR_BUILDER_NOT_ELIGIBLE",
6
6
  INVALID_BUILDER_VERSION = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_BUILDER_VERSION",
7
7
  NON_ZERO_EXECUTION_PAYMENT = "EXECUTION_PAYLOAD_BID_ERROR_NON_ZERO_EXECUTION_PAYMENT",
8
+ BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH",
8
9
  INCOMPATIBLE_WITH_HEAD = "EXECUTION_PAYLOAD_BID_ERROR_INCOMPATIBLE_WITH_HEAD",
9
10
  BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN",
10
11
  BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW",
@@ -63,6 +64,7 @@ export type ExecutionPayloadBidErrorType =
63
64
  }
64
65
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
65
66
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
67
+ | {code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH; blockHash: RootHex}
66
68
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
67
69
  | {code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
68
70
  | {
@@ -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> {
@@ -135,6 +135,7 @@ export function initializeForkChoiceFromFinalizedState(
135
135
  blockRoot: toRootHex(checkpoint.root),
136
136
  timeliness: true, // Optimistically assume is timely
137
137
  ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
138
+ importedTimely: true, // Optimistically assume is timely
138
139
  proposerIndex: blockHeader.proposerIndex,
139
140
 
140
141
  justifiedEpoch: justifiedCheckpoint.epoch,
@@ -233,6 +234,7 @@ export function initializeForkChoiceFromUnfinalizedState(
233
234
  targetRoot: headRoot,
234
235
  timeliness: true, // Optimistically assume is timely
235
236
  ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
237
+ importedTimely: true, // Optimistically assume is timely
236
238
  proposerIndex: blockHeader.proposerIndex,
237
239
 
238
240
  justifiedEpoch: justifiedCheckpoint.epoch,
@@ -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 =
@@ -113,6 +113,15 @@ export async function validateApiExecutionPayloadBid(
113
113
  });
114
114
  }
115
115
 
116
+ // [REJECT] The bid's block hash is not equal to its parent block hash -- i.e.
117
+ // `bid.block_hash != bid.parent_block_hash`.
118
+ if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
119
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
120
+ code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH,
121
+ blockHash: toRootHex(bid.blockHash),
122
+ });
123
+ }
124
+
116
125
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
117
126
  // consensus layer -- i.e. validate that
118
127
  // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
@@ -266,6 +275,15 @@ async function validateExecutionPayloadBid(
266
275
  });
267
276
  }
268
277
 
278
+ // [REJECT] The bid's block hash is not equal to its parent block hash -- i.e.
279
+ // `bid.block_hash != bid.parent_block_hash`.
280
+ if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
281
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
282
+ code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH,
283
+ blockHash: toRootHex(bid.blockHash),
284
+ });
285
+ }
286
+
269
287
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
270
288
  // consensus layer -- i.e. validate that
271
289
  // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
@@ -56,6 +56,10 @@ export async function validateBuilderApiExecutionPayloadBid(
56
56
  );
57
57
  }
58
58
 
59
+ if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
60
+ throw Error(`Bid block hash ${toRootHex(bid.blockHash)} must not equal its parent block hash`);
61
+ }
62
+
59
63
  const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
60
64
  const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
61
65
  if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
@@ -2,6 +2,7 @@
2
2
  import {NotReorgedReason} from "@lodestar/fork-choice";
3
3
  import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
+ import type {LateCanonicalBlockReason} from "../../chain/archiveStore/utils/archiveBlocks.js";
5
6
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
7
  import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
7
8
  import {
@@ -1047,6 +1048,11 @@ export function createLodestarMetrics(
1047
1048
  name: "lodestar_import_block_set_head_after_cutoff_total",
1048
1049
  help: "Total times an imported block is set as head after ATTESTATION_DUE_BPS of the slot",
1049
1050
  }),
1051
+ lateCanonicalBlock: register.counter<{reason: LateCanonicalBlockReason}>({
1052
+ name: "lodestar_import_block_late_canonical_total",
1053
+ help: "Total finalized-canonical blocks this node imported after the attestation cutoff; reason distinguishes this node's processing lag (slow_import) from late reception (late_receive)",
1054
+ labelNames: ["reason"],
1055
+ }),
1050
1056
  bySource: register.gauge<{source: BlockInputSource}>({
1051
1057
  name: "lodestar_import_block_by_source_total",
1052
1058
  help: "Total number of imported blocks by source",
@@ -1352,11 +1358,6 @@ export function createLodestarMetrics(
1352
1358
  help: "Total number of InsertOutcome as a result of adding an execution payload bid from gossip to the pool",
1353
1359
  labelNames: ["insertOutcome"],
1354
1360
  }),
1355
- apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
1356
- name: "lodestar_oppool_execution_payload_bid_pool_api_insert_outcome_total",
1357
- help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
1358
- labelNames: ["insertOutcome"],
1359
- }),
1360
1361
  apiValidationTime: register.histogram({
1361
1362
  name: "lodestar_api_execution_payload_bid_validation_time_seconds",
1362
1363
  help: "Time elapsed for signed execution payload bid validation - api path",