@aztec/p2p 0.54.0 → 0.55.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 (94) 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/index.d.ts +2 -2
  4. package/dest/client/index.d.ts.map +1 -1
  5. package/dest/client/index.js +43 -38
  6. package/dest/client/p2p_client.d.ts +27 -2
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +33 -4
  9. package/dest/config.d.ts +51 -1
  10. package/dest/config.d.ts.map +1 -1
  11. package/dest/config.js +68 -2
  12. package/dest/errors/reqresp.error.d.ts +17 -0
  13. package/dest/errors/reqresp.error.d.ts.map +1 -0
  14. package/dest/errors/reqresp.error.js +21 -0
  15. package/dest/index.d.ts +1 -0
  16. package/dest/index.d.ts.map +1 -1
  17. package/dest/index.js +2 -1
  18. package/dest/mocks/index.d.ts.map +1 -1
  19. package/dest/mocks/index.js +6 -2
  20. package/dest/service/libp2p_service.d.ts +10 -11
  21. package/dest/service/libp2p_service.d.ts.map +1 -1
  22. package/dest/service/libp2p_service.js +105 -20
  23. package/dest/service/peer_manager.d.ts +9 -13
  24. package/dest/service/peer_manager.d.ts.map +1 -1
  25. package/dest/service/peer_manager.js +15 -1
  26. package/dest/service/peer_scoring.d.ts +32 -0
  27. package/dest/service/peer_scoring.d.ts.map +1 -0
  28. package/dest/service/peer_scoring.js +67 -0
  29. package/dest/service/reqresp/config.d.ts +16 -0
  30. package/dest/service/reqresp/config.d.ts.map +1 -0
  31. package/dest/service/reqresp/config.js +21 -0
  32. package/dest/service/reqresp/interface.d.ts +30 -3
  33. package/dest/service/reqresp/interface.d.ts.map +1 -1
  34. package/dest/service/reqresp/interface.js +4 -4
  35. package/dest/service/reqresp/rate_limiter/index.d.ts +2 -0
  36. package/dest/service/reqresp/rate_limiter/index.d.ts.map +1 -0
  37. package/dest/service/reqresp/rate_limiter/index.js +2 -0
  38. package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts +97 -0
  39. package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts.map +1 -0
  40. package/dest/service/reqresp/rate_limiter/rate_limiter.js +148 -0
  41. package/dest/service/reqresp/rate_limiter/rate_limits.d.ts +3 -0
  42. package/dest/service/reqresp/rate_limiter/rate_limits.d.ts.map +1 -0
  43. package/dest/service/reqresp/rate_limiter/rate_limits.js +35 -0
  44. package/dest/service/reqresp/reqresp.d.ts +5 -1
  45. package/dest/service/reqresp/reqresp.d.ts.map +1 -1
  46. package/dest/service/reqresp/reqresp.js +55 -17
  47. package/dest/service/service.d.ts +1 -1
  48. package/dest/service/service.d.ts.map +1 -1
  49. package/dest/tx_validator/aggregate_tx_validator.d.ts +7 -0
  50. package/dest/tx_validator/aggregate_tx_validator.d.ts.map +1 -0
  51. package/dest/tx_validator/aggregate_tx_validator.js +23 -0
  52. package/dest/tx_validator/data_validator.d.ts +6 -0
  53. package/dest/tx_validator/data_validator.d.ts.map +1 -0
  54. package/dest/tx_validator/data_validator.js +47 -0
  55. package/dest/tx_validator/double_spend_validator.d.ts +12 -0
  56. package/dest/tx_validator/double_spend_validator.d.ts.map +1 -0
  57. package/dest/tx_validator/double_spend_validator.js +53 -0
  58. package/dest/tx_validator/index.d.ts +6 -0
  59. package/dest/tx_validator/index.d.ts.map +1 -0
  60. package/dest/tx_validator/index.js +6 -0
  61. package/dest/tx_validator/metadata_validator.d.ts +10 -0
  62. package/dest/tx_validator/metadata_validator.d.ts.map +1 -0
  63. package/dest/tx_validator/metadata_validator.js +50 -0
  64. package/dest/tx_validator/tx_proof_validator.d.ts +9 -0
  65. package/dest/tx_validator/tx_proof_validator.d.ts.map +1 -0
  66. package/dest/tx_validator/tx_proof_validator.js +29 -0
  67. package/dest/util.d.ts +7 -0
  68. package/dest/util.d.ts.map +1 -1
  69. package/dest/util.js +1 -1
  70. package/package.json +6 -6
  71. package/src/attestation_pool/mocks.ts +6 -3
  72. package/src/client/index.ts +65 -47
  73. package/src/client/p2p_client.ts +44 -5
  74. package/src/config.ts +131 -1
  75. package/src/errors/reqresp.error.ts +21 -0
  76. package/src/index.ts +1 -0
  77. package/src/mocks/index.ts +6 -1
  78. package/src/service/libp2p_service.ts +139 -26
  79. package/src/service/peer_manager.ts +23 -4
  80. package/src/service/peer_scoring.ts +81 -0
  81. package/src/service/reqresp/config.ts +35 -0
  82. package/src/service/reqresp/interface.ts +33 -3
  83. package/src/service/reqresp/rate_limiter/index.ts +1 -0
  84. package/src/service/reqresp/rate_limiter/rate_limiter.ts +198 -0
  85. package/src/service/reqresp/rate_limiter/rate_limits.ts +35 -0
  86. package/src/service/reqresp/reqresp.ts +78 -20
  87. package/src/service/service.ts +1 -1
  88. package/src/tx_validator/aggregate_tx_validator.ts +24 -0
  89. package/src/tx_validator/data_validator.ts +61 -0
  90. package/src/tx_validator/double_spend_validator.ts +65 -0
  91. package/src/tx_validator/index.ts +5 -0
  92. package/src/tx_validator/metadata_validator.ts +61 -0
  93. package/src/tx_validator/tx_proof_validator.ts +28 -0
  94. package/src/util.ts +8 -0
