@lodestar/beacon-node 1.48.0-dev.67df6eac67 → 1.48.0-dev.68ce7b06a0

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 (114) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +4 -4
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +39 -39
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  8. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  9. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts +5 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +9 -16
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/index.d.ts +8 -4
  15. package/lib/chain/blocks/index.d.ts.map +1 -1
  16. package/lib/chain/blocks/index.js +83 -17
  17. package/lib/chain/blocks/index.js.map +1 -1
  18. package/lib/chain/blocks/types.d.ts +8 -0
  19. package/lib/chain/blocks/types.d.ts.map +1 -1
  20. package/lib/chain/blocks/types.js.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +10 -1
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/chain.d.ts +9 -4
  26. package/lib/chain/chain.d.ts.map +1 -1
  27. package/lib/chain/chain.js +82 -51
  28. package/lib/chain/chain.js.map +1 -1
  29. package/lib/chain/emitter.d.ts +1 -1
  30. package/lib/chain/emitter.js +1 -1
  31. package/lib/chain/errors/blockError.d.ts +3 -5
  32. package/lib/chain/errors/blockError.d.ts.map +1 -1
  33. package/lib/chain/errors/blockError.js +2 -2
  34. package/lib/chain/errors/blockError.js.map +1 -1
  35. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  36. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  37. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  38. package/lib/chain/errors/executionPayloadEnvelope.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/interface.d.ts +4 -4
  44. package/lib/chain/interface.d.ts.map +1 -1
  45. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  46. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  47. package/lib/chain/prepareNextSlot.js +13 -13
  48. package/lib/chain/prepareNextSlot.js.map +1 -1
  49. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  50. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  51. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  52. package/lib/chain/validation/block.d.ts.map +1 -1
  53. package/lib/chain/validation/block.js +9 -50
  54. package/lib/chain/validation/block.js.map +1 -1
  55. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  56. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  57. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  58. package/lib/chain/validatorMonitor.d.ts +7 -1
  59. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  60. package/lib/chain/validatorMonitor.js +22 -5
  61. package/lib/chain/validatorMonitor.js.map +1 -1
  62. package/lib/execution/engine/mock.d.ts.map +1 -1
  63. package/lib/execution/engine/mock.js +4 -1
  64. package/lib/execution/engine/mock.js.map +1 -1
  65. package/lib/network/gossip/encoding.d.ts.map +1 -1
  66. package/lib/network/gossip/encoding.js +0 -6
  67. package/lib/network/gossip/encoding.js.map +1 -1
  68. package/lib/network/gossip/topic.d.ts +2 -1
  69. package/lib/network/gossip/topic.d.ts.map +1 -1
  70. package/lib/network/gossip/topic.js +4 -20
  71. package/lib/network/gossip/topic.js.map +1 -1
  72. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  73. package/lib/network/processor/gossipHandlers.js +1 -1
  74. package/lib/network/processor/gossipHandlers.js.map +1 -1
  75. package/lib/network/processor/index.d.ts +49 -3
  76. package/lib/network/processor/index.d.ts.map +1 -1
  77. package/lib/network/processor/index.js +243 -98
  78. package/lib/network/processor/index.js.map +1 -1
  79. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  80. package/lib/network/reqresp/protocols.js +6 -22
  81. package/lib/network/reqresp/protocols.js.map +1 -1
  82. package/lib/network/reqresp/rateLimit.js +2 -1
  83. package/lib/network/reqresp/rateLimit.js.map +1 -1
  84. package/lib/sync/range/range.d.ts.map +1 -1
  85. package/lib/sync/range/range.js +24 -1
  86. package/lib/sync/range/range.js.map +1 -1
  87. package/package.json +15 -15
  88. package/src/api/impl/beacon/blocks/index.ts +8 -4
  89. package/src/api/impl/validator/index.ts +44 -43
  90. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  91. package/src/chain/blocks/importBlock.ts +21 -18
  92. package/src/chain/blocks/index.ts +120 -23
  93. package/src/chain/blocks/types.ts +9 -0
  94. package/src/chain/blocks/utils/chainSegment.ts +10 -1
  95. package/src/chain/chain.ts +100 -61
  96. package/src/chain/emitter.ts +1 -1
  97. package/src/chain/errors/blockError.ts +3 -3
  98. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  99. package/src/chain/errors/gossipValidation.ts +1 -0
  100. package/src/chain/interface.ts +4 -4
  101. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  102. package/src/chain/prepareNextSlot.ts +16 -16
  103. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  104. package/src/chain/validation/block.ts +9 -66
  105. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  106. package/src/chain/validatorMonitor.ts +24 -7
  107. package/src/execution/engine/mock.ts +4 -1
  108. package/src/network/gossip/encoding.ts +0 -6
  109. package/src/network/gossip/topic.ts +3 -25
  110. package/src/network/processor/gossipHandlers.ts +6 -1
  111. package/src/network/processor/index.ts +276 -148
  112. package/src/network/reqresp/protocols.ts +7 -24
  113. package/src/network/reqresp/rateLimit.ts +2 -1
  114. 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}>();
