@aztec/p2p 0.42.0 → 0.44.0

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 (59) hide show
  1. package/dest/bootstrap/bootstrap.d.ts +1 -1
  2. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  3. package/dest/bootstrap/bootstrap.js +8 -4
  4. package/dest/client/index.d.ts.map +1 -1
  5. package/dest/client/index.js +24 -21
  6. package/dest/client/p2p_client.d.ts.map +1 -1
  7. package/dest/client/p2p_client.js +2 -3
  8. package/dest/config.d.ts +9 -22
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +8 -9
  11. package/dest/service/discV5_service.d.ts +5 -2
  12. package/dest/service/discV5_service.d.ts.map +1 -1
  13. package/dest/service/discV5_service.js +38 -12
  14. package/dest/service/dummy_service.d.ts +4 -1
  15. package/dest/service/dummy_service.d.ts.map +1 -1
  16. package/dest/service/dummy_service.js +7 -1
  17. package/dest/service/libp2p_service.d.ts +5 -14
  18. package/dest/service/libp2p_service.d.ts.map +1 -1
  19. package/dest/service/libp2p_service.js +56 -97
  20. package/dest/service/peer_manager.d.ts +25 -3
  21. package/dest/service/peer_manager.d.ts.map +1 -1
  22. package/dest/service/peer_manager.js +152 -12
  23. package/dest/service/service.d.ts +12 -6
  24. package/dest/service/service.d.ts.map +1 -1
  25. package/dest/service/service.js +1 -1
  26. package/dest/tx_pool/aztec_kv_tx_pool.d.ts +2 -1
  27. package/dest/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
  28. package/dest/tx_pool/aztec_kv_tx_pool.js +11 -6
  29. package/dest/tx_pool/instrumentation.d.ts +23 -0
  30. package/dest/tx_pool/instrumentation.d.ts.map +1 -0
  31. package/dest/tx_pool/instrumentation.js +48 -0
  32. package/dest/tx_pool/memory_tx_pool.d.ts +3 -1
  33. package/dest/tx_pool/memory_tx_pool.d.ts.map +1 -1
  34. package/dest/tx_pool/memory_tx_pool.js +6 -2
  35. package/dest/util.d.ts +21 -0
  36. package/dest/util.d.ts.map +1 -0
  37. package/dest/util.js +59 -0
  38. package/package.json +15 -6
  39. package/src/bootstrap/bootstrap.ts +11 -5
  40. package/src/client/index.ts +28 -21
  41. package/src/client/p2p_client.ts +1 -2
  42. package/src/config.ts +19 -39
  43. package/src/service/discV5_service.ts +45 -15
  44. package/src/service/dummy_service.ts +10 -1
  45. package/src/service/libp2p_service.ts +60 -108
  46. package/src/service/peer_manager.ts +187 -12
  47. package/src/service/service.ts +14 -7
  48. package/src/tx_pool/aztec_kv_tx_pool.ts +12 -3
  49. package/src/tx_pool/instrumentation.ts +58 -0
  50. package/src/tx_pool/memory_tx_pool.ts +8 -1
  51. package/src/util.ts +62 -0
  52. package/dest/service/ip_query.d.ts +0 -2
  53. package/dest/service/ip_query.d.ts.map +0 -1
  54. package/dest/service/ip_query.js +0 -6
  55. package/dest/service/known_txs.d.ts +0 -31
  56. package/dest/service/known_txs.d.ts.map +0 -1
  57. package/dest/service/known_txs.js +0 -52
  58. package/src/service/ip_query.ts +0 -5
  59. package/src/service/known_txs.ts +0 -56
@@ -1,17 +1,20 @@
1
1
  import { createDebugLogger } from '@aztec/foundation/log';
2
- import { RunningPromise } from '@aztec/foundation/running-promise';
2
+ import { sleep } from '@aztec/foundation/sleep';
3
3
 
4
4
  import { Discv5, type Discv5EventEmitter } from '@chainsafe/discv5';
5
- import { type ENR, SignableENR } from '@chainsafe/enr';
5
+ import { ENR, SignableENR } from '@chainsafe/enr';
6
6
  import type { PeerId } from '@libp2p/interface';
7
7
  import { multiaddr } from '@multiformats/multiaddr';
8
8
  import EventEmitter from 'events';
9
9
 
10
10
  import type { P2PConfig } from '../config.js';
11
+ import { convertToMultiaddr } from '../util.js';
11
12
  import { type PeerDiscoveryService, PeerDiscoveryState } from './service.js';
12
13
 