@@ -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
  }
@@ -0,0 +1,24 @@
1
+ import { type ProcessedTx, type Tx, type TxValidator } from '@aztec/circuit-types';
2
+
3
+ export class AggregateTxValidator<T extends Tx | ProcessedTx> implements TxValidator<T> {
4
+ #validators: TxValidator<T>[];
5
+ constructor(...validators: TxValidator<T>[]) {
6
+ if (validators.length === 0) {
7
+ throw new Error('At least one validator must be provided');
8
+ }
9
+
10
+ this.#validators = validators;
11
+ }
12
+
13
+ async validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> {
14
+ const invalidTxs: T[] = [];
15
+ let txPool = txs;
16
+ for (const validator of this.#validators) {
17
+ const [valid, invalid] = await validator.validateTxs(txPool);
18
+ invalidTxs.push(...invalid);
19
+ txPool = valid;
20
+ }
21
+
22
+ return [txPool, invalidTxs];
23
+ }
24
+ }
@@ -0,0 +1,61 @@
1
+ import { Tx, type TxValidator } from '@aztec/circuit-types';
2
+ import { createDebugLogger } from '@aztec/foundation/log';
3
+
4
+ export class DataTxValidator implements TxValidator<Tx> {
5
+ #log = createDebugLogger('aztec:sequencer:tx_validator:tx_data');
6
+
7
+ validateTxs(txs: Tx[]): Promise<[validTxs: Tx[], invalidTxs: Tx[]]> {
8
+ const validTxs: Tx[] = [];
9
+ const invalidTxs: Tx[] = [];
10
+ for (const tx of txs) {
11
+ if (!this.#hasCorrectExecutionRequests(tx)) {
12
+ invalidTxs.push(tx);
13
+ continue;
14
+ }
15
+
16
+ validTxs.push(tx);
17
+ }
18
+
19
+ return Promise.resolve([validTxs, invalidTxs]);
20
+ }
21
+
22
+ #hasCorrectExecutionRequests(tx: Tx): boolean {
23
+ const callRequests = [
24
+ ...tx.data.getRevertiblePublicCallRequests(),
25
+ ...tx.data.getNonRevertiblePublicCallRequests(),
26
+ ];
27
+ if (callRequests.length !== tx.enqueuedPublicFunctionCalls.length) {
28
+ this.#log.warn(
29
+ `Rejecting tx ${Tx.getHash(tx)} because of mismatch number of execution requests for public calls. Expected ${
30
+ callRequests.length
31
+ }. Got ${tx.enqueuedPublicFunctionCalls.length}.`,
32
+ );
33
+ return false;
34
+ }
35
+
36
+ const invalidExecutionRequestIndex = tx.enqueuedPublicFunctionCalls.findIndex(
37
+ (execRequest, i) => !execRequest.isForCallRequest(callRequests[i]),
38
+ );
39
+ if (invalidExecutionRequestIndex !== -1) {
40
+ this.#log.warn(
41
+ `Rejecting tx ${Tx.getHash(
42
+ tx,
43
+ )} because of incorrect execution requests for public call at index ${invalidExecutionRequestIndex}.`,
44
+ );
45
+ return false;
46
+ }
47
+
48
+ const teardownCallRequest = tx.data.getTeardownPublicCallRequest();
49
+ const isInvalidTeardownExecutionRequest =
50
+ (!teardownCallRequest && !tx.publicTeardownFunctionCall.isEmpty()) ||
51
+ (teardownCallRequest && !tx.publicTeardownFunctionCall.isForCallRequest(teardownCallRequest));
52
+ if (isInvalidTeardownExecutionRequest) {
53
+ this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} because of incorrect teardown execution requests.`);
54
+ return false;
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ // TODO: Check logs.
61
+ }
@@ -0,0 +1,65 @@
1
+ import { type AnyTx, Tx, type TxValidator } from '@aztec/circuit-types';
2
+ import { Fr } from '@aztec/circuits.js';
3
+ import { createDebugLogger } from '@aztec/foundation/log';
4
+
5
+ export interface NullifierSource {
6
+ getNullifierIndex: (nullifier: Fr) => Promise<bigint | undefined>;
7
+ }
8
+
9
+ export class DoubleSpendTxValidator<T extends AnyTx> implements TxValidator<T> {
10
+ #log = createDebugLogger('aztec:sequencer:tx_validator:tx_double_spend');
11
+ #nullifierSource: NullifierSource;
12
+
13
+ constructor(nullifierSource: NullifierSource, private readonly isValidatingBlock: boolean = true) {
14
+ this.#nullifierSource = nullifierSource;
15
+ }
16
+
17
+ async validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> {
18
+ const validTxs: T[] = [];
19
+ const invalidTxs: T[] = [];
20
+ const thisBlockNullifiers = new Set<bigint>();
21
+
22
+ for (const tx of txs) {
23
+ if (!(await this.#uniqueNullifiers(tx, thisBlockNullifiers))) {
24
+ invalidTxs.push(tx);
25
+ continue;
26
+ }
27
+
28
+ validTxs.push(tx);
29
+ }
30
+
31
+ return [validTxs, invalidTxs];
32
+ }
33
+
34
+ async #uniqueNullifiers(tx: AnyTx, thisBlockNullifiers: Set<bigint>): Promise<boolean> {
35
+ const nullifiers = tx.data.getNonEmptyNullifiers().map(x => x.toBigInt());
36
+
37
+ // Ditch this tx if it has repeated nullifiers
38
+ const uniqueNullifiers = new Set(nullifiers);
39
+ if (uniqueNullifiers.size !== nullifiers.length) {
40
+ this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for emitting duplicate nullifiers`);
41
+ return false;
42
+ }
43
+
44
+ if (this.isValidatingBlock) {
45
+ for (const nullifier of nullifiers) {
46
+ if (thisBlockNullifiers.has(nullifier)) {
47
+ this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating a nullifier in the same block`);
48
+ return false;
49
+ }
50
+
51
+ thisBlockNullifiers.add(nullifier);
52
+ }
53
+ }
54
+
55
+ const nullifierIndexes = await Promise.all(nullifiers.map(n => this.#nullifierSource.getNullifierIndex(new Fr(n))));
56
+
57
+ const hasDuplicates = nullifierIndexes.some(index => index !== undefined);
58
+ if (hasDuplicates) {
59
+ this.#log.warn(`Rejecting tx ${Tx.getHash(tx)} for repeating nullifiers present in state trees`);
60
+ return false;
61
+ }
62
+
63
+ return true;
64
+ }
65
+ }
@@ -0,0 +1,5 @@
1
+ export * from './aggregate_tx_validator.js';
2
+ export * from './data_validator.js';
3
+ export * from './double_spend_validator.js';
4
+ export * from './metadata_validator.js';
5
+ export * from './tx_proof_validator.js';
@@ -0,0 +1,61 @@
1
+ import { type AnyTx, Tx, type TxValidator } from '@aztec/circuit-types';
2
+ import { type Fr } from '@aztec/circuits.js';
3
+ import { createDebugLogger } from '@aztec/foundation/log';
4
+
5
+ export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
6
+ #log = createDebugLogger('aztec:sequencer:tx_validator:tx_metadata');
7
+
8
+ constructor(private chainId: Fr, private blockNumber: Fr) {}
9
+
10
+ validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[]]> {
11
+ const validTxs: T[] = [];
12
+ const invalidTxs: T[] = [];
13
+ for (const tx of txs) {
14
+ if (!this.#hasCorrectChainId(tx)) {
15
+ invalidTxs.push(tx);
16
+ continue;
17
+ }
18
+
19
+ if (!this.#isValidForBlockNumber(tx)) {
20
+ invalidTxs.push(tx);
21
+ continue;
22
+ }
23
+
24
+ validTxs.push(tx);
25
+ }
26
+
27
+ return Promise.resolve([validTxs, invalidTxs]);
28
+ }
29
+
30
+ #hasCorrectChainId(tx: T): boolean {
31
+ if (!tx.data.constants.txContext.chainId.equals(this.chainId)) {
32
+ this.#log.warn(
33
+ `Rejecting tx ${Tx.getHash(
34
+ tx,
35
+ )} because of incorrect chain ${tx.data.constants.txContext.chainId.toNumber()} != ${this.chainId.toNumber()}`,
36
+ );
37
+ return false;
38
+ } else {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ #isValidForBlockNumber(tx: T): boolean {
44
+ const target =
45
+ tx instanceof Tx
46
+ ? tx.data.forRollup?.rollupValidationRequests || tx.data.forPublic!.validationRequests.forRollup
47
+ : tx.data.rollupValidationRequests;
48
+ const maxBlockNumber = target.maxBlockNumber;
49
+
50
+ if (maxBlockNumber.isSome && maxBlockNumber.value < this.blockNumber) {
51
+ this.#log.warn(
52
+ `Rejecting tx ${Tx.getHash(tx)} for low max block number. Tx max block number: ${
53
+ maxBlockNumber.value
54
+ }, current block number: ${this.blockNumber}.`,
55
+ );
56
+ return false;
57
+ } else {
58
+ return true;
59
+ }
60
+ }
61
+ }