@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.
- package/dest/attestation_pool/mocks.d.ts.map +1 -1
- package/dest/attestation_pool/mocks.js +6 -4
- package/dest/client/index.d.ts +2 -2
- package/dest/client/index.d.ts.map +1 -1
- package/dest/client/index.js +43 -38
- 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 +51 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +68 -2
- 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/index.d.ts +1 -0
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -1
- package/dest/mocks/index.d.ts.map +1 -1
- package/dest/mocks/index.js +6 -2
- package/dest/service/libp2p_service.d.ts +10 -11
- package/dest/service/libp2p_service.d.ts.map +1 -1
- package/dest/service/libp2p_service.js +105 -20
- package/dest/service/peer_manager.d.ts +9 -13
- package/dest/service/peer_manager.d.ts.map +1 -1
- package/dest/service/peer_manager.js +15 -1
- package/dest/service/peer_scoring.d.ts +32 -0
- package/dest/service/peer_scoring.d.ts.map +1 -0
- package/dest/service/peer_scoring.js +67 -0
- 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/dest/tx_validator/aggregate_tx_validator.d.ts +7 -0
- package/dest/tx_validator/aggregate_tx_validator.d.ts.map +1 -0
- package/dest/tx_validator/aggregate_tx_validator.js +23 -0
- package/dest/tx_validator/data_validator.d.ts +6 -0
- package/dest/tx_validator/data_validator.d.ts.map +1 -0
- package/dest/tx_validator/data_validator.js +47 -0
- package/dest/tx_validator/double_spend_validator.d.ts +12 -0
- package/dest/tx_validator/double_spend_validator.d.ts.map +1 -0
- package/dest/tx_validator/double_spend_validator.js +53 -0
- package/dest/tx_validator/index.d.ts +6 -0
- package/dest/tx_validator/index.d.ts.map +1 -0
- package/dest/tx_validator/index.js +6 -0
- package/dest/tx_validator/metadata_validator.d.ts +10 -0
- package/dest/tx_validator/metadata_validator.d.ts.map +1 -0
- package/dest/tx_validator/metadata_validator.js +50 -0
- package/dest/tx_validator/tx_proof_validator.d.ts +9 -0
- package/dest/tx_validator/tx_proof_validator.d.ts.map +1 -0
- package/dest/tx_validator/tx_proof_validator.js +29 -0
- package/dest/util.d.ts +7 -0
- package/dest/util.d.ts.map +1 -1
- package/dest/util.js +1 -1
- package/package.json +6 -6
- package/src/attestation_pool/mocks.ts +6 -3
- package/src/client/index.ts +65 -47
- package/src/client/p2p_client.ts +44 -5
- package/src/config.ts +131 -1
- package/src/errors/reqresp.error.ts +21 -0
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +6 -1
- package/src/service/libp2p_service.ts +139 -26
- package/src/service/peer_manager.ts +23 -4
- package/src/service/peer_scoring.ts +81 -0
- 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/tx_validator/aggregate_tx_validator.ts +24 -0
- package/src/tx_validator/data_validator.ts +61 -0
- package/src/tx_validator/double_spend_validator.ts +65 -0
- package/src/tx_validator/index.ts +5 -0
- package/src/tx_validator/metadata_validator.ts +61 -0
- package/src/tx_validator/tx_proof_validator.ts +28 -0
- 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/
|
|
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
|
}
|
|
@@ -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,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
|
+
}
|