@lodestar/beacon-node 1.40.0-dev.1020f27ca9 → 1.40.0-dev.13ab628623

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 (101) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +8 -2
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/lodestar/index.d.ts.map +1 -1
  5. package/lib/api/impl/lodestar/index.js +14 -0
  6. package/lib/api/impl/lodestar/index.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js.map +1 -1
  9. package/lib/chain/blocks/blockInput/blockInput.d.ts +28 -0
  10. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  11. package/lib/chain/blocks/blockInput/blockInput.js +36 -1
  12. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  13. package/lib/chain/blocks/importBlock.js +1 -1
  14. package/lib/chain/blocks/importBlock.js.map +1 -1
  15. package/lib/chain/blocks/writeBlockInputToDb.d.ts.map +1 -1
  16. package/lib/chain/blocks/writeBlockInputToDb.js +8 -0
  17. package/lib/chain/blocks/writeBlockInputToDb.js.map +1 -1
  18. package/lib/chain/chain.d.ts +1 -1
  19. package/lib/chain/chain.d.ts.map +1 -1
  20. package/lib/chain/chain.js +12 -25
  21. package/lib/chain/chain.js.map +1 -1
  22. package/lib/chain/options.d.ts +0 -1
  23. package/lib/chain/options.d.ts.map +1 -1
  24. package/lib/chain/options.js +0 -1
  25. package/lib/chain/options.js.map +1 -1
  26. package/lib/chain/regen/interface.d.ts +1 -1
  27. package/lib/chain/regen/queued.d.ts +1 -1
  28. package/lib/chain/regen/queued.d.ts.map +1 -1
  29. package/lib/chain/regen/queued.js.map +1 -1
  30. package/lib/chain/stateCache/index.d.ts +0 -2
  31. package/lib/chain/stateCache/index.d.ts.map +1 -1
  32. package/lib/chain/stateCache/index.js +0 -2
  33. package/lib/chain/stateCache/index.js.map +1 -1
  34. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +2 -1
  35. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  36. package/lib/chain/stateCache/persistentCheckpointsCache.js +3 -0
  37. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  38. package/lib/chain/validation/block.d.ts.map +1 -1
  39. package/lib/chain/validation/block.js +1 -2
  40. package/lib/chain/validation/block.js.map +1 -1
  41. package/lib/network/core/networkCore.d.ts +3 -0
  42. package/lib/network/core/networkCore.d.ts.map +1 -1
  43. package/lib/network/core/networkCore.js +9 -0
  44. package/lib/network/core/networkCore.js.map +1 -1
  45. package/lib/network/core/networkCoreWorker.js +3 -0
  46. package/lib/network/core/networkCoreWorker.js.map +1 -1
  47. package/lib/network/core/networkCoreWorkerHandler.d.ts +3 -0
  48. package/lib/network/core/networkCoreWorkerHandler.d.ts.map +1 -1
  49. package/lib/network/core/networkCoreWorkerHandler.js +9 -0
  50. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  51. package/lib/network/core/types.d.ts +3 -0
  52. package/lib/network/core/types.d.ts.map +1 -1
  53. package/lib/network/gossip/gossipsub.d.ts +34 -0
  54. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  55. package/lib/network/gossip/gossipsub.js +123 -0
  56. package/lib/network/gossip/gossipsub.js.map +1 -1
  57. package/lib/network/network.d.ts +3 -0
  58. package/lib/network/network.d.ts.map +1 -1
  59. package/lib/network/network.js +9 -0
  60. package/lib/network/network.js.map +1 -1
  61. package/lib/network/options.d.ts +6 -0
  62. package/lib/network/options.d.ts.map +1 -1
  63. package/lib/network/options.js.map +1 -1
  64. package/lib/network/peers/peerManager.d.ts.map +1 -1
  65. package/lib/network/peers/peerManager.js +9 -0
  66. package/lib/network/peers/peerManager.js.map +1 -1
  67. package/lib/network/processor/gossipHandlers.js +1 -1
  68. package/lib/network/processor/gossipHandlers.js.map +1 -1
  69. package/package.json +15 -15
  70. package/src/api/impl/beacon/blocks/index.ts +22 -12
  71. package/src/api/impl/lodestar/index.ts +17 -0
  72. package/src/api/impl/validator/index.ts +2 -1
  73. package/src/chain/blocks/blockInput/blockInput.ts +45 -2
  74. package/src/chain/blocks/importBlock.ts +1 -1
  75. package/src/chain/blocks/writeBlockInputToDb.ts +9 -0
  76. package/src/chain/chain.ts +17 -29
  77. package/src/chain/options.ts +0 -2
  78. package/src/chain/regen/interface.ts +1 -1
  79. package/src/chain/regen/queued.ts +1 -2
  80. package/src/chain/stateCache/index.ts +0 -2
  81. package/src/chain/stateCache/persistentCheckpointsCache.ts +6 -2
  82. package/src/chain/validation/block.ts +1 -2
  83. package/src/network/core/networkCore.ts +12 -0
  84. package/src/network/core/networkCoreWorker.ts +3 -0
  85. package/src/network/core/networkCoreWorkerHandler.ts +9 -0
  86. package/src/network/core/types.ts +6 -0
  87. package/src/network/gossip/gossipsub.ts +147 -1
  88. package/src/network/network.ts +12 -0
  89. package/src/network/options.ts +6 -0
  90. package/src/network/peers/peerManager.ts +11 -0
  91. package/src/network/processor/gossipHandlers.ts +1 -1
  92. package/lib/chain/stateCache/blockStateCacheImpl.d.ts +0 -54
  93. package/lib/chain/stateCache/blockStateCacheImpl.d.ts.map +0 -1
  94. package/lib/chain/stateCache/blockStateCacheImpl.js +0 -130
  95. package/lib/chain/stateCache/blockStateCacheImpl.js.map +0 -1
  96. package/lib/chain/stateCache/inMemoryCheckpointsCache.d.ts +0 -60
  97. package/lib/chain/stateCache/inMemoryCheckpointsCache.d.ts.map +0 -1
  98. package/lib/chain/stateCache/inMemoryCheckpointsCache.js +0 -156
  99. package/lib/chain/stateCache/inMemoryCheckpointsCache.js.map +0 -1
  100. package/src/chain/stateCache/blockStateCacheImpl.ts +0 -149
  101. package/src/chain/stateCache/inMemoryCheckpointsCache.ts +0 -192