13
14
  export const AZTEC_ENR_KEY = 'aztec_network';
14
15
 
16
+ const delayBeforeStart = 2000; // 2sec
17
+
15
18
  export enum AztecENR {
16
19
  devnet = 0x01,
17
20
  testnet = 0x02,
@@ -31,26 +34,33 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
31
34
  /** This instance's ENR */
32
35
  private enr: SignableENR;
33
36
 
34
- private runningPromise: RunningPromise;
35
-
36
37
  private currentState = PeerDiscoveryState.STOPPED;
37
38
 
38
39
  private bootstrapNodes: string[];
40
+ private bootstrapNodePeerIds: PeerId[] = [];
41
+
42
+ private startTime = 0;
39
43
 
40
44
  constructor(private peerId: PeerId, config: P2PConfig, private logger = createDebugLogger('aztec:discv5_service')) {
41
45
  super();
42
- const { announceUdpHostname, announceTcpHostname, tcpListenPort, udpListenIp, udpListenPort, bootstrapNodes } =
43
- config;
46
+ const { tcpAnnounceAddress, udpAnnounceAddress, udpListenAddress, bootstrapNodes } = config;
44
47
  this.bootstrapNodes = bootstrapNodes;
45
48
  // create ENR from PeerId
46
49
  this.enr = SignableENR.createFromPeerId(peerId);
47
50
  // Add aztec identification to ENR
48
51
  this.enr.set(AZTEC_ENR_KEY, Uint8Array.from([AZTEC_NET]));
49
52
 
50
- const multiAddrUdp = multiaddr(`${announceUdpHostname}/udp/${udpListenPort}/p2p/${peerId.toString()}`);
51
- const multiAddrTcp = multiaddr(`${announceTcpHostname}/tcp/${tcpListenPort}/p2p/${peerId.toString()}`);
53
+ if (!tcpAnnounceAddress) {
54
+ throw new Error('You need to provide at least a TCP announce address.');
55
+ }
56
+
57
+ const multiAddrTcp = multiaddr(`${convertToMultiaddr(tcpAnnounceAddress, 'tcp')}/p2p/${peerId.toString()}`);
58
+ // if no udp announce address is provided, use the tcp announce address
59
+ const multiAddrUdp = multiaddr(
60
+ `${convertToMultiaddr(udpAnnounceAddress || tcpAnnounceAddress, 'udp')}/p2p/${peerId.toString()}`,
61
+ );
52
62
 
53
- const listenMultiAddrUdp = multiaddr(`/ip4/${udpListenIp}/udp/${udpListenPort}`);
63
+ const listenMultiAddrUdp = multiaddr(convertToMultiaddr(udpListenAddress, 'udp'));
54
64
 
55
65
  // set location multiaddr in ENR record
56
66
  this.enr.setLocationMultiaddr(multiAddrUdp);
@@ -75,18 +85,18 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
75
85
  const multiAddrUdp = await enr.getFullMultiaddr('udp');
76
86
  this.logger.debug(`ENR multiaddr: ${multiAddrTcp?.toString()}, ${multiAddrUdp?.toString()}`);
77
87
  });
78
-
79
- this.runningPromise = new RunningPromise(async () => {
80
- await this.discv5.findRandomNode();
81
- }, config.p2pPeerCheckIntervalMS);
82
88
  }
83
89
 
84
90
  public async start(): Promise<void> {
91
+ // Do this conversion once since it involves an async function call
92
+ this.bootstrapNodePeerIds = await Promise.all(this.bootstrapNodes.map(enr => ENR.decodeTxt(enr).peerId()));
85
93
  if (this.currentState === PeerDiscoveryState.RUNNING) {
86
94
  throw new Error('DiscV5Service already started');
87
95
  }
88
96
  this.logger.info('Starting DiscV5');
89
97
  await this.discv5.start();
98
+ this.startTime = Date.now();
99
+
90
100
  this.logger.info('DiscV5 started');
91
101
  this.currentState = PeerDiscoveryState.RUNNING;
92
102
 
@@ -101,8 +111,25 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
101
111
  this.logger.error(`Error adding bootnode ENRs: ${e}`);
102
112
  }
103
113
  }
114
+ }
104
115
 
