@lodestar/beacon-node 1.40.0-dev.787d0f5eee → 1.40.0-dev.8ed981ca92

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 (115) 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/rest/base.d.ts.map +1 -1
  8. package/lib/api/rest/base.js +12 -10
  9. package/lib/api/rest/base.js.map +1 -1
  10. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.js +10 -4
  12. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  13. package/lib/chain/archiveStore/historicalState/getHistoricalState.d.ts.map +1 -1
  14. package/lib/chain/archiveStore/historicalState/getHistoricalState.js +2 -1
  15. package/lib/chain/archiveStore/historicalState/getHistoricalState.js.map +1 -1
  16. package/lib/chain/blocks/blockInput/blockInput.d.ts +28 -0
  17. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  18. package/lib/chain/blocks/blockInput/blockInput.js +38 -3
  19. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  20. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  21. package/lib/chain/blocks/importBlock.js +5 -1
  22. package/lib/chain/blocks/importBlock.js.map +1 -1
  23. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  24. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +1 -2
  25. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  26. package/lib/chain/blocks/writeBlockInputToDb.d.ts.map +1 -1
  27. package/lib/chain/blocks/writeBlockInputToDb.js +8 -0
  28. package/lib/chain/blocks/writeBlockInputToDb.js.map +1 -1
  29. package/lib/chain/chain.d.ts.map +1 -1
  30. package/lib/chain/chain.js +2 -3
  31. package/lib/chain/chain.js.map +1 -1
  32. package/lib/chain/initState.d.ts.map +1 -1
  33. package/lib/chain/initState.js +2 -2
  34. package/lib/chain/initState.js.map +1 -1
  35. package/lib/chain/lightClient/index.d.ts +2 -0
  36. package/lib/chain/lightClient/index.d.ts.map +1 -1
  37. package/lib/chain/lightClient/index.js +11 -6
  38. package/lib/chain/lightClient/index.js.map +1 -1
  39. package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
  40. package/lib/chain/seenCache/seenGossipBlockInput.js +2 -2
  41. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  42. package/lib/chain/serializeState.d.ts.map +1 -1
  43. package/lib/chain/serializeState.js +2 -1
  44. package/lib/chain/serializeState.js.map +1 -1
  45. package/lib/chain/validation/blobSidecar.js +2 -2
  46. package/lib/chain/validation/blobSidecar.js.map +1 -1
  47. package/lib/chain/validation/dataColumnSidecar.js +2 -2
  48. package/lib/chain/validation/dataColumnSidecar.js.map +1 -1
  49. package/lib/network/core/networkCore.d.ts +3 -0
  50. package/lib/network/core/networkCore.d.ts.map +1 -1
  51. package/lib/network/core/networkCore.js +9 -0
  52. package/lib/network/core/networkCore.js.map +1 -1
  53. package/lib/network/core/networkCoreWorker.js +3 -0
  54. package/lib/network/core/networkCoreWorker.js.map +1 -1
  55. package/lib/network/core/networkCoreWorkerHandler.d.ts +3 -0
  56. package/lib/network/core/networkCoreWorkerHandler.d.ts.map +1 -1
  57. package/lib/network/core/networkCoreWorkerHandler.js +9 -0
  58. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  59. package/lib/network/core/types.d.ts +3 -0
  60. package/lib/network/core/types.d.ts.map +1 -1
  61. package/lib/network/gossip/gossipsub.d.ts +34 -0
  62. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  63. package/lib/network/gossip/gossipsub.js +123 -0
  64. package/lib/network/gossip/gossipsub.js.map +1 -1
  65. package/lib/network/network.d.ts +3 -0
  66. package/lib/network/network.d.ts.map +1 -1
  67. package/lib/network/network.js +9 -0
  68. package/lib/network/network.js.map +1 -1
  69. package/lib/network/options.d.ts +6 -0
  70. package/lib/network/options.d.ts.map +1 -1
  71. package/lib/network/options.js.map +1 -1
  72. package/lib/network/processor/gossipHandlers.js +1 -1
  73. package/lib/network/processor/gossipHandlers.js.map +1 -1
  74. package/lib/sync/backfill/backfill.d.ts.map +1 -1
  75. package/lib/sync/backfill/backfill.js +1 -2
  76. package/lib/sync/backfill/backfill.js.map +1 -1
  77. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  78. package/lib/sync/utils/downloadByRange.js +2 -2
  79. package/lib/sync/utils/downloadByRange.js.map +1 -1
  80. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  81. package/lib/sync/utils/downloadByRoot.js +1 -2
  82. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  83. package/package.json +16 -16
  84. package/src/api/impl/beacon/blocks/index.ts +22 -12
  85. package/src/api/impl/lodestar/index.ts +17 -0
  86. package/src/api/rest/base.ts +15 -13
  87. package/src/chain/archiveStore/archiveStore.ts +10 -4
  88. package/src/chain/archiveStore/historicalState/getHistoricalState.ts +2 -1
  89. package/src/chain/blocks/blockInput/blockInput.ts +47 -4
  90. package/src/chain/blocks/importBlock.ts +5 -1
  91. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +1 -2
  92. package/src/chain/blocks/writeBlockInputToDb.ts +9 -0
  93. package/src/chain/chain.ts +2 -3
  94. package/src/chain/initState.ts +2 -2
  95. package/src/chain/lightClient/index.ts +12 -6
  96. package/src/chain/seenCache/seenGossipBlockInput.ts +2 -2
  97. package/src/chain/serializeState.ts +2 -1
  98. package/src/chain/validation/blobSidecar.ts +2 -2
  99. package/src/chain/validation/dataColumnSidecar.ts +2 -2
  100. package/src/network/core/networkCore.ts +12 -0
  101. package/src/network/core/networkCoreWorker.ts +3 -0
  102. package/src/network/core/networkCoreWorkerHandler.ts +9 -0
  103. package/src/network/core/types.ts +6 -0
  104. package/src/network/gossip/gossipsub.ts +147 -1
  105. package/src/network/network.ts +12 -0
  106. package/src/network/options.ts +6 -0
  107. package/src/network/processor/gossipHandlers.ts +1 -1
  108. package/src/sync/backfill/backfill.ts +1 -2
  109. package/src/sync/utils/downloadByRange.ts +2 -2
  110. package/src/sync/utils/downloadByRoot.ts +1 -2
  111. package/lib/util/bytes.d.ts +0 -3
  112. package/lib/util/bytes.d.ts.map +0 -1
  113. package/lib/util/bytes.js +0 -11
  114. package/lib/util/bytes.js.map +0 -1
  115. package/src/util/bytes.ts +0 -11
