@aztec/p2p 0.85.0 → 0.86.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 (90) hide show
  1. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  2. package/dest/bootstrap/bootstrap.js +6 -3
  3. package/dest/client/p2p_client.d.ts +41 -3
  4. package/dest/client/p2p_client.d.ts.map +1 -1
  5. package/dest/client/p2p_client.js +58 -18
  6. package/dest/config.d.ts +13 -2
  7. package/dest/config.d.ts.map +1 -1
  8. package/dest/config.js +15 -3
  9. package/dest/enr/generate-enr.d.ts +1 -1
  10. package/dest/enr/generate-enr.d.ts.map +1 -1
  11. package/dest/enr/generate-enr.js +2 -2
  12. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  13. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +11 -11
  14. package/dest/mem_pools/attestation_pool/kv_attestation_pool.js +2 -2
  15. package/dest/mem_pools/attestation_pool/memory_attestation_pool.d.ts.map +1 -1
  16. package/dest/mem_pools/attestation_pool/memory_attestation_pool.js +4 -4
  17. package/dest/mem_pools/attestation_pool/mocks.d.ts +1 -1
  18. package/dest/mem_pools/attestation_pool/mocks.d.ts.map +1 -1
  19. package/dest/mem_pools/attestation_pool/mocks.js +2 -2
  20. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts +3 -0
  21. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
  22. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.js +18 -0
  23. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts +3 -0
  24. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool/memory_tx_pool.js +9 -0
  26. package/dest/mem_pools/tx_pool/tx_pool.d.ts +17 -0
  27. package/dest/mem_pools/tx_pool/tx_pool.d.ts.map +1 -1
  28. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  29. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +57 -0
  30. package/dest/msg_validators/attestation_validator/attestation_validator.js +1 -1
  31. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.d.ts +1 -0
  32. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.d.ts.map +1 -1
  33. package/dest/msg_validators/block_proposal_validator/block_proposal_validator.js +6 -1
  34. package/dest/services/discv5/discV5_service.d.ts +2 -1
  35. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  36. package/dest/services/discv5/discV5_service.js +22 -7
  37. package/dest/services/dummy_service.d.ts +2 -2
  38. package/dest/services/dummy_service.d.ts.map +1 -1
  39. package/dest/services/dummy_service.js +2 -2
  40. package/dest/services/libp2p/libp2p_service.d.ts +3 -1
  41. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  42. package/dest/services/libp2p/libp2p_service.js +28 -12
  43. package/dest/services/peer-manager/peer_manager.d.ts +21 -2
  44. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  45. package/dest/services/peer-manager/peer_manager.js +63 -18
  46. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.js +2 -2
  47. package/dest/services/reqresp/connection-sampler/connection_sampler.d.ts +17 -1
  48. package/dest/services/reqresp/connection-sampler/connection_sampler.d.ts.map +1 -1
  49. package/dest/services/reqresp/connection-sampler/connection_sampler.js +84 -36
  50. package/dest/services/reqresp/reqresp.d.ts +2 -2
  51. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  52. package/dest/services/reqresp/reqresp.js +7 -2
  53. package/dest/services/reqresp/status.d.ts +2 -1
  54. package/dest/services/reqresp/status.d.ts.map +1 -1
  55. package/dest/services/reqresp/status.js +3 -0
  56. package/dest/services/service.d.ts +4 -4
  57. package/dest/services/service.d.ts.map +1 -1
  58. package/dest/test-helpers/make-test-p2p-clients.d.ts +6 -1
  59. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  60. package/dest/test-helpers/make-test-p2p-clients.js +19 -2
  61. package/dest/testbench/p2p_client_testbench_worker.js +4 -1
  62. package/dest/util.d.ts.map +1 -1
  63. package/dest/util.js +5 -1
  64. package/package.json +12 -14
  65. package/src/bootstrap/bootstrap.ts +8 -4
  66. package/src/client/p2p_client.ts +212 -131
  67. package/src/config.ts +34 -4
  68. package/src/enr/generate-enr.ts +2 -2
  69. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +11 -15
  70. package/src/mem_pools/attestation_pool/kv_attestation_pool.ts +2 -2
  71. package/src/mem_pools/attestation_pool/memory_attestation_pool.ts +4 -4
  72. package/src/mem_pools/attestation_pool/mocks.ts +3 -3
  73. package/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +21 -0
  74. package/src/mem_pools/tx_pool/memory_tx_pool.ts +11 -0
  75. package/src/mem_pools/tx_pool/tx_pool.ts +20 -0
  76. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +43 -0
  77. package/src/msg_validators/attestation_validator/attestation_validator.ts +1 -1
  78. package/src/msg_validators/block_proposal_validator/block_proposal_validator.ts +10 -1
  79. package/src/services/discv5/discV5_service.ts +32 -6
  80. package/src/services/dummy_service.ts +2 -2
  81. package/src/services/libp2p/libp2p_service.ts +37 -12
  82. package/src/services/peer-manager/peer_manager.ts +79 -22
  83. package/src/services/reqresp/connection-sampler/batch_connection_sampler.ts +2 -2
  84. package/src/services/reqresp/connection-sampler/connection_sampler.ts +82 -41
  85. package/src/services/reqresp/reqresp.ts +12 -6
  86. package/src/services/reqresp/status.ts +3 -0
  87. package/src/services/service.ts +4 -4
  88. package/src/test-helpers/make-test-p2p-clients.ts +20 -2
  89. package/src/testbench/p2p_client_testbench_worker.ts +3 -0
  90. package/src/util.ts +6 -1
