@aztec/p2p 0.71.0 → 0.72.1

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 (107) hide show
  1. package/dest/client/p2p_client.d.ts.map +1 -1
  2. package/dest/client/p2p_client.js +6 -6
  3. package/dest/mem_pools/attestation_pool/mocks.d.ts +2 -1
  4. package/dest/mem_pools/attestation_pool/mocks.d.ts.map +1 -1
  5. package/dest/mem_pools/attestation_pool/mocks.js +1 -1
  6. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
  7. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.js +2 -2
  8. package/dest/mocks/index.d.ts +3 -3
  9. package/dest/mocks/index.d.ts.map +1 -1
  10. package/dest/mocks/index.js +19 -18
  11. package/dest/services/dummy_service.d.ts +7 -0
  12. package/dest/services/dummy_service.d.ts.map +1 -1
  13. package/dest/services/dummy_service.js +10 -1
  14. package/dest/services/libp2p/libp2p_service.d.ts +17 -13
  15. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  16. package/dest/services/libp2p/libp2p_service.js +109 -101
  17. package/dest/services/peer-manager/metrics.d.ts +12 -0
  18. package/dest/services/peer-manager/metrics.d.ts.map +1 -0
  19. package/dest/services/peer-manager/metrics.js +26 -0
  20. package/dest/services/{peer_manager.d.ts → peer-manager/peer_manager.d.ts} +23 -8
  21. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -0
  22. package/dest/services/peer-manager/peer_manager.js +392 -0
  23. package/dest/services/{peer-scoring → peer-manager}/peer_scoring.d.ts +3 -0
  24. package/dest/services/peer-manager/peer_scoring.d.ts.map +1 -0
  25. package/dest/services/peer-manager/peer_scoring.js +84 -0
  26. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts +45 -0
  27. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts.map +1 -0
  28. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.js +81 -0
  29. package/dest/services/reqresp/connection-sampler/connection_sampler.d.ts +61 -0
  30. package/dest/services/reqresp/connection-sampler/connection_sampler.d.ts.map +1 -0
  31. package/dest/services/reqresp/connection-sampler/connection_sampler.js +175 -0
  32. package/dest/services/reqresp/interface.d.ts +17 -4
  33. package/dest/services/reqresp/interface.d.ts.map +1 -1
  34. package/dest/services/reqresp/interface.js +34 -11
  35. package/dest/services/reqresp/metrics.d.ts +15 -0
  36. package/dest/services/reqresp/metrics.d.ts.map +1 -0
  37. package/dest/services/reqresp/metrics.js +42 -0
  38. package/dest/services/reqresp/protocols/block.d.ts +4 -0
  39. package/dest/services/reqresp/protocols/block.d.ts.map +1 -0
  40. package/dest/services/reqresp/protocols/block.js +9 -0
  41. package/dest/services/reqresp/protocols/goodbye.d.ts +51 -0
  42. package/dest/services/reqresp/protocols/goodbye.d.ts.map +1 -0
  43. package/dest/services/reqresp/protocols/goodbye.js +92 -0
  44. package/dest/services/reqresp/protocols/index.d.ts +9 -0
  45. package/dest/services/reqresp/protocols/index.d.ts.map +1 -0
  46. package/dest/services/reqresp/protocols/index.js +9 -0
  47. package/dest/services/reqresp/protocols/ping.d.ts +9 -0
  48. package/dest/services/reqresp/protocols/ping.d.ts.map +1 -0
  49. package/dest/services/reqresp/protocols/ping.js +9 -0
  50. package/dest/services/reqresp/{handlers.d.ts → protocols/status.d.ts} +1 -7
  51. package/dest/services/reqresp/protocols/status.d.ts.map +1 -0
  52. package/dest/services/reqresp/protocols/status.js +9 -0
  53. package/dest/services/reqresp/protocols/tx.d.ts +13 -0
  54. package/dest/services/reqresp/protocols/tx.d.ts.map +1 -0
  55. package/dest/services/reqresp/protocols/tx.js +23 -0
  56. package/dest/services/reqresp/rate-limiter/index.d.ts.map +1 -0
  57. package/dest/services/reqresp/{rate_limiter → rate-limiter}/index.js +1 -1
  58. package/dest/services/reqresp/{rate_limiter → rate-limiter}/rate_limiter.d.ts +3 -3
  59. package/dest/services/reqresp/{rate_limiter → rate-limiter}/rate_limiter.d.ts.map +1 -1
  60. package/dest/services/reqresp/{rate_limiter → rate-limiter}/rate_limiter.js +4 -4
  61. package/dest/services/reqresp/rate-limiter/rate_limits.d.ts.map +1 -0
  62. package/dest/services/reqresp/rate-limiter/rate_limits.js +55 -0
  63. package/dest/services/reqresp/reqresp.d.ts +33 -6
  64. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  65. package/dest/services/reqresp/reqresp.js +414 -249
  66. package/dest/services/service.d.ts +8 -0
  67. package/dest/services/service.d.ts.map +1 -1
  68. package/dest/util.d.ts +4 -0
  69. package/dest/util.d.ts.map +1 -1
  70. package/dest/util.js +1 -1
  71. package/package.json +8 -8
  72. package/src/client/p2p_client.ts +5 -5
  73. package/src/mem_pools/attestation_pool/mocks.ts +2 -2
  74. package/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +0 -1
  75. package/src/mocks/index.ts +19 -20
  76. package/src/services/dummy_service.ts +13 -0
  77. package/src/services/libp2p/libp2p_service.ts +143 -128
  78. package/src/services/peer-manager/metrics.ts +41 -0
  79. package/src/services/{peer_manager.ts → peer-manager/peer_manager.ts} +67 -22
  80. package/src/services/{peer-scoring → peer-manager}/peer_scoring.ts +16 -3
  81. package/src/services/reqresp/connection-sampler/batch_connection_sampler.ts +94 -0
  82. package/src/services/reqresp/connection-sampler/connection_sampler.ts +211 -0
  83. package/src/services/reqresp/interface.ts +39 -16
  84. package/src/services/reqresp/metrics.ts +57 -0
  85. package/src/services/reqresp/protocols/block.ts +15 -0
  86. package/src/services/reqresp/protocols/goodbye.ts +101 -0
  87. package/src/services/reqresp/protocols/index.ts +8 -0
  88. package/src/services/reqresp/protocols/ping.ts +8 -0
  89. package/src/services/reqresp/{handlers.ts → protocols/status.ts} +0 -9
  90. package/src/services/reqresp/protocols/tx.ts +29 -0
  91. package/src/services/reqresp/{rate_limiter → rate-limiter}/rate_limiter.ts +3 -3
  92. package/src/services/reqresp/{rate_limiter → rate-limiter}/rate_limits.ts +24 -4
  93. package/src/services/reqresp/reqresp.ts +224 -25
  94. package/src/services/service.ts +12 -0
  95. package/src/util.ts +4 -0
  96. package/dest/services/peer-scoring/peer_scoring.d.ts.map +0 -1
  97. package/dest/services/peer-scoring/peer_scoring.js +0 -75
  98. package/dest/services/peer_manager.d.ts.map +0 -1
  99. package/dest/services/peer_manager.js +0 -358
  100. package/dest/services/reqresp/handlers.d.ts.map +0 -1
  101. package/dest/services/reqresp/handlers.js +0 -17
  102. package/dest/services/reqresp/rate_limiter/index.d.ts.map +0 -1
  103. package/dest/services/reqresp/rate_limiter/rate_limits.d.ts.map +0 -1
  104. package/dest/services/reqresp/rate_limiter/rate_limits.js +0 -35
  105. /package/dest/services/reqresp/{rate_limiter → rate-limiter}/index.d.ts +0 -0
  106. /package/dest/services/reqresp/{rate_limiter → rate-limiter}/rate_limits.d.ts +0 -0
  107. /package/src/services/reqresp/{rate_limiter → rate-limiter}/index.ts +0 -0
