@lodestar/beacon-node 1.48.0-dev.0dbea0de1b → 1.48.0-dev.27e5bfcdb6

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 (31) hide show
  1. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  2. package/lib/chain/archiveStore/archiveStore.js +4 -1
  3. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  4. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +9 -1
  5. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  6. package/lib/chain/archiveStore/utils/archiveBlocks.js +37 -4
  7. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  8. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  9. package/lib/chain/blocks/importBlock.js +3 -2
  10. package/lib/chain/blocks/importBlock.js.map +1 -1
  11. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  12. package/lib/chain/forkChoice/index.js +2 -0
  13. package/lib/chain/forkChoice/index.js.map +1 -1
  14. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  15. package/lib/chain/validation/payloadAttestationMessage.js +5 -6
  16. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  17. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  18. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  19. package/lib/metrics/metrics/lodestar.js +5 -0
  20. package/lib/metrics/metrics/lodestar.js.map +1 -1
  21. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  22. package/lib/network/processor/gossipHandlers.js +10 -6
  23. package/lib/network/processor/gossipHandlers.js.map +1 -1
  24. package/package.json +14 -14
  25. package/src/chain/archiveStore/archiveStore.ts +6 -0
  26. package/src/chain/archiveStore/utils/archiveBlocks.ts +39 -3
  27. package/src/chain/blocks/importBlock.ts +4 -2
  28. package/src/chain/forkChoice/index.ts +2 -0
  29. package/src/chain/validation/payloadAttestationMessage.ts +8 -9
  30. package/src/metrics/metrics/lodestar.ts +6 -0
  31. package/src/network/processor/gossipHandlers.ts +15 -9
@@ -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
 
@@ -85,7 +85,7 @@ export async function importBlock(
85
85
  const currentEpoch = computeEpochAtSlot(currentSlot);
86
86
  const blockEpoch = computeEpochAtSlot(blockSlot);
87
87
  const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
88
- const blockDelaySec =
88
+ const receiveDelaySec =
89
89
  fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime);
90
90
  const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
91
91
  const fork = this.config.getForkSeq(blockSlot);
@@ -122,10 +122,12 @@ export async function importBlock(
122
122
  executionStatus = parentBlock.executionStatus;
123
123
  }
124
124
 
125
+ const importDelaySec = this.clock.secFromSlot(blockSlot);
125
126
  const blockSummary = this.forkChoice.onBlock(
126
127
  block.message,
127
128
  postState,
128
- blockDelaySec,
129
+ receiveDelaySec,
130
+ importDelaySec,
129
131
  currentSlot,
130
132
  executionStatus,
131
133
  dataAvailabilityStatus
@@ -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,
@@ -7,7 +7,6 @@ import {RootHex, gloas, ssz} from "@lodestar/types";
7
7
  import {toRootHex} from "@lodestar/utils";
8
8
  import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js";
9
9
  import {IBeaconChain} from "../index.js";
10
- import {RegenCaller} from "../regen/index.js";
11
10
 
12
11
  export type PayloadAttestationValidationResult = {
13
12
  attDataRootHex: RootHex;
@@ -82,15 +81,15 @@ async function validatePayloadAttestationMessage(
82
81
  // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice,
83
82
  // it is possible that the block didn't pass the validation
84
83
 
85
- // Use the referenced block's branch state for the PTC committee check
86
- const state = await chain.regen
87
- .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage)
88
- .catch(() => {
89
- throw new PayloadAttestationError(GossipAction.IGNORE, {
90
- code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
91
- blockRoot: toRootHex(data.beaconBlockRoot),
92
- });
84
+ // block.slot === data.slot is enforced above, so use the block's post-state directly to avoid
85
+ // getting through regen queue
86
+ const state = chain.regen.getStateSync(block.stateRoot);
87
+ if (state == null) {
88
+ throw new PayloadAttestationError(GossipAction.IGNORE, {
89
+ code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
90
+ blockRoot: toRootHex(data.beaconBlockRoot),
93
91
  });
92
+ }
94
93
 
95
94
  if (!isStatePostGloas(state)) {
96
95
  throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`);
@@ -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",
@@ -1268,16 +1268,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1268
1268
  const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1269
1269
  metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1270
1270
 
1271
- try {
1272
- const insertOutcome = chain.payloadAttestationPool.add(
1273
- payloadAttestationMessage,
1274
- validationResult.attDataRootHex,
1275
- validationResult.validatorCommitteeIndices
1276
- );
1277
- metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1278
- } catch (e) {
1279
- logger.error("Error adding to payloadAttestation pool", {}, e as Error);
1271
+ const nextSlotProposer = chain.getHeadState().getBeaconProposer(payloadAttestationMessage.data.slot + 1);
1272
+ const isNextSlotProposer = chain.beaconProposerCache.get(nextSlotProposer) !== undefined;
1273
+
1274
+ if (isNextSlotProposer) {
1275
+ try {
1276
+ const insertOutcome = chain.payloadAttestationPool.add(
1277
+ payloadAttestationMessage,
1278
+ validationResult.attDataRootHex,
1279
+ validationResult.validatorCommitteeIndices
1280
+ );
1281
+ metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1282
+ } catch (e) {
1283
+ logger.debug("Error adding to payloadAttestation pool", {}, e as Error);
1284
+ }
1280
1285
  }
1286
+
1281
1287
  chain.forkChoice.notifyPtcMessages(
1282
1288
  toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1283
1289
  payloadAttestationMessage.data.slot,