@@ -107,12 +107,10 @@ import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js";
107
107
  import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js";
108
108
  import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js";
109
109
  import {ShufflingCache} from "./shufflingCache.js";
110
- import {BlockStateCacheImpl} from "./stateCache/blockStateCacheImpl.js";
111
110
  import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js";
112
111
  import {FileCPStateDatastore} from "./stateCache/datastore/file.js";
113
112
  import {CPStateDatastore} from "./stateCache/datastore/types.js";
114
113
  import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js";
115
- import {InMemoryCheckpointStateCache} from "./stateCache/inMemoryCheckpointsCache.js";
116
114
  import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js";
117
115
  import {CheckpointStateCache} from "./stateCache/types.js";
118
116
  import {ValidatorMonitor} from "./validatorMonitor.js";
@@ -142,7 +140,7 @@ export class BeaconChain implements IBeaconChain {
142
140
  readonly logger: Logger;
143
141
  readonly metrics: Metrics | null;
144
142
  readonly validatorMonitor: ValidatorMonitor | null;
145
- readonly bufferPool: BufferPool | null;
143
+ readonly bufferPool: BufferPool;
146
144
 
147
145
  readonly anchorStateLatestBlockSlot: Slot;
148
146
 
@@ -339,32 +337,22 @@ export class BeaconChain implements IBeaconChain {
339
337
  this.index2pubkey = index2pubkey;
340
338
 
341
339
  const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true;
342
- const blockStateCache = this.opts.nHistoricalStates
343
- ? new FIFOBlockStateCache(this.opts, {metrics})
344
- : new BlockStateCacheImpl({metrics});
345
- this.bufferPool = this.opts.nHistoricalStates
346
- ? new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics)
347
- : null;
348
-
349
- let checkpointStateCache: CheckpointStateCache;
350
- this.cpStateDatastore = undefined;
351
- if (this.opts.nHistoricalStates) {
352
- this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db);
353
- checkpointStateCache = new PersistentCheckpointStateCache(
354
- {
355
- config,
356
- metrics,
357
- logger,
358
- clock,
359
- blockStateCache,
360
- bufferPool: this.bufferPool,
361
- datastore: this.cpStateDatastore,
362
- },
363
- this.opts
364
- );
365
- } else {
366
- checkpointStateCache = new InMemoryCheckpointStateCache({metrics});
367
- }
340
+ const blockStateCache = new FIFOBlockStateCache(this.opts, {metrics});
341
+ this.bufferPool = new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics);
342
+
343
+ this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db);
344
+ const checkpointStateCache: CheckpointStateCache = new PersistentCheckpointStateCache(
345
+ {
346
+ config,
347
+ metrics,
348
+ logger,
349
+ clock,
350
+ blockStateCache,
351
+ bufferPool: this.bufferPool,
352
+ datastore: this.cpStateDatastore,
353
+ },
354
+ this.opts
355
+ );
368
356
 
369
357
  const {checkpoint} = computeAnchorCheckpoint(config, anchorState);