@@ -0,0 +1,41 @@
1
+ import {
2
+ Attributes,
3
+ Metrics,
4
+ type TelemetryClient,
5
+ type Tracer,
6
+ type UpDownCounter,
7
+ ValueType,
8
+ } from '@aztec/telemetry-client';
9
+
10
+ import { type GoodByeReason, prettyGoodbyeReason } from '../reqresp/protocols/index.js';
11
+
12
+ export class PeerManagerMetrics {
13
+ private sentGoodbyes: UpDownCounter;
14
+ private receivedGoodbyes: UpDownCounter;
15
+
16
+ public readonly tracer: Tracer;
17
+
18
+ constructor(public readonly telemetryClient: TelemetryClient, name = 'PeerManager') {
19
+ this.tracer = telemetryClient.getTracer(name);
20
+
21
+ const meter = telemetryClient.getMeter(name);
22
+ this.sentGoodbyes = meter.createUpDownCounter(Metrics.PEER_MANAGER_GOODBYES_SENT, {
23
+ description: 'Number of goodbyes sent to peers',
24
+ unit: 'peers',
25
+ valueType: ValueType.INT,
26
+ });
27
+ this.receivedGoodbyes = meter.createUpDownCounter(Metrics.PEER_MANAGER_GOODBYES_RECEIVED, {
28
+ description: 'Number of goodbyes received from peers',
29
+ unit: 'peers',
30
+ valueType: ValueType.INT,
31
+ });
32
+ }
33
+
34
+ public recordGoodbyeSent(reason: GoodByeReason) {
35
+ this.sentGoodbyes.add(1, { [Attributes.P2P_GOODBYE_REASON]: prettyGoodbyeReason(reason) });
36
+ }
37
+
38
+ public recordGoodbyeReceived(reason: GoodByeReason) {
39
+ this.receivedGoodbyes.add(1, { [Attributes.P2P_GOODBYE_REASON]: prettyGoodbyeReason(reason) });
40
+ }
41
+ }
@@ -1,17 +1,21 @@
1
1
  import { type PeerErrorSeverity, type PeerInfo } from '@aztec/circuit-types';