@@ -508,19 +524,6 @@ export async function importBlock(
508
524
  this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch);
509
525
  this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch);
510
526
  }
511
- const finalizedCheckpoint = checkpointState.finalizedCheckpoint;
512
- const finalizedEpoch = finalizedCheckpoint.epoch;
513
- const preFinalizedEpoch = parentBlockSummary.finalizedEpoch;
514
- if (finalizedEpoch > preFinalizedEpoch) {
515
- this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
516
- block: toRootHex(finalizedCheckpoint.root),
517
- epoch: finalizedCheckpoint.epoch,
518
- state: toRootHex(checkpointState.hashTreeRoot()),
519
- executionOptimistic: false,
520
- });
521
- this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint));
522
- this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch);
523
- }
524
527
  }
525
528
  }
526
529
 
@@ -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
@@ -187,8 +209,19 @@ export async function processBlocks(
187
209
  const blockSlot = signedBlock.message.slot;
188
210
  const {state} = err.type;
189
211
  const forkTypes = this.config.getForkTypes(blockSlot);
190
- this.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `${blockSlot}_invalid_signature`);
191
- this.persistInvalidSszBytes("BeaconState", state.serialize(), `${state.slot}_invalid_signature`);
212
+ const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
213
+ this.persistInvalidSszValue(
214
+ forkTypes.SignedBeaconBlock,
215
+ signedBlock,
216
+ `${blockSlot}_invalid_signature`,
217
+ blockRootHex
218
+ );
219
+ this.persistInvalidSszBytes(
220
+ "BeaconState",
221
+ state.serialize(),
222
+ toRootHex(state.hashTreeRoot()),
223
+ `${state.slot}_invalid_signature`
224
+ );
192
225
  } else if (err.type.code === BlockErrorCode.INVALID_STATE_ROOT) {
193
226
  const {signedBlock} = err;
194
227
  const blockSlot = signedBlock.message.slot;
@@ -209,6 +242,70 @@ export async function processBlocks(
209
242
  }
210
243
  }
211
244
 