370
358
  blockStateCache.add(anchorState);
@@ -45,7 +45,6 @@ export type IChainOptions = BlockProcessOpts &
45
45
  broadcastValidationStrictness?: string;
46
46
  minSameMessageSignatureSetsToBatch: number;
47
47
  archiveDateEpochs?: number;
48
- nHistoricalStates?: boolean;
49
48
  nHistoricalStatesFileDataStore?: boolean;
50
49
  };
51
50
 
@@ -119,7 +118,6 @@ export const defaultChainOptions: IChainOptions = {
119
118
  // batching too much may block the I/O thread so if useWorker=false, suggest this value to be 32
120
119
  // since this batch attestation work is designed to work with useWorker=true, make this the lowest value
121
120
  minSameMessageSignatureSetsToBatch: 2,
122
- nHistoricalStates: true,
123
121
  // as of Feb 2025, this option turned out to be very useful:
124
122
  // - it allows to share a persisted checkpoint state to other nodes
125
123
  // - users can prune the persisted checkpoint state files manually to save disc space
@@ -2,7 +2,7 @@ import {routes} from "@lodestar/api";
2
2
  import {ProtoBlock} from "@lodestar/fork-choice";
3
3
  import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
4
4
  import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types";
5
- import {CheckpointHex} from "../stateCache/index.js";
5
+ import {CheckpointHex} from "../stateCache/types.js";
6
6
 
7
7
  export enum RegenCaller {
8
8
  getDuties = "getDuties",
@@ -5,8 +5,7 @@ import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types";
5
5
  import {Logger, toRootHex} from "@lodestar/utils";
6
6
  import {Metrics} from "../../metrics/index.js";
7
7
  import {JobItemQueue} from "../../util/queue/index.js";
8
- import {CheckpointHex} from "../stateCache/index.js";
9
- import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js";
8
+ import {BlockStateCache, CheckpointHex, CheckpointStateCache} from "../stateCache/types.js";
10
9
  import {RegenError, RegenErrorCode} from "./errors.js";
11
10
  import {
12
11
  IStateRegenerator,
@@ -1,3 +1 @@
1
- export * from "./blockStateCacheImpl.js";
2
1
  export * from "./fifoBlockStateCache.js";
3
- export * from "./inMemoryCheckpointsCache.js";
@@ -31,7 +31,7 @@ type PersistentCheckpointStateCacheModules = {
31
31
  signal?: AbortSignal;
32
32
  datastore: CPStateDatastore;
33
33
  blockStateCache: BlockStateCache;
34
- bufferPool?: BufferPool | null;
34
+ bufferPool?: BufferPool;
35
35
  };
36
36
 
37
37
  /** checkpoint serialized as a string */
@@ -119,7 +119,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
119
119
  private readonly maxEpochsOnDisk: number;
120
120
  private readonly datastore: CPStateDatastore;
121
121
  private readonly blockStateCache: BlockStateCache;
122
- private readonly bufferPool?: BufferPool | null;
122
+ private readonly bufferPool?: BufferPool;
123
123
 
124
124
  constructor(
125
125
  {
@@ -851,6 +851,10 @@ export function toCheckpointHex(checkpoint: phase0.Checkpoint): CheckpointHex {
851
851
  };
852
852
  }
853
853
 
854
+ export function toCheckpointKey(cp: CheckpointHex): string {
855
+ return `${cp.rootHex}:${cp.epoch}`;
856
+ }
857
+
854
858
  function toCacheKey(cp: CheckpointHex | phase0.Checkpoint): CacheKey {
855
859
  if (isCheckpointHex(cp)) {
856
860
  return `${cp.rootHex}_${cp.epoch}`;
@@ -138,11 +138,10 @@ export async function validateGossipBlock(
138
138
  // in forky condition, make sure to populate ShufflingCache with regened state
139
139
  chain.shufflingCache.processState(blockState);
140
140
 
141
- // Extra conditions for merge fork blocks
142
141
  // [REJECT] The block's execution payload timestamp is correct with respect to the slot
143
142
  // -- i.e. execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot).
144
143
  if (isForkPostBellatrix(fork) && !isForkPostGloas(fork)) {
145
- if (!isExecutionBlockBodyType(block.body)) throw Error("Not merge block type");
144
+ if (!isExecutionBlockBodyType(block.body)) throw Error("Not execution block body type");
146
145
  const executionPayload = block.body.executionPayload;
147
146
  if (isExecutionStateType(blockState) && isExecutionEnabled(blockState, block)) {
148
147
  const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime);
@@ -454,6 +454,18 @@ export class NetworkCore implements INetworkCore {
454
454
  await this.libp2p.hangUp(peerIdFromString(peerIdStr));
455
455
  }
456
456
 
457
+ async addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
458
+ return this.gossip.addDirectPeer(peer);
459
+ }
460
+
461
+ async removeDirectPeer(peerIdStr: PeerIdStr): Promise<boolean> {
462
+ return this.gossip.removeDirectPeer(peerIdStr);
463
+ }
464
+
465
+ async getDirectPeers(): Promise<string[]> {
466
+ return this.gossip.getDirectPeers();
467
+ }
468
+
457
469
  private _dumpPeer(peerIdStr: string, connections: Connection[]): routes.lodestar.LodestarNodePeer {
458
470
  const peerData = this.peersData.connectedPeers.get(peerIdStr);
459
471
  const fork = this.config.getForkName(this.clock.currentSlot);
@@ -153,6 +153,9 @@ const libp2pWorkerApi: NetworkWorkerApi = {
153
153
  getConnectedPeerCount: () => core.getConnectedPeerCount(),
154
154
  connectToPeer: (peer, multiaddr) => core.connectToPeer(peer, multiaddr),
155
155
  disconnectPeer: (peer) => core.disconnectPeer(peer),
156
+ addDirectPeer: (peer) => core.addDirectPeer(peer),
157
+ removeDirectPeer: (peerId) => core.removeDirectPeer(peerId),
158
+ getDirectPeers: () => core.getDirectPeers(),
156
159
  dumpPeers: () => core.dumpPeers(),
157
160
  dumpPeer: (peerIdStr) => core.dumpPeer(peerIdStr),
158
161
  dumpPeerScoreStats: () => core.dumpPeerScoreStats(),
@@ -247,6 +247,15 @@ export class WorkerNetworkCore implements INetworkCore {
247
247
  disconnectPeer(peer: PeerIdStr): Promise<void> {
248
248
  return this.getApi().disconnectPeer(peer);
249
249
  }
250
+ addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
251
+ return this.getApi().addDirectPeer(peer);
252
+ }
253
+ removeDirectPeer(peerId: PeerIdStr): Promise<boolean> {
254
+ return this.getApi().removeDirectPeer(peerId);
255
+ }
256
+ getDirectPeers(): Promise<string[]> {
257
+ return this.getApi().getDirectPeers();
258
+ }
250
259
  dumpPeers(): Promise<routes.lodestar.LodestarNodePeer[]> {
251
260
  return this.getApi().dumpPeers();
252
261
  }
@@ -30,6 +30,12 @@ export interface INetworkCorePublic {
30
30
  // Debug
31
31
  connectToPeer(peer: PeerIdStr, multiaddr: MultiaddrStr[]): Promise<void>;
32
32
  disconnectPeer(peer: PeerIdStr): Promise<void>;
33
+
34
+ // Direct peers management
35
+ addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null>;
36
+ removeDirectPeer(peerId: PeerIdStr): Promise<boolean>;
37
+ getDirectPeers(): Promise<string[]>;
38
+
33
39
  dumpPeers(): Promise<routes.lodestar.LodestarNodePeer[]>;
34
40
  dumpPeer(peerIdStr: PeerIdStr): Promise<routes.lodestar.LodestarNodePeer | undefined>;
35
41
  dumpPeerScoreStats(): Promise<PeerScoreStats>;
@@ -1,7 +1,11 @@
1
+ import {peerIdFromString} from "@libp2p/peer-id";
2
+ import {multiaddr} from "@multiformats/multiaddr";
3
+ import {ENR} from "@chainsafe/enr";
1
4
  import {GossipSub, GossipsubEvents} from "@chainsafe/libp2p-gossipsub";
2
5
  import {MetricsRegister, TopicLabel, TopicStrToLabel} from "@chainsafe/libp2p-gossipsub/metrics";
3
6
  import {PeerScoreParams} from "@chainsafe/libp2p-gossipsub/score";
4
- import {SignaturePolicy, TopicStr} from "@chainsafe/libp2p-gossipsub/types";
7
+ import {AddrInfo, SignaturePolicy, TopicStr} from "@chainsafe/libp2p-gossipsub/types";
8
+ import {routes} from "@lodestar/api";
5
9
  import {BeaconConfig, ForkBoundary} from "@lodestar/config";
6
10
  import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params";
7
11
  import {SubnetID} from "@lodestar/types";
@@ -55,6 +59,12 @@ export type Eth2GossipsubOpts = {
55
59
  disableFloodPublish?: boolean;
56
60
  skipParamsLog?: boolean;
57
61
  disableLightClientServer?: boolean;
62
+ /**
63
+ * Direct peers for GossipSub - these peers maintain permanent mesh connections without GRAFT/PRUNE.
64
+ * Supports multiaddr strings with peer ID (e.g., "/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...")
65
+ * or ENR strings (e.g., "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...")
66
+ */
67
+ directPeers?: string[];
58
68
  };
59
69
 
60
70
  export type ForkBoundaryLabel = string;
@@ -78,6 +88,7 @@ export class Eth2Gossipsub extends GossipSub {
78
88
  private readonly logger: Logger;
79
89
  private readonly peersData: PeersData;
80
90
  private readonly events: NetworkEventBus;
91
+ private readonly libp2p: Libp2p;
81
92
 
82
93
  // Internal caches
83
94
  private readonly gossipTopicCache: GossipTopicCache;
@@ -97,6 +108,9 @@ export class Eth2Gossipsub extends GossipSub {
97
108
  );
98
109
  }
99
110
 
111
+ // Parse direct peers from multiaddr strings to AddrInfo objects
112
+ const directPeers = parseDirectPeers(opts.directPeers ?? [], logger);
113
+
100
114
  // Gossipsub parameters defined here:
101
115
  // https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#the-gossip-domain-gossipsub
102
116
  super(modules.libp2p.services.components, {
@@ -106,6 +120,7 @@ export class Eth2Gossipsub extends GossipSub {
106
120
  Dlo: gossipsubDLow ?? GOSSIP_D_LOW,
107
121
  Dhi: gossipsubDHigh ?? GOSSIP_D_HIGH,
108
122
  Dlazy: 6,
123
+ directPeers,
109
124
  heartbeatInterval: GOSSIPSUB_HEARTBEAT_INTERVAL,
110
125
  fanoutTTL: 60 * 1000,
111
126
  mcacheLength: 6,
@@ -146,6 +161,7 @@ export class Eth2Gossipsub extends GossipSub {
146
161
  this.logger = logger;
147
162
  this.peersData = peersData;
148
163
  this.events = events;
164
+ this.libp2p = modules.libp2p;
149
165
  this.gossipTopicCache = gossipTopicCache;
150
166
 
151
167
  this.addEventListener("gossipsub:message", this.onGossipsubMessage.bind(this));
@@ -328,6 +344,64 @@ export class Eth2Gossipsub extends GossipSub {
328
344
  this.reportMessageValidationResult(data.msgId, data.propagationSource, data.acceptance);
329
345
  });
330
346
  }
347
+
348
+ /**
349
+ * Add a peer as a direct peer at runtime. Accepts multiaddr with peer ID or ENR string.
350
+ * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
351
+ */
352
+ async addDirectPeer(peerStr: routes.lodestar.DirectPeer): Promise<string | null> {
353
+ const parsed = parseDirectPeers([peerStr], this.logger);
354
+ if (parsed.length === 0) {
355
+ return null;
356
+ }
357
+
358
+ const {id: peerId, addrs} = parsed[0];
359
+ const peerIdStr = peerId.toString();
360
+
361
+ // Prevent adding self as a direct peer
362
+ if (peerId.equals(this.libp2p.peerId)) {
363
+ this.logger.warn("Cannot add self as a direct peer", {peerId: peerIdStr});
364
+ return null;
365
+ }
366
+
367
+ // Direct peers need addresses to connect - reject if none provided
368
+ if (addrs.length === 0) {
369
+ this.logger.warn("Cannot add direct peer without addresses", {peerId: peerIdStr});
370
+ return null;
371
+ }
372
+
373
+ // Add addresses to peer store first so we can connect
374
+ try {
375
+ await this.libp2p.peerStore.merge(peerId, {multiaddrs: addrs});
376
+ } catch (e) {
377
+ this.logger.warn("Failed to add direct peer addresses to peer store", {peerId: peerIdStr}, e as Error);
378
+ return null;
379
+ }
380
+
381
+ // Add to direct peers set only after addresses are stored
382
+ this.direct.add(peerIdStr);
383
+
384
+ this.logger.info("Added direct peer via API", {peerId: peerIdStr});
385
+ return peerIdStr;
386
+ }
387
+
388
+ /**
389
+ * Remove a peer from direct peers.
390
+ */
391
+ removeDirectPeer(peerIdStr: string): boolean {
392
+ const removed = this.direct.delete(peerIdStr);
393
+ if (removed) {
394
+ this.logger.info("Removed direct peer via API", {peerId: peerIdStr});
395
+ }
396
+ return removed;
397
+ }
398
+
399
+ /**
400
+ * Get list of current direct peer IDs.
401
+ */
402
+ getDirectPeers(): string[] {
403
+ return Array.from(this.direct);
404
+ }
331
405
  }
332
406
 
333
407
  /**
@@ -381,3 +455,75 @@ function getForkBoundaryLabel(boundary: ForkBoundary): ForkBoundaryLabel {
381
455
 
382
456
  return label;
383
457
  }
458
+
459
+ /**
460
+ * Parse direct peer strings into AddrInfo objects for GossipSub.
461
+ * Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
462
+ *
463
+ * Supported formats:
464
+ * - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...`
465
+ * - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...`
466
+ *
467
+ * For multiaddrs, the string must contain a /p2p/ component with the peer ID.
468
+ * For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record.
469
+ */
470
+ export function parseDirectPeers(directPeerStrs: routes.lodestar.DirectPeer[], logger: Logger): AddrInfo[] {
471
+ const directPeers: AddrInfo[] = [];
472
+
473
+ for (const peerStr of directPeerStrs) {
474
+ // Check if this is an ENR (starts with "enr:")
475
+ if (peerStr.startsWith("enr:")) {
476
+ try {
477
+ const enr = ENR.decodeTxt(peerStr);
478
+ const peerId = enr.peerId;
479
+
480
+ // Get TCP multiaddr from ENR
481
+ const multiaddrTCP = enr.getLocationMultiaddr("tcp");
482
+ if (!multiaddrTCP) {
483
+ logger.warn("ENR does not contain TCP multiaddr", {enr: peerStr});
484
+ continue;
485
+ }
486
+
487
+ directPeers.push({
488
+ id: peerId,
489
+ addrs: [multiaddrTCP],
490
+ });
491
+
492
+ logger.info("Added direct peer from ENR", {peerId: peerId.toString(), addr: multiaddrTCP.toString()});
493
+ } catch (e) {
494
+ logger.warn("Failed to parse direct peer ENR", {enr: peerStr}, e as Error);
495
+ }
496
+ } else {
497
+ // Parse as multiaddr
498
+ try {
499
+ const ma = multiaddr(peerStr);
500
+
501
+ const peerIdStr = ma.getPeerId();
502
+ if (!peerIdStr) {
503
+ logger.warn("Direct peer multiaddr must contain /p2p/ component with peer ID", {multiaddr: peerStr});
504
+ continue;
505
+ }
506
+
507
+ try {
508
+ const peerId = peerIdFromString(peerIdStr);
509
+
510
+ // Get the address without the /p2p/ component
511
+ const addr = ma.decapsulate("/p2p/" + peerIdStr);
512
+
513
+ directPeers.push({
514
+ id: peerId,
515
+ addrs: [addr],
516
+ });
517
+
518
+ logger.info("Added direct peer", {peerId: peerIdStr, addr: addr.toString()});
519
+ } catch (e) {
520
+ logger.warn("Invalid peer ID in direct peer multiaddr", {multiaddr: peerStr, peerId: peerIdStr}, e as Error);
521
+ }
522
+ } catch (e) {
523
+ logger.warn("Failed to parse direct peer multiaddr", {multiaddr: peerStr}, e as Error);
524
+ }
525
+ }
526
+ }
527
+
528
+ return directPeers;
529
+ }
@@ -641,6 +641,18 @@ export class Network implements INetwork {
641
641
  return this.core.disconnectPeer(peer);
642
642
  }
643
643
 
644
+ addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
645
+ return this.core.addDirectPeer(peer);
646
+ }
647
+
648
+ removeDirectPeer(peerId: string): Promise<boolean> {
649
+ return this.core.removeDirectPeer(peerId);
650
+ }
651
+
652
+ getDirectPeers(): Promise<string[]> {
653
+ return this.core.getDirectPeers();
654
+ }
655
+
644
656
  dumpPeer(peerIdStr: string): Promise<routes.lodestar.LodestarNodePeer | undefined> {
645
657
  return this.core.dumpPeer(peerIdStr);
646
658
  }
@@ -15,6 +15,12 @@ export interface NetworkOptions
15
15
  Omit<Eth2GossipsubOpts, "disableLightClientServer"> {
16
16
  localMultiaddrs: string[];
17
17
  bootMultiaddrs?: string[];
18
+ /**
19
+ * Direct peers for GossipSub - these peers maintain permanent mesh connections without GRAFT/PRUNE.
20
+ * Format: multiaddr strings with peer ID, e.g., "/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7..."
21
+ * Both peers must configure each other as direct peers for the feature to work properly.
22
+ */
23
+ directPeers?: string[];
18
24
  subscribeAllSubnets?: boolean;
19
25
  mdns?: boolean;
20
26
  connectToDiscv5Bootnodes?: boolean;
@@ -721,6 +721,17 @@ export class PeerManager {
721
721
  // NOTE: libp2p may emit two "peer:connect" events: One for inbound, one for outbound
722
722
  // If that happens, it's okay. Only the "outbound" connection triggers immediate action
723
723
  const now = Date.now();
724
+
725
+ // Ethereum uses secp256k1 for node IDs, reject peers with other key types
726
+ if (remotePeer.type !== "secp256k1") {
727
+ this.logger.debug("Peer does not have secp256k1 key, disconnecting", {
728
+ peer: remotePeerPrettyStr,
729
+ type: remotePeer.type,
730
+ });
731
+ void this.goodbyeAndDisconnect(remotePeer, GoodByeReasonCode.IRRELEVANT_NETWORK);
732
+ return;
733
+ }
734
+
724
735
  const nodeId = computeNodeId(remotePeer);
725
736
  const peerData: PeerData = {
726
737
  lastReceivedMsgUnixTsMs: direction === "outbound" ? 0 : now,
@@ -579,7 +579,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
579
579
  break;
580
580
  }
581
581
 
582
- if (!blockInput.hasAllData()) {
582
+ if (!blockInput.hasComputedAllData()) {
583
583
  // immediately attempt fetch of data columns from execution engine
584
584
  chain.getBlobsTracker.triggerGetBlobs(blockInput);
585
585
  // if we've received at least half of the columns, trigger reconstruction of the rest
@@ -1,54 +0,0 @@
1
- import { routes } from "@lodestar/api";
2
- import { CachedBeaconStateAllForks } from "@lodestar/state-transition";
3
- import { Epoch, RootHex } from "@lodestar/types";
4
- import { Metrics } from "../../metrics/index.js";
5
- import { BlockStateCache } from "./types.js";
6
- /**
7
- * Old implementation of StateCache (used to call `StateContextCache`)
8
- * - Prune per checkpoint so number of states ranges from 96 to 128
9
- * - Keep a separate head state to make sure it is always available
10
- */
11
- export declare class BlockStateCacheImpl implements BlockStateCache {
12
- /**
13
- * Max number of states allowed in the cache
14
- */
15
- readonly maxStates: number;
16
- private readonly cache;
17
- /** Epoch -> Set<blockRoot> */
18
- private readonly epochIndex;
19
- private readonly metrics;
20
- /**
21
- * Strong reference to prevent head state from being pruned.
22
- * null if head state is being regen and not available at the moment.
23
- */
24
- private head;
25
- constructor({ maxStates, metrics }: {
26
- maxStates?: number;
27
- metrics?: Metrics | null;
28
- });
29
- get(rootHex: RootHex): CachedBeaconStateAllForks | null;
30
- add(item: CachedBeaconStateAllForks): void;
31
- setHeadState(item: CachedBeaconStateAllForks | null): void;
32
- /**
33
- * Get a seed state for state reload.
34
- * This is to conform to the api only as this cache is not used in n-historical state.
35
- * See ./fifoBlockStateCache.ts for implementation
36
- */
37
- getSeedState(): CachedBeaconStateAllForks;
38
- clear(): void;
39
- get size(): number;
40
- /**
41
- * TODO make this more robust.
42
- * Without more thought, this currently breaks our assumptions about recent state availablity
43
- */
44
- prune(headStateRootHex: RootHex): void;
45
- /**
46
- * Prune per finalized epoch.
47
- */
48
- deleteAllBeforeEpoch(finalizedEpoch: Epoch): void;
49
- /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
50
- dumpSummary(): routes.lodestar.StateCacheItem[];
51
- getStates(): IterableIterator<CachedBeaconStateAllForks>;
52
- private deleteAllEpochItems;
53
- }
54
- //# sourceMappingURL=blockStateCacheImpl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"blockStateCacheImpl.d.ts","sourceRoot":"","sources":["../../../src/chain/stateCache/blockStateCacheImpl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,eAAe,CAAC;AACrC,OAAO,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAC,KAAK,EAAE,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EAAC,OAAO,EAAC,MAAM,wBAAwB,CAAC;AAE/C,OAAO,EAAC,eAAe,EAAC,MAAM,YAAY,CAAC;AAI3C;;;;GAIG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACzD;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgD;IACtE,8BAA8B;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IACnE;;;OAGG;IACH,OAAO,CAAC,IAAI,CAAuE;gBAEvE,EAAC,SAAsB,EAAE,OAAO,EAAC,EAAE;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAC;IAS7F,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,GAAG,IAAI;IAavD,GAAG,CAAC,IAAI,EAAE,yBAAyB,GAAG,IAAI;IAgB1C,YAAY,CAAC,IAAI,EAAE,yBAAyB,GAAG,IAAI,GAAG,IAAI;IAS1D;;;;OAIG;IACH,YAAY,IAAI,yBAAyB;IAIzC,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,KAAK,CAAC,gBAAgB,EAAE,OAAO,GAAG,IAAI;IAgBtC;;OAEG;IACH,oBAAoB,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI;IAQjD,0DAA0D;IAC1D,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE;IAU/C,SAAS,IAAI,gBAAgB,CAAC,yBAAyB,CAAC;IAIxD,OAAO,CAAC,mBAAmB;CAM5B"}
@@ -1,130 +0,0 @@
1
- import { toRootHex } from "@lodestar/utils";
2
- import { MapTracker } from "./mapMetrics.js";
3
- const MAX_STATES = 3 * 32;
4
- /**
5
- * Old implementation of StateCache (used to call `StateContextCache`)
6
- * - Prune per checkpoint so number of states ranges from 96 to 128
7
- * - Keep a separate head state to make sure it is always available
8
- */
9
- export class BlockStateCacheImpl {
10
- /**
11
- * Max number of states allowed in the cache
12
- */
13
- maxStates;
14
- cache;
15
- /** Epoch -> Set<blockRoot> */
16
- epochIndex = new Map();
17
- metrics;
18
- /**
19
- * Strong reference to prevent head state from being pruned.
20
- * null if head state is being regen and not available at the moment.
21
- */
22
- head = null;
23
- constructor({ maxStates = MAX_STATES, metrics }) {
24
- this.maxStates = maxStates;
25
- this.cache = new MapTracker(metrics?.stateCache);
26
- if (metrics) {
27
- this.metrics = metrics.stateCache;
28
- metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size));
29
- }
30
- }
31
- get(rootHex) {
32
- this.metrics?.lookups.inc();
33
- const item = this.head?.stateRoot === rootHex ? this.head.state : this.cache.get(rootHex);
34
- if (!item) {
35
- return null;
36
- }
37
- this.metrics?.hits.inc();
38
- this.metrics?.stateClonedCount.observe(item.clonedCount);
39
- return item;
40
- }
41
- add(item) {
42
- const key = toRootHex(item.hashTreeRoot());
43
- if (this.cache.get(key)) {
44
- return;
45
- }
46
- this.metrics?.adds.inc();
47
- this.cache.set(key, item);
48
- const epoch = item.epochCtx.epoch;
49
- const blockRoots = this.epochIndex.get(epoch);
50
- if (blockRoots) {
51
- blockRoots.add(key);
52
- }
53
- else {
54
- this.epochIndex.set(epoch, new Set([key]));
55
- }
56
- }
57
- setHeadState(item) {
58
- if (item) {
59
- const key = toRootHex(item.hashTreeRoot());
60
- this.head = { state: item, stateRoot: key };
61
- }
62
- else {
63
- this.head = null;
64
- }
65
- }
66
- /**
67
- * Get a seed state for state reload.
68
- * This is to conform to the api only as this cache is not used in n-historical state.
69
- * See ./fifoBlockStateCache.ts for implementation
70
- */
71
- getSeedState() {
72
- throw Error("Not implemented for BlockStateCacheImpl");
73
- }
74
- clear() {
75
- this.cache.clear();
76
- this.epochIndex.clear();
77
- }
78
- get size() {
79
- return this.cache.size;
80
- }
81
- /**
82
- * TODO make this more robust.
83
- * Without more thought, this currently breaks our assumptions about recent state availablity
84
- */
85
- prune(headStateRootHex) {
86
- const keys = Array.from(this.cache.keys());
87
- if (keys.length > this.maxStates) {
88
- // object keys are stored in insertion order, delete keys starting from the front
89
- for (const key of keys.slice(0, keys.length - this.maxStates)) {
90
- if (key !== headStateRootHex) {
91
- const item = this.cache.get(key);
92
- if (item) {
93
- this.epochIndex.get(item.epochCtx.epoch)?.delete(key);
94
- this.cache.delete(key);
95
- }
96
- }
97
- }
98
- }
99
- }
100
- /**
101
- * Prune per finalized epoch.
102
- */
103
- deleteAllBeforeEpoch(finalizedEpoch) {
104
- for (const epoch of this.epochIndex.keys()) {
105
- if (epoch < finalizedEpoch) {
106
- this.deleteAllEpochItems(epoch);
107
- }
108
- }
109
- }
110
- /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
111
- dumpSummary() {
112
- return Array.from(this.cache.entries()).map(([key, state]) => ({
113
- slot: state.slot,
114
- root: toRootHex(state.hashTreeRoot()),
115
- reads: this.cache.readCount.get(key) ?? 0,
116
- lastRead: this.cache.lastRead.get(key) ?? 0,
117
- checkpointState: false,
118
- }));
119
- }
120
- getStates() {
121
- return this.cache.values();
122
- }
123
- deleteAllEpochItems(epoch) {
124
- for (const rootHex of this.epochIndex.get(epoch) || []) {
125
- this.cache.delete(rootHex);
126
- }
127
- this.epochIndex.delete(epoch);
128
- }
129
- }
130
- //# sourceMappingURL=blockStateCacheImpl.js.map