@lodestar/beacon-node 1.44.0-dev.435241b764 → 1.44.0-dev.4ac6f45830

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 (66) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +13 -5
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/config/constants.d.ts +1 -0
  5. package/lib/api/impl/config/constants.d.ts.map +1 -1
  6. package/lib/api/impl/config/constants.js +2 -1
  7. package/lib/api/impl/config/constants.js.map +1 -1
  8. package/lib/chain/archiveStore/archiveStore.d.ts +0 -1
  9. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  10. package/lib/chain/archiveStore/archiveStore.js +0 -4
  11. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  12. package/lib/chain/regen/interface.d.ts +0 -1
  13. package/lib/chain/regen/interface.d.ts.map +1 -1
  14. package/lib/chain/regen/queued.d.ts +0 -1
  15. package/lib/chain/regen/queued.d.ts.map +1 -1
  16. package/lib/chain/regen/queued.js +0 -4
  17. package/lib/chain/regen/queued.js.map +1 -1
  18. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +0 -5
  19. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  20. package/lib/chain/stateCache/fifoBlockStateCache.js +0 -5
  21. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  22. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +1 -4
  23. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  24. package/lib/chain/stateCache/persistentCheckpointsCache.js +5 -2
  25. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  26. package/lib/chain/stateCache/types.d.ts +0 -2
  27. package/lib/chain/stateCache/types.d.ts.map +1 -1
  28. package/lib/chain/stateCache/types.js.map +1 -1
  29. package/lib/network/gossip/topic.d.ts +749 -2
  30. package/lib/network/gossip/topic.d.ts.map +1 -1
  31. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  32. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +9 -5
  33. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  34. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  35. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +8 -3
  36. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  37. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +1 -1
  38. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  39. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts +2 -1
  40. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  41. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +13 -3
  42. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  43. package/lib/network/reqresp/handlers/index.js +2 -2
  44. package/lib/network/reqresp/handlers/index.js.map +1 -1
  45. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  46. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +14 -3
  47. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  48. package/lib/util/dataColumns.d.ts.map +1 -1
  49. package/lib/util/dataColumns.js +16 -11
  50. package/lib/util/dataColumns.js.map +1 -1
  51. package/package.json +14 -16
  52. package/src/api/impl/beacon/blocks/index.ts +13 -5
  53. package/src/api/impl/config/constants.ts +2 -0
  54. package/src/chain/archiveStore/archiveStore.ts +0 -5
  55. package/src/chain/regen/interface.ts +0 -1
  56. package/src/chain/regen/queued.ts +0 -5
  57. package/src/chain/stateCache/fifoBlockStateCache.ts +0 -6
  58. package/src/chain/stateCache/persistentCheckpointsCache.ts +6 -2
  59. package/src/chain/stateCache/types.ts +0 -2
  60. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +12 -5
  61. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +11 -3
  62. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +1 -1
  63. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +19 -3
  64. package/src/network/reqresp/handlers/index.ts +2 -2
  65. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +13 -3
  66. package/src/util/dataColumns.ts +17 -12
@@ -143,11 +143,6 @@ export class QueuedStateRegenerator implements IStateRegenerator {
143
143
  this.blockStateCache.prune(headStateRoot);
144
144
  }
145
145
 
