@aztec/p2p 0.53.0 → 0.55.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 (48) hide show
  1. package/dest/attestation_pool/mocks.d.ts.map +1 -1
  2. package/dest/attestation_pool/mocks.js +6 -4
  3. package/dest/client/p2p_client.d.ts +27 -2
  4. package/dest/client/p2p_client.d.ts.map +1 -1
  5. package/dest/client/p2p_client.js +33 -4
  6. package/dest/config.d.ts +2 -1
  7. package/dest/config.d.ts.map +1 -1
  8. package/dest/config.js +3 -1
  9. package/dest/errors/reqresp.error.d.ts +17 -0
  10. package/dest/errors/reqresp.error.d.ts.map +1 -0
  11. package/dest/errors/reqresp.error.js +21 -0
  12. package/dest/mocks/index.d.ts.map +1 -1
  13. package/dest/mocks/index.js +6 -2
  14. package/dest/service/libp2p_service.js +3 -3
  15. package/dest/service/reqresp/config.d.ts +16 -0
  16. package/dest/service/reqresp/config.d.ts.map +1 -0
  17. package/dest/service/reqresp/config.js +21 -0
  18. package/dest/service/reqresp/interface.d.ts +30 -3
  19. package/dest/service/reqresp/interface.d.ts.map +1 -1
  20. package/dest/service/reqresp/interface.js +4 -4
  21. package/dest/service/reqresp/rate_limiter/index.d.ts +2 -0
  22. package/dest/service/reqresp/rate_limiter/index.d.ts.map +1 -0
  23. package/dest/service/reqresp/rate_limiter/index.js +2 -0
  24. package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts +97 -0
  25. package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts.map +1 -0
  26. package/dest/service/reqresp/rate_limiter/rate_limiter.js +148 -0
  27. package/dest/service/reqresp/rate_limiter/rate_limits.d.ts +3 -0
  28. package/dest/service/reqresp/rate_limiter/rate_limits.d.ts.map +1 -0
  29. package/dest/service/reqresp/rate_limiter/rate_limits.js +35 -0
  30. package/dest/service/reqresp/reqresp.d.ts +5 -1
  31. package/dest/service/reqresp/reqresp.d.ts.map +1 -1
  32. package/dest/service/reqresp/reqresp.js +55 -17
  33. package/dest/service/service.d.ts +1 -1
  34. package/dest/service/service.d.ts.map +1 -1
  35. package/package.json +6 -6
  36. package/src/attestation_pool/mocks.ts +6 -3
  37. package/src/client/p2p_client.ts +44 -5
  38. package/src/config.ts +4 -1
  39. package/src/errors/reqresp.error.ts +21 -0
  40. package/src/mocks/index.ts +6 -1
  41. package/src/service/libp2p_service.ts +2 -2
  42. package/src/service/reqresp/config.ts +35 -0
  43. package/src/service/reqresp/interface.ts +33 -3
  44. package/src/service/reqresp/rate_limiter/index.ts +1 -0
  45. package/src/service/reqresp/rate_limiter/rate_limiter.ts +198 -0
  46. package/src/service/reqresp/rate_limiter/rate_limits.ts +35 -0
  47. package/src/service/reqresp/reqresp.ts +78 -20
  48. package/src/service/service.ts +1 -1
package/src/config.ts CHANGED
@@ -6,10 +6,12 @@ import {
6
6
  pickConfigMappings,
7
7
  } from '@aztec/foundation/config';
8
8
 
9
+ import { type P2PReqRespConfig, p2pReqRespConfigMappings } from './service/reqresp/config.js';
10
+
9
11
  /**
10
12
  * P2P client configuration values.
11
13
  */
12
- export interface P2PConfig {
14
+ export interface P2PConfig extends P2PReqRespConfig {
13
15
  /**
14
16
  * A flag dictating whether the P2P subsystem should be enabled.
15
17
  */
@@ -170,6 +172,7 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
170
172
  'How many blocks have to pass after a block is proven before its txs are deleted (zero to delete immediately once proven)',
171
173
  ...numberConfigHelper(0),
172
174
  },
175
+ ...p2pReqRespConfigMappings,
173
176
  };
174
177
 