245
+ /**
246
+ * A peer can serve a block without its envelope, the block is then imported with a PENDING payload. Later batches
247
+ * carry the envelope but all their blocks are known, without importing it here every child building on the FULL
248
+ * variant fails with PARENT_PAYLOAD_UNKNOWN and range sync never progresses. If our head already descends from the
249
+ * block's EMPTY variant the chain did not build on the payload, importing it would only add a dead FULL leaf. As for
250
+ * orphaned payloads in a segment this only applies while the block attestations are not imported, a recent payload
251
+ * is imported anyway so a reorg building on it does not have to fetch it through unknown block sync.
252
+ */
253
+ async function importPayloadEnvelopesOfKnownBlocks(
254
+ this: BeaconChain,
255
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
256
+ opts: ImportBlockOpts
257
+ ): Promise<ProcessBlocksResult> {
258
+ if (payloadEnvelopes === null) {
259
+ return {orphaned: [], skipped: false};
260
+ }
261
+
262
+ const head = this.forkChoice.getHead();
263
+ const currentEpoch = this.clock.currentEpoch;
264
+ const payloadInputs: PayloadEnvelopeInput[] = [];
265
+ const orphaned: OrphanedPayloadEnvelope[] = [];
266
+ for (const payloadInput of payloadEnvelopes.values()) {
267
+ const {blockRootHex, slot} = payloadInput;
268
+ if (
269
+ !payloadInput.hasPayloadEnvelope() ||
270
+ !this.forkChoice.hasBlockHex(blockRootHex) ||
271
+ this.forkChoice.getBlockHexAndBlockHash(blockRootHex, payloadInput.getBlockHashHex()) !== null
272
+ ) {
273
+ continue;
274
+ }
275
+ if (
276
+ head.blockRoot !== blockRootHex &&
277
+ !importsBlockAttestations(opts, computeEpochAtSlot(slot), currentEpoch) &&
278
+ this.forkChoice.isDescendant(blockRootHex, PayloadStatus.EMPTY, head.blockRoot, head.payloadStatus)
279
+ ) {
280
+ // never validated, see processBlocks()
281
+ this.seenPayloadEnvelopeInputCache.prune(blockRootHex);
282
+ orphaned.push({slot, payloadEnvelopeInput: payloadInput});
283
+ continue;
284
+ }
285
+ payloadInputs.push(payloadInput);
286
+ }
287
+
288
+ // orphaned envelopes on this path are always skipped, range sync logs them with peer/client context
289
+ const result: ProcessBlocksResult = {orphaned, skipped: orphaned.length > 0};
290
+
291
+ if (payloadInputs.length === 0) {
292
+ return result;
293
+ }
294
+
295
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(payloadInputs, new AbortController().signal);
296
+ for (let i = 0; i < payloadInputs.length; i++) {
297
+ this.logger.debug("Importing payload envelope of known block", {
298
+ slot: payloadInputs[i].slot,
299
+ blockRoot: payloadInputs[i].blockRootHex,
300
+ });
301
+ await importExecutionPayload.call(this, payloadInputs[i], dataAvailabilityStatuses[i], {validSignature: false});
302
+ }
303
+ // the new FULL variants are not seen by the head cached from the last block import
304
+ this.recomputeForkChoiceHead(ForkchoiceCaller.importBlock);
305
+
306
+ return result;
307
+ }
308
+
212
309
  function getBlockOrPayloadError(e: unknown, block: SignedBeaconBlock): BlockError | PayloadError {
213
310
  if (e instanceof BlockError) {
214
311
  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> {
@@ -1271,59 +1271,65 @@ export class BeaconChain implements IBeaconChain {
1271
1271
  try {
1272
1272
  const prevHead = this.forkChoice.getHead();
1273
1273
  const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
1274
+ this.emitHeadEvents(prevHead, head);
1275
+ return head;
1276
+ } catch (e) {
1277
+ this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1278
+ throw e;
1279
+ } finally {
1280
+ timer?.();
1281
+ }
1282
+ }
1274
1283
 
1275
- const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1276
-
1277
- if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1278
- return head;
1279
- }
1284
+ /**
1285
+ * Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
1286
+ * can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
1287
+ */
1288
+ private emitHeadEvents(prevHead: ProtoBlock, head: ProtoBlock): void {
1289
+ const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1280
1290
 
1281
- try {
1282
- const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1283
- const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1284
- const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1285
- const executionOptimistic = isOptimisticBlock(head);
1286
-
1287
- if (headRootChanged) {
1288
- this.emitter.emit(routes.events.EventType.head, {
1289
- block: head.blockRoot,
1290
- epochTransition,
1291
- slot: head.slot,
1292
- state: head.stateRoot,
1293
- previousDutyDependentRoot,
1294
- currentDutyDependentRoot,
1295
- executionOptimistic,
1296
- });
1297
- }
1291
+ if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1292
+ return;
1293
+ }
1298
1294
 
1299
- this.emitter.emit(routes.events.EventType.headV2, {
1300
- version: this.config.getForkName(head.slot),
1301
- data: {
1302
- slot: head.slot,
1303
- block: head.blockRoot,
1304
- state: head.stateRoot,
1305
- payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1306
- epochTransition,
1307
- currentEpochDependentRoot: previousDutyDependentRoot,
1308
- nextEpochDependentRoot: currentDutyDependentRoot,
1309
- executionOptimistic,
1310
- },
1295
+ try {
1296
+ const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1297
+ const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1298
+ const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1299
+ const executionOptimistic = isOptimisticBlock(head);
1300
+
1301
+ if (headRootChanged) {
1302
+ this.emitter.emit(routes.events.EventType.head, {
1303
+ block: head.blockRoot,
1304
+ epochTransition,
1305
+ slot: head.slot,
1306
+ state: head.stateRoot,
1307
+ previousDutyDependentRoot,
1308
+ currentDutyDependentRoot,
1309
+ executionOptimistic,
1311
1310
  });
1312
- } catch (e) {
1313
- // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1314
- this.logger.debug(
1315
- "Error emitting head/head_v2 event",
1316
- {slot: head.slot, root: head.blockRoot, headRootChanged},
1317
- e as Error
1318
- );
1319
1311
  }
1320
1312
 
1321
- return head;
1313
+ this.emitter.emit(routes.events.EventType.headV2, {
1314
+ version: this.config.getForkName(head.slot),
1315
+ data: {
1316
+ slot: head.slot,
1317
+ block: head.blockRoot,
1318
+ state: head.stateRoot,
1319
+ payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1320
+ epochTransition,
1321
+ currentEpochDependentRoot: previousDutyDependentRoot,
1322
+ nextEpochDependentRoot: currentDutyDependentRoot,
1323
+ executionOptimistic,
1324
+ },
1325
+ });
1322
1326
  } catch (e) {
1323
- this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1324
- throw e;
1325
- } finally {
1326
- timer?.();
1327
+ // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1328
+ this.logger.debug(
1329
+ "Error emitting head/head_v2 event",
1330
+ {slot: head.slot, root: head.blockRoot, headRootChanged},
1331
+ e as Error
1332
+ );
1327
1333
  }
1328
1334
  }
1329
1335
 
@@ -1381,10 +1387,15 @@ export class BeaconChain implements IBeaconChain {
1381
1387
  const slot = data.slot;
1382
1388
  if (isBlindedBeaconBlock(data)) {
1383
1389
  const sszType = this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock;
1384
- void this.persistSszObject("BlindedBeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix);
1390
+ void this.persistSszObject(
1391
+ "BlindedBeaconBlock",
1392
+ sszType.serialize(data),
1393
+ toRootHex(sszType.hashTreeRoot(data)),
1394
+ suffix
1395
+ );
1385
1396
  } else {
1386
1397
  const sszType = this.config.getForkTypes(slot).BeaconBlock;
1387
- void this.persistSszObject("BeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix);
1398
+ void this.persistSszObject("BeaconBlock", sszType.serialize(data), toRootHex(sszType.hashTreeRoot(data)), suffix);
1388
1399
  }
1389
1400
  }
1390
1401
 
@@ -1405,33 +1416,39 @@ export class BeaconChain implements IBeaconChain {
1405
1416
  this.persistSszObject(
1406
1417
  `SignedBeaconBlock_slot_${blockSlot}`,
1407
1418
  blockType.serialize(block),
1408
- blockType.hashTreeRoot(block),
1419
+ toRootHex(this.config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message)),
1409
1420
  `${logStr}_block`
1410
1421
  ),
1411
1422
  this.persistSszObject(
1412
1423
  `preState_slot_${preState.slot}_BeaconState`,
1413
1424
  preState.serialize(),
1414
- preState.hashTreeRoot(),
1425
+ toRootHex(preState.hashTreeRoot()),
1415
1426
  `${logStr}_pre_state`
1416
1427
  ),
1417
1428
  this.persistSszObject(
1418
1429
  `postState_slot_${postState.slot}_BeaconState`,
1419
1430
  postState.serialize(),
1420
- postState.hashTreeRoot(),
1431
+ toRootHex(postStateRoot),
1421
1432
  `${logStr}_post_state`
1422
1433
  ),
1423
1434
  ]);
1424
1435
  }
1425
1436
 
1426
- persistInvalidSszValue<T>(type: Type<T>, sszObject: T, suffix?: string): void {
1437
+ persistInvalidSszValue<T>(type: Type<T>, sszObject: T, suffix?: string, rootHex?: RootHex): void {
1427
1438
  if (this.opts.persistInvalidSszObjects) {
1428
- void this.persistSszObject(type.typeName, type.serialize(sszObject), type.hashTreeRoot(sszObject), suffix);
1439
+ void this.persistSszObject(
1440
+ type.typeName,
1441
+ type.serialize(sszObject),
1442
+ // in SignedBeaconBlock case, we want to use BeaconBlock root instead
1443
+ rootHex ?? toRootHex(type.hashTreeRoot(sszObject)),
1444
+ suffix
1445
+ );
1429
1446
  }
1430
1447
  }
1431
1448
 
1432
- persistInvalidSszBytes(typeName: string, sszBytes: Uint8Array, suffix?: string): void {
1449
+ persistInvalidSszBytes(typeName: string, sszBytes: Uint8Array, rootHex: RootHex, suffix?: string): void {
1433
1450
  if (this.opts.persistInvalidSszObjects) {
1434
- void this.persistSszObject(typeName, sszBytes, sszBytes, suffix);
1451
+ void this.persistSszObject(typeName, sszBytes, rootHex, suffix);
1435
1452
  }
1436
1453
  }
1437
1454
 
@@ -1562,14 +1579,14 @@ export class BeaconChain implements IBeaconChain {
1562
1579
  return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true};
1563
1580
  }
1564
1581
 
1565
- private async persistSszObject(prefix: string, bytes: Uint8Array, root: Uint8Array, logStr?: string): Promise<void> {
1582
+ private async persistSszObject(prefix: string, bytes: Uint8Array, rootHex: RootHex, logStr?: string): Promise<void> {
1566
1583
  const now = new Date();
1567
1584
  // yyyy-MM-dd
1568
1585
  const dateStr = now.toISOString().split("T")[0];
1569
1586
 
1570
1587
  // by default store to lodestar_archive of current dir
1571
1588
  const dirpath = path.join(this.opts.persistInvalidSszObjectsDir ?? "invalid_ssz_objects", dateStr);
1572
- const filepath = path.join(dirpath, `${prefix}_${toRootHex(root)}.ssz`);
1589
+ const filepath = path.join(dirpath, `${prefix}_${rootHex}.ssz`);
1573
1590
 
1574
1591
  await ensureDir(dirpath);
1575
1592
 
@@ -1612,7 +1629,10 @@ export class BeaconChain implements IBeaconChain {
1612
1629
  this.processShutdownCallback(this.forkChoice.irrecoverableError);
1613
1630
  }
1614
1631
 
1632
+ // updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
1633
+ const prevHead = this.forkChoice.getHead();
1615
1634
  this.forkChoice.updateTime(slot);
1635
+ this.emitHeadEvents(prevHead, this.forkChoice.getHead());
1616
1636
  this.metrics?.clockSlot.set(slot);
1617
1637
 
1618
1638
  this.attestationPool.prune(slot);
@@ -1670,6 +1690,25 @@ export class BeaconChain implements IBeaconChain {
1670
1690
 
1671
1691
  private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
1672
1692
  this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex});
1693
+ this.metrics?.finalizedEpoch.set(cp.epoch);
1694
+ const finalizedBlock = this.forkChoice.getBlockHexDefaultStatus(cp.rootHex);
1695
+ if (finalizedBlock) {
1696
+ // The callback runs synchronously inside fork choice checkpoint updates, defer writing to subscribers
1697
+ callInNextEventLoop(() => {
1698
+ this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
1699
+ block: cp.rootHex,
1700
+ epoch: cp.epoch,
1701
+ state: finalizedBlock.stateRoot,
1702
+ executionOptimistic: isOptimisticBlock(finalizedBlock),
1703
+ });
1704
+ });
1705
+ } else {
1706
+ this.logger.debug("Finalized block not found in fork choice, skip finalized_checkpoint event", {
1707
+ epoch: cp.epoch,
1708
+ root: cp.rootHex,
1709
+ });
1710
+ }
1711
+
1673
1712
  const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
1674
1713
  this.seenBlockProposers.prune(finalizedSlot);
1675
1714
 
@@ -33,7 +33,7 @@ export enum ChainEvent {
33
33
  /**
34
34
  * This event signals that the fork choice store has been updated.
35
35
  *
36
- * This event is guaranteed to be triggered whenever the fork choice justified checkpoint is updated. This is in response to a newly processed block.
36
+ * This event is guaranteed to be triggered whenever the fork choice finalized checkpoint is updated. This is either in response to a newly processed block or a new clock tick.
37
37
  */
38
38
  forkChoiceFinalized = "forkChoice:finalized",
39
39
  /**
@@ -78,8 +78,8 @@ export enum BlockErrorCode {
78
78
  TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
79
79
  /** Bid parent block root does not match block parent root */
80
80
  BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
81
- /** A block body operation or parent execution request list exceeds its per-block limit */
82
- TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS",
81
+ /** A block contains deposits where none are allowed */
82
+ NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
83
83
  /** The parent block's execution payload has been verified as invalid */
84
84
  PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
85
85
  /**
@@ -153,7 +153,7 @@ export type BlockErrorType =
153
153
  | {code: BlockErrorCode.DATA_UNAVAILABLE}
154
154
  | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
155
155
  | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
156
- | {code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number}
156
+ | {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: number}
157
157
  | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
158
158
  | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}
159
159
  | {code: BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS; parentBlockHash: RootHex; expectedBlockHash: RootHex};
@@ -12,8 +12,6 @@ export enum ExecutionPayloadEnvelopeErrorCode {
12
12
  BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
13
13
  BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
14
14
  EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
15
- EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED",
16
- WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED",
17
15
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
18
16
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
19
17
  }
@@ -44,13 +42,6 @@ export type ExecutionPayloadEnvelopeErrorType =
44
42
  envelopeRequestsRoot: RootHex;
45
43
  bidRequestsRoot: RootHex;
46
44
  }
47
- | {
48
- code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED;
49
- name: string;
50
- count: number;
51
- limit: number;
52
- }
53
- | {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number}
54
45
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
55
46
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
56
47