146
- pruneOnFinalized(finalizedEpoch: number): void {
147
- this.checkpointStateCache.pruneFinalized(finalizedEpoch);
148
- this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch);
149
- }
150
-
151
146
  processState(blockRootHex: RootHex, postState: IBeaconStateView): void {
152
147
  this.blockStateCache.add(postState);
153
148
  this.checkpointStateCache.processState(blockRootHex, postState).catch((e) => {
@@ -167,12 +167,6 @@ export class FIFOBlockStateCache implements BlockStateCache {
167
167
  }
168
168
  }
169
169
 
170
- /**
171
- * No need for this implementation
172
- * This is only to conform to the old api
173
- */
174
- deleteAllBeforeEpoch(): void {}
175
-
176
170
  /**
177
171
  * ONLY FOR DEBUGGING PURPOSES. For lodestar debug API.
178
172
  */
@@ -414,11 +414,12 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
414
414
 
415
415
  /**
416
416
  * Prune all checkpoint states before the provided finalized epoch.
417
+ * Driven sequentially from processState() so it never interleaves with persist.
417
418
  */
418
- pruneFinalized(finalizedEpoch: Epoch): void {
419
+ private async pruneFinalized(finalizedEpoch: Epoch): Promise<void> {
419
420
  for (const epoch of this.epochIndex.keys()) {
420
421
  if (epoch < finalizedEpoch) {
421
- this.deleteAllEpochItems(epoch).catch((e) =>
422
+ await this.deleteAllEpochItems(epoch).catch((e) =>
422
423
  this.logger.debug("Error delete all epoch items", {epoch, finalizedEpoch}, e as Error)
423
424
  );
424
425
  }
@@ -476,6 +477,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
476
477
  * As of Mar 2024, it takes <=350ms to persist a holesky state on fast server
477
478
  */
478
479
  async processState(blockRootHex: RootHex, state: IBeaconStateView): Promise<number> {
480
+ // prune finalized in the same flow so a finalized cp state is pruned, never persisted
481
+ await this.pruneFinalized(state.finalizedCheckpoint.epoch);
482
+
479
483
  let persistCount = 0;
480
484
  // it's important to sort the epochs in ascending order, in case of big reorg we always want to keep the most recent checkpoint states
481
485
  const sortedEpochs = Array.from(this.epochIndex.keys()).sort((a, b) => a - b);
@@ -30,7 +30,6 @@ export interface BlockStateCache {
30
30
  clear(): void;
31
31
  size: number;
32
32
  prune(headStateRootHex: RootHex): void;
33
- deleteAllBeforeEpoch(finalizedEpoch: Epoch): void;
34
33
  dumpSummary(): routes.lodestar.StateCacheItem[];
35
34
  /** Expose beacon states stored in cache. Use with caution */
36
35
  getStates(): IterableIterator<IBeaconStateView>;
@@ -67,7 +66,6 @@ export interface CheckpointStateCache {
67
66
  getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise<IBeaconStateView | null>;
68
67
  updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null;
69
68
  prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void;
70
- pruneFinalized(finalizedEpoch: Epoch): void;
71
69
  processState(blockRootHex: RootHex, state: IBeaconStateView): Promise<number>;
72
70
  clear(): void;
73
71
  dumpSummary(): routes.lodestar.StateCacheItem[];
@@ -1,6 +1,6 @@
1
1
  import {PeerId} from "@libp2p/interface";
2
2
  import {BeaconConfig} from "@lodestar/config";
3
- import {GENESIS_SLOT, isForkPostDeneb, isForkPostFulu} from "@lodestar/params";
3
+ import {GENESIS_SLOT, isForkPostDeneb} from "@lodestar/params";
4
4
  import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
5
  import {computeEpochAtSlot} from "@lodestar/state-transition";
6
6
  import {deneb, phase0} from "@lodestar/types";
@@ -29,13 +29,20 @@ export async function* onBeaconBlocksByRange(
29
29
  // starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
30
30
  const archiveMaxSlot = finalizedSlot;
31
31
 
32
- const forkName = chain.config.getForkName(startSlot);
33
- if (isForkPostFulu(forkName) && startSlot < chain.earliestAvailableSlot) {
34
- chain.logger.verbose("Peer did not respect earliestAvailableSlot for BeaconBlocksByRange", {
32
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
33
+ // Throw only when the entire requested range is below earliestAvailableSlot.
34
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
35
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for BeaconBlocksByRange", {
35
36
  peer: prettyPrintPeerId(peerId),
36
37
  client: peerClient,
38
+ startSlot,
39
+ count,
40
+ earliestAvailableSlot: chain.earliestAvailableSlot,
37
41
  });
38
- return;
42
+ throw new ResponseError(
43
+ RespStatus.RESOURCE_UNAVAILABLE,
44
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
45
+ );
39
46
  }
40
47
 
41
48
  // Finalized range of blocks
@@ -34,12 +34,20 @@ export async function* onDataColumnSidecarsByRange(
34
34
  return;
35
35
  }
36
36
 
37
- if (startSlot < chain.earliestAvailableSlot) {
38
- chain.logger.verbose("Peer did not respect earliestAvailableSlot for DataColumnSidecarsByRange", {
37
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
38
+ // Throw only when the entire requested range is below earliestAvailableSlot.
39
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
40
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for DataColumnSidecarsByRange", {
39
41
  peer: prettyPrintPeerId(peerId),
40
42
  client: peerClient,
43
+ startSlot,
44
+ count,
45
+ earliestAvailableSlot: chain.earliestAvailableSlot,
41
46
  });
42
- return;
47
+ throw new ResponseError(
48
+ RespStatus.RESOURCE_UNAVAILABLE,
49
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
50
+ );
43
51
  }
44
52
 
45
53
  const finalized = db.dataColumnSidecarArchive;
@@ -30,7 +30,7 @@ export async function* onDataColumnSidecarsByRoot(
30
30
  const {blockRoot, columns: requestedColumns} = dataColumnsByRootIdentifier;
31
31
  const availableColumns = validateRequestedDataColumns(chain, requestedColumns);
32
32
  if (availableColumns.length === 0) {
33
- return;
33
+ continue;
34
34
  }
35
35
 
36
36
  const blockRootHex = toRootHex(blockRoot);
@@ -1,3 +1,4 @@
1
+ import {PeerId} from "@libp2p/interface";
1
2
  import {ChainConfig} from "@lodestar/config";
2
3
  import {PayloadStatus} from "@lodestar/fork-choice";
3
4
  import {GENESIS_SLOT} from "@lodestar/params";
@@ -6,17 +7,32 @@ import {computeEpochAtSlot} from "@lodestar/state-transition";
6
7
  import {gloas} from "@lodestar/types";
7
8
  import {IBeaconChain} from "../../../chain/index.js";
8
9
  import {IBeaconDb} from "../../../db/index.js";
10
+ import {prettyPrintPeerId} from "../../util.js";
9
11
 
10
12
  export async function* onExecutionPayloadEnvelopesByRange(
11
13
  request: gloas.ExecutionPayloadEnvelopesByRangeRequest,
12
14
  chain: IBeaconChain,
13
- db: IBeaconDb
15
+ db: IBeaconDb,
16
+ peerId: PeerId,
17
+ peerClient: string
14
18
  ): AsyncIterable<ResponseOutgoing> {
15
19
  const {startSlot, count} = validateExecutionPayloadEnvelopesByRangeRequest(chain.config, request);
16
20
  const endSlot = startSlot + count;
17
21
 
18
- if (startSlot < chain.earliestAvailableSlot) {
19
- return;
22
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
23
+ // Throw only when the entire requested range is below earliestAvailableSlot.
24
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
25
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for ExecutionPayloadEnvelopesByRange", {
26
+ peer: prettyPrintPeerId(peerId),
27
+ client: peerClient,
28
+ startSlot,
29
+ count,
30
+ earliestAvailableSlot: chain.earliestAvailableSlot,
31
+ });
32
+ throw new ResponseError(
33
+ RespStatus.RESOURCE_UNAVAILABLE,
34
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
35
+ );
20
36
  }
21
37
 
22
38
  const finalized = db.executionPayloadEnvelopeArchive;
@@ -74,9 +74,9 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
74
74
  const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data);
75
75
  return onExecutionPayloadEnvelopesByRoot(body, chain, db, peerId, peerClient);
76
76
  },
77
- [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req) => {
77
+ [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => {
78
78
  const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
79
- return onExecutionPayloadEnvelopesByRange(body, chain, db);
79
+ return onExecutionPayloadEnvelopesByRange(body, chain, db, peerId, peerClient);
80
80
  },
81
81
 
82
82
  [ReqRespMethod.LightClientBootstrap]: (req) => {
@@ -79,9 +79,19 @@ export function validateRequestedDataColumns(chain: IBeaconChain, requestedColum
79
79
  throw new ResponseError(RespStatus.INVALID_REQUEST, "dataColumnSidecar requested without column indices");
80
80
  }
81
81
 
82
- const custodyColumns = chain.custodyConfig.custodyColumns;
83
- const availableColumns = requestedColumns.filter((c) => custodyColumns.includes(c));
84
- const missingColumns = requestedColumns.filter((c) => !custodyColumns.includes(c));
82
+ const {custodyColumns, custodyColumnsIndex} = chain.custodyConfig;
83
+ const availableColumns: ColumnIndex[] = [];
84
+ const missingColumns: ColumnIndex[] = [];
85
+ for (const c of requestedColumns) {
86
+ // `c` is peer-controlled and SSZ-deserialized as `uint64`, so it may exceed
87
+ // `NUMBER_OF_COLUMNS - 1`; `Uint8Array` returns `undefined` for OOB reads,
88
+ // and `undefined !== 0` would silently classify OOB indices as custodied.
89
+ if ((custodyColumnsIndex[c] ?? 0) !== 0) {
90
+ availableColumns.push(c);
91
+ } else {
92
+ missingColumns.push(c);
93
+ }
94
+ }
85
95
 
86
96
  if (missingColumns.length > 0) {
87
97
  chain.logger.verbose("Requested dataColumnSidecar for non-custody columns", {
@@ -470,15 +470,17 @@ export async function recoverDataColumnSidecars(
470
470
  return DataColumnReconstructionCode.SuccessLate;
471
471
  }
472
472
 
473
- // Once the node obtains a column through reconstruction,
474
- // the node MUST expose the new column as if it had received it over the network.
475
- // If the node is subscribed to the subnet corresponding to the column,
476
- // it MUST send the reconstructed DataColumnSidecar to its topic mesh neighbors.
477
- // If instead the node is not subscribed to the corresponding subnet,
478
- // it SHOULD still expose the availability of the DataColumnSidecar as part of the gossip emission process.
479
- // After exposing the reconstructed DataColumnSidecar to the network,
480
- // the node MAY delete the DataColumnSidecar if it is not part of the node's custody requirement.
481
- const sidecarsToPublish = [];
473
+ // Per consensus-specs PR #4657, only publish reconstructed columns the node is
474
+ // subscribed to (custody + sampling). Eagerly cross-seeding non-subscribed
475
+ // columns floods the network with duplicates because the sender has no
476
+ // visibility into which peers already saw the message via the topic mesh.
477
+ // This matches the getBlobsV2 path in `getDataColumnSidecarsFromExecution` and
478
+ // aligns with Lighthouse/Prysm. Capture missing sampled indices before adding
479
+ // any reconstructed columns so they are not filtered out by the subsequent
480
+ // `addColumn` calls.
481
+ const missingSampledColumns = new Set(input.getMissingSampledColumnMeta().missing);
482
+ const sidecarsReconstructed: DataColumnSidecar[] = [];
483
+ const sidecarsToPublish: DataColumnSidecar[] = [];
482
484
  for (const columnSidecar of fullSidecars) {
483
485
  if (!input.hasColumn(columnSidecar.index)) {
484
486
  if (input instanceof PayloadEnvelopeInput) {
@@ -501,11 +503,14 @@ export async function recoverDataColumnSidecars(
501
503
  source: BlockInputSource.recovery,
502
504
  });
503
505
  }
504
- sidecarsToPublish.push(columnSidecar);
506
+ sidecarsReconstructed.push(columnSidecar);
507
+ if (missingSampledColumns.has(columnSidecar.index)) {
508
+ sidecarsToPublish.push(columnSidecar);
509
+ }
505
510
  }
506
511
  }
507
- metrics?.peerDas.reconstructedColumns.inc(sidecarsToPublish.length);
508
- metrics?.dataColumns.bySource.inc({source: BlockInputSource.recovery}, sidecarsToPublish.length);
512
+ metrics?.peerDas.reconstructedColumns.inc(sidecarsReconstructed.length);
513
+ metrics?.dataColumns.bySource.inc({source: BlockInputSource.recovery}, sidecarsReconstructed.length);
509
514
  emitter.emit(ChainEvent.publishDataColumns, sidecarsToPublish);
510
515
  // TODO: Can we record dataColumns.sentPeersPerSubnet metric somehow
511
516
  return DataColumnReconstructionCode.SuccessResolved;