@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.
- package/dest/attestation_pool/mocks.d.ts.map +1 -1
- package/dest/attestation_pool/mocks.js +6 -4
- package/dest/client/p2p_client.d.ts +27 -2
- package/dest/client/p2p_client.d.ts.map +1 -1
- package/dest/client/p2p_client.js +33 -4
- package/dest/config.d.ts +2 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +3 -1
- package/dest/errors/reqresp.error.d.ts +17 -0
- package/dest/errors/reqresp.error.d.ts.map +1 -0
- package/dest/errors/reqresp.error.js +21 -0
- package/dest/mocks/index.d.ts.map +1 -1
- package/dest/mocks/index.js +6 -2
- package/dest/service/libp2p_service.js +3 -3
- package/dest/service/reqresp/config.d.ts +16 -0
- package/dest/service/reqresp/config.d.ts.map +1 -0
- package/dest/service/reqresp/config.js +21 -0
- package/dest/service/reqresp/interface.d.ts +30 -3
- package/dest/service/reqresp/interface.d.ts.map +1 -1
- package/dest/service/reqresp/interface.js +4 -4
- package/dest/service/reqresp/rate_limiter/index.d.ts +2 -0
- package/dest/service/reqresp/rate_limiter/index.d.ts.map +1 -0
- package/dest/service/reqresp/rate_limiter/index.js +2 -0
- package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts +97 -0
- package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts.map +1 -0
- package/dest/service/reqresp/rate_limiter/rate_limiter.js +148 -0
- package/dest/service/reqresp/rate_limiter/rate_limits.d.ts +3 -0
- package/dest/service/reqresp/rate_limiter/rate_limits.d.ts.map +1 -0
- package/dest/service/reqresp/rate_limiter/rate_limits.js +35 -0
- package/dest/service/reqresp/reqresp.d.ts +5 -1
- package/dest/service/reqresp/reqresp.d.ts.map +1 -1
- package/dest/service/reqresp/reqresp.js +55 -17
- package/dest/service/service.d.ts +1 -1
- package/dest/service/service.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/attestation_pool/mocks.ts +6 -3
- package/src/client/p2p_client.ts +44 -5
- package/src/config.ts +4 -1
- package/src/errors/reqresp.error.ts +21 -0
- package/src/mocks/index.ts +6 -1
- package/src/service/libp2p_service.ts +2 -2
- package/src/service/reqresp/config.ts +35 -0
- package/src/service/reqresp/interface.ts +33 -3
- package/src/service/reqresp/rate_limiter/index.ts +1 -0
- package/src/service/reqresp/rate_limiter/rate_limiter.ts +198 -0
- package/src/service/reqresp/rate_limiter/rate_limits.ts +35 -0
- package/src/service/reqresp/reqresp.ts +78 -20
- 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
|
+
}
|
package/src/mocks/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
104
|
-
|
|
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 {
|
package/src/service/service.ts
CHANGED
|
@@ -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
|
}
|