@@ -15,12 +15,14 @@ import {
15
15
  type Gossipable,
16
16
  P2PClientType,
17
17
  PeerErrorSeverity,
18
- TopicTypeMap,
18
+ TopicType,
19
+ createTopicString,
19
20
  getTopicTypeForClientType,
20
21
  metricsTopicStrToLabels,
21
22
  } from '@aztec/stdlib/p2p';
22
23
  import { DatabasePublicStateSource, MerkleTreeId } from '@aztec/stdlib/trees';
23
24
  import { Tx, type TxHash, type TxValidationResult } from '@aztec/stdlib/tx';
25
+ import { compressComponentVersions } from '@aztec/stdlib/versioning';
24
26
  import { Attributes, OtelMetricsAdapter, type TelemetryClient, WithTracer, trackSpan } from '@aztec/telemetry-client';
25
27
 
26
28
  import type { ENR } from '@chainsafe/enr';
@@ -57,6 +59,7 @@ import {
57
59
  } from '../../msg_validators/tx_validator/index.js';
58
60
  import { GossipSubEvent } from '../../types/index.js';
59
61
  import { type PubSubLibp2p, convertToMultiaddr } from '../../util.js';
62
+ import { getVersions } from '../../versioning.js';
60
63
  import { AztecDatastore } from '../data_store.js';
61
64
  import { SnappyTransform, fastMsgIdFn, getMsgIdFn, msgIdToStrFn } from '../encoding.js';
62
65
  import { gossipScoreThresholds } from '../gossipsub/scoring.js';
@@ -95,6 +98,9 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
95
98
  private attestationValidator: AttestationValidator;
96
99
  private blockProposalValidator: BlockProposalValidator;
97
100
 
101
+ private protocolVersion = '';
102
+ private topicStrings: Record<TopicType, string> = {} as Record<TopicType, string>;
103
+
98
104
  // Request and response sub service
99
105
  public reqresp: ReqResp;
100
106
 
@@ -127,6 +133,17 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
127
133
  ) {
128
134
  super(telemetry, 'LibP2PService');
129
135
 
136
+ const versions = getVersions(config);
137
+ this.protocolVersion = compressComponentVersions(versions);
138
+ logger.info(`Started libp2p service with protocol version ${this.protocolVersion}`);
139
+
140
+ this.topicStrings[TopicType.tx] = createTopicString(TopicType.tx, this.protocolVersion);
141
+ this.topicStrings[TopicType.block_proposal] = createTopicString(TopicType.block_proposal, this.protocolVersion);
142
+ this.topicStrings[TopicType.block_attestation] = createTopicString(
143
+ TopicType.block_attestation,
144
+ this.protocolVersion,
145
+ );
146
+
130
147
  const peerScoring = new PeerScoring(config);
131
148
  this.reqresp = new ReqResp(config, node, peerScoring);
132
149
 
@@ -198,6 +215,13 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
198
215
  peerDiscovery.push(bootstrap({ list: bootstrapNodes }));
199
216
  }
200
217
 