105
- this.runningPromise.start();
116
+ public async runRandomNodesQuery(): Promise<void> {
117
+ if (this.currentState !== PeerDiscoveryState.RUNNING) {
118
+ throw new Error('DiscV5Service not running');
119
+ }
120
+
121
+ // First, wait some time before starting the peer discovery
122
+ // reference: https://github.com/ChainSafe/lodestar/issues/3423
123
+ const msSinceStart = Date.now() - this.startTime;
124
+ if (Date.now() - this.startTime <= delayBeforeStart) {
125
+ await sleep(delayBeforeStart - msSinceStart);
126
+ }
127
+
128
+ try {
129
+ await this.discv5.findRandomNode();
130
+ } catch (err) {
131
+ this.logger.error(`Error running discV5 random node query: ${err}`);
132
+ }
106
133
  }
107
134
 
108
135
  public getAllPeers(): ENR[] {
@@ -121,8 +148,11 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
121
148
  return this.currentState;
122
149
  }
123
150
 
151
+ public isBootstrapPeer(peerId: PeerId): boolean {
152
+ return this.bootstrapNodePeerIds.some(node => node.equals(peerId));
153
+ }
154
+
124
155
  public async stop(): Promise<void> {
125
- await this.runningPromise.stop();
126
156
  await this.discv5.stop();
127
157
  this.currentState = PeerDiscoveryState.STOPPED;
128
158
  }
@@ -1,5 +1,6 @@
1
- import { type Tx, type TxHash } from '@aztec/circuit-types';
1
+ import type { Tx, TxHash } from '@aztec/circuit-types';
2
2
 
3
+ import type { PeerId } from '@libp2p/interface';
3
4
  import EventEmitter from 'events';
4
5
 
5
6
  import { type P2PService, type PeerDiscoveryService, PeerDiscoveryState } from './service.js';
@@ -66,6 +67,14 @@ export class DummyPeerDiscoveryService extends EventEmitter implements PeerDisco
66
67
  return [];
67
68
  }
68
69
 
70
+ public runRandomNodesQuery(): Promise<void> {
71
+ return Promise.resolve();
72
+ }
73
+
74
+ public isBootstrapPeer(_: PeerId): boolean {
75
+ return false;
76
+ }
77
+
69
78
  public getStatus(): PeerDiscoveryState {
70
79
  return this.currentState;
71
80
  }
@@ -1,25 +1,24 @@
1
- import { type Tx, type TxHash } from '@aztec/circuit-types';
1
+ import { type Tx } from '@aztec/circuit-types';
2
2
  import { SerialQueue } from '@aztec/foundation/fifo';
3
3
  import { createDebugLogger } from '@aztec/foundation/log';
4
- import { type AztecKVStore } from '@aztec/kv-store';
4
+ import { RunningPromise } from '@aztec/foundation/running-promise';
5
+ import type { AztecKVStore } from '@aztec/kv-store';
5
6
 
6
- import { ENR } from '@chainsafe/enr';
7
7
  import { type GossipsubEvents, gossipsub } from '@chainsafe/libp2p-gossipsub';
8
8
  import { noise } from '@chainsafe/libp2p-noise';
9
9
  import { yamux } from '@chainsafe/libp2p-yamux';
10
10
  import { identify } from '@libp2p/identify';
11
- import type { PeerId, PubSub, Stream } from '@libp2p/interface';
11
+ import type { PeerId, PubSub } from '@libp2p/interface';
12
12
  import '@libp2p/kad-dht';
13
13
  import { mplex } from '@libp2p/mplex';
14
- import { peerIdFromString } from '@libp2p/peer-id';
15
14
  import { createFromJSON, createSecp256k1PeerId } from '@libp2p/peer-id-factory';
16
15
  import { tcp } from '@libp2p/tcp';
17
16
  import { type Libp2p, createLibp2p } from 'libp2p';
18
17
 
19
18
  import { type P2PConfig } from '../config.js';
20
19
  import { type TxPool } from '../tx_pool/index.js';
20
+ import { convertToMultiaddr } from '../util.js';
21
21
  import { AztecDatastore } from './data_store.js';
22
- import { KnownTxLookup } from './known_txs.js';
23
22
  import { PeerManager } from './peer_manager.js';
24
23
  import type { P2PService, PeerDiscoveryService } from './service.js';
25
24
  import { AztecTxMessageCreator, fromTxMessage } from './tx_messages.js';
@@ -29,7 +28,6 @@ export interface PubSubLibp2p extends Libp2p {
29
28
  pubsub: PubSub<GossipsubEvents>;
30
29
  };
31
30
  }
32
-
33
31
  /**
34
32
  * Create a libp2p peer ID from the private key if provided, otherwise creates a new random ID.
35
33
  * @param privateKey - Optional peer ID private key as hex string
@@ -51,16 +49,14 @@ export async function createLibP2PPeerId(privateKey?: string): Promise<PeerId> {
51
49
  */