@@ -2,7 +2,7 @@ import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ZERO_HASH} from "@lodestar/params";
3
3
  import {BeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
4
  import {SignedBeaconBlock, ssz} from "@lodestar/types";
5
- import {Logger, toHex, toRootHex} from "@lodestar/utils";
5
+ import {Logger, byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
6
6
  import {GENESIS_SLOT} from "../constants/index.js";
7
7
  import {IBeaconDb} from "../db/index.js";
8
8
  import {Metrics} from "../metrics/index.js";
@@ -26,7 +26,7 @@ export async function persistAnchorState(
26
26
 
27
27
  const latestBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(latestBlockHeader);
28
28
 
29
- if (Buffer.compare(blockRoot, latestBlockRoot) !== 0) {
29
+ if (!byteArrayEquals(blockRoot, latestBlockRoot)) {
30
30
  throw Error(
31
31
  `Genesis block root ${toRootHex(blockRoot)} does not match genesis state latest block root ${toRootHex(latestBlockRoot)}`
32
32
  );
@@ -46,12 +46,11 @@ import {
46
46
  ssz,
47
47
  sszTypesFor,
48
48
  } from "@lodestar/types";
49
- import {Logger, MapDef, pruneSetToMax, toRootHex} from "@lodestar/utils";
49
+ import {Logger, MapDef, byteArrayEquals, pruneSetToMax, toRootHex} from "@lodestar/utils";
50
50
  import {ZERO_HASH} from "../../constants/index.js";
51
51
  import {IBeaconDb} from "../../db/index.js";
52
52
  import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js";
53
53
  import {Metrics} from "../../metrics/index.js";
54
- import {byteArrayEquals} from "../../util/bytes.js";
55
54
  import {IClock} from "../../util/clock.js";
56
55
  import {ChainEventEmitter} from "../emitter.js";
57
56
  import {LightClientServerError, LightClientServerErrorCode} from "../errors/lightClientError.js";
@@ -93,6 +92,7 @@ type LightClientServerModules = {
93
92
  metrics: Metrics | null;
94
93
  emitter: ChainEventEmitter;
95
94
  logger: Logger;
95
+ signal: AbortSignal;
96
96
  };
97
97
 
98
98
  const MAX_CACHED_FINALIZED_HEADERS = 3;
@@ -205,6 +205,7 @@ export class LightClientServer {
205
205
  private readonly emitter: ChainEventEmitter;
206
206
  private readonly logger: Logger;
207
207
  private readonly clock: IClock;
208
+ private readonly signal: AbortSignal;
208
209
  private readonly knownSyncCommittee = new MapDef<SyncPeriod, Set<DependentRootHex>>(() => new Set());
209
210
  private storedCurrentSyncCommittee = false;
210
211
 
@@ -225,13 +226,14 @@ export class LightClientServer {
225
226
  private readonly opts: LightClientServerOpts,
226
227
  modules: LightClientServerModules
227
228
  ) {
228
- const {config, clock, db, metrics, emitter, logger} = modules;
229
+ const {config, clock, db, metrics, emitter, logger, signal} = modules;
229
230
  this.config = config;
230
231
  this.clock = clock;
231
232
  this.db = db;
232
233
  this.metrics = metrics;
233
234
  this.emitter = emitter;
234
235
  this.logger = logger;
236
+ this.signal = signal;
235
237
 
236
238
  this.zero = {
237
239
  // Assign the hightest fork's default value because it can always be typecasted down to correct fork
@@ -288,12 +290,16 @@ export class LightClientServer {
288
290
  const syncPeriod = computeSyncPeriodAtSlot(block.slot);
289
291
 
290
292
  this.onSyncAggregate(syncPeriod, block.body.syncAggregate, block.slot, signedBlockRoot).catch((e) => {
291
- this.logger.error("Error onSyncAggregate", {}, e);
292
- this.metrics?.lightclientServer.onSyncAggregate.inc({event: "error"});
293
+ if (!this.signal.aborted) {
294
+ this.logger.error("Error onSyncAggregate", {}, e);
295
+ this.metrics?.lightclientServer.onSyncAggregate.inc({event: "error"});
296
+ }
293
297
  });
294
298
 
295
299
  this.persistPostBlockImportData(block, postState, parentBlockSlot).catch((e) => {
296
- this.logger.error("Error persistPostBlockImportData", {}, e);
300
+ if (!this.signal.aborted) {
301
+ this.logger.error("Error persistPostBlockImportData", {}, e);
302
+ }
297
303
  });
298
304
  }
299
305
 
@@ -3,7 +3,7 @@ import {CheckpointWithHex} from "@lodestar/fork-choice";
3
3
  import {ForkName, ForkPostFulu, ForkPreGloas, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
4
4
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
5
5
  import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
6
- import {LodestarError, Logger, pruneSetToMax} from "@lodestar/utils";
6
+ import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils";
7
7
  import {Metrics} from "../../metrics/metrics.js";
8
8
  import {IClock} from "../../util/clock.js";
9
9
  import {CustodyConfig} from "../../util/dataColumns.js";
@@ -344,7 +344,7 @@ export class SeenBlockInput {
344
344
  return false;
345
345
  }
346
346
  // Only consider verified if the signature matches
347
- return Buffer.compare(cachedSignature, signature) === 0;
347
+ return byteArrayEquals(cachedSignature, signature);
348
348
  }
349
349
 
350
350
  /**
@@ -20,7 +20,8 @@ export async function serializeState<T>(
20
20
  stateBytes = bufferWithKey.buffer;
21
21
  const dataView = new DataView(stateBytes.buffer, stateBytes.byteOffset, stateBytes.byteLength);
22
22
  state.serializeToBytes({uint8Array: stateBytes, dataView}, 0);
23
- return processFn(stateBytes);
23
+ // Await to ensure buffer is not released back to pool until processFn completes
24
+ return await processFn(stateBytes);
24
25
  }
25
26
  // release the buffer back to the pool automatically
26
27
  }
@@ -12,7 +12,7 @@ import {
12
12
  getBlockHeaderProposerSignatureSetByParentStateSlot,
13
13
  } from "@lodestar/state-transition";
14
14
  import {BlobIndex, Root, Slot, SubnetID, deneb, ssz} from "@lodestar/types";
15
- import {toRootHex, verifyMerkleBranch} from "@lodestar/utils";
15
+ import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils";
16
16
  import {kzg} from "../../util/kzg.js";
17
17
  import {BlobSidecarErrorCode, BlobSidecarGossipError, BlobSidecarValidationError} from "../errors/blobSidecarError.js";
18
18
  import {GossipAction} from "../errors/gossipValidation.js";
@@ -226,7 +226,7 @@ export async function validateBlockBlobSidecars(
226
226
  const firstSidecarSignedBlockHeader = blobSidecars[0].signedBlockHeader;
227
227
  const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message;
228
228
  const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader);
229
- if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) {
229
+ if (!byteArrayEquals(blockRoot, firstBlockRoot)) {
230
230
  throw new BlobSidecarValidationError(
231
231
  {
232
232
  code: BlobSidecarErrorCode.INCORRECT_BLOCK,
@@ -11,7 +11,7 @@ import {
11
11
  getBlockHeaderProposerSignatureSetByParentStateSlot,
12
12
  } from "@lodestar/state-transition";
13
13
  import {Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types";
14
- import {toRootHex, verifyMerkleBranch} from "@lodestar/utils";
14
+ import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils";
15
15
  import {Metrics} from "../../metrics/metrics.js";
16
16
  import {kzg} from "../../util/kzg.js";
17
17
  import {
@@ -318,7 +318,7 @@ export async function validateBlockDataColumnSidecars(
318
318
  const firstSidecarSignedBlockHeader = dataColumnSidecars[0].signedBlockHeader;
319
319
  const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message;
320
320
  const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader);
321
- if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) {
321
+ if (!byteArrayEquals(blockRoot, firstBlockRoot)) {
322
322
  throw new DataColumnSidecarValidationError(
323
323
  {
324
324
  code: DataColumnSidecarErrorCode.INCORRECT_BLOCK,
@@ -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;
@@ -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
@@ -4,13 +4,12 @@ import {BeaconConfig, ChainForkConfig} from "@lodestar/config";
4
4
  import {SLOTS_PER_EPOCH} from "@lodestar/params";
5
5
  import {BeaconStateAllForks, blockToHeader, computeAnchorCheckpoint} from "@lodestar/state-transition";
6
6
  import {Root, SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types";
7
- import {ErrorAborted, Logger, sleep, toRootHex} from "@lodestar/utils";
7
+ import {ErrorAborted, Logger, byteArrayEquals, sleep, toRootHex} from "@lodestar/utils";
8
8
  import {IBeaconChain} from "../../chain/index.js";
9
9
  import {GENESIS_SLOT, ZERO_HASH} from "../../constants/index.js";
10
10
  import {IBeaconDb} from "../../db/index.js";
11
11
  import {Metrics} from "../../metrics/metrics.js";
12
12
  import {INetwork, NetworkEvent, NetworkEventData, PeerAction} from "../../network/index.js";
13
- import {byteArrayEquals} from "../../util/bytes.js";
14
13
  import {ItTrigger} from "../../util/itTrigger.js";
15
14
  import {PeerIdStr} from "../../util/peerId.js";
16
15
  import {shuffleOne} from "../../util/shuffle.js";
@@ -8,7 +8,7 @@ import {
8
8
  isForkPostGloas,
9
9
  } from "@lodestar/params";
10
10
  import {SignedBeaconBlock, Slot, deneb, fulu, phase0} from "@lodestar/types";
11
- import {LodestarError, Logger, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
11
+ import {LodestarError, Logger, byteArrayEquals, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
12
12
  import {
13
13
  BlockInputSource,
14
14
  DAType,
@@ -475,7 +475,7 @@ export function validateBlockByRangeResponse(
475
475
  if (i < blocks.length - 1) {
476
476
  // compare the block root against the next block's parent root
477
477
  const parentRoot = blocks[i + 1].message.parentRoot;
478
- if (Buffer.compare(blockRoot, parentRoot) !== 0) {
478
+ if (!byteArrayEquals(blockRoot, parentRoot)) {
479
479
  throw new DownloadByRangeError(
480
480
  {
481
481
  code: DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH,
@@ -9,7 +9,7 @@ import {
9
9
  isForkPostFulu,
10
10
  } from "@lodestar/params";
11
11
  import {BeaconBlockBody, BlobIndex, ColumnIndex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
12
- import {LodestarError, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils";
12
+ import {LodestarError, byteArrayEquals, fromHex, prettyPrintIndices, toHex, toRootHex} from "@lodestar/utils";
13
13
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
14
14
  import {BlockInputSource, IBlockInput} from "../../chain/blocks/blockInput/types.js";
15
15
  import {ChainEventEmitter} from "../../chain/emitter.js";
@@ -19,7 +19,6 @@ import {validateBlockDataColumnSidecars} from "../../chain/validation/dataColumn
19
19
  import {INetwork} from "../../network/interface.js";
20
20
  import {PeerSyncMeta} from "../../network/peers/peersData.js";
21
21
  import {prettyPrintPeerIdStr} from "../../network/util.js";
22
- import {byteArrayEquals} from "../../util/bytes.js";
23
22
  import {PeerIdStr} from "../../util/peerId.js";
24
23
  import {WarnResult} from "../../util/wrapError.js";
25
24
  import {
@@ -1,3 +0,0 @@
1
- import { Root } from "@lodestar/types";
2
- export declare function byteArrayEquals(a: Uint8Array | Root, b: Uint8Array | Root): boolean;
3
- //# sourceMappingURL=bytes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../../src/util/bytes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAErC,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO,CAQnF"}
package/lib/util/bytes.js DELETED
@@ -1,11 +0,0 @@
1
- export function byteArrayEquals(a, b) {
2
- if (a.length !== b.length) {
3
- return false;
4
- }
5
- for (let i = 0; i < a.length; i++) {
6
- if (a[i] !== b[i])
7
- return false;
8
- }
9
- return true;
10
- }
11
- //# sourceMappingURL=bytes.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../../src/util/bytes.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,eAAe,CAAC,CAAoB,EAAE,CAAoB;IACxE,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/src/util/bytes.ts DELETED
@@ -1,11 +0,0 @@
1
- import {Root} from "@lodestar/types";
2
-
3
- export function byteArrayEquals(a: Uint8Array | Root, b: Uint8Array | Root): boolean {
4
- if (a.length !== b.length) {
5
- return false;
6
- }
7
- for (let i = 0; i < a.length; i++) {
8
- if (a[i] !== b[i]) return false;
9
- }
10
- return true;
11
- }