@aztec/p2p 0.55.0 → 0.56.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/memory_attestation_pool.d.ts.map +1 -1
- package/dest/attestation_pool/memory_attestation_pool.js +8 -7
- package/dest/attestation_pool/mocks.d.ts.map +1 -1
- package/dest/attestation_pool/mocks.js +6 -5
- 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.map +1 -1
- package/dest/client/p2p_client.js +1 -3
- package/dest/config.d.ts +49 -0
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +66 -2
- 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 +13 -4
- package/dest/mocks/index.d.ts.map +1 -1
- package/dest/mocks/index.js +26 -9
- package/dest/service/libp2p_service.d.ts +25 -11
- package/dest/service/libp2p_service.d.ts.map +1 -1
- package/dest/service/libp2p_service.js +143 -28
- 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/interface.d.ts +7 -0
- package/dest/service/reqresp/interface.d.ts.map +1 -1
- package/dest/service/reqresp/interface.js +7 -1
- package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts +13 -3
- package/dest/service/reqresp/rate_limiter/rate_limiter.d.ts.map +1 -1
- package/dest/service/reqresp/rate_limiter/rate_limiter.js +29 -7
- package/dest/service/reqresp/reqresp.d.ts +56 -5
- package/dest/service/reqresp/reqresp.d.ts.map +1 -1
- package/dest/service/reqresp/reqresp.js +73 -9
- package/dest/tx_validator/aggregate_tx_validator.d.ts +8 -0
- package/dest/tx_validator/aggregate_tx_validator.d.ts.map +1 -0
- package/dest/tx_validator/aggregate_tx_validator.js +32 -0
- package/dest/tx_validator/data_validator.d.ts +7 -0
- package/dest/tx_validator/data_validator.d.ts.map +1 -0
- package/dest/tx_validator/data_validator.js +50 -0
- package/dest/tx_validator/double_spend_validator.d.ts +13 -0
- package/dest/tx_validator/double_spend_validator.d.ts.map +1 -0
- package/dest/tx_validator/double_spend_validator.js +56 -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 +11 -0
- package/dest/tx_validator/metadata_validator.d.ts.map +1 -0
- package/dest/tx_validator/metadata_validator.js +53 -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/memory_attestation_pool.ts +7 -6
- package/src/attestation_pool/mocks.ts +6 -4
- package/src/client/index.ts +65 -47
- package/src/client/p2p_client.ts +0 -2
- package/src/config.ts +127 -0
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +35 -7
- package/src/service/libp2p_service.ts +182 -36
- package/src/service/peer_manager.ts +23 -4
- package/src/service/peer_scoring.ts +81 -0
- package/src/service/reqresp/interface.ts +20 -0
- package/src/service/reqresp/rate_limiter/rate_limiter.ts +30 -7
- package/src/service/reqresp/reqresp.ts +82 -8
- package/src/tx_validator/aggregate_tx_validator.ts +34 -0
- package/src/tx_validator/data_validator.ts +65 -0
- package/src/tx_validator/double_spend_validator.ts +69 -0
- package/src/tx_validator/index.ts +5 -0
- package/src/tx_validator/metadata_validator.ts +65 -0
- package/src/tx_validator/tx_proof_validator.ts +28 -0
- package/src/util.ts +8 -0
|
@@ -1,55 +1,63 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlockAttestation,
|
|
3
3
|
BlockProposal,
|
|
4
|
+
type ClientProtocolCircuitVerifier,
|
|
4
5
|
type Gossipable,
|
|
6
|
+
type L2BlockSource,
|
|
7
|
+
MerkleTreeId,
|
|
5
8
|
type RawGossipMessage,
|
|
6
9
|
TopicType,
|
|
7
10
|
TopicTypeMap,
|
|
8
11
|
Tx,
|
|
9
12
|
TxHash,
|
|
13
|
+
type WorldStateSynchronizer,
|
|
10
14
|
} from '@aztec/circuit-types';
|
|
15
|
+
import { Fr } from '@aztec/circuits.js';
|
|
11
16
|
import { createDebugLogger } from '@aztec/foundation/log';
|
|
12
17
|
import { SerialQueue } from '@aztec/foundation/queue';
|
|
13
18
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
14
19
|
import type { AztecKVStore } from '@aztec/kv-store';
|
|
15
20
|
|
|
16
21
|
import { type ENR } from '@chainsafe/enr';
|
|
17
|
-
import { type
|
|
22
|
+
import { type GossipSub, type GossipSubComponents, gossipsub } from '@chainsafe/libp2p-gossipsub';
|
|
23
|
+
import { createPeerScoreParams, createTopicScoreParams } from '@chainsafe/libp2p-gossipsub/score';
|
|
18
24
|
import { noise } from '@chainsafe/libp2p-noise';
|
|
19
25
|
import { yamux } from '@chainsafe/libp2p-yamux';
|
|
20
26
|
import { identify } from '@libp2p/identify';
|
|
21
|
-
import type { PeerId
|
|
27
|
+
import type { PeerId } from '@libp2p/interface';
|
|
22
28
|
import '@libp2p/kad-dht';
|
|
23
29
|
import { mplex } from '@libp2p/mplex';
|
|
24
30
|
import { createFromJSON, createSecp256k1PeerId } from '@libp2p/peer-id-factory';
|
|
25
31
|
import { tcp } from '@libp2p/tcp';
|
|
26
|
-
import {
|
|
32
|
+
import { createLibp2p } from 'libp2p';
|
|
27
33
|
|
|
28
34
|
import { type AttestationPool } from '../attestation_pool/attestation_pool.js';
|
|
29
35
|
import { type P2PConfig } from '../config.js';
|
|
30
36
|
import { type TxPool } from '../tx_pool/index.js';
|
|
31
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
DataTxValidator,
|
|
39
|
+
DoubleSpendTxValidator,
|
|
40
|
+
MetadataTxValidator,
|
|
41
|
+
TxProofValidator,
|
|
42
|
+
} from '../tx_validator/index.js';
|
|
43
|
+
import { type PubSubLibp2p, convertToMultiaddr } from '../util.js';
|
|
32
44
|
import { AztecDatastore } from './data_store.js';
|
|
33
45
|
import { PeerManager } from './peer_manager.js';
|
|
46
|
+
import { PeerErrorSeverity } from './peer_scoring.js';
|
|
34
47
|
import { pingHandler, statusHandler } from './reqresp/handlers.js';
|
|
35
48
|
import {
|
|
36
49
|
DEFAULT_SUB_PROTOCOL_HANDLERS,
|
|
50
|
+
DEFAULT_SUB_PROTOCOL_VALIDATORS,
|
|
37
51
|
PING_PROTOCOL,
|
|
38
52
|
type ReqRespSubProtocol,
|
|
39
53
|
type ReqRespSubProtocolHandlers,
|
|
40
54
|
STATUS_PROTOCOL,
|
|
41
55
|
type SubProtocolMap,
|
|
42
56
|
TX_REQ_PROTOCOL,
|
|
43
|
-
subProtocolMap,
|
|
44
57
|
} from './reqresp/interface.js';
|
|
45
58
|
import { ReqResp } from './reqresp/reqresp.js';
|
|
46
59
|
import type { P2PService, PeerDiscoveryService } from './service.js';
|
|
47
60
|
|
|
48
|
-
export interface PubSubLibp2p extends Libp2p {
|
|
49
|
-
services: {
|
|
50
|
-
pubsub: PubSub<GossipsubEvents>;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
61
|
/**
|
|
54
62
|
* Create a libp2p peer ID from the private key if provided, otherwise creates a new random ID.
|
|
55
63
|
* @param privateKey - Optional peer ID private key as hex string
|
|
@@ -90,11 +98,18 @@ export class LibP2PService implements P2PService {
|
|
|
90
98
|
private peerDiscoveryService: PeerDiscoveryService,
|
|
91
99
|
private txPool: TxPool,
|
|
92
100
|
private attestationPool: AttestationPool,
|
|
101
|
+
private l2BlockSource: L2BlockSource,
|
|
102
|
+
private proofVerifier: ClientProtocolCircuitVerifier,
|
|
103
|
+
private worldStateSynchronizer: WorldStateSynchronizer,
|
|
93
104
|
private requestResponseHandlers: ReqRespSubProtocolHandlers = DEFAULT_SUB_PROTOCOL_HANDLERS,
|
|
94
105
|
private logger = createDebugLogger('aztec:libp2p_service'),
|
|
95
106
|
) {
|
|
96
107
|
this.peerManager = new PeerManager(node, peerDiscoveryService, config, logger);
|
|
97
|
-
this.
|
|
108
|
+
this.node.services.pubsub.score.params.appSpecificScore = (peerId: string) => {
|
|
109
|
+
return this.peerManager.getPeerScore(peerId);
|
|
110
|
+
};
|
|
111
|
+
this.node.services.pubsub.score.params.appSpecificWeight = 10;
|
|
112
|
+
this.reqresp = new ReqResp(config, node, this.peerManager);
|
|
98
113
|
|
|
99
114
|
this.blockReceivedCallback = (block: BlockProposal): Promise<BlockAttestation | undefined> => {
|
|
100
115
|
this.logger.verbose(
|
|
@@ -136,18 +151,24 @@ export class LibP2PService implements P2PService {
|
|
|
136
151
|
|
|
137
152
|
// add GossipSub listener
|
|
138
153
|
this.node.services.pubsub.addEventListener('gossipsub:message', async e => {
|
|
139
|
-
const { msg } = e.detail;
|
|
154
|
+
const { msg, propagationSource: peerId } = e.detail;
|
|
140
155
|
this.logger.debug(`Received PUBSUB message.`);
|
|
141
156
|
|
|
142
|
-
await this.jobQueue.put(() => this.handleNewGossipMessage(msg));
|
|
157
|
+
await this.jobQueue.put(() => this.handleNewGossipMessage(msg, peerId));
|
|
143
158
|
});
|
|
144
159
|
|
|
145
160
|
// Start running promise for peer discovery
|
|
146
161
|
this.discoveryRunningPromise = new RunningPromise(() => {
|
|
147
|
-
this.peerManager.
|
|
162
|
+
this.peerManager.heartbeat();
|
|
148
163
|
}, this.config.peerCheckIntervalMS);
|
|
149
164
|
this.discoveryRunningPromise.start();
|
|
150
|
-
|
|
165
|
+
|
|
166
|
+
// Define the sub protocol validators - This is done within this start() method to gain a callback to the existing validateTx function
|
|
167
|
+
const reqrespSubProtocolValidators = {
|
|
168
|
+
...DEFAULT_SUB_PROTOCOL_VALIDATORS,
|
|
169
|
+
[TX_REQ_PROTOCOL]: this.validateRequestedTx.bind(this),
|
|
170
|
+
};
|
|
171
|
+
await this.reqresp.start(this.requestResponseHandlers, reqrespSubProtocolValidators);
|
|
151
172
|
}
|
|
152
173
|
|
|
153
174
|
/**
|
|
@@ -181,6 +202,9 @@ export class LibP2PService implements P2PService {
|
|
|
181
202
|
peerId: PeerId,
|
|
182
203
|
txPool: TxPool,
|
|
183
204
|
attestationPool: AttestationPool,
|
|
205
|
+
l2BlockSource: L2BlockSource,
|
|
206
|
+
proofVerifier: ClientProtocolCircuitVerifier,
|
|
207
|
+
worldStateSynchronizer: WorldStateSynchronizer,
|
|
184
208
|
store: AztecKVStore,
|
|
185
209
|
) {
|
|
186
210
|
const { tcpListenAddress, tcpAnnounceAddress, minPeerCount, maxPeerCount } = config;
|
|
@@ -223,13 +247,22 @@ export class LibP2PService implements P2PService {
|
|
|
223
247
|
}),
|
|
224
248
|
pubsub: gossipsub({
|
|
225
249
|
allowPublishToZeroTopicPeers: true,
|
|
226
|
-
D:
|
|
227
|
-
Dlo:
|
|
228
|
-
Dhi:
|
|
229
|
-
heartbeatInterval:
|
|
230
|
-
mcacheLength:
|
|
231
|
-
mcacheGossip:
|
|
232
|
-
|
|
250
|
+
D: config.gossipsubD,
|
|
251
|
+
Dlo: config.gossipsubDlo,
|
|
252
|
+
Dhi: config.gossipsubDhi,
|
|
253
|
+
heartbeatInterval: config.gossipsubInterval,
|
|
254
|
+
mcacheLength: config.gossipsubMcacheLength,
|
|
255
|
+
mcacheGossip: config.gossipsubMcacheGossip,
|
|
256
|
+
scoreParams: createPeerScoreParams({
|
|
257
|
+
topics: {
|
|
258
|
+
[Tx.p2pTopic]: createTopicScoreParams({
|
|
259
|
+
topicWeight: 1,
|
|
260
|
+
invalidMessageDeliveriesWeight: -20,
|
|
261
|
+
invalidMessageDeliveriesDecay: 0.5,
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
}) as (components: GossipSubComponents) => GossipSub,
|
|
233
266
|
},
|
|
234
267
|
});
|
|
235
268
|
|
|
@@ -252,7 +285,17 @@ export class LibP2PService implements P2PService {
|
|
|
252
285
|
[TX_REQ_PROTOCOL]: txHandler,
|
|
253
286
|
};
|
|
254
287
|
|
|
255
|
-
return new LibP2PService(
|
|
288
|
+
return new LibP2PService(
|
|
289
|
+
config,
|
|
290
|
+
node,
|
|
291
|
+
peerDiscoveryService,
|
|
292
|
+
txPool,
|
|
293
|
+
attestationPool,
|
|
294
|
+
l2BlockSource,
|
|
295
|
+
proofVerifier,
|
|
296
|
+
worldStateSynchronizer,
|
|
297
|
+
requestResponseHandlers,
|
|
298
|
+
);
|
|
256
299
|
}
|
|
257
300
|
|
|
258
301
|
/**
|
|
@@ -265,18 +308,11 @@ export class LibP2PService implements P2PService {
|
|
|
265
308
|
* @param request The request type to send
|
|
266
309
|
* @returns
|
|
267
310
|
*/
|
|
268
|
-
|
|
311
|
+
sendRequest<SubProtocol extends ReqRespSubProtocol>(
|
|
269
312
|
protocol: SubProtocol,
|
|
270
313
|
request: InstanceType<SubProtocolMap[SubProtocol]['request']>,
|
|
271
314
|
): Promise<InstanceType<SubProtocolMap[SubProtocol]['response']> | undefined> {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const res = await this.reqresp.sendRequest(protocol, request.toBuffer());
|
|
275
|
-
if (!res) {
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return pair.response.fromBuffer(res!);
|
|
315
|
+
return this.reqresp.sendRequest(protocol, request);
|
|
280
316
|
}
|
|
281
317
|
|
|
282
318
|
/**
|
|
@@ -323,10 +359,10 @@ export class LibP2PService implements P2PService {
|
|
|
323
359
|
* @param topic - The message's topic.
|
|
324
360
|
* @param data - The message data
|
|
325
361
|
*/
|
|
326
|
-
private async handleNewGossipMessage(message: RawGossipMessage) {
|
|
362
|
+
private async handleNewGossipMessage(message: RawGossipMessage, peerId: PeerId) {
|
|
327
363
|
if (message.topic === Tx.p2pTopic) {
|
|
328
364
|
const tx = Tx.fromBuffer(Buffer.from(message.data));
|
|
329
|
-
await this.processTxFromPeer(tx);
|
|
365
|
+
await this.processTxFromPeer(tx, peerId);
|
|
330
366
|
}
|
|
331
367
|
if (message.topic === BlockAttestation.p2pTopic) {
|
|
332
368
|
const attestation = BlockAttestation.fromBuffer(Buffer.from(message.data));
|
|
@@ -376,11 +412,121 @@ export class LibP2PService implements P2PService {
|
|
|
376
412
|
void this.jobQueue.put(() => Promise.resolve(this.sendToPeers(message)));
|
|
377
413
|
}
|
|
378
414
|
|
|
379
|
-
private async processTxFromPeer(tx: Tx): Promise<void> {
|
|
415
|
+
private async processTxFromPeer(tx: Tx, peerId: PeerId): Promise<void> {
|
|
380
416
|
const txHash = tx.getTxHash();
|
|
381
417
|
const txHashString = txHash.toString();
|
|
382
418
|
this.logger.verbose(`Received tx ${txHashString} from external peer.`);
|
|
383
|
-
|
|
419
|
+
|
|
420
|
+
const isValidTx = await this.validatePropagatedTx(tx, peerId);
|
|
421
|
+
|
|
422
|
+
if (isValidTx) {
|
|
423
|
+
await this.txPool.addTxs([tx]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Validate a tx that has been requested from a peer.
|
|
429
|
+
*
|
|
430
|
+
* The core component of this validator is that the tx hash MUST match the requested tx hash,
|
|
431
|
+
* In order to perform this check, the tx proof must be verified.
|
|
432
|
+
*
|
|
433
|
+
* Note: This function is called from within `ReqResp.sendRequest` as part of the
|
|
434
|
+
* TX_REQ_PROTOCOL subprotocol validation.
|
|
435
|
+
*
|
|
436
|
+
* @param requestedTxHash - The hash of the tx that was requested.
|
|
437
|
+
* @param responseTx - The tx that was received as a response to the request.
|
|
438
|
+
* @param peerId - The peer ID of the peer that sent the tx.
|
|
439
|
+
* @returns True if the tx is valid, false otherwise.
|
|
440
|
+
*/
|
|
441
|
+
private async validateRequestedTx(requestedTxHash: TxHash, responseTx: Tx, peerId: PeerId): Promise<boolean> {
|
|
442
|
+
const proofValidator = new TxProofValidator(this.proofVerifier);
|
|
443
|
+
const validProof = await proofValidator.validateTx(responseTx);
|
|
444
|
+
|
|
445
|
+
// If the node returns the wrong data, we penalize it
|
|
446
|
+
if (!requestedTxHash.equals(responseTx.getTxHash())) {
|
|
447
|
+
// Returning the wrong data is a low tolerance error
|
|
448
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.MidToleranceError);
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!validProof) {
|
|
453
|
+
// If the proof is invalid, but the txHash is correct, then this is an active attack and we severly punish
|
|
454
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async validatePropagatedTx(tx: Tx, peerId: PeerId): Promise<boolean> {
|
|
462
|
+
const blockNumber = (await this.l2BlockSource.getBlockNumber()) + 1;
|
|
463
|
+
// basic data validation
|
|
464
|
+
const dataValidator = new DataTxValidator();
|
|
465
|
+
const validData = await dataValidator.validateTx(tx);
|
|
466
|
+
if (!validData) {
|
|
467
|
+
// penalize
|
|
468
|
+
this.node.services.pubsub.score.markInvalidMessageDelivery(peerId.toString(), Tx.p2pTopic);
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// metadata validation
|
|
473
|
+
const metadataValidator = new MetadataTxValidator(new Fr(this.config.l1ChainId), new Fr(blockNumber));
|
|
474
|
+
const validMetadata = await metadataValidator.validateTx(tx);
|
|
475
|
+
if (!validMetadata) {
|
|
476
|
+
// penalize
|
|
477
|
+
this.node.services.pubsub.score.markInvalidMessageDelivery(peerId.toString(), Tx.p2pTopic);
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// double spend validation
|
|
482
|
+
const doubleSpendValidator = new DoubleSpendTxValidator({
|
|
483
|
+
getNullifierIndex: async (nullifier: Fr) => {
|
|
484
|
+
const merkleTree = this.worldStateSynchronizer.getLatest();
|
|
485
|
+
const index = await merkleTree.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer());
|
|
486
|
+
return index;
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
const validDoubleSpend = await doubleSpendValidator.validateTx(tx);
|
|
490
|
+
if (!validDoubleSpend) {
|
|
491
|
+
// check if nullifier is older than 20 blocks
|
|
492
|
+
if (blockNumber - this.config.severePeerPenaltyBlockLength > 0) {
|
|
493
|
+
const snapshotValidator = new DoubleSpendTxValidator({
|
|
494
|
+
getNullifierIndex: async (nullifier: Fr) => {
|
|
495
|
+
const merkleTree = this.worldStateSynchronizer.getSnapshot(
|
|
496
|
+
blockNumber - this.config.severePeerPenaltyBlockLength,
|
|
497
|
+
);
|
|
498
|
+
const index = await merkleTree.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer());
|
|
499
|
+
return index;
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const validSnapshot = await snapshotValidator.validateTx(tx);
|
|
504
|
+
// High penalty if nullifier is older than 20 blocks
|
|
505
|
+
if (!validSnapshot) {
|
|
506
|
+
// penalize
|
|
507
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// penalize
|
|
512
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// proof validation
|
|
517
|
+
const proofValidator = new TxProofValidator(this.proofVerifier);
|
|
518
|
+
const validProof = await proofValidator.validateTx(tx);
|
|
519
|
+
if (!validProof) {
|
|
520
|
+
// penalize
|
|
521
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.MidToleranceError);
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public getPeerScore(peerId: PeerId): number {
|
|
529
|
+
return this.node.services.pubsub.score.score(peerId.toString());
|
|
384
530
|
}
|
|
385
531
|
|
|
386
532
|
private async sendToPeers<T extends Gossipable>(message: T) {
|
|
@@ -3,9 +3,10 @@ import { createDebugLogger } from '@aztec/foundation/log';
|
|
|
3
3
|
import { type ENR } from '@chainsafe/enr';
|
|
4
4
|
import { type PeerId } from '@libp2p/interface';
|
|
5
5
|
import { type Multiaddr } from '@multiformats/multiaddr';
|
|
6
|
-
import { type Libp2p } from 'libp2p';
|
|
7
6
|
|
|
8
7
|
import { type P2PConfig } from '../config.js';
|
|
8
|
+
import { type PubSubLibp2p } from '../util.js';
|
|
9
|
+
import { type PeerErrorSeverity, PeerScoring } from './peer_scoring.js';
|
|
9
10
|
import { type PeerDiscoveryService } from './service.js';
|
|
10
11
|
|
|
11
12
|
const MAX_DIAL_ATTEMPTS = 3;
|
|
@@ -20,12 +21,15 @@ type CachedPeer = {
|
|
|
20
21
|
|
|
21
22
|
export class PeerManager {
|
|
22
23
|
private cachedPeers: Map<string, CachedPeer> = new Map();
|
|
24
|
+
private peerScoring: PeerScoring;
|
|
25
|
+
|
|
23
26
|
constructor(
|
|
24
|
-
private libP2PNode:
|
|
27
|
+
private libP2PNode: PubSubLibp2p,
|
|
25
28
|
private peerDiscoveryService: PeerDiscoveryService,
|
|
26
29
|
private config: P2PConfig,
|
|
27
30
|
private logger = createDebugLogger('aztec:p2p:peer_manager'),
|
|
28
31
|
) {
|
|
32
|
+
this.peerScoring = new PeerScoring(config);
|
|
29
33
|
// Handle new established connections
|
|
30
34
|
this.libP2PNode.addEventListener('peer:connect', evt => {
|
|
31
35
|
const peerId = evt.detail;
|
|
@@ -52,10 +56,25 @@ export class PeerManager {
|
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
public heartbeat() {
|
|
60
|
+
this.discover();
|
|
61
|
+
this.peerScoring.decayAllScores();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity) {
|
|
65
|
+
const id = peerId.toString();
|
|
66
|
+
const penaltyValue = this.peerScoring.peerPenalties[penalty];
|
|
67
|
+
this.peerScoring.updateScore(id, -penaltyValue);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getPeerScore(peerId: string): number {
|
|
71
|
+
return this.peerScoring.getScore(peerId);
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/**
|
|
56
75
|
* Discovers peers.
|
|
57
76
|
*/
|
|
58
|
-
|
|
77
|
+
private discover() {
|
|
59
78
|
// Get current connections
|
|
60
79
|
const connections = this.libP2PNode.getConnections();
|
|
61
80
|
|
|
@@ -155,7 +174,7 @@ export class PeerManager {
|
|
|
155
174
|
}
|
|
156
175
|
}
|
|
157
176
|
|
|
158
|
-
async dialPeer(peer: CachedPeer) {
|
|
177
|
+
private async dialPeer(peer: CachedPeer) {
|
|
159
178
|
const id = peer.peerId.toString();
|
|
160
179
|
await this.libP2PNode.peerStore.merge(peer.peerId, { multiaddrs: [peer.multiaddrTcp] });
|
|
161
180
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type P2PConfig } from '../config.js';
|
|
2
|
+
|
|
3
|
+
export enum PeerErrorSeverity {
|
|
4
|
+
/**
|
|
5
|
+
* Not malicious action, but it must not be tolerated
|
|
6
|
+
* ~2 occurrences will get the peer banned
|
|
7
|
+
*/
|
|
8
|
+
LowToleranceError = 'LowToleranceError',
|
|
9
|
+
/**
|
|
10
|
+
* Negative action that can be tolerated only sometimes
|
|
11
|
+
* ~10 occurrences will get the peer banned
|
|
12
|
+
*/
|
|
13
|
+
MidToleranceError = 'MidToleranceError',
|
|
14
|
+
/**
|
|
15
|
+
* Some error that can be tolerated multiple times
|
|
16
|
+
* ~50 occurrences will get the peer banned
|
|
17
|
+
*/
|
|
18
|
+
HighToleranceError = 'HighToleranceError',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DefaultPeerPenalties = {
|
|
22
|
+
[PeerErrorSeverity.LowToleranceError]: 2,
|
|
23
|
+
[PeerErrorSeverity.MidToleranceError]: 10,
|
|
24
|
+
[PeerErrorSeverity.HighToleranceError]: 50,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class PeerScoring {
|
|
28
|
+
private scores: Map<string, number> = new Map();
|
|
29
|
+
private lastUpdateTime: Map<string, number> = new Map();
|
|
30
|
+
private decayInterval = 1000 * 60; // 1 minute
|
|
31
|
+
private decayFactor = 0.9;
|
|
32
|
+
peerPenalties: { [key in PeerErrorSeverity]: number };
|
|
33
|
+
|
|
34
|
+
constructor(config: P2PConfig) {
|
|
35
|
+
const orderedValues = config.peerPenaltyValues.sort((a, b) => a - b);
|
|
36
|
+
this.peerPenalties = {
|
|
37
|
+
[PeerErrorSeverity.HighToleranceError]:
|
|
38
|
+
orderedValues[0] ?? DefaultPeerPenalties[PeerErrorSeverity.LowToleranceError],
|
|
39
|
+
[PeerErrorSeverity.MidToleranceError]:
|
|
40
|
+
orderedValues[1] ?? DefaultPeerPenalties[PeerErrorSeverity.MidToleranceError],
|
|
41
|
+
[PeerErrorSeverity.LowToleranceError]:
|
|
42
|
+
orderedValues[2] ?? DefaultPeerPenalties[PeerErrorSeverity.HighToleranceError],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updateScore(peerId: string, scoreDelta: number): void {
|
|
47
|
+
const currentTime = Date.now();
|
|
48
|
+
const lastUpdate = this.lastUpdateTime.get(peerId) || currentTime;
|
|
49
|
+
const timePassed = currentTime - lastUpdate;
|
|
50
|
+
const decayPeriods = Math.floor(timePassed / this.decayInterval);
|
|
51
|
+
|
|
52
|
+
let currentScore = this.scores.get(peerId) || 0;
|
|
53
|
+
|
|
54
|
+
// Apply decay
|
|
55
|
+
currentScore *= Math.pow(this.decayFactor, decayPeriods);
|
|
56
|
+
|
|
57
|
+
// Apply new score delta
|
|
58
|
+
currentScore += scoreDelta;
|
|
59
|
+
|
|
60
|
+
this.scores.set(peerId, currentScore);
|
|
61
|
+
this.lastUpdateTime.set(peerId, currentTime);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
decayAllScores(): void {
|
|
65
|
+
const currentTime = Date.now();
|
|
66
|
+
for (const [peerId, lastUpdate] of this.lastUpdateTime.entries()) {
|
|
67
|
+
const timePassed = currentTime - lastUpdate;
|
|
68
|
+
const decayPeriods = Math.floor(timePassed / this.decayInterval);
|
|
69
|
+
if (decayPeriods > 0) {
|
|
70
|
+
let score = this.scores.get(peerId) || 0;
|
|
71
|
+
score *= Math.pow(this.decayFactor, decayPeriods);
|
|
72
|
+
this.scores.set(peerId, score);
|
|
73
|
+
this.lastUpdateTime.set(peerId, currentTime);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getScore(peerId: string): number {
|
|
79
|
+
return this.scores.get(peerId) || 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Tx, TxHash } from '@aztec/circuit-types';
|
|
2
2
|
|
|
3
|
+
import { type PeerId } from '@libp2p/interface';
|
|
4
|
+
|
|
3
5
|
/*
|
|
4
6
|
* Request Response Sub Protocols
|
|
5
7
|
*/
|
|
@@ -46,11 +48,29 @@ export interface ProtocolRateLimitQuota {
|
|
|
46
48
|
globalLimit: RateLimitQuota;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export const noopValidator = () => Promise.resolve(true);
|
|
52
|
+
|
|
49
53
|
/**
|
|
50
54
|
* A type mapping from supprotocol to it's handling funciton
|
|
51
55
|
*/
|
|
52
56
|
export type ReqRespSubProtocolHandlers = Record<ReqRespSubProtocol, ReqRespSubProtocolHandler>;
|
|
53
57
|
|
|
58
|
+
type ResponseValidator<RequestIdentifier, Response> = (
|
|
59
|
+
request: RequestIdentifier,
|
|
60
|
+
response: Response,
|
|
61
|
+
peerId: PeerId,
|
|
62
|
+
) => Promise<boolean>;
|
|
63
|
+
|
|
64
|
+
export type ReqRespSubProtocolValidators = {
|
|
65
|
+
[S in ReqRespSubProtocol]: ResponseValidator<any, any>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_SUB_PROTOCOL_VALIDATORS: ReqRespSubProtocolValidators = {
|
|
69
|
+
[PING_PROTOCOL]: noopValidator,
|
|
70
|
+
[STATUS_PROTOCOL]: noopValidator,
|
|
71
|
+
[TX_REQ_PROTOCOL]: noopValidator,
|
|
72
|
+
};
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* Sub protocol map determines the request and response types for each
|
|
56
76
|
* Req Resp protocol
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type PeerId } from '@libp2p/interface';
|
|
7
7
|
|
|
8
|
+
import { type PeerManager } from '../../peer_manager.js';
|
|
9
|
+
import { PeerErrorSeverity } from '../../peer_scoring.js';
|
|
8
10
|
import { type ReqRespSubProtocol, type ReqRespSubProtocolRateLimits } from '../interface.js';
|
|
9
11
|
import { DEFAULT_RATE_LIMITS } from './rate_limits.js';
|
|
10
12
|
|
|
@@ -69,6 +71,12 @@ interface PeerRateLimiter {
|
|
|
69
71
|
lastAccess: number;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
enum RateLimitStatus {
|
|
75
|
+
Allowed,
|
|
76
|
+
DeniedGlobal,
|
|
77
|
+
DeniedPeer,
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
/**
|
|
73
81
|
* SubProtocolRateLimiter: A rate limiter for managing request rates on a per-peer and global basis for a specific subprotocol.
|
|
74
82
|
*
|
|
@@ -98,9 +106,9 @@ export class SubProtocolRateLimiter {
|
|
|
98
106
|
this.peerQuotaTimeMs = peerQuotaTimeMs;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
allow(peerId: PeerId):
|
|
109
|
+
allow(peerId: PeerId): RateLimitStatus {
|
|
102
110
|
if (!this.globalLimiter.allow()) {
|
|
103
|
-
return
|
|
111
|
+
return RateLimitStatus.DeniedGlobal;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
const peerIdStr = peerId.toString();
|
|
@@ -115,7 +123,11 @@ export class SubProtocolRateLimiter {
|
|
|
115
123
|
} else {
|
|
116
124
|
peerLimiter.lastAccess = Date.now();
|
|
117
125
|
}
|
|
118
|
-
|
|
126
|
+
const peerLimitAllowed = peerLimiter.limiter.allow();
|
|
127
|
+
if (!peerLimitAllowed) {
|
|
128
|
+
return RateLimitStatus.DeniedPeer;
|
|
129
|
+
}
|
|
130
|
+
return RateLimitStatus.Allowed;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
cleanupInactivePeers() {
|
|
@@ -138,14 +150,16 @@ export class SubProtocolRateLimiter {
|
|
|
138
150
|
* - Initializes with a set of rate limit configurations for different subprotocols.
|
|
139
151
|
* - Creates a separate SubProtocolRateLimiter for each configured subprotocol.
|
|
140
152
|
* - When a request comes in, it routes the rate limiting decision to the appropriate subprotocol limiter.
|
|
153
|
+
* - Peers who exceed their peer rate limits will be penalised by the peer manager.
|
|
141
154
|
*
|
|
142
155
|
* Usage:
|
|
143
156
|
* ```
|
|
157
|
+
* const peerManager = new PeerManager(...);
|
|
144
158
|
* const rateLimits = {
|
|
145
159
|
* subprotocol1: { peerLimit: { quotaCount: 10, quotaTimeMs: 1000 }, globalLimit: { quotaCount: 100, quotaTimeMs: 1000 } },
|
|
146
160
|
* subprotocol2: { peerLimit: { quotaCount: 5, quotaTimeMs: 1000 }, globalLimit: { quotaCount: 50, quotaTimeMs: 1000 } }
|
|
147
161
|
* };
|
|
148
|
-
* const limiter = new RequestResponseRateLimiter(rateLimits);
|
|
162
|
+
* const limiter = new RequestResponseRateLimiter(peerManager, rateLimits);
|
|
149
163
|
*
|
|
150
164
|
* Note: Ensure to call `stop()` when shutting down to properly clean up all subprotocol limiters.
|
|
151
165
|
*/
|
|
@@ -154,7 +168,7 @@ export class RequestResponseRateLimiter {
|
|
|
154
168
|
|
|
155
169
|
private cleanupInterval: NodeJS.Timeout | undefined = undefined;
|
|
156
170
|
|
|
157
|
-
constructor(rateLimits: ReqRespSubProtocolRateLimits = DEFAULT_RATE_LIMITS) {
|
|
171
|
+
constructor(private peerManager: PeerManager, rateLimits: ReqRespSubProtocolRateLimits = DEFAULT_RATE_LIMITS) {
|
|
158
172
|
this.subProtocolRateLimiters = new Map();
|
|
159
173
|
|
|
160
174
|
for (const [subProtocol, protocolLimits] of Object.entries(rateLimits)) {
|
|
@@ -179,10 +193,19 @@ export class RequestResponseRateLimiter {
|
|
|
179
193
|
allow(subProtocol: ReqRespSubProtocol, peerId: PeerId): boolean {
|
|
180
194
|
const limiter = this.subProtocolRateLimiters.get(subProtocol);
|
|
181
195
|
if (!limiter) {
|
|
182
|
-
// TODO: maybe throw an error here if no rate limiter is configured?
|
|
183
196
|
return true;
|
|
184
197
|
}
|
|
185
|
-
|
|
198
|
+
const rateLimitStatus = limiter.allow(peerId);
|
|
199
|
+
|
|
200
|
+
switch (rateLimitStatus) {
|
|
201
|
+
case RateLimitStatus.DeniedPeer:
|
|
202
|
+
this.peerManager.penalizePeer(peerId, PeerErrorSeverity.MidToleranceError);
|
|
203
|
+
return false;
|
|
204
|
+
case RateLimitStatus.DeniedGlobal:
|
|
205
|
+
return false;
|
|
206
|
+
default:
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
cleanupInactivePeers() {
|