175
178
  /**
@@ -0,0 +1,21 @@
1
+ /** Individual request timeout error
2
+ *
3
+ * This error will be thrown when a request to a specific peer times out.
4
+ * @category Errors
5
+ */
6
+ export class IndiviualReqRespTimeoutError extends Error {
7
+ constructor() {
8
+ super(`Request to peer timed out`);
9
+ }
10
+ }
11
+
12
+ /** Collective request timeout error
13
+ *
14
+ * This error will be thrown when a req resp request times out regardless of the peer.
15
+ * @category Errors
16
+ */
17
+ export class CollectiveReqRespTimeoutError extends Error {
18
+ constructor() {
19
+ super(`Request to all peers timed out`);
20
+ }
21
+ }
@@ -4,6 +4,7 @@ import { bootstrap } from '@libp2p/bootstrap';
4
4
  import { tcp } from '@libp2p/tcp';
5
5
  import { type Libp2p, type Libp2pOptions, createLibp2p } from 'libp2p';
6
6
 
7
+ import { type P2PReqRespConfig } from '../service/reqresp/config.js';
7
8
  import { pingHandler, statusHandler } from '../service/reqresp/handlers.js';
8
9
  import {
9
10
  PING_PROTOCOL,
@@ -80,7 +81,11 @@ export const stopNodes = async (nodes: ReqRespNode[]): Promise<void> => {
80
81
  // Create a req resp node, exposing the underlying p2p node
81
82
  export const createReqResp = async (): Promise<ReqRespNode> => {
82
83
  const p2p = await createLibp2pNode();
83
- const req = new ReqResp(p2p);
84
+ const config: P2PReqRespConfig = {
85
+ overallRequestTimeoutMs: 4000,
86
+ individualRequestTimeoutMs: 2000,
87
+ };
88
+ const req = new ReqResp(config, p2p);
84
89
  return {
85
90
  p2p,
86
91
  req,
@@ -94,7 +94,7 @@ export class LibP2PService implements P2PService {
94
94
  private logger = createDebugLogger('aztec:libp2p_service'),
95
95
  ) {
96
96
  this.peerManager = new PeerManager(node, peerDiscoveryService, config, logger);
97
- this.reqresp = new ReqResp(node);
97
+ this.reqresp = new ReqResp(config, node);
98
98
 
99
99
  this.blockReceivedCallback = (block: BlockProposal): Promise<BlockAttestation | undefined> => {
100
100
  this.logger.verbose(
@@ -390,7 +390,7 @@ export class LibP2PService implements P2PService {
390
390
  this.logger.verbose(`Sending message ${identifier} to peers`);
391
391
 
392
392
  const recipientsNum = await this.publishToTopic(parent.p2pTopic, message.toBuffer());
393
- this.logger.verbose(`Sent tx ${identifier} to ${recipientsNum} peers`);
393
+ this.logger.verbose(`Sent message ${identifier} to ${recipientsNum} peers`);
394
394
  }
395
395
 
396
396
  // Libp2p seems to hang sometimes if new peers are initiating connections.
@@ -0,0 +1,35 @@
1
+ import { type ConfigMapping, numberConfigHelper } from '@aztec/foundation/config';
2
+
3
+ export const DEFAULT_INDIVIDUAL_REQUEST_TIMEOUT_MS = 2000;
4
+ export const DEFAULT_OVERALL_REQUEST_TIMEOUT_MS = 4000;
5
+
6
+ // For use in tests.
7
+ export const DEFAULT_P2P_REQRESP_CONFIG: P2PReqRespConfig = {
8
+ overallRequestTimeoutMs: DEFAULT_OVERALL_REQUEST_TIMEOUT_MS,
9
+ individualRequestTimeoutMs: DEFAULT_INDIVIDUAL_REQUEST_TIMEOUT_MS,
10
+ };
11
+
12
+ export interface P2PReqRespConfig {
13
+ /**
14
+ * The overall timeout for a request response operation.
15
+ */
16
+ overallRequestTimeoutMs: number;
17
+
18
+ /**
19
+ * The timeout for an individual request response peer interaction.
20
+ */
21
+ individualRequestTimeoutMs: number;
22
+ }
23
+
24
+ export const p2pReqRespConfigMappings: Record<keyof P2PReqRespConfig, ConfigMapping> = {
25
+ overallRequestTimeoutMs: {
26
+ env: 'P2P_REQRESP_OVERALL_REQUEST_TIMEOUT_MS',
27
+ description: 'The overall timeout for a request response operation.',
28
+ ...numberConfigHelper(DEFAULT_OVERALL_REQUEST_TIMEOUT_MS),
29
+ },
30
+ individualRequestTimeoutMs: {
31
+ env: 'P2P_REQRESP_INDIVIDUAL_REQUEST_TIMEOUT_MS',
32
+ description: 'The timeout for an individual request response peer interaction.',
33
+ ...numberConfigHelper(DEFAULT_INDIVIDUAL_REQUEST_TIMEOUT_MS),
34
+ },
35
+ };
@@ -3,9 +3,9 @@ import { Tx, TxHash } from '@aztec/circuit-types';
3
3
  /*
4
4
  * Request Response Sub Protocols
5
5
  */
6
- export const PING_PROTOCOL = '/aztec/ping/0.1.0';
7
- export const STATUS_PROTOCOL = '/aztec/status/0.1.0';
8
- export const TX_REQ_PROTOCOL = '/aztec/tx_req/0.1.0';
6
+ export const PING_PROTOCOL = '/aztec/req/ping/0.1.0';
7
+ export const STATUS_PROTOCOL = '/aztec/req/status/0.1.0';
8
+ export const TX_REQ_PROTOCOL = '/aztec/req/tx/0.1.0';
9
9
 
10
10
  // Sum type for sub protocols
11
11
  export type ReqRespSubProtocol = typeof PING_PROTOCOL | typeof STATUS_PROTOCOL | typeof TX_REQ_PROTOCOL;
@@ -16,6 +16,36 @@ export type ReqRespSubProtocol = typeof PING_PROTOCOL | typeof STATUS_PROTOCOL |
16
16
  */
17
17
  export type ReqRespSubProtocolHandler = (msg: Buffer) => Promise<Uint8Array>;
18
18
 
19
+ /**
20
+ * A type mapping from supprotocol to it's rate limits
21
+ */
22
+ export type ReqRespSubProtocolRateLimits = Record<ReqRespSubProtocol, ProtocolRateLimitQuota>;
23
+
24
+ /**
25
+ * A rate limit quota
26
+ */
27
+ export interface RateLimitQuota {
28
+ /**
29
+ * The time window in ms
30
+ */
31
+ quotaTimeMs: number;
32
+ /**
33
+ * The number of requests allowed within the time window
34
+ */
35
+ quotaCount: number;
36
+ }
37
+
38
+ export interface ProtocolRateLimitQuota {
39
+ /**
40
+ * The rate limit quota for a single peer
41
+ */
42
+ peerLimit: RateLimitQuota;
43
+ /**
44
+ * The rate limit quota for the global peer set
45
+ */
46
+ globalLimit: RateLimitQuota;
47
+ }
48
+
19
49
  /**
20
50
  * A type mapping from supprotocol to it's handling funciton
21
51
  */
@@ -0,0 +1 @@
1
+ export { RequestResponseRateLimiter } from './rate_limiter.js';
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @attribution Rate limiter approach implemented in the lodestar ethereum 2 client.
3
+ * Rationale is that if it was good enough for them, then it should be good enough for us.
4
+ * https://github.com/ChainSafe/lodestar
5
+ */
6
+ import { type PeerId } from '@libp2p/interface';
7
+
8
+ import { type ReqRespSubProtocol, type ReqRespSubProtocolRateLimits } from '../interface.js';
9
+ import { DEFAULT_RATE_LIMITS } from './rate_limits.js';
10
+
11
+ // Check for disconnected peers every 10 minutes
12
+ const CHECK_DISCONNECTED_PEERS_INTERVAL_MS = 10 * 60 * 1000;
13
+
14
+ /**
15
+ * GCRARateLimiter: A Generic Cell Rate Algorithm (GCRA) based rate limiter.
16
+ *
17
+ * How it works:
18
+ * 1. The rate limiter allows a certain number of operations (quotaCount) within a specified
19
+ * time interval (quotaTimeMs).
20
+ * 2. It uses a "virtual scheduling time" (VST) to determine when the next operation should be allowed.
21
+ * 3. When an operation is requested, the limiter checks if enough time has passed since the last
22
+ * allowed operation.
23
+ * 4. If sufficient time has passed, the operation is allowed, and the VST is updated.
24
+ * 5. If not enough time has passed, the operation is denied.
25
+ *
26
+ * The limiter also allows for short bursts of activity, as long as the overall rate doesn't exceed
27
+ * the specified quota over time.
28
+ *
29
+ * Usage example:
30
+ * ```
31
+ * const limiter = new GCRARateLimiter(100, 60000); // 100 operations per minute
32
+ * ```
33
+ */
34
+ export class GCRARateLimiter {
35
+ // Virtual scheduling time: i.e. the time at which we should allow the next request
36
+ private vst: number;
37
+ // The interval at which we emit a new token
38
+ private readonly emissionInterval: number;
39
+ // The interval over which we limit the number of requests
40
+ private readonly limitInterval: number;
41
+
42
+ /**
43
+ * @param quotaCount - The number of requests to allow over the limit interval
44
+ * @param quotaTimeMs - The time interval over which the quotaCount applies
45
+ */
46
+ constructor(quotaCount: number, quotaTimeMs: number) {
47
+ this.emissionInterval = quotaTimeMs / quotaCount;
48
+ this.limitInterval = quotaTimeMs;
49
+ this.vst = Date.now();
50
+ }
51
+
52
+ allow(): boolean {
53
+ const now = Date.now();
54
+
55
+ const newVst = Math.max(this.vst, now) + this.emissionInterval;
56
+ if (newVst - now <= this.limitInterval) {
57
+ this.vst = newVst;
58
+ return true;
59
+ }
60
+
61
+ return false;
62
+ }
63
+ }
64
+
65
+ interface PeerRateLimiter {
66
+ // The rate limiter for this peer
67
+ limiter: GCRARateLimiter;
68
+ // The last time the peer was accessed - used to determine if the peer is still connected
69
+ lastAccess: number;
70
+ }
71
+
72
+ /**
73
+ * SubProtocolRateLimiter: A rate limiter for managing request rates on a per-peer and global basis for a specific subprotocol.
74
+ *
75
+ * This class provides a two-tier rate limiting system:
76
+ * 1. A global rate limit for all requests across all peers for this subprotocol.
77
+ * 2. Individual rate limits for each peer.
78
+ *
79
+ * How it works:
80
+ * - When a request comes in, it first checks against the global rate limit.
81
+ * - If the global limit allows, it then checks against the specific peer's rate limit.
82
+ * - The request is only allowed if both the global and peer-specific limits allow it.
83
+ * - It automatically creates and manages rate limiters for new peers as they make requests.
84
+ * - It periodically cleans up rate limiters for inactive peers to conserve memory.
85
+ *
86
+ * Note: Remember to call `start()` to begin the cleanup process and `stop()` when shutting down to clear the cleanup interval.
87
+ */
88
+ export class SubProtocolRateLimiter {
89
+ private peerLimiters: Map<string, PeerRateLimiter> = new Map();
90
+ private globalLimiter: GCRARateLimiter;
91
+ private readonly peerQuotaCount: number;
92
+ private readonly peerQuotaTimeMs: number;
93
+
94
+ constructor(peerQuotaCount: number, peerQuotaTimeMs: number, globalQuotaCount: number, globalQuotaTimeMs: number) {
95
+ this.peerLimiters = new Map();
96
+ this.globalLimiter = new GCRARateLimiter(globalQuotaCount, globalQuotaTimeMs);
97
+ this.peerQuotaCount = peerQuotaCount;
98
+ this.peerQuotaTimeMs = peerQuotaTimeMs;
99
+ }
100
+
101
+ allow(peerId: PeerId): boolean {
102
+ if (!this.globalLimiter.allow()) {
103
+ return false;
104
+ }
105
+
106
+ const peerIdStr = peerId.toString();
107
+ let peerLimiter: PeerRateLimiter | undefined = this.peerLimiters.get(peerIdStr);
108
+ if (!peerLimiter) {
109
+ // Create a limiter for this peer
110
+ peerLimiter = {
111
+ limiter: new GCRARateLimiter(this.peerQuotaCount, this.peerQuotaTimeMs),
112
+ lastAccess: Date.now(),
113
+ };
114
+ this.peerLimiters.set(peerIdStr, peerLimiter);
115
+ } else {
116
+ peerLimiter.lastAccess = Date.now();
117
+ }
118
+ return peerLimiter.limiter.allow();
119
+ }
120
+
121
+ cleanupInactivePeers() {
122
+ const now = Date.now();
123
+ this.peerLimiters.forEach((peerLimiter, peerId) => {
124
+ if (now - peerLimiter.lastAccess > CHECK_DISCONNECTED_PEERS_INTERVAL_MS) {
125
+ this.peerLimiters.delete(peerId);
126
+ }
127
+ });
128
+ }
129
+ }
130
+
131
+ /**
132
+ * RequestResponseRateLimiter.
133
+ *
134
+ * A rate limiter that is protocol aware, then peer aware.
135
+ * SubProtocols can have their own global / peer level rate limits.
136
+ *
137
+ * How it works:
138
+ * - Initializes with a set of rate limit configurations for different subprotocols.
139
+ * - Creates a separate SubProtocolRateLimiter for each configured subprotocol.
140
+ * - When a request comes in, it routes the rate limiting decision to the appropriate subprotocol limiter.
141
+ *
142
+ * Usage:
143
+ * ```
144
+ * const rateLimits = {
145
+ * subprotocol1: { peerLimit: { quotaCount: 10, quotaTimeMs: 1000 }, globalLimit: { quotaCount: 100, quotaTimeMs: 1000 } },
146
+ * subprotocol2: { peerLimit: { quotaCount: 5, quotaTimeMs: 1000 }, globalLimit: { quotaCount: 50, quotaTimeMs: 1000 } }
147
+ * };
148
+ * const limiter = new RequestResponseRateLimiter(rateLimits);
149
+ *
150
+ * Note: Ensure to call `stop()` when shutting down to properly clean up all subprotocol limiters.
151
+ */
152
+ export class RequestResponseRateLimiter {
153
+ private subProtocolRateLimiters: Map<ReqRespSubProtocol, SubProtocolRateLimiter>;
154
+
155
+ private cleanupInterval: NodeJS.Timeout | undefined = undefined;
156
+
157
+ constructor(rateLimits: ReqRespSubProtocolRateLimits = DEFAULT_RATE_LIMITS) {
158
+ this.subProtocolRateLimiters = new Map();
159
+
160
+ for (const [subProtocol, protocolLimits] of Object.entries(rateLimits)) {
161
+ this.subProtocolRateLimiters.set(
162
+ subProtocol as ReqRespSubProtocol,
163
+ new SubProtocolRateLimiter(
164
+ protocolLimits.peerLimit.quotaCount,
165
+ protocolLimits.peerLimit.quotaTimeMs,
166
+ protocolLimits.globalLimit.quotaCount,
167
+ protocolLimits.globalLimit.quotaTimeMs,
168
+ ),
169
+ );
170
+ }
171
+ }
172
+
173
+ start() {
174
+ this.cleanupInterval = setInterval(() => {
175
+ this.cleanupInactivePeers();
176
+ }, CHECK_DISCONNECTED_PEERS_INTERVAL_MS);
177
+ }
178
+
179
+ allow(subProtocol: ReqRespSubProtocol, peerId: PeerId): boolean {
180
+ const limiter = this.subProtocolRateLimiters.get(subProtocol);
181
+ if (!limiter) {
182
+ // TODO: maybe throw an error here if no rate limiter is configured?
183
+ return true;
184
+ }
185
+ return limiter.allow(peerId);
186
+ }
187
+
188
+ cleanupInactivePeers() {
189
+ this.subProtocolRateLimiters.forEach(limiter => limiter.cleanupInactivePeers());
190
+ }
191
+
192
+ /**
193
+ * Make sure to call destroy on each of the sub protocol rate limiters when cleaning up
194
+ */
195
+ stop() {
196
+ clearInterval(this.cleanupInterval);
197
+ }
198
+ }
@@ -0,0 +1,35 @@
1
+ import { PING_PROTOCOL, type ReqRespSubProtocolRateLimits, STATUS_PROTOCOL, TX_REQ_PROTOCOL } from '../interface.js';
2
+
3
+ // TODO(md): these defaults need to be tuned
4
+ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
5
+ [PING_PROTOCOL]: {
6
+ peerLimit: {
7
+ quotaTimeMs: 1000,
8
+ quotaCount: 5,
9
+ },
10
+ globalLimit: {
11
+ quotaTimeMs: 1000,
12
+ quotaCount: 10,
13
+ },
14
+ },
15
+ [STATUS_PROTOCOL]: {
16
+ peerLimit: {
17
+ quotaTimeMs: 1000,
18
+ quotaCount: 5,
19
+ },
20
+ globalLimit: {
21
+ quotaTimeMs: 1000,
22
+ quotaCount: 10,
23
+ },
24
+ },
25
+ [TX_REQ_PROTOCOL]: {
26
+ peerLimit: {
27
+ quotaTimeMs: 1000,
28
+ quotaCount: 5,
29
+ },
30
+ globalLimit: {
31
+ quotaTimeMs: 1000,
32
+ quotaCount: 10,
33
+ },
34
+ },
35
+ };
@@ -1,16 +1,20 @@
1
1
  // @attribution: lodestar impl for inspiration
2
2
  import { type Logger, createDebugLogger } from '@aztec/foundation/log';
3
+ import { executeTimeoutWithCustomError } from '@aztec/foundation/timer';
3
4
 
4
- import { type IncomingStreamData, type PeerId } from '@libp2p/interface';
5
+ import { type IncomingStreamData, type PeerId, type Stream } from '@libp2p/interface';
5
6
  import { pipe } from 'it-pipe';
6
7
  import { type Libp2p } from 'libp2p';
7
8
  import { type Uint8ArrayList } from 'uint8arraylist';
8
9
 
10
+ import { CollectiveReqRespTimeoutError, IndiviualReqRespTimeoutError } from '../../errors/reqresp.error.js';
11
+ import { type P2PReqRespConfig } from './config.js';
9
12
  import {
10
13
  DEFAULT_SUB_PROTOCOL_HANDLERS,
11
14
  type ReqRespSubProtocol,
12
15
  type ReqRespSubProtocolHandlers,
13
16
  } from './interface.js';
17
+ import { RequestResponseRateLimiter } from './rate_limiter/rate_limiter.js';
14
18
 
15
19
  /**
16
20
  * The Request Response Service
@@ -28,10 +32,19 @@ export class ReqResp {
28
32
 
29
33
  private abortController: AbortController = new AbortController();
30
34
 
35
+ private overallRequestTimeoutMs: number;
36
+ private individualRequestTimeoutMs: number;
37
+
31
38
  private subProtocolHandlers: ReqRespSubProtocolHandlers = DEFAULT_SUB_PROTOCOL_HANDLERS;
39
+ private rateLimiter: RequestResponseRateLimiter;
32
40
 
33
- constructor(protected readonly libp2p: Libp2p) {
41
+ constructor(config: P2PReqRespConfig, protected readonly libp2p: Libp2p) {
34
42
  this.logger = createDebugLogger('aztec:p2p:reqresp');
43
+
44
+ this.overallRequestTimeoutMs = config.overallRequestTimeoutMs;
45
+ this.individualRequestTimeoutMs = config.individualRequestTimeoutMs;
46
+
47
+ this.rateLimiter = new RequestResponseRateLimiter();
35
48
  }
36
49
 
37
50
  /**
@@ -43,6 +56,7 @@ export class ReqResp {
43
56
  for (const subProtocol of Object.keys(this.subProtocolHandlers)) {
44
57
  await this.libp2p.handle(subProtocol, this.streamHandler.bind(this, subProtocol as ReqRespSubProtocol));
45
58
  }
59
+ this.rateLimiter.start();
46
60
  }
47
61
 
48
62
  /**
@@ -53,6 +67,7 @@ export class ReqResp {
53
67
  for (const protocol of Object.keys(this.subProtocolHandlers)) {
54
68
  await this.libp2p.unhandle(protocol);
55
69
  }
70
+ this.rateLimiter.stop();
56
71
  await this.libp2p.stop();
57
72
  this.abortController.abort();
58
73
  }
@@ -65,20 +80,33 @@ export class ReqResp {
65
80
  * @returns - The response from the peer, otherwise undefined
66
81
  */
67
82
  async sendRequest(subProtocol: ReqRespSubProtocol, payload: Buffer): Promise<Buffer | undefined> {
68
- // Get active peers
69
- const peers = this.libp2p.getPeers();
70
-
71
- // Attempt to ask all of our peers
72
- for (const peer of peers) {
73
- const response = await this.sendRequestToPeer(peer, subProtocol, payload);
74
-
75
- // If we get a response, return it, otherwise we iterate onto the next peer
76
- // We do not consider it a success if we have an empty buffer
77
- if (response && response.length > 0) {
78
- return response;
83
+ const requestFunction = async () => {
84
+ // Get active peers
85
+ const peers = this.libp2p.getPeers();
86
+
87
+ // Attempt to ask all of our peers
88
+ for (const peer of peers) {
89
+ const response = await this.sendRequestToPeer(peer, subProtocol, payload);
90
+
91
+ // If we get a response, return it, otherwise we iterate onto the next peer
92
+ // We do not consider it a success if we have an empty buffer
93
+ if (response && response.length > 0) {
94
+ return response;
95
+ }
79
96
  }
97
+ return undefined;
98
+ };
99
+
100
+ try {
101
+ return await executeTimeoutWithCustomError<Buffer | undefined>(
102
+ requestFunction,
103
+ this.overallRequestTimeoutMs,
104
+ () => new CollectiveReqRespTimeoutError(),
105
+ );
106
+ } catch (e: any) {
107
+ this.logger.error(`${e.message} | subProtocol: ${subProtocol}`);
108
+ return undefined;
80
109
  }
81
- return undefined;
82
110
  }
83
111
 
84
112
  /**
@@ -94,15 +122,37 @@ export class ReqResp {
94
122
  subProtocol: ReqRespSubProtocol,
95
123
  payload: Buffer,
96
124
  ): Promise<Buffer | undefined> {
125
+ let stream: Stream | undefined;
97
126
  try {
98
- const stream = await this.libp2p.dialProtocol(peerId, subProtocol);
127
+ stream = await this.libp2p.dialProtocol(peerId, subProtocol);
128
+
129
+ this.logger.debug(`Stream opened with ${peerId.toString()} for ${subProtocol}`);
130
+
131
+ const result = await executeTimeoutWithCustomError<Buffer>(
132
+ (): Promise<Buffer> => pipe([payload], stream!, this.readMessage),
133
+ this.individualRequestTimeoutMs,
134
+ () => new IndiviualReqRespTimeoutError(),
135
+ );
136
+
137
+ await stream.close();
138
+ this.logger.debug(`Stream closed with ${peerId.toString()} for ${subProtocol}`);
99
139
 
100
- const result = await pipe([payload], stream, this.readMessage);
101
140
  return result;
102
- } catch (e) {
103
- this.logger.warn(`Failed to send request to peer ${peerId.publicKey}`);
104
- return undefined;
141
+ } catch (e: any) {
142
+ this.logger.error(`${e.message} | peerId: ${peerId.toString()} | subProtocol: ${subProtocol}`);
143
+ } finally {
144
+ if (stream) {
145
+ try {
146
+ await stream.close();
147
+ this.logger.debug(`Stream closed with ${peerId.toString()} for ${subProtocol}`);
148
+ } catch (closeError) {
149
+ this.logger.error(
150
+ `Error closing stream: ${closeError instanceof Error ? closeError.message : 'Unknown error'}`,
151
+ );
152
+ }
153
+ }
105
154
  }
155
+ return undefined;
106
156
  }
107
157
 
108
158
  /**
@@ -123,8 +173,16 @@ export class ReqResp {
123
173
  *
124
174
  * @param param0 - The incoming stream data
125
175
  */
126
- private async streamHandler(protocol: ReqRespSubProtocol, { stream }: IncomingStreamData) {
176
+ private async streamHandler(protocol: ReqRespSubProtocol, { stream, connection }: IncomingStreamData) {
127
177
  // Store a reference to from this for the async generator
178
+ if (!this.rateLimiter.allow(protocol, connection.remotePeer)) {
179
+ this.logger.warn(`Rate limit exceeded for ${protocol} from ${connection.remotePeer}`);
180
+
181
+ // TODO(#8483): handle changing peer scoring for failed rate limit, maybe differentiate between global and peer limits here when punishing
182
+ await stream.close();
183
+ return;
184
+ }
185
+
128
186
  const handler = this.subProtocolHandlers[protocol];
129
187
 
130
188
  try {
@@ -46,7 +46,7 @@ export interface P2PService {
46
46
  ): Promise<InstanceType<SubProtocolMap[Protocol]['response']> | undefined>;
47
47
 
48
48
  // Leaky abstraction: fix https://github.com/AztecProtocol/aztec-packages/issues/7963
49
- registerBlockReceivedCallback(callback: (block: BlockProposal) => Promise<BlockAttestation>): void;
49
+ registerBlockReceivedCallback(callback: (block: BlockProposal) => Promise<BlockAttestation | undefined>): void;
50
50
 
51
51
  getEnr(): ENR | undefined;
52
52
  }