218
+ const versions = getVersions(config);
219
+ const protocolVersion = compressComponentVersions(versions);
220
+
221
+ const txTopic = createTopicString(TopicType.tx, protocolVersion);
222
+ const blockProposalTopic = createTopicString(TopicType.block_proposal, protocolVersion);
223
+ const blockAttestationTopic = createTopicString(TopicType.block_attestation, protocolVersion);
224
+
201
225
  const node = await createLibp2p({
202
226
  start: false,
203
227
  peerId,
@@ -251,24 +275,24 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
251
275
  fastMsgIdFn: fastMsgIdFn,
252
276
  dataTransform: new SnappyTransform(),
253
277
  metricsRegister: otelMetricsAdapter,
254
- metricsTopicStrToLabel: metricsTopicStrToLabels(),
278
+ metricsTopicStrToLabel: metricsTopicStrToLabels(protocolVersion),
255
279
  asyncValidation: true,
256
280
  scoreThresholds: gossipScoreThresholds,
257
281
  scoreParams: createPeerScoreParams({
258
282
  // IPColocation factor can be disabled for local testing - default to -5
259
283
  IPColocationFactorWeight: config.debugDisableColocationPenalty ? 0 : -5.0,
260
284
  topics: {
261
- [Tx.p2pTopic]: createTopicScoreParams({
285
+ [txTopic]: createTopicScoreParams({
262
286
  topicWeight: 1,
263
287
  invalidMessageDeliveriesWeight: -20,
264
288
  invalidMessageDeliveriesDecay: 0.5,
265
289
  }),
266
- [BlockAttestation.p2pTopic]: createTopicScoreParams({
290
+ [blockAttestationTopic]: createTopicScoreParams({
267
291
  topicWeight: 1,
268
292
  invalidMessageDeliveriesWeight: -20,
269
293
  invalidMessageDeliveriesDecay: 0.5,
270
294
  }),
271
- [BlockProposal.p2pTopic]: createTopicScoreParams({
295
+ [blockProposalTopic]: createTopicScoreParams({
272
296
  topicWeight: 1,
273
297
  invalidMessageDeliveriesWeight: -20,
274
298
  invalidMessageDeliveriesDecay: 0.5,
@@ -318,13 +342,13 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
318
342
  // Start job queue, peer discovery service and libp2p node
319
343
  this.jobQueue.start();
320
344
 
321
- await this.peerManager.initializeTrustedPeers();
345
+ await this.peerManager.initializePeers();
322
346
  await this.peerDiscoveryService.start();
323
347
  await this.node.start();
324
348
 
325
349
  // Subscribe to standard GossipSub topics by default
326
350
  for (const topic of getTopicTypeForClientType(this.clientType)) {
327
- this.subscribeToTopic(TopicTypeMap[topic].p2pTopic);
351
+ this.subscribeToTopic(this.topicStrings[topic]);
328
352
  }
329
353
 
330
354
  // Create request response protocol handlers
@@ -434,7 +458,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
434
458
  sendBatchRequest<SubProtocol extends ReqRespSubProtocol>(
435
459
  protocol: SubProtocol,
436
460
  requests: InstanceType<SubProtocolMap[SubProtocol]['request']>[],
437
- ): Promise<InstanceType<SubProtocolMap[SubProtocol]['response']>[] | undefined> {
461
+ ): Promise<(InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined)[]> {
438
462
  return this.reqresp.sendBatchRequest(protocol, requests);
439
463
  }
440
464
 
@@ -483,13 +507,13 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
483
507
  * @param data - The message data
484
508
  */
485
509
  protected async handleNewGossipMessage(msg: Message, msgId: string, source: PeerId) {
486
- if (msg.topic === Tx.p2pTopic) {
510
+ if (msg.topic === this.topicStrings[TopicType.tx]) {
487
511
  await this.handleGossipedTx(msg, msgId, source);
488
512
  }
489
- if (msg.topic === BlockAttestation.p2pTopic && this.clientType === P2PClientType.Full) {
513
+ if (msg.topic === this.topicStrings[TopicType.block_attestation] && this.clientType === P2PClientType.Full) {
490
514
  await this.processAttestationFromPeer(msg, msgId, source);
491
515
  }
492
- if (msg.topic == BlockProposal.p2pTopic) {
516
+ if (msg.topic === this.topicStrings[TopicType.block_proposal]) {
493
517
  await this.processBlockFromPeer(msg, msgId, source);
494
518
  }
495
519
 
@@ -884,6 +908,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
884
908
  public async validateBlockProposal(peerId: PeerId, block: BlockProposal): Promise<boolean> {
885
909
  const severity = await this.blockProposalValidator.validate(block);
886
910
  if (severity) {
911
+ this.logger.debug(`Penalizing peer ${peerId} for block proposal validation failure`);
887
912
  this.peerManager.penalizePeer(peerId, severity);
888
913
  return false;
889
914
  }
@@ -901,7 +926,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
901
926
  const identifier = await message.p2pMessageIdentifier().then(i => i.toString());
902
927
  this.logger.trace(`Sending message ${identifier}`, { p2pMessageIdentifier: identifier });
903
928
 
904
- const recipientsNum = await this.publishToTopic(parent.p2pTopic, message.toBuffer());
929
+ const recipientsNum = await this.publishToTopic(this.topicStrings[parent.p2pTopic], message.toBuffer());
905
930
  this.logger.debug(`Sent message ${identifier} to ${recipientsNum} peers`, {
906
931
  p2pMessageIdentifier: identifier,
907
932
  sourcePeer: this.node.peerId.toString(),
@@ -41,8 +41,10 @@ export class PeerManager {
41
41
  private heartbeatCounter: number = 0;
42
42
  private displayPeerCountsPeerHeartbeat: number = 0;
43
43
  private timedOutPeers: Map<string, TimedOutPeer> = new Map();
44
- private trustedPeers: Set<PeerId> = new Set();
44
+ private trustedPeers: Set<string> = new Set();
45
45
  private trustedPeersInitialized: boolean = false;
46
+ private privatePeers: Set<string> = new Set();
47
+ private privatePeersInitialized: boolean = false;
46
48
 
47
49
  private metrics: PeerManagerMetrics;
48
50
  private handlers: {
@@ -87,14 +89,34 @@ export class PeerManager {
87
89
  *
88
90
  * This function is called when the peer manager is initialized.
89
91
  */
90
- async initializeTrustedPeers() {
91
- const trustedPeersEnrs: ENR[] = this.config.trustedPeers.map(enr => ENR.decodeTxt(enr));
92
- await Promise.all(trustedPeersEnrs.map(enr => enr.peerId()))
93
- .then(peerIds => peerIds.forEach(peerId => this.trustedPeers.add(peerId)))
94
- .finally(() => {
95
- this.trustedPeersInitialized = true;
96
- })
97
- .catch(e => this.logger.error('Error initializing trusted peers', e));
92
+ async initializePeers() {
93
+ if (this.config.trustedPeers) {
94
+ const trustedPeersEnrs: ENR[] = this.config.trustedPeers.map(enr => ENR.decodeTxt(enr));
95
+ await Promise.all(trustedPeersEnrs.map(enr => enr.peerId()))
96
+ .then(peerIds => peerIds.forEach(peerId => this.trustedPeers.add(peerId.toString())))
97
+ .finally(() => {
98
+ this.trustedPeersInitialized = true;
99
+ })
100
+ .catch(e => this.logger.error('Error initializing trusted peers', e));
101
+ }
102
+
103
+ if (this.config.privatePeers) {
104
+ const privatePeersEnrs: ENR[] = this.config.privatePeers.map(enr => ENR.decodeTxt(enr));
105
+ await Promise.all(privatePeersEnrs.map(enr => enr.peerId()))
106
+ .then(peerIds =>
107
+ peerIds.forEach(peerId => {
108
+ this.trustedPeers.add(peerId.toString());
109
+ this.privatePeers.add(peerId.toString());
110
+ }),
111
+ )
112
+ .finally(() => {
113
+ if (!this.config.trustedPeers) {
114
+ this.trustedPeersInitialized = true;
115
+ }
116
+ this.privatePeersInitialized = true;
117
+ })
118
+ .catch(e => this.logger.error('Error initializing private peers', e));
119
+ }
98
120
  }
99
121
 
100
122
  get tracer() {
@@ -165,7 +187,7 @@ export class PeerManager {
165
187
  this.logger.warn('Trusted peers not initialized, returning false');
166
188
  return false;
167
189
  }
168
- return this.trustedPeers.has(peerId);
190
+ return this.trustedPeers.has(peerId.toString());
169
191
  }
170
192
 
171
193
  /**
@@ -173,9 +195,47 @@ export class PeerManager {
173
195
  * @param peerId - The peer ID to add to trusted peers.
174
196
  */
175
197
  public addTrustedPeer(peerId: PeerId): void {
176
- this.trustedPeers.add(peerId);
198
+ const peerIdStr = peerId.toString();
199
+
200
+ this.trustedPeers.add(peerIdStr);
201
+ this.trustedPeersInitialized = true;
202
+ this.logger.verbose(`Added trusted peer ${peerIdStr}`);
203
+ }
204
+
205
+ /**
206
+ * Adds a peer to the private peers set.
207
+ * @param peerId - The peer ID to add to private peers.
208
+ */
209
+ public addPrivatePeer(peerId: PeerId): void {
210
+ const peerIdStr = peerId.toString();
211
+
212
+ this.trustedPeers.add(peerIdStr);
213
+ this.privatePeers.add(peerIdStr);
177
214
  this.trustedPeersInitialized = true;
178
- this.logger.verbose(`Added trusted peer ${peerId.toString()}`);
215
+ this.privatePeersInitialized = true;
216
+ this.logger.verbose(`Added private peer ${peerIdStr}`);
217
+ }
218
+
219
+ /**
220
+ * Checks if a peer is private.
221
+ * @param peerId - The peer ID.
222
+ * @returns True if the peer is private, false otherwise.
223
+ */
224
+ private isPrivatePeer(peerId: PeerId): boolean {
225
+ if (!this.privatePeersInitialized) {
226
+ this.logger.warn('Private peers not initialized, returning false');
227
+ return false;
228
+ }
229
+ return this.privatePeers.has(peerId.toString());
230
+ }
231
+
232
+ /**
233
+ * Checks if a peer is protected (either trusted or private).
234
+ * @param peerId - The peer ID.
235
+ * @returns True if the peer is protected, false otherwise.
236
+ */
237
+ private isProtectedPeer(peerId: PeerId): boolean {
238
+ return this.isTrustedPeer(peerId) || this.isPrivatePeer(peerId);
179
239
  }
180
240
 
181
241
  /**
@@ -241,15 +301,16 @@ export class PeerManager {
241
301
  const connections = this.libP2PNode.getConnections();
242
302
 
243
303
  const healthyConnections = this.prioritizePeers(
244
- this.onlyNotTrustedPeers(this.pruneUnhealthyPeers(this.pruneDuplicatePeers(connections))),
304
+ this.pruneUnhealthyPeers(this.getNonProtectedPeers(this.pruneDuplicatePeers(connections))),
245
305
  );
246
306
 
247
307
  // Calculate how many connections we're looking to make
248
308
  const peersToConnect = this.config.maxPeerCount - healthyConnections.length - this.trustedPeers.size;
249
309
 
250
310
  const logLevel = this.heartbeatCounter % this.displayPeerCountsPeerHeartbeat === 0 ? 'info' : 'debug';
251
- this.logger[logLevel](`Connected to ${healthyConnections.length} peers`, {
252
- connections: healthyConnections.length,
311
+ this.logger[logLevel](`Connected to ${healthyConnections.length + this.trustedPeers.size} peers`, {
312
+ discoveredConnections: healthyConnections.length,
313
+ protectedConnections: this.trustedPeers.size,
253
314
  maxPeerCount: this.config.maxPeerCount,
254
315
  cachedPeers: this.cachedPeers.size,
255
316
  ...this.peerScoring.getStats(),
@@ -302,18 +363,14 @@ export class PeerManager {
302
363
  }
303
364
  }
304
365
 
305
- private onlyNotTrustedPeers(connections: Connection[]): Connection[] {
306
- return connections.filter(conn => !this.isTrustedPeer(conn.remotePeer));
366
+ private getNonProtectedPeers(connections: Connection[]): Connection[] {
367
+ return connections.filter(conn => !this.isProtectedPeer(conn.remotePeer));
307
368
  }
308
369
 
309
370
  private pruneUnhealthyPeers(connections: Connection[]): Connection[] {
310
371
  const connectedHealthyPeers: Connection[] = [];
311
372
 
312
373
  for (const peer of connections) {
313
- if (this.isTrustedPeer(peer.remotePeer)) {
314
- this.logger.debug(`Not pruning trusted peer ${peer.remotePeer.toString()}`);
315
- continue;
316
- }
317
374
  const score = this.peerScoring.getScoreState(peer.remotePeer.toString());
318
375
  switch (score) {
319
376
  case PeerScoreState.Banned:
@@ -526,7 +583,7 @@ export class PeerManager {
526
583
 
527
584
  // Remove the oldest peers
528
585
  for (const [key, value] of this.cachedPeers.entries()) {
529
- if (this.isTrustedPeer(value.peerId)) {
586
+ if (this.isProtectedPeer(value.peerId)) {
530
587
  this.logger.debug(`Not pruning trusted peer ${key}`);
531
588
  continue;
532
589
  }
@@ -70,11 +70,11 @@ export class BatchConnectionSampler {
70
70
 
71
71
  if (newPeer) {
72
72
  this.batch[index] = newPeer;
73
- this.logger.trace(`Replaced peer ${peerId} with ${newPeer}`, { peerId, newPeer });
73
+ this.logger.trace('Replaced peer', { peerId, newPeer });
74
74
  } else {
75
75
  // If we couldn't get a replacement, remove the peer and compact the array
76
76
  this.batch.splice(index, 1);
77
- this.logger.trace(`Removed peer ${peerId}`, { peerId });
77
+ this.logger.trace('Removed peer', { peerId });
78
78
  }
79
79
  }
80
80
 
@@ -17,7 +17,7 @@ export class RandomSampler {
17
17
  }
18
18
 
19
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.
20
+ * A class that samples peers from the libp2p node and returns a peer that we don't already have a reqresp connection open to.
21
21
  * If we already have a connection open, we try to sample a different peer.
22
22
  * We do this MAX_SAMPLE_ATTEMPTS times, if we still don't find a peer we just go for it.
23
23
  *
@@ -69,32 +69,65 @@ export class ConnectionSampler {
69
69
  getPeer(excluding?: Map<string, boolean>): PeerId | undefined {
70
70
  // In libp2p getPeers performs a shallow copy, so this array can be sliced from safetly
71
71
  const peers = this.libp2p.getPeers();
72
+ const { peer } = this.getPeerFromList(peers, excluding);
73
+ return peer;
74
+ }
72
75
 
76
+ /**
77
+ * Samples a peer from a list of peers, excluding those that have active (reqresp) connections or are in the exclusion list
78
+ *
79
+ * @param peers - The list of peers to sample from
80
+ * @param excluding - The peers to exclude from the sampling
81
+ * @returns - A peer from the list, or undefined if no peers are available,
82
+ * - a boolean indicating if the peer has active connections, and
83
+ * - all sampled peers - to enable optional resampling
84
+ *
85
+ * @dev The provided list peers, should be mutated by this function. This allows batch sampling
86
+ * to be performed without making extra copies of the list.
87
+ */
88
+ getPeerFromList(
89
+ peers: PeerId[],
90
+ excluding?: Map<string, boolean>,
91
+ ): {
92
+ peer: PeerId | undefined;
93
+ sampledPeers: PeerId[];
94
+ } {
73
95
  if (peers.length === 0) {
74
- return undefined;
96
+ return { peer: undefined, sampledPeers: [] };
75
97
  }
76
98
 
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
- ) {
99
+ const sampledPeers: PeerId[] = [];
100
+ // Try to find a peer that has no active connections and is not in the exclusion list
101
+ for (let attempts = 0; attempts < MAX_SAMPLE_ATTEMPTS && peers.length > 0; attempts++) {
102
+ const randomIndex = this.sampler.random(peers.length);
103
+ const peer = peers[randomIndex];
104
+ const hasActiveConnections = (this.activeConnectionsCount.get(peer) ?? 0) > 0;
105
+ const isExcluded = excluding?.get(peer.toString()) ?? false;
106
+
107
+ // Remove this peer from consideration
88
108
  peers.splice(randomIndex, 1);
89
- randomIndex = this.sampler.random(peers.length);
90
- attempts++;
109
+
110
+ // If peer is suitable (no active connections and not excluded), return it
111
+ if (!hasActiveConnections && !isExcluded) {
112
+ this.logger.trace('Sampled peer', {
113
+ attempts,
114
+ peer,
115
+ });
116
+ return { peer, sampledPeers };
117
+ }
118
+
119
+ // Keep track of peers that have active reqresp channels, batch sampling will use these to resample
120
+ sampledPeers.push(peer);
91
121
  }
92
122
 
93
- this.logger.trace(`Sampled peer in ${attempts} attempts`, {
94
- attempts,
95
- peer: peers[randomIndex]?.toString(),
123
+ // If we've exhausted our attempts or peers list is empty, return the last peer if available
124
+ const lastPeer = peers.length > 0 ? peers[this.sampler.random(peers.length)] : undefined;
125
+
126
+ this.logger.trace('Sampled peer', {
127
+ attempts: MAX_SAMPLE_ATTEMPTS,
128
+ peer: lastPeer?.toString(),
96
129
  });
97
- return peers[randomIndex];
130
+ return { peer: lastPeer, sampledPeers };
98
131
  }
99
132
 
100
133
  /**
@@ -105,34 +138,41 @@ export class ConnectionSampler {
105
138
  */
106
139
  samplePeersBatch(numberToSample: number): PeerId[] {
107
140
  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
141
+ this.logger.debug('Sampling peers batch', { numberToSample, peers });
110
142
 
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);
143
+ // Only sample as many peers as we have available
144
+ numberToSample = Math.min(numberToSample, peers.length);
145
+
146
+ const batch: PeerId[] = [];
147
+ const withActiveConnections: Set<PeerId> = new Set();
148
+ for (let i = 0; i < numberToSample; i++) {
149
+ const { peer, sampledPeers } = this.getPeerFromList(peers, undefined);
150
+ if (peer) {
151
+ batch.push(peer);
152
+ }
153
+ if (sampledPeers.length > 0) {
154
+ sampledPeers.forEach(peer => withActiveConnections.add(peer));
119
155
  }
120
156
  }
157
+ const lengthWithoutConnections = batch.length;
121
158
 
122
159
  // 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);
160
+ while (batch.length < numberToSample && withActiveConnections.size > 0) {
161
+ const randomIndex = this.sampler.random(withActiveConnections.size);
162
+
163
+ const peer = Array.from(withActiveConnections)[randomIndex];
164
+ withActiveConnections.delete(peer);
165
+ batch.push(peer);
127
166
  }
128
167
 
129
- this.logger.trace(`Batch sampled ${sampledPeers.length} unique peers`, {
130
- peers: sampledPeers,
131
- withoutConnections: sampledPeers.length - peersWithConnections.length,
132
- withConnections: peersWithConnections.length,
168
+ this.logger.trace('Batch sampled peers', {
169
+ length: batch.length,
170
+ peers: batch,
171
+ withoutConnections: lengthWithoutConnections,
172
+ withConnections: numberToSample - lengthWithoutConnections,
133
173
  });
134
174
 
135
- return sampledPeers;
175
+ return batch;
136
176
  }
137
177
 
138
178
  // Set of passthrough functions to keep track of active connections
@@ -155,8 +195,9 @@ export class ConnectionSampler {
155
195
  const updatedActiveConnectionsCount = (this.activeConnectionsCount.get(peerId) ?? 0) + 1;
156
196
  this.activeConnectionsCount.set(peerId, updatedActiveConnectionsCount);
157
197
 
158
- this.logger.trace(`Dialed protocol ${protocol} with peer ${peerId.toString()}`, {
198
+ this.logger.trace('Dialed protocol', {
159
199
  streamId: stream.id,
200
+ protocol,
160
201
  peerId: peerId.toString(),
161
202
  activeConnectionsCount: updatedActiveConnectionsCount,
162
203
  });
@@ -181,7 +222,7 @@ export class ConnectionSampler {
181
222
  const updatedActiveConnectionsCount = (this.activeConnectionsCount.get(peerId) ?? 1) - 1;
182
223
  this.activeConnectionsCount.set(peerId, updatedActiveConnectionsCount);
183
224
 
184
- this.logger.trace(`Closing connection to peer ${peerId.toString()}`, {
225
+ this.logger.trace('Closing connection', {
185
226
  streamId,
186
227
  peerId: peerId.toString(),
187
228
  protocol: stream.protocol,
@@ -207,7 +248,7 @@ export class ConnectionSampler {
207
248
  // Check if we have lost track of accounting
208
249
  if (this.activeConnectionsCount.get(peerId) === 0) {
209
250
  await this.close(streamId);
210
- this.logger.debug(`Cleaned up stale connection ${streamId} to peer ${peerId.toString()}`);
251
+ this.logger.debug('Cleaned up stale connection', { streamId, peerId: peerId.toString() });
211
252
  }
212
253
  } catch (error) {
213
254
  this.logger.error(`Error cleaning up stale connection ${streamId}`, { error });
@@ -196,7 +196,7 @@ export class ReqResp {
196
196
  this.logger.trace(`Sending request to peer: ${peer.toString()}`);
197
197
  const response = await this.sendRequestToPeer(peer, subProtocol, requestBuffer);
198
198
 
199
- if (response && response.status !== ReqRespStatus.SUCCESS) {
199
+ if (response.status !== ReqRespStatus.SUCCESS) {
200
200
  this.logger.debug(
201
201
  `Request to peer ${peer.toString()} failed with status ${prettyPrintReqRespStatus(response.status)}`,
202
202
  );
@@ -264,9 +264,9 @@ export class ReqResp {
264
264
  timeoutMs = 10000,
265
265
  maxPeers = Math.min(10, requests.length),
266
266
  maxRetryAttempts = 3,
267
- ): Promise<InstanceType<SubProtocolMap[SubProtocol]['response']>[]> {
267
+ ): Promise<(InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined)[]> {
268
268
  const responseValidator = this.subProtocolValidators[subProtocol];
269
- const responses: InstanceType<SubProtocolMap[SubProtocol]['response']>[] = new Array(requests.length);
269
+ const responses: (InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined)[] = new Array(requests.length);
270
270
  const requestBuffers = requests.map(req => req.toBuffer());
271
271
 
272
272
  const requestFunction = async () => {
@@ -325,7 +325,7 @@ export class ReqResp {
325
325
  const response = await this.sendRequestToPeer(peer, subProtocol, requestBuffers[index]);
326
326
 
327
327
  // Check the status of the response buffer
328
- if (response && response.status !== ReqRespStatus.SUCCESS) {
328
+ if (response.status !== ReqRespStatus.SUCCESS) {
329
329
  this.logger.debug(
330
330
  `Request to peer ${peer.toString()} failed with status ${prettyPrintReqRespStatus(
331
331
  response.status,
@@ -378,7 +378,7 @@ export class ReqResp {
378
378
  };
379
379
 
380
380
  try {
381
- return await executeTimeout<InstanceType<SubProtocolMap[SubProtocol]['response']>[]>(
381
+ return await executeTimeout<(InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined)[]>(
382
382
  requestFunction,
383
383
  timeoutMs,
384
384
  () => new CollectiveReqRespTimeoutError(),
@@ -421,7 +421,7 @@ export class ReqResp {
421
421
  peerId: PeerId,
422
422
  subProtocol: ReqRespSubProtocol,
423
423
  payload: Buffer,
424
- ): Promise<ReqRespResponse | undefined> {
424
+ ): Promise<ReqRespResponse> {
425
425
  let stream: Stream | undefined;
426
426
  try {
427
427
  this.metrics.recordRequestSent(subProtocol);
@@ -439,6 +439,12 @@ export class ReqResp {
439
439
  } catch (e: any) {
440
440
  this.metrics.recordRequestError(subProtocol);
441
441
  this.handleResponseError(e, peerId, subProtocol);
442
+
443
+ // If there is an exception, we return an unknown response
444
+ return {
445
+ status: ReqRespStatus.FAILURE,
446
+ data: Buffer.from([]),
447
+ };
442
448
  } finally {
443
449
  // Only close the stream if we created it
444
450
  if (stream) {
@@ -5,6 +5,7 @@ export enum ReqRespStatus {
5
5
  SUCCESS = 0,
6
6
  RATE_LIMIT_EXCEEDED = 1,
7
7
  BADLY_FORMED_REQUEST = 2,
8
+ FAILURE = 126,
8
9
  UNKNOWN = 127,
9
10
  }
10
11
 
@@ -53,6 +54,8 @@ export function prettyPrintReqRespStatus(status: ReqRespStatus) {
53
54
  return 'RATE_LIMIT_EXCEEDED';
54
55
  case ReqRespStatus.BADLY_FORMED_REQUEST:
55
56
  return 'BADLY_FORMED_REQUEST';
57
+ case ReqRespStatus.FAILURE:
58
+ return 'FAILURE';
56
59
  case ReqRespStatus.UNKNOWN:
57
60
  return 'UNKNOWN';
58
61
  }
@@ -56,7 +56,7 @@ export interface P2PService {
56
56
  sendBatchRequest<Protocol extends ReqRespSubProtocol>(
57
57
  protocol: Protocol,
58
58
  requests: InstanceType<SubProtocolMap[Protocol]['request']>[],
59
- ): Promise<InstanceType<SubProtocolMap[Protocol]['response']>[] | undefined>;
59
+ ): Promise<(InstanceType<SubProtocolMap[Protocol]['response']> | undefined)[]>;
60
60
 
61
61
  // Leaky abstraction: fix https://github.com/AztecProtocol/aztec-packages/issues/7963
62
62
  registerBlockReceivedCallback(callback: (block: BlockProposal) => Promise<BlockAttestation | undefined>): void;
@@ -81,10 +81,10 @@ export interface PeerDiscoveryService extends EventEmitter {
81
81
  stop(): Promise<void>;
82
82
 
83
83
  /**
84
- * Gets all peers.
85
- * @returns An array of peer ENRs.
84
+ * Gets all KadValues.
85
+ * @returns An array of ENRs.
86
86
  */
87
- getAllPeers(): ENR[];
87
+ getKadValues(): ENR[];
88
88
 
89
89
  /**
90
90
  * Runs findRandomNode query.
@@ -1,6 +1,7 @@
1
1
  import { MockL2BlockSource } from '@aztec/archiver/test';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
+ import { sleep } from '@aztec/foundation/sleep';
4
5
  import type { DataStoreConfig } from '@aztec/kv-store/config';
5
6
  import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
6
7
  import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
@@ -57,6 +58,7 @@ export async function makeTestP2PClient(
57
58
  p2pEnabled: true,
58
59
  peerIdPrivateKey,
59
60
  p2pIp: `127.0.0.1`,
61
+ listenAddress: `127.0.0.1`,
60
62
  p2pPort: port,
61
63
  bootstrapNodes: peers,
62
64
  peerCheckIntervalMS: 1000,
@@ -101,7 +103,16 @@ export async function makeTestP2PClients(numberOfPeers: number, testConfig: Make
101
103
  const clients: P2PClient[] = [];
102
104
  const peerIdPrivateKeys = generatePeerIdPrivateKeys(numberOfPeers);
103
105
 
104
- const ports = await getPorts(numberOfPeers);
106
+ let ports = [];
107
+ while (true) {
108
+ try {
109
+ ports = await getPorts(numberOfPeers);
110
+ break;
111
+ } catch (err) {
112
+ await sleep(1000);
113
+ }
114
+ }
115
+
105
116
  const peerEnrs = await makeEnrs(peerIdPrivateKeys, ports, testConfig.p2pBaseConfig);
106
117
 
107
118
  for (let i = 0; i < numberOfPeers; i++) {
@@ -113,5 +124,12 @@ export async function makeTestP2PClients(numberOfPeers: number, testConfig: Make
113
124
  }
114
125
 
115
126
  await Promise.all(clients.map(client => client.isReady()));
116
- return clients;
127
+ return clients.map((client, index) => {
128
+ return {
129
+ client,
130
+ peerPrivateKey: peerIdPrivateKeys[index],
131
+ port: ports[index],
132
+ enr: peerEnrs[index],
133
+ };
134
+ });
117
135
  }