2
2
  import { createLogger } from '@aztec/foundation/log';
3
- import { type TelemetryClient, WithTracer, trackSpan } from '@aztec/telemetry-client';
3
+ import { type TelemetryClient, trackSpan } from '@aztec/telemetry-client';
4
4
 
5
5
  import { type ENR } from '@chainsafe/enr';
6
6
  import { type Connection, type PeerId } from '@libp2p/interface';
7
7
  import { type Multiaddr } from '@multiformats/multiaddr';
8
8
  import { inspect } from 'util';
9
9
 
10
- import { type P2PConfig } from '../config.js';
11
- import { type PubSubLibp2p } from '../util.js';
12
- import { PeerScoreState, PeerScoring } from './peer-scoring/peer_scoring.js';
13
- import { type PeerDiscoveryService } from './service.js';
14
- import { PeerEvent } from './types.js';
10
+ import { type P2PConfig } from '../../config.js';
11
+ import { type PubSubLibp2p } from '../../util.js';
12
+ import { ReqRespSubProtocol } from '../reqresp/interface.js';
13
+ import { GoodByeReason, prettyGoodbyeReason } from '../reqresp/protocols/goodbye.js';
14
+ import { type ReqResp } from '../reqresp/reqresp.js';
15
+ import { type PeerDiscoveryService } from '../service.js';
16
+ import { PeerEvent } from '../types.js';
17
+ import { PeerManagerMetrics } from './metrics.js';
18
+ import { PeerScoreState, type PeerScoring } from './peer_scoring.js';
15
19
 
16
20
  const MAX_DIAL_ATTEMPTS = 3;
17
21
  const MAX_CACHED_PEERS = 100;
@@ -31,23 +35,25 @@ type TimedOutPeer = {
31
35
  timeoutUntilMs: number;
32
36
  };
33
37
 