52
50
  export class LibP2PService implements P2PService {
53
51
  private jobQueue: SerialQueue = new SerialQueue();
54
- private knownTxLookup: KnownTxLookup = new KnownTxLookup();
55
52
  private messageCreator: AztecTxMessageCreator;
56
53
  private peerManager: PeerManager;
54
+ private discoveryRunningPromise?: RunningPromise;
57
55
  constructor(
58
56
  private config: P2PConfig,
59
57
  private node: PubSubLibp2p,
60
58
  private peerDiscoveryService: PeerDiscoveryService,
61
- private protocolId: string,
62
59
  private txPool: TxPool,
63
- private bootstrapPeerIds: PeerId[] = [],
64
60
  private logger = createDebugLogger('aztec:libp2p_service'),
65
61
  ) {
66
62
  this.messageCreator = new AztecTxMessageCreator(config.txGossipVersion);
@@ -72,53 +68,42 @@ export class LibP2PService implements P2PService {
72
68
  * @returns An empty promise.
73
69
  */
74
70
  public async start() {
71
+ // Check if service is already started
75
72
  if (this.node.status === 'started') {
76
73
  throw new Error('P2P service already started');
77
74
  }
78
- const { enableNat, tcpListenIp, tcpListenPort, announceTcpHostname, announcePort } = this.config;
79
- this.logger.info(`Starting P2P node on ${tcpListenIp}:${tcpListenPort}`);
80
- if (announceTcpHostname) {
81
- this.logger.info(`Announcing at ${announceTcpHostname}/tcp/${announcePort ?? tcpListenPort}`);
82
- }
83
- if (enableNat) {
84
- this.logger.info(`Enabling NAT in libp2p module`);
85
- }
86
-
87
- // handle discovered peers from external discovery service
88
- this.peerDiscoveryService.on('peer:discovered', async (enr: ENR) => {
89
- await this.addPeer(enr);
90
- });
91
75
 
92
- this.node.addEventListener('peer:connect', async evt => {
93
- const peerId = evt.detail;
94
- await this.handleNewConnection(peerId as PeerId);
95
- });
96
-
97
- this.node.addEventListener('peer:disconnect', async evt => {
98
- const peerId = evt.detail;
99
- if (this.isBootstrapPeer(peerId)) {
100
- this.logger.verbose(`Disconnect from bootstrap peer ${peerId.toString()}`);
101
- } else {
102
- this.logger.verbose(`Disconnected from transaction peer ${peerId.toString()}`);
103
- await this.peerManager.updateDiscoveryService();
104
- }
105
- });
76
+ // Log listen & announce addresses
77
+ const { tcpListenAddress, tcpAnnounceAddress } = this.config;
78
+ this.logger.info(`Starting P2P node on ${tcpListenAddress}`);
79
+ if (!tcpAnnounceAddress) {
80
+ throw new Error('Announce address not provided.');
81
+ }
82
+ const announceTcpMultiaddr = convertToMultiaddr(tcpAnnounceAddress, 'tcp');
83
+ this.logger.info(`Announcing at ${announceTcpMultiaddr}`);
106
84
 
85
+ // Start job queue, peer discovery service and libp2p node
107
86
  this.jobQueue.start();
108
87
  await this.peerDiscoveryService.start();
109
88
  await this.node.start();
110
89
  this.logger.info(`Started P2P client with Peer ID ${this.node.peerId.toString()}`);
111
90
 
112
- // Subscribe to standard topics by default
91
+ // Subscribe to standard GossipSub topics by default
113
92
  this.subscribeToTopic(this.messageCreator.getTopic());
114
93
 
115
- // add gossipsub listener
94
+ // add GossipSub listener
116
95
  this.node.services.pubsub.addEventListener('gossipsub:message', async e => {
117
96
  const { msg } = e.detail;
118
97
  this.logger.debug(`Received PUBSUB message.`);
119
98
 
120
99
  await this.jobQueue.put(() => this.handleNewGossipMessage(msg.topic, msg.data));
121
100
  });
101
+
102
+ // Start running promise for peer discovery
103
+ this.discoveryRunningPromise = new RunningPromise(() => {
104
+ this.peerManager.discover();
105
+ }, this.config.p2pPeerCheckIntervalMS);
106
+ this.discoveryRunningPromise.start();
122
107
  }
123
108
 
124
109
  /**
@@ -128,8 +113,12 @@ export class LibP2PService implements P2PService {
128
113
  public async stop() {
129
114
  this.logger.debug('Stopping job queue...');
130
115
  await this.jobQueue.end();
116
+ this.logger.debug('Stopping running promise...');
117
+ await this.discoveryRunningPromise?.stop();
118
+ this.logger.debug('Stopping peer discovery service...');
119
+ await this.peerDiscoveryService.stop();
131
120
  this.logger.debug('Stopping LibP2P...');
132
- await this.node.stop();
121
+ await this.stopLibP2P();
133
122
  this.logger.info('LibP2P service stopped');
134
123
  }
135
124
 
@@ -146,8 +135,12 @@ export class LibP2PService implements P2PService {
146
135
  txPool: TxPool,
147
136
  store: AztecKVStore,
148
137
  ) {
149
- const { tcpListenIp, tcpListenPort, minPeerCount, maxPeerCount, transactionProtocol: protocolId } = config;
150
- const bindAddrTcp = `/ip4/${tcpListenIp}/tcp/${tcpListenPort}`;
138
+ const { tcpListenAddress, tcpAnnounceAddress, minPeerCount, maxPeerCount } = config;
139
+ const bindAddrTcp = convertToMultiaddr(tcpListenAddress, 'tcp');
140
+ // We know tcpAnnounceAddress cannot be null here because we set it or throw when setting up the service.
141
+ const announceAddrTcp = convertToMultiaddr(tcpAnnounceAddress!, 'tcp');
142
+
143
+ const datastore = new AztecDatastore(store);
151
144
 
152
145
  // The autonat service seems quite problematic in that using it seems to cause a lot of attempts
153
146
  // to dial ephemeral ports. I suspect that it works better if you can get the uPNPnat service to
@@ -163,17 +156,24 @@ export class LibP2PService implements P2PService {
163
156
  // services.uPnPNAT = uPnPNATService();
164
157
  // }
165
158
 
166
- const datastore = new AztecDatastore(store);
167
-
168
159
  const node = await createLibp2p({
169
160
  start: false,
170
161
  peerId,
171
162
  addresses: {
172
163
  listen: [bindAddrTcp],
164
+ announce: [announceAddrTcp],
173
165
  },
174
166
  transports: [
175
167
  tcp({
176
168
  maxConnections: config.maxPeerCount,
169
+ // socket option: the maximum length of the queue of pending connections
170
+ // https://nodejs.org/dist/latest-v18.x/docs/api/net.html#serverlisten
171
+ // it's not safe if we increase this number
172
+ backlog: 5,
173
+ closeServerOnMaxConnections: {
174
+ closeAbove: maxPeerCount ?? Infinity,
175
+ listenBelow: maxPeerCount ?? Infinity,
176
+ },
177
177
  }),
178
178
  ],
179
179
  datastore,
@@ -199,15 +199,7 @@ export class LibP2PService implements P2PService {
199
199
  },
200
200
  });
201
201
 
202
- // extract bootstrap node peer IDs
203
- let bootstrapPeerIds: PeerId[] = [];
204
- if (config.bootstrapNodes.length) {
205
- bootstrapPeerIds = await Promise.all(
206
- config.bootstrapNodes.map(bootnodeEnr => ENR.decodeTxt(bootnodeEnr).peerId()),
207
- );
208
- }
209
-
210
- return new LibP2PService(config, node, peerDiscoveryService, protocolId, txPool, bootstrapPeerIds);
202
+ return new LibP2PService(config, node, peerDiscoveryService, txPool);
211
203
  }
212
204
 
213
205
  /**
@@ -259,71 +251,31 @@ export class LibP2PService implements P2PService {
259
251
  void this.jobQueue.put(() => Promise.resolve(this.sendTxToPeers(tx)));
260
252
  }
261
253
 
262
- /**
263
- * Handles the settling of a new batch of transactions.
264
- * @param txHashes - The hashes of the newly settled transactions.
265
- */
266
- public settledTxs(txHashes: TxHash[]): void {
267
- this.knownTxLookup.handleSettledTxs(txHashes.map(x => x.toString()));
268
- }
269
-
270
- private async addPeer(enr: ENR) {
271
- const peerMultiAddr = await enr.getFullMultiaddr('tcp');
272
- if (!peerMultiAddr) {
273
- // No TCP address, can't connect
274
- return;
275
- }
276
- const peerIdStr = peerMultiAddr.getPeerId();
277
-
278
- if (!peerIdStr) {
279
- this.logger.debug(`Peer ID not found in discovered node's multiaddr: ${peerMultiAddr}`);
280
- return;
281
- }
282
-
283
- // check if peer is already known
284
- const peerId = peerIdFromString(peerIdStr);
285
- const hasPeer = await this.node.peerStore.has(peerId);
286
-
287
- // add to peer store if not already known
288
- if (!hasPeer) {
289
- this.logger.info(`Discovered peer ${peerIdStr}. Adding to libp2p peer list`);
290
- let stream: Stream | undefined;
291
- try {
292
- stream = await this.node.dialProtocol(peerMultiAddr, this.protocolId);
293
- } catch (err) {
294
- this.logger.error(`Failed to dial peer ${peerIdStr}`, err);
295
- } finally {
296
- if (stream) {
297
- await stream.close();
298
- }
299
- }
300
- }
301
- }
302
-
303
- private async handleNewConnection(peerId: PeerId) {
304
- if (this.isBootstrapPeer(peerId)) {
305
- this.logger.verbose(`Connected to bootstrap peer ${peerId.toString()}`);
306
- } else {
307
- this.logger.verbose(`Connected to transaction peer ${peerId.toString()}`);
308
- await this.peerManager.updateDiscoveryService();
309
- }
310
- }
311
-
312
254
  private async processTxFromPeer(tx: Tx): Promise<void> {
313
255
  const txHash = tx.getTxHash();
314
256
  const txHashString = txHash.toString();
315
- this.logger.debug(`Received tx ${txHashString} from external peer.`);
257
+ this.logger.verbose(`Received tx ${txHashString} from external peer.`);
316
258
  await this.txPool.addTxs([tx]);
317
259
  }
318
260
 
319
261
  private async sendTxToPeers(tx: Tx) {
320
262
  const { data: txData } = this.messageCreator.createTxMessage(tx);
321
- this.logger.debug(`Sending tx ${tx.getTxHash().toString()} to peers`);
263
+ this.logger.verbose(`Sending tx ${tx.getTxHash().toString()} to peers`);
322
264
  const recipientsNum = await this.publishToTopic(this.messageCreator.getTopic(), txData);
323
- this.logger.debug(`Sent tx ${tx.getTxHash().toString()} to ${recipientsNum} peers`);
265
+ this.logger.verbose(`Sent tx ${tx.getTxHash().toString()} to ${recipientsNum} peers`);
324
266
  }
325
267
 
326
- private isBootstrapPeer(peer: PeerId) {
327
- return this.bootstrapPeerIds.some(bootstrapPeer => bootstrapPeer.equals(peer));
268
+ // Libp2p seems to hang sometimes if new peers are initiating connections.
269
+ private async stopLibP2P() {
270
+ const TIMEOUT_MS = 5000; // 5 seconds timeout
271
+ const timeout = new Promise((resolve, reject) => {
272
+ setTimeout(() => reject(new Error('Timeout during libp2p.stop()')), TIMEOUT_MS);
273
+ });
274
+ try {
275
+ await Promise.race([this.node.stop(), timeout]);
276
+ this.logger.debug('Libp2p stopped');
277
+ } catch (error) {
278
+ this.logger.error('Error during stop or timeout:', error);
279
+ }
328
280
  }
329
281
  }
@@ -1,26 +1,201 @@
1
1
  import { createDebugLogger } from '@aztec/foundation/log';
2
2
 
3
+ import { type ENR } from '@chainsafe/enr';
4
+ import { type PeerId } from '@libp2p/interface';
5
+ import { type Multiaddr } from '@multiformats/multiaddr';
3
6
  import { type Libp2p } from 'libp2p';
4
7
 
5
8
  import { type P2PConfig } from '../config.js';
6
- import { type PeerDiscoveryService, PeerDiscoveryState } from './service.js';
9
+ import { type PeerDiscoveryService } from './service.js';
10
+
11
+ const MAX_DIAL_ATTEMPTS = 3;
12
+ const MAX_CACHED_PEERS = 100;
13
+
14
+ type CachedPeer = {
15
+ peerId: PeerId;
16
+ enr: ENR;
17
+ multiaddrTcp: Multiaddr;
18
+ dialAttempts: number;
19
+ };
7
20
 
8
21
  export class PeerManager {
22
+ private cachedPeers: Map<string, CachedPeer> = new Map();
9
23
  constructor(
10
24
  private libP2PNode: Libp2p,
11
- private discV5Node: PeerDiscoveryService,
25
+ private peerDiscoveryService: PeerDiscoveryService,
12
26
  private config: P2PConfig,
13
27
  private logger = createDebugLogger('aztec:p2p:peer_manager'),
14
- ) {}
15
-
16
- async updateDiscoveryService() {
17
- const peerCount = this.libP2PNode.getPeers().length;
18
- if (peerCount >= this.config.maxPeerCount && this.discV5Node.getStatus() === PeerDiscoveryState.RUNNING) {
19
- this.logger.debug('Max peer count reached, stopping discovery service');
20
- await this.discV5Node.stop();
21
- } else if (peerCount <= this.config.minPeerCount && this.discV5Node.getStatus() === PeerDiscoveryState.STOPPED) {
22
- this.logger.debug('Min peer count reached, starting discovery service');
23
- await this.discV5Node.start();
28
+ ) {
29
+ // Handle new established connections
30
+ this.libP2PNode.addEventListener('peer:connect', evt => {
31
+ const peerId = evt.detail;
32
+ if (this.peerDiscoveryService.isBootstrapPeer(peerId)) {
33
+ this.logger.debug(`Connected to bootstrap peer ${peerId.toString()}`);
34
+ } else {
35
+ this.logger.debug(`Connected to transaction peer ${peerId.toString()}`);
36
+ }
37
+ });
38
+
39
+ // Handle lost connections
40
+ this.libP2PNode.addEventListener('peer:disconnect', evt => {
41
+ const peerId = evt.detail;
42
+ if (this.peerDiscoveryService.isBootstrapPeer(peerId)) {
43
+ this.logger.debug(`Disconnected from bootstrap peer ${peerId.toString()}`);
44
+ } else {
45
+ this.logger.debug(`Disconnected from transaction peer ${peerId.toString()}`);
46
+ }
47
+ });
48
+
49
+ // Handle Discovered peers
50
+ this.peerDiscoveryService.on('peer:discovered', async (enr: ENR) => {
51
+ await this.handleDiscoveredPeer(enr);
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Discovers peers.
57
+ */
58
+ public discover() {
59
+ // Get current connections
60
+ const connections = this.libP2PNode.getConnections();
61
+
62
+ // Calculate how many connections we're looking to make
63
+ const peersToConnect = this.config.maxPeerCount - connections.length;
64
+
65
+ this.logger.debug(
66
+ `Connections: ${connections.length}, Peers to connect: ${peersToConnect}, maxPeerCount: ${this.config.maxPeerCount}, cachedPeers: ${this.cachedPeers.size}`,
67
+ );
68
+
69
+ // Exit if no peers to connect
70
+ if (peersToConnect <= 0) {
71
+ return;
72
+ }
73
+
74
+ const cachedPeersToDial: CachedPeer[] = [];
75
+
76
+ const pendingDials = new Set(
77
+ this.libP2PNode
78
+ .getDialQueue()
79
+ .map(pendingDial => pendingDial.peerId?.toString())
80
+ .filter(Boolean) as string[],
81
+ );
82
+
83
+ for (const [id, peerData] of this.cachedPeers.entries()) {
84
+ // if already dialling or connected to, remove from cache
85
+ if (pendingDials.has(id) || connections.some(conn => conn.remotePeer.equals(peerData.peerId))) {
86
+ this.cachedPeers.delete(id);
87
+ } else {
88
+ // cachedPeersToDial.set(id, enr);
89
+ cachedPeersToDial.push(peerData);
90
+ }
91
+ }
92
+
93
+ // reverse to dial older entries first
94
+ cachedPeersToDial.reverse();
95
+
96
+ for (const peer of cachedPeersToDial) {
97
+ this.cachedPeers.delete(peer.peerId.toString());
98
+ void this.dialPeer(peer);
99
+ }
100
+
101
+ // if we need more peers, start randomNodesQuery
102
+ if (peersToConnect > 0) {
103
+ this.logger.debug('Running random nodes query');
104
+ void this.peerDiscoveryService.runRandomNodesQuery();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Handles a discovered peer.
110
+ * @param enr - The discovered peer's ENR.
111
+ */
112
+ private async handleDiscoveredPeer(enr: ENR) {
113
+ // TODO: Will be handling peer scoring here
114
+
115
+ // check if peer is already connected
116
+ const [peerId, multiaddrTcp] = await Promise.all([enr.peerId(), enr.getFullMultiaddr('tcp')]);
117
+
118
+ this.logger.debug(`Handling discovered peer ${peerId.toString()}, ${multiaddrTcp?.toString()}`);
119
+
120
+ // throw if no tcp addr in multiaddr
121
+ if (!multiaddrTcp) {
122
+ this.logger.debug(`No TCP address in discovered node's multiaddr: ${enr.toString()}`);
123
+ return;
124
+ }
125
+ const connections = this.libP2PNode.getConnections();
126
+ if (connections.some(conn => conn.remotePeer.equals(peerId))) {
127
+ this.logger.debug(`Already connected to peer ${peerId.toString()}`);
128
+ return;
129
+ }
130
+
131
+ // check if peer is already in cache
132
+ const id = peerId.toString();
133
+ if (this.cachedPeers.has(id)) {
134
+ this.logger.debug(`Already in cache ${id}`);
135
+ return;
136
+ }
137
+
138
+ // create cached peer object
139
+ const cachedPeer: CachedPeer = {
140
+ peerId,
141
+ enr,
142
+ multiaddrTcp,
143
+ dialAttempts: 0,
144
+ };
145
+
146
+ // Determine if we should dial immediately or not
147
+ if (this.shouldDialPeer()) {
148
+ this.logger.debug(`Dialing peer ${id}`);
149
+ void this.dialPeer(cachedPeer);
150
+ } else {
151
+ this.logger.debug(`Caching peer ${id}`);
152
+ this.cachedPeers.set(id, cachedPeer);
153
+ // Prune set of cached peers
154
+ this.pruneCachedPeers();
155
+ }
156
+ }
157
+
158
+ async dialPeer(peer: CachedPeer) {
159
+ const id = peer.peerId.toString();
160
+ await this.libP2PNode.peerStore.merge(peer.peerId, { multiaddrs: [peer.multiaddrTcp] });
161
+
162
+ this.logger.debug(`Dialing peer ${id}`);
163
+ try {
164
+ await this.libP2PNode.dial(peer.multiaddrTcp);
165
+ } catch {
166
+ this.logger.debug(`Failed to dial peer ${id}`);
167
+ peer.dialAttempts++;
168
+ if (peer.dialAttempts < MAX_DIAL_ATTEMPTS) {
169
+ this.cachedPeers.set(id, peer);
170
+ } else {
171
+ this.cachedPeers.delete(id);
172
+ }
173
+ }
174
+ }
175
+
176
+ private shouldDialPeer(): boolean {
177
+ const connections = this.libP2PNode.getConnections().length;
178
+ this.logger.debug(`Connections: ${connections}, maxPeerCount: ${this.config.maxPeerCount}`);
179
+ if (connections >= this.config.maxPeerCount) {
180
+ this.logger.debug('Not dialing peer, maxPeerCount reached');
181
+ return false;
182
+ }
183
+ return true;
184
+ }
185
+
186
+ private pruneCachedPeers() {
187
+ let peersToDelete = this.cachedPeers.size - MAX_CACHED_PEERS;
188
+ if (peersToDelete <= 0) {
189
+ return;
190
+ }
191
+
192
+ // Remove the oldest peers
193
+ for (const key of this.cachedPeers.keys()) {
194
+ this.cachedPeers.delete(key);
195
+ peersToDelete--;
196
+ if (peersToDelete <= 0) {
197
+ break;
198
+ }
24
199
  }
25
200
  }
26
201
  }
@@ -1,6 +1,7 @@
1
- import type { Tx, TxHash } from '@aztec/circuit-types';
1
+ import type { Tx } from '@aztec/circuit-types';
2
2
 
3
3
  import type { ENR } from '@chainsafe/enr';
4
+ import type { PeerId } from '@libp2p/interface';
4
5
  import type EventEmitter from 'events';
5
6
 
6
7
  export enum PeerDiscoveryState {
@@ -29,12 +30,6 @@ export interface P2PService {
29
30
  * @param tx - The transaction to be propagated.
30
31
  */
31
32
  propagateTx(tx: Tx): void;
32
-
33
- /**
34
- * Called upon receipt of settled transactions.
35
- * @param txHashes - The hashes of the settled transactions.
36
- */
37
- settledTxs(txHashes: TxHash[]): void;
38
33
  }
39
34
 
40
35
  /**
@@ -57,6 +52,18 @@ export interface PeerDiscoveryService extends EventEmitter {
57
52
  */
58
53
  getAllPeers(): ENR[];
59
54
 
55
+ /**
56
+ * Runs findRandomNode query.
57
+ */
58
+ runRandomNodesQuery(): Promise<void>;
59
+
60
+ /**
61
+ * Checks if the given peer is a bootstrap peer.
62
+ * @param peerId - The peer ID to check.
63
+ * @returns True if the peer is a bootstrap peer.
64
+ */
65
+ isBootstrapPeer(peerId: PeerId): boolean;
66
+
60
67
  /**
61
68
  * Event emitted when a new peer is discovered.
62
69
  */