34
- export class PeerManager extends WithTracer {
38
+ export class PeerManager {
35
39
  private cachedPeers: Map<string, CachedPeer> = new Map();
36
- private peerScoring: PeerScoring;
37
40
  private heartbeatCounter: number = 0;
38
41
  private displayPeerCountsPeerHeartbeat: number = 0;
39
42
  private timedOutPeers: Map<string, TimedOutPeer> = new Map();
40
43
 
44
+ private metrics: PeerManagerMetrics;
45
+
41
46
  constructor(
42
47
  private libP2PNode: PubSubLibp2p,
43
48
  private peerDiscoveryService: PeerDiscoveryService,
44
49
  private config: P2PConfig,
45
50
  telemetryClient: TelemetryClient,
46
51
  private logger = createLogger('p2p:peer-manager'),
52
+ private peerScoring: PeerScoring,
53
+ private reqresp: ReqResp,
47
54
  ) {
48
- super(telemetryClient, 'PeerManager');
55
+ this.metrics = new PeerManagerMetrics(telemetryClient, 'PeerManager');
49
56
 
50
- this.peerScoring = new PeerScoring(config);
51
57
  // Handle new established connections
52
58
  this.libP2PNode.addEventListener(PeerEvent.CONNECTED, this.handleConnectedPeerEvent.bind(this));
53
59
  // Handle lost connections
@@ -60,6 +66,10 @@ export class PeerManager extends WithTracer {
60
66
  this.displayPeerCountsPeerHeartbeat = Math.floor(60_000 / this.config.peerCheckIntervalMS);
61
67
  }
62
68
 
69
+ get tracer() {
70
+ return this.metrics.tracer;
71
+ }
72
+
63
73
  @trackSpan('PeerManager.heartbeat')
64
74
  public heartbeat() {
65
75
  this.heartbeatCounter++;
@@ -113,11 +123,23 @@ export class PeerManager extends WithTracer {
113
123
  }
114
124
  }
115
125
 
126
+ /**
127
+ * Handles a goodbye received from a peer.
128
+ *
129
+ * Used as the reqresp handler when a peer sends us goodbye message.
130
+ * @param peerId - The peer ID.
131
+ * @param reason - The reason for the goodbye.
132
+ */
133
+ public goodbyeReceived(peerId: PeerId, reason: GoodByeReason) {
134
+ this.logger.debug(`Goodbye received from peer ${peerId.toString()} with reason ${prettyGoodbyeReason(reason)}`);
135
+
136
+ this.metrics.recordGoodbyeReceived(reason);
137
+
138
+ void this.disconnectPeer(peerId);
139
+ }
140
+
116
141
  public penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity) {
117
- const id = peerId.toString();
118
- const penaltyValue = this.peerScoring.peerPenalties[penalty];
119
- const newScore = this.peerScoring.updateScore(id, -penaltyValue);
120
- this.logger.verbose(`Penalizing peer ${id} with ${penalty} (new score is ${newScore})`);
142
+ this.peerScoring.penalizePeer(peerId, penalty);
121
143
  }
122
144
 
123
145
  public getPeerScore(peerId: string): number {
@@ -227,10 +249,11 @@ export class PeerManager extends WithTracer {
227
249
  for (const peer of connections) {
228
250
  const score = this.peerScoring.getScoreState(peer.remotePeer.toString());
229
251
  switch (score) {
230
- // TODO: add goodbye and give reasons
231
252
  case PeerScoreState.Banned:
253
+ void this.goodbyeAndDisconnectPeer(peer.remotePeer, GoodByeReason.BANNED);
254
+ break;
232
255
  case PeerScoreState.Disconnect:
233
- void this.disconnectPeer(peer.remotePeer);
256
+ void this.goodbyeAndDisconnectPeer(peer.remotePeer, GoodByeReason.DISCONNECTED);
234
257
  break;
235
258
  case PeerScoreState.Healthy:
236
259
  connectedHealthyPeers.push(peer);
@@ -240,10 +263,26 @@ export class PeerManager extends WithTracer {
240
263
  return connectedHealthyPeers;
241
264
  }
242
265
 
243
- // TODO: send a goodbye with a reason to the peer
266
+ private async goodbyeAndDisconnectPeer(peer: PeerId, reason: GoodByeReason) {
267
+ this.logger.debug(`Disconnecting peer ${peer.toString()} with reason ${prettyGoodbyeReason(reason)}`);
268
+
269
+ this.metrics.recordGoodbyeSent(reason);
270
+
271
+ try {
272
+ await this.reqresp.sendRequestToPeer(peer, ReqRespSubProtocol.GOODBYE, Buffer.from([reason]));
273
+ } catch (error) {
274
+ this.logger.debug(`Failed to send goodbye to peer ${peer.toString()}: ${error}`);
275
+ } finally {
276
+ await this.disconnectPeer(peer);
277
+ }
278
+ }
279
+
244
280
  private async disconnectPeer(peer: PeerId) {
245
- this.logger.debug(`Disconnecting peer ${peer.toString()}`);
246
- await this.libP2PNode.hangUp(peer);
281
+ try {
282
+ await this.libP2PNode.hangUp(peer);
283
+ } catch (error) {
284
+ this.logger.debug(`Failed to disconnect peer ${peer.toString()}`, { error: inspect(error) });
285
+ }
247
286
  }
248
287
 
249
288
  /**
@@ -281,7 +320,7 @@ export class PeerManager extends WithTracer {
281
320
  }
282
321
  // check if peer is already connected
283
322
  const connections = this.libP2PNode.getConnections();
284
- if (connections.some(conn => conn.remotePeer.equals(peerId))) {
323
+ if (connections.some((conn: Connection) => conn.remotePeer.equals(peerId))) {
285
324
  this.logger.trace(`Already connected to peer ${peerId}`);
286
325
  return;
287
326
  }
@@ -371,10 +410,16 @@ export class PeerManager extends WithTracer {
371
410
  * Stops the peer manager.
372
411
  * Removing all event listeners.
373
412
  */
374
- public stop() {
413
+ public async stop() {
414
+ this.peerDiscoveryService.off(PeerEvent.DISCOVERED, this.handleDiscoveredPeer);
415
+
416
+ // Send goodbyes to all peers
417
+ await Promise.all(
418
+ this.libP2PNode.getPeers().map(peer => this.goodbyeAndDisconnectPeer(peer, GoodByeReason.SHUTDOWN)),
419
+ );
420
+
375
421
  this.libP2PNode.removeEventListener(PeerEvent.CONNECTED, this.handleConnectedPeerEvent);
376
422
  this.libP2PNode.removeEventListener(PeerEvent.DISCONNECTED, this.handleDisconnectedPeerEvent);
377
- this.peerDiscoveryService.off(PeerEvent.DISCOVERED, this.handleDiscoveredPeer);
378
423
  }
379
424
  }
380
425
 
@@ -1,5 +1,8 @@
1
1
  import { PeerErrorSeverity } from '@aztec/circuit-types';
2
2
  import { median } from '@aztec/foundation/collection';
3
+ import { createLogger } from '@aztec/foundation/log';
4
+
5
+ import { type PeerId } from '@libp2p/interface';
3
6
 
4
7
  import { type P2PConfig } from '../../config.js';
5
8
 
@@ -20,6 +23,7 @@ const MIN_SCORE_BEFORE_BAN = -100;
20
23
  const MIN_SCORE_BEFORE_DISCONNECT = -50;
21
24
 
22
25
  export class PeerScoring {
26
+ private logger = createLogger('p2p:peer-scoring');
23
27
  private scores: Map<string, number> = new Map();
24
28
  private lastUpdateTime: Map<string, number> = new Map();
25
29
  private decayInterval = 1000 * 60; // 1 minute
@@ -38,6 +42,14 @@ export class PeerScoring {
38
42
  };
39
43
  }
40
44
 
45
+ public penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity) {
46
+ const id = peerId.toString();
47
+ const penaltyValue = this.peerPenalties[penalty];
48
+ const newScore = this.updateScore(id, -penaltyValue);
49
+ this.logger.verbose(`Penalizing peer ${id} with ${penalty} (new score is ${newScore})`);
50
+ return newScore;
51
+ }
52
+
41
53
  updateScore(peerId: string, scoreDelta: number): number {
42
54
  const currentTime = Date.now();
43
55
  const lastUpdate = this.lastUpdateTime.get(peerId) || currentTime;
@@ -75,12 +87,13 @@ export class PeerScoring {
75
87
  return this.scores.get(peerId) || 0;
76
88
  }
77
89
 
78
- getScoreState(peerId: string) {
79
- // TODO: permanently store banned peers???
90
+ public getScoreState(peerId: string): PeerScoreState {
91
+ // TODO(#11329): permanently store banned peers?
80
92
  const score = this.getScore(peerId);
81
93
  if (score < MIN_SCORE_BEFORE_BAN) {
82
94
  return PeerScoreState.Banned;
83
- } else if (score < MIN_SCORE_BEFORE_DISCONNECT) {
95
+ }
96
+ if (score < MIN_SCORE_BEFORE_DISCONNECT) {
84
97
  return PeerScoreState.Disconnect;
85
98
  }
86
99
  return PeerScoreState.Healthy;
@@ -0,0 +1,94 @@
1
+ import { createLogger } from '@aztec/foundation/log';
2
+
3
+ import { type PeerId } from '@libp2p/interface';
4
+
5
+ import { type ConnectionSampler } from './connection_sampler.js';
6
+
7
+ /**
8
+ * Manages batches of peers for parallel request processing.
9
+ * Tracks active peers and provides deterministic peer assignment for requests.
10
+ *
11
+ * Example with 3 peers and 10 requests:
12
+ *
13
+ * Peers: [P1] [P2] [P3]
14
+ * ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
15
+ * Requests: 0,1,2,9 | 3,4,5 | 6,7,8
16
+ *
17
+ * Each peer handles a bucket of consecutive requests.
18
+ * If a peer fails, it is replaced while maintaining the same bucket.
19
+ */
20
+ export class BatchConnectionSampler {
21
+ private readonly logger = createLogger('p2p:reqresp:batch-connection-sampler');
22
+ private readonly batch: PeerId[] = [];
23
+ private readonly requestsPerPeer: number;
24
+
25
+ constructor(private readonly connectionSampler: ConnectionSampler, batchSize: number, maxPeers: number) {
26
+ if (maxPeers <= 0) {
27
+ throw new Error('Max peers cannot be 0');
28
+ }
29
+ if (batchSize <= 0) {
30
+ throw new Error('Batch size cannot be 0');
31
+ }
32
+
33
+ // Calculate how many requests each peer should handle, cannot be 0
34
+ this.requestsPerPeer = Math.max(1, Math.floor(batchSize / maxPeers));
35
+
36
+ // Sample initial peers
37
+ this.batch = this.connectionSampler.samplePeersBatch(maxPeers);
38
+ }
39
+
40
+ /**
41
+ * Gets the peer responsible for handling a specific request index
42
+ *
43
+ * @param index - The request index
44
+ * @returns The peer assigned to handle this request
45
+ */
46
+ getPeerForRequest(index: number): PeerId | undefined {
47
+ if (this.batch.length === 0) {
48
+ return undefined;
49
+ }
50
+
51
+ // Calculate which peer bucket this index belongs to
52
+ const peerIndex = Math.floor(index / this.requestsPerPeer) % this.batch.length;
53
+ return this.batch[peerIndex];
54
+ }
55
+
56
+ /**
57
+ * Removes a peer and replaces it with a new one, maintaining the same position
58
+ * in the batch array to keep request distribution consistent
59
+ *
60
+ * @param peerId - The peer to remove and replace
61
+ */
62
+ removePeerAndReplace(peerId: PeerId): void {
63
+ const index = this.batch.findIndex(p => p === peerId);
64
+ if (index === -1) {
65
+ return;
66
+ }
67
+
68
+ const excluding = new Map([[peerId.toString(), true]]);
69
+ const newPeer = this.connectionSampler.getPeer(excluding);
70
+
71
+ if (newPeer) {
72
+ this.batch[index] = newPeer;
73
+ this.logger.trace(`Replaced peer ${peerId} with ${newPeer}`, { peerId, newPeer });
74
+ } else {
75
+ // If we couldn't get a replacement, remove the peer and compact the array
76
+ this.batch.splice(index, 1);
77
+ this.logger.trace(`Removed peer ${peerId}`, { peerId });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Gets the number of active peers
83
+ */
84
+ get activePeerCount(): number {
85
+ return this.batch.length;
86
+ }
87
+
88
+ /**
89
+ * Gets the number of requests each peer is assigned to handle
90
+ */
91
+ get requestsPerBucket(): number {
92
+ return this.requestsPerPeer;
93
+ }
94
+ }
@@ -0,0 +1,211 @@
1
+ import { createLogger } from '@aztec/foundation/log';
2
+ import { SerialQueue } from '@aztec/foundation/queue';
3
+
4
+ import { type Libp2p, type PeerId, type Stream } from '@libp2p/interface';
5
+
6
+ const MAX_SAMPLE_ATTEMPTS = 4;
7
+
8
+ interface StreamAndPeerId {
9
+ stream: Stream;
10
+ peerId: PeerId;
11
+ }
12
+
13
+ export class RandomSampler {
14
+ random(max: number) {
15
+ return Math.floor(Math.random() * max);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * A class that samples peers from the libp2p node and returns a peer that we don't already have a connection open to.
21
+ * If we already have a connection open, we try to sample a different peer.
22
+ * We do this MAX_SAMPLE_ATTEMPTS times, if we still don't find a peer we just go for it.
23
+ *
24
+ * @dev Close must always be called on connections, else memory leak
25
+ */
26
+ export class ConnectionSampler {
27
+ private readonly logger = createLogger('p2p:reqresp:connection-sampler');
28
+ private cleanupInterval: NodeJS.Timeout;
29
+ private abortController: AbortController = new AbortController();
30
+
31
+ private readonly activeConnectionsCount: Map<PeerId, number> = new Map();
32
+ private readonly streams: Map<string, StreamAndPeerId> = new Map();
33
+
34
+ // Serial queue to ensure that we only dial one peer at a time
35
+ private dialQueue: SerialQueue = new SerialQueue();
36
+
37
+ constructor(
38
+ private readonly libp2p: Libp2p,
39
+ private readonly cleanupIntervalMs: number = 60000, // Default to 1 minute
40
+ private readonly sampler: RandomSampler = new RandomSampler(), // Allow randomness to be mocked for testing
41
+ ) {
42
+ this.cleanupInterval = setInterval(() => void this.cleanupStaleConnections(), this.cleanupIntervalMs);
43
+
44
+ this.dialQueue.start();
45
+ }
46
+
47
+ /**
48
+ * Stops the cleanup job and closes all active connections
49
+ */
50
+ async stop() {
51
+ this.logger.info('Stopping connection sampler');
52
+ clearInterval(this.cleanupInterval);
53
+
54
+ this.abortController.abort();
55
+ await this.dialQueue.end();
56
+
57
+ // Close all active streams
58
+ const closePromises = Array.from(this.streams.keys()).map(streamId => this.close(streamId));
59
+ await Promise.all(closePromises);
60
+ this.logger.info('Connection sampler stopped');
61
+ }
62
+
63
+ /**
64
+ *
65
+ * @param excluding - The peers to exclude from the sampling
66
+ * This is to prevent sampling with replacement
67
+ * @returns
68
+ */
69
+ getPeer(excluding?: Map<string, boolean>): PeerId | undefined {
70
+ // In libp2p getPeers performs a shallow copy, so this array can be sliced from safetly
71
+ const peers = this.libp2p.getPeers();
72
+
73
+ if (peers.length === 0) {
74
+ return undefined;
75
+ }
76
+
77
+ let randomIndex = this.sampler.random(peers.length);
78
+ let attempts = 0;
79
+
80
+ // Keep sampling while:
81
+ // - we haven't exceeded max attempts AND
82
+ // - either the peer has active connections OR is in the exclusion list
83
+ while (
84
+ attempts < MAX_SAMPLE_ATTEMPTS &&
85
+ ((this.activeConnectionsCount.get(peers[randomIndex]) ?? 0) > 0 ||
86
+ (excluding?.get(peers[randomIndex]?.toString()) ?? false))
87
+ ) {
88
+ peers.splice(randomIndex, 1);
89
+ randomIndex = this.sampler.random(peers.length);
90
+ attempts++;
91
+ }
92
+
93
+ this.logger.trace(`Sampled peer in ${attempts} attempts`, {
94
+ attempts,
95
+ peer: peers[randomIndex]?.toString(),
96
+ });
97
+ return peers[randomIndex];
98
+ }
99
+
100
+ /**
101
+ * Samples a batch of unique peers from the libp2p node, prioritizing peers without active connections
102
+ *
103
+ * @param numberToSample - The number of peers to sample
104
+ * @returns Array of unique sampled peers, prioritizing those without active connections
105
+ */
106
+ samplePeersBatch(numberToSample: number): PeerId[] {
107
+ const peers = this.libp2p.getPeers();
108
+ const sampledPeers: PeerId[] = [];
109
+ const peersWithConnections: PeerId[] = []; // Hold onto peers with active connections incase we need to sample more
110
+
111
+ for (const peer of peers) {
112
+ const activeConnections = this.activeConnectionsCount.get(peer) ?? 0;
113
+ if (activeConnections === 0) {
114
+ if (sampledPeers.push(peer) === numberToSample) {
115
+ return sampledPeers;
116
+ }
117
+ } else {
118
+ peersWithConnections.push(peer);
119
+ }
120
+ }
121
+
122
+ // If we still need more peers, sample from those with connections
123
+ while (sampledPeers.length < numberToSample && peersWithConnections.length > 0) {
124
+ const randomIndex = this.sampler.random(peersWithConnections.length);
125
+ const [peer] = peersWithConnections.splice(randomIndex, 1);
126
+ sampledPeers.push(peer);
127
+ }
128
+
129
+ this.logger.trace(`Batch sampled ${sampledPeers.length} unique peers`, {
130
+ peers: sampledPeers,
131
+ withoutConnections: sampledPeers.length - peersWithConnections.length,
132
+ withConnections: peersWithConnections.length,
133
+ });
134
+
135
+ return sampledPeers;
136
+ }
137
+
138
+ // Set of passthrough functions to keep track of active connections
139
+
140
+ /**
141
+ * Dials a protocol and returns the stream
142
+ *
143
+ * @param peerId - The peer id
144
+ * @param protocol - The protocol
145
+ * @returns The stream
146
+ */
147
+ async dialProtocol(peerId: PeerId, protocol: string): Promise<Stream> {
148
+ // Dialling at the same time can cause race conditions where two different streams
149
+ // end up with the same id, hence a serial queue
150
+ const stream = await this.dialQueue.put(() =>
151
+ this.libp2p.dialProtocol(peerId, protocol, { signal: this.abortController.signal }),
152
+ );
153
+
154
+ this.streams.set(stream.id, { stream, peerId });
155
+ const updatedActiveConnectionsCount = (this.activeConnectionsCount.get(peerId) ?? 0) + 1;
156
+ this.activeConnectionsCount.set(peerId, updatedActiveConnectionsCount);
157
+
158
+ this.logger.trace(`Dialed protocol ${protocol} with peer ${peerId.toString()}`, {
159
+ streamId: stream.id,
160
+ peerId: peerId.toString(),
161
+ activeConnectionsCount: updatedActiveConnectionsCount,
162
+ });
163
+ return stream;
164
+ }
165
+
166
+ /**
167
+ * Closes a stream and updates the active connections count
168
+ *
169
+ * @param streamId - The stream id
170
+ */
171
+ async close(streamId: string): Promise<void> {
172
+ try {
173
+ const { stream, peerId } = this.streams.get(streamId)!;
174
+
175
+ const updatedActiveConnectionsCount = (this.activeConnectionsCount.get(peerId) ?? 1) - 1;
176
+ this.activeConnectionsCount.set(peerId, updatedActiveConnectionsCount);
177
+
178
+ this.logger.trace(`Closing connection to peer ${peerId.toString()}`, {
179
+ streamId,
180
+ peerId: peerId.toString(),
181
+ protocol: stream.protocol,
182
+ activeConnectionsCount: updatedActiveConnectionsCount,
183
+ });
184
+
185
+ await stream?.close();
186
+ } catch (error) {
187
+ this.logger.warn(`Failed to close connection to peer with stream id ${streamId}`);
188
+ } finally {
189
+ this.streams.delete(streamId);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Cleans up stale connections that we have lost accounting for
195
+ */
196
+ private async cleanupStaleConnections() {
197
+ // Look for streams without anything in the activeConnectionsCount
198
+ // If we find anything, close the stream
199
+ for (const [streamId, { peerId }] of this.streams.entries()) {
200
+ try {
201
+ // Check if we have lost track of accounting
202
+ if (this.activeConnectionsCount.get(peerId) === 0) {
203
+ await this.close(streamId);
204
+ this.logger.debug(`Cleaned up stale connection ${streamId} to peer ${peerId.toString()}`);
205
+ }
206
+ } catch (error) {
207
+ this.logger.error(`Error cleaning up stale connection ${streamId}`, { error });
208
+ }
209
+ }
210
+ }
211
+ }
@@ -1,4 +1,5 @@
1
- import { Tx, TxHash } from '@aztec/circuit-types';
1
+ import { L2Block, Tx, TxHash } from '@aztec/circuit-types';
2
+ import { Fr } from '@aztec/foundation/fields';
2
3
 
3
4
  import { type PeerId } from '@libp2p/interface';
4
5
 
@@ -7,16 +8,23 @@ import { type PeerId } from '@libp2p/interface';
7
8
  */
8
9
  export const PING_PROTOCOL = '/aztec/req/ping/0.1.0';
9
10
  export const STATUS_PROTOCOL = '/aztec/req/status/0.1.0';
11
+ export const GOODBYE_PROTOCOL = '/aztec/req/goodbye/0.1.0';
10
12
  export const TX_REQ_PROTOCOL = '/aztec/req/tx/0.1.0';
11
-
12
- // Sum type for sub protocols
13
- export type ReqRespSubProtocol = typeof PING_PROTOCOL | typeof STATUS_PROTOCOL | typeof TX_REQ_PROTOCOL;
13
+ export const BLOCK_REQ_PROTOCOL = '/aztec/req/block/0.1.0';
14
+
15
+ export enum ReqRespSubProtocol {
16
+ PING = PING_PROTOCOL,
17
+ STATUS = STATUS_PROTOCOL,
18
+ GOODBYE = GOODBYE_PROTOCOL,
19
+ TX = TX_REQ_PROTOCOL,
20
+ BLOCK = BLOCK_REQ_PROTOCOL,
21
+ }
14
22
 
15
23
  /**
16
24
  * A handler for a sub protocol
17
25
  * The message will arrive as a buffer, and the handler must return a buffer
18
26
  */
19
- export type ReqRespSubProtocolHandler = (msg: Buffer) => Promise<Buffer>;
27
+ export type ReqRespSubProtocolHandler = (peerId: PeerId, msg: Buffer) => Promise<Buffer>;
20
28
 
21
29
  /**
22
30
  * A type mapping from supprotocol to it's rate limits
@@ -51,7 +59,7 @@ export interface ProtocolRateLimitQuota {
51
59
  export const noopValidator = () => Promise.resolve(true);
52
60
 
53
61
  /**
54
- * A type mapping from supprotocol to it's handling funciton
62
+ * A type mapping from supprotocol to it's handling function
55
63
  */
56
64
  export type ReqRespSubProtocolHandlers = Record<ReqRespSubProtocol, ReqRespSubProtocolHandler>;
57
65
 
@@ -66,9 +74,11 @@ export type ReqRespSubProtocolValidators = {
66
74
  };
67
75
 
68
76
  export const DEFAULT_SUB_PROTOCOL_VALIDATORS: ReqRespSubProtocolValidators = {
69
- [PING_PROTOCOL]: noopValidator,
70
- [STATUS_PROTOCOL]: noopValidator,
71
- [TX_REQ_PROTOCOL]: noopValidator,
77
+ [ReqRespSubProtocol.PING]: noopValidator,
78
+ [ReqRespSubProtocol.STATUS]: noopValidator,
79
+ [ReqRespSubProtocol.TX]: noopValidator,
80
+ [ReqRespSubProtocol.GOODBYE]: noopValidator,
81
+ [ReqRespSubProtocol.BLOCK]: noopValidator,
72
82
  };
73
83
 
74
84
  /**
@@ -91,16 +101,21 @@ const defaultHandler = (_msg: any): Promise<Buffer> => {
91
101
  * Default sub protocol handlers - this SHOULD be overwritten by the service,
92
102
  */
93
103
  export const DEFAULT_SUB_PROTOCOL_HANDLERS: ReqRespSubProtocolHandlers = {
94
- [PING_PROTOCOL]: defaultHandler,
95
- [STATUS_PROTOCOL]: defaultHandler,
96
- [TX_REQ_PROTOCOL]: defaultHandler,
104
+ [ReqRespSubProtocol.PING]: defaultHandler,
105
+ [ReqRespSubProtocol.STATUS]: defaultHandler,
106
+ [ReqRespSubProtocol.TX]: defaultHandler,
107
+ [ReqRespSubProtocol.GOODBYE]: defaultHandler,
108
+ [ReqRespSubProtocol.BLOCK]: defaultHandler,
97
109
  };
98
110
 
99
111
  /**
100
112
  * The Request Response Pair interface defines the methods that each
101
113
  * request response pair must implement
102
114
  */
103
- interface RequestResponsePair<Req, Res> {
115
+ interface RequestResponsePair<Req extends { toBuffer(): Buffer }, Res> {
116
+ /**
117
+ * The request must implement the toBuffer method (generic serialisation)
118
+ */
104
119
  request: new (...args: any[]) => Req;
105
120
  /**
106
121
  * The response must implement the static fromBuffer method (generic serialisation)
@@ -135,16 +150,24 @@ export class RequestableBuffer {
135
150
  * as a type rather than an object
136
151
  */
137
152
  export const subProtocolMap: SubProtocolMap = {
138
- [PING_PROTOCOL]: {
153
+ [ReqRespSubProtocol.PING]: {
139
154
  request: RequestableBuffer,
140
155
  response: RequestableBuffer,
141
156
  },
142
- [STATUS_PROTOCOL]: {
157
+ [ReqRespSubProtocol.STATUS]: {
143
158
  request: RequestableBuffer,
144
159
  response: RequestableBuffer,
145
160
  },
146
- [TX_REQ_PROTOCOL]: {
161
+ [ReqRespSubProtocol.TX]: {
147
162
  request: TxHash,
148
163
  response: Tx,
149
164
  },
165
+ [ReqRespSubProtocol.GOODBYE]: {
166
+ request: RequestableBuffer,
167
+ response: RequestableBuffer,
168
+ },
169
+ [ReqRespSubProtocol.BLOCK]: {
170
+ request: Fr, // block number
171
+ response: L2Block,
172
+ },
150
173
  };