@aztec/p2p 5.0.0-rc.1 → 5.0.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/README.md +3 -5
- package/dest/bootstrap/bootstrap.d.ts +1 -1
- package/dest/bootstrap/bootstrap.d.ts.map +1 -1
- package/dest/bootstrap/bootstrap.js +20 -12
- package/dest/client/factory.d.ts +1 -1
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +2 -1
- package/dest/client/interface.d.ts +14 -2
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/client/p2p_client.d.ts +4 -2
- package/dest/client/p2p_client.d.ts.map +1 -1
- package/dest/client/p2p_client.js +10 -2
- package/dest/config.d.ts +7 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +10 -5
- package/dest/errors/reqresp.error.d.ts +13 -1
- package/dest/errors/reqresp.error.d.ts.map +1 -1
- package/dest/errors/reqresp.error.js +14 -0
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool.js +6 -2
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +43 -0
- package/dest/mem_pools/instrumentation.d.ts +9 -4
- package/dest/mem_pools/instrumentation.d.ts.map +1 -1
- package/dest/mem_pools/instrumentation.js +11 -8
- package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +12 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +9 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +21 -0
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +2 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +12 -4
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +10 -2
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +56 -8
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +10 -6
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.js +25 -12
- package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
- package/dest/services/discv5/discV5_service.d.ts +12 -2
- package/dest/services/discv5/discV5_service.d.ts.map +1 -1
- package/dest/services/discv5/discV5_service.js +84 -15
- package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
- package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
- package/dest/services/discv5/persisted_enr_store.js +145 -0
- package/dest/services/dummy_service.d.ts +6 -2
- package/dest/services/dummy_service.d.ts.map +1 -1
- package/dest/services/dummy_service.js +3 -0
- package/dest/services/encoding.d.ts +19 -6
- package/dest/services/encoding.d.ts.map +1 -1
- package/dest/services/encoding.js +25 -15
- package/dest/services/libp2p/libp2p_service.d.ts +11 -3
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +98 -34
- package/dest/services/peer-manager/peer_manager.d.ts +1 -1
- package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
- package/dest/services/peer-manager/peer_manager.js +21 -12
- package/dest/services/reqresp/config.d.ts +13 -1
- package/dest/services/reqresp/config.d.ts.map +1 -1
- package/dest/services/reqresp/config.js +11 -0
- package/dest/services/reqresp/rate-limiter/rate_limits.js +3 -3
- package/dest/services/reqresp/reqresp.d.ts +1 -1
- package/dest/services/reqresp/reqresp.d.ts.map +1 -1
- package/dest/services/reqresp/reqresp.js +34 -3
- package/dest/services/service.d.ts +17 -1
- package/dest/services/service.d.ts.map +1 -1
- package/dest/test-helpers/testbench-utils.d.ts +2 -1
- package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
- package/dest/test-helpers/testbench-utils.js +3 -0
- package/package.json +16 -17
- package/src/bootstrap/bootstrap.ts +15 -9
- package/src/client/factory.ts +1 -0
- package/src/client/interface.ts +15 -0
- package/src/client/p2p_client.ts +14 -3
- package/src/config.ts +17 -4
- package/src/errors/reqresp.error.ts +19 -0
- package/src/mem_pools/attestation_pool/attestation_pool.ts +6 -2
- package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +40 -1
- package/src/mem_pools/instrumentation.ts +13 -11
- package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
- package/src/mem_pools/tx_pool_v2/interfaces.ts +13 -0
- package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +23 -0
- package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +14 -4
- package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +71 -7
- package/src/msg_validators/proposal_validator/proposal_validator.ts +22 -15
- package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
- package/src/services/discv5/discV5_service.ts +88 -9
- package/src/services/discv5/persisted_enr_store.ts +158 -0
- package/src/services/dummy_service.ts +6 -0
- package/src/services/encoding.ts +28 -20
- package/src/services/libp2p/libp2p_service.ts +107 -37
- package/src/services/peer-manager/peer_manager.ts +18 -11
- package/src/services/reqresp/README.md +2 -23
- package/src/services/reqresp/config.ts +17 -0
- package/src/services/reqresp/rate-limiter/rate_limits.ts +3 -3
- package/src/services/reqresp/reqresp.ts +42 -2
- package/src/services/service.ts +19 -0
- package/src/test-helpers/testbench-utils.ts +4 -0
package/src/services/encoding.ts
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
3
|
import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
|
|
4
4
|
|
|
5
|
-
import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
|
|
6
5
|
import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
|
|
7
6
|
import type { Message } from '@libp2p/interface';
|
|
8
7
|
import { webcrypto } from 'node:crypto';
|
|
9
8
|
import { compressSync, uncompressSync } from 'snappy';
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fallback maximum response size (in KB) applied when no explicit per-request bound is supplied.
|
|
12
|
+
* Used both as the `SnappyTransform` default and as the reception-side fallback in `readMessage`.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_MAX_RESPONSE_SIZE_KB = 10 * 1024;
|
|
11
15
|
|
|
12
16
|
/** Thrown when a Snappy-compressed response exceeds the allowed decompressed size. */
|
|
13
17
|
export class OversizedSnappyResponseError extends Error {
|
|
@@ -17,26 +21,21 @@ export class OversizedSnappyResponseError extends Error {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
// Load WASM
|
|
21
|
-
const xxhash = await xxhashFactory();
|
|
22
|
-
|
|
23
|
-
// Use salt to prevent msgId from being mined for collisions
|
|
24
|
-
const h64Seed = BigInt(Math.floor(Math.random() * 1e9));
|
|
25
|
-
|
|
26
|
-
// Shared buffer to convert msgId to string
|
|
27
|
-
const sharedMsgIdBuf = Buffer.alloc(20);
|
|
28
|
-
|
|
29
24
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* Thrown when the compressed bytes accumulated while reading a response stream exceed the reception
|
|
26
|
+
* bound, before the full payload is buffered. Distinct from {@link OversizedSnappyResponseError},
|
|
27
|
+
* which guards the decompressed size after the stream has already been fully received.
|
|
32
28
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
export class ResponseSizeLimitExceededError extends Error {
|
|
30
|
+
constructor(accumulatedBytes: number, limitBytes: number) {
|
|
31
|
+
super(`Accumulated response size ${accumulatedBytes} bytes exceeds reception limit of ${limitBytes} bytes`);
|
|
32
|
+
this.name = 'ResponseSizeLimitExceededError';
|
|
36
33
|
}
|
|
37
|
-
return '0000000000000000';
|
|
38
34
|
}
|
|
39
35
|
|
|
36
|
+
// Shared buffer to convert msgId to string
|
|
37
|
+
const sharedMsgIdBuf = Buffer.alloc(20);
|
|
38
|
+
|
|
40
39
|
export function msgIdToStrFn(msgId: Uint8Array): string {
|
|
41
40
|
// This happens serially, no need to reallocate the buffer
|
|
42
41
|
sharedMsgIdBuf.set(msgId);
|
|
@@ -49,11 +48,20 @@ export function msgIdToStrFn(msgId: Uint8Array): string {
|
|
|
49
48
|
* Follows similarly to:
|
|
50
49
|
* https://github.com/ethereum/consensus-specs/blob/v1.1.0-alpha.7/specs/altair/p2p-interface.md#topics-and-messages
|
|
51
50
|
*
|
|
51
|
+
* The topic length is framed into the hash input (`uint32be(topicLen) || topic || data`) so the
|
|
52
|
+
* `(topic, data)` boundary is unambiguous. A raw `topic || data` concatenation is not injective —
|
|
53
|
+
* shifting bytes across the boundary (e.g. `(topic + data[0], data[1:])`) yields the same bytes and
|
|
54
|
+
* thus the same id, letting an unsubscribed-topic message pre-occupy a real message's seenCache slot
|
|
55
|
+
* and suppress it as a duplicate.
|
|
56
|
+
*
|
|
52
57
|
* @param message - The libp2p message
|
|
53
58
|
* @returns The message identifier
|
|
54
59
|
*/
|
|
55
|
-
export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
|
|
56
|
-
const
|
|
60
|
+
export async function getMsgIdFn({ topic, data }: Pick<Message, 'topic' | 'data'>): Promise<Uint8Array> {
|
|
61
|
+
const topicBytes = Buffer.from(topic);
|
|
62
|
+
const framedTopicLength = Buffer.allocUnsafe(4);
|
|
63
|
+
framedTopicLength.writeUInt32BE(topicBytes.length);
|
|
64
|
+
const buffer = Buffer.concat([framedTopicLength, topicBytes, data]);
|
|
57
65
|
const hash = await webcrypto.subtle.digest('SHA-256', buffer);
|
|
58
66
|
return Buffer.from(hash.slice(0, 20));
|
|
59
67
|
}
|
|
@@ -76,7 +84,7 @@ const DefaultMaxSizesKb: Record<TopicType, number> = {
|
|
|
76
84
|
export class SnappyTransform implements DataTransform {
|
|
77
85
|
constructor(
|
|
78
86
|
private maxSizesKb: Record<TopicType, number> = DefaultMaxSizesKb,
|
|
79
|
-
private defaultMaxSizeKb: number =
|
|
87
|
+
private defaultMaxSizeKb: number = DEFAULT_MAX_RESPONSE_SIZE_KB,
|
|
80
88
|
private logger = createLogger('p2p:snappy-transform'),
|
|
81
89
|
) {}
|
|
82
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
2
|
import { BlockNumber, type SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
-
import { maxBy, merge } from '@aztec/foundation/collection';
|
|
3
|
+
import { compactArray, maxBy, merge } from '@aztec/foundation/collection';
|
|
4
4
|
import { type Logger, createLibp2pComponentLogger, createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
6
|
import { Timer } from '@aztec/foundation/timer';
|
|
@@ -83,14 +83,14 @@ import { type PubSubLibp2p, convertToMultiaddr } from '../../util.js';
|
|
|
83
83
|
import { getVersions } from '../../versioning.js';
|
|
84
84
|
import { AztecDatastore } from '../data_store.js';
|
|
85
85
|
import { DiscV5Service } from '../discv5/discV5_service.js';
|
|
86
|
-
import { SnappyTransform,
|
|
86
|
+
import { SnappyTransform, getMsgIdFn, msgIdToStrFn } from '../encoding.js';
|
|
87
87
|
import { APP_SPECIFIC_WEIGHT, gossipScoreThresholds } from '../gossipsub/scoring.js';
|
|
88
88
|
import { createAllTopicScoreParams } from '../gossipsub/topic_score_params.js';
|
|
89
89
|
import type { PeerManagerInterface } from '../peer-manager/interface.js';
|
|
90
90
|
import { PeerManager } from '../peer-manager/peer_manager.js';
|
|
91
91
|
import { PeerScoring } from '../peer-manager/peer_scoring.js';
|
|
92
92
|
import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js';
|
|
93
|
-
import type
|
|
93
|
+
import { type P2PReqRespConfig, YAMUX_MAX_MESSAGE_SIZE_BYTES } from '../reqresp/config.js';
|
|
94
94
|
import {
|
|
95
95
|
AuthRequest,
|
|
96
96
|
BlockTxsRequest,
|
|
@@ -114,6 +114,7 @@ import type {
|
|
|
114
114
|
P2PCheckpointAttestationCallback,
|
|
115
115
|
P2PCheckpointReceivedCallback,
|
|
116
116
|
P2PDuplicateAttestationCallback,
|
|
117
|
+
P2POversizedProposalCallback,
|
|
117
118
|
P2PService,
|
|
118
119
|
PeerDiscoveryService,
|
|
119
120
|
} from '../service.js';
|
|
@@ -172,6 +173,9 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
172
173
|
type: 'checkpoint' | 'block';
|
|
173
174
|
}) => void;
|
|
174
175
|
|
|
176
|
+
/** Callback invoked when an oversized block proposal is stored as slashing evidence (triggers slashing). */
|
|
177
|
+
private oversizedProposalCallback?: P2POversizedProposalCallback;
|
|
178
|
+
|
|
175
179
|
/** Callback invoked when a duplicate attestation is detected (triggers slashing). */
|
|
176
180
|
private duplicateAttestationCallback?: P2PDuplicateAttestationCallback;
|
|
177
181
|
|
|
@@ -364,6 +368,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
364
368
|
packageVersion,
|
|
365
369
|
telemetry,
|
|
366
370
|
createLogger(`${logger.module}:discv5_service`, logger.getBindings()),
|
|
371
|
+
peerStore,
|
|
367
372
|
);
|
|
368
373
|
|
|
369
374
|
// Seed libp2p's bootstrap discovery with private and trusted peers
|
|
@@ -378,21 +383,27 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
378
383
|
const protocolVersion = compressComponentVersions(versions);
|
|
379
384
|
|
|
380
385
|
const preferredPeersEnrs: ENR[] = config.preferredPeers.map(enr => ENR.decodeTxt(enr));
|
|
381
|
-
const directPeers = (
|
|
386
|
+
const directPeers = compactArray(
|
|
382
387
|
await Promise.all(
|
|
383
388
|
preferredPeersEnrs.map(async enr => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
try {
|
|
390
|
+
const peerId = await enr.peerId();
|
|
391
|
+
const address = enr.getLocationMultiaddr('tcp');
|
|
392
|
+
if (address === undefined) {
|
|
393
|
+
throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
id: peerId,
|
|
397
|
+
addrs: [address],
|
|
398
|
+
};
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// A malformed configured ENR shouldn't abort node setup — skip it and log.
|
|
401
|
+
logger.warn(`Skipping preferred peer with invalid ENR`, { err });
|
|
402
|
+
return undefined;
|
|
388
403
|
}
|
|
389
|
-
return {
|
|
390
|
-
id: peerId,
|
|
391
|
-
addrs: [address],
|
|
392
|
-
};
|
|
393
404
|
}),
|
|
394
|
-
)
|
|
395
|
-
)
|
|
405
|
+
),
|
|
406
|
+
);
|
|
396
407
|
|
|
397
408
|
const announceTcpMultiaddr = config.p2pIp ? [convertToMultiaddr(config.p2pIp, p2pPort, 'tcp')] : [];
|
|
398
409
|
|
|
@@ -408,6 +419,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
408
419
|
expectedBlockProposalsPerSlot: config.expectedBlockProposalsPerSlot,
|
|
409
420
|
});
|
|
410
421
|
|
|
422
|
+
// Restrict gossipsub to exactly the topics we subscribe to. Without this, an arbitrary-topic
|
|
423
|
+
// message is transformed, msg-id'd and inserted into the seenCache before the subscription check,
|
|
424
|
+
// so a crafted topic colliding on msg id can suppress a real message as a duplicate.
|
|
425
|
+
const allowedTopics = getTopicsForConfig(config.disableTransactions).map(topic =>
|
|
426
|
+
createTopicString(topic, protocolVersion),
|
|
427
|
+
);
|
|
428
|
+
|
|
411
429
|
const node = await createLibp2p({
|
|
412
430
|
start: false,
|
|
413
431
|
peerId,
|
|
@@ -439,7 +457,8 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
439
457
|
],
|
|
440
458
|
datastore,
|
|
441
459
|
peerDiscovery,
|
|
442
|
-
|
|
460
|
+
// Pin the yamux frame size: MAX_REQRESP_REQUEST_SIZE_BYTES relies on a reqresp request fitting in one frame.
|
|
461
|
+
streamMuxers: [yamux({ maxMessageSize: YAMUX_MAX_MESSAGE_SIZE_BYTES }), mplex()],
|
|
443
462
|
connectionEncryption: [noise()],
|
|
444
463
|
connectionManager: {
|
|
445
464
|
minConnections: 0, // Disable libp2p peer dialing, we do it manually
|
|
@@ -492,9 +511,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
492
511
|
mcacheLength: config.gossipsubMcacheLength,
|
|
493
512
|
mcacheGossip: config.gossipsubMcacheGossip,
|
|
494
513
|
seenTTL: config.gossipsubSeenTTL,
|
|
514
|
+
allowedTopics,
|
|
515
|
+
// No fastMsgIdFn: the fast-path dedup cache keys on a non-cryptographic 64-bit hash of the
|
|
516
|
+
// raw data only (no topic), so a collision — accidental or engineered via a weak seed — drops
|
|
517
|
+
// a different message with no fallback to the full id. Dedup instead rests solely on the
|
|
518
|
+
// cryptographic, topic-framed msgIdFn below.
|
|
495
519
|
msgIdFn: getMsgIdFn,
|
|
496
520
|
msgIdToStrFn: msgIdToStrFn,
|
|
497
|
-
fastMsgIdFn: fastMsgIdFn,
|
|
498
521
|
dataTransform: new SnappyTransform(),
|
|
499
522
|
metricsRegister: otelMetricsAdapter,
|
|
500
523
|
metricsTopicStrToLabel: metricsTopicStrToLabels(protocolVersion),
|
|
@@ -762,6 +785,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
762
785
|
this.duplicateProposalCallback = callback;
|
|
763
786
|
}
|
|
764
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Registers a callback to be invoked when an oversized block proposal is stored as slashing evidence.
|
|
790
|
+
*/
|
|
791
|
+
public registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void {
|
|
792
|
+
this.oversizedProposalCallback = callback;
|
|
793
|
+
}
|
|
794
|
+
|
|
765
795
|
/**
|
|
766
796
|
* Registers a callback to be invoked when a duplicate attestation is detected.
|
|
767
797
|
* A validator signing attestations for different proposals at the same slot.
|
|
@@ -1243,16 +1273,17 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1243
1273
|
const {
|
|
1244
1274
|
result,
|
|
1245
1275
|
obj: block,
|
|
1246
|
-
metadata: { isEquivocated } = {},
|
|
1247
|
-
} = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean }>(
|
|
1276
|
+
metadata: { isEquivocated, isOversized } = {},
|
|
1277
|
+
} = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>(
|
|
1248
1278
|
() => this.validateAndStoreBlockProposal(source, BlockProposal.fromBuffer(payloadData)),
|
|
1249
1279
|
msgId,
|
|
1250
1280
|
source,
|
|
1251
1281
|
TopicType.block_proposal,
|
|
1252
1282
|
);
|
|
1253
1283
|
|
|
1254
|
-
// If not accepted or
|
|
1255
|
-
|
|
1284
|
+
// If not accepted, equivocated, or oversized, return. Oversized proposals are re-broadcast as
|
|
1285
|
+
// slashing evidence but never attested or processed.
|
|
1286
|
+
if (result !== TopicValidatorResult.Accept || !block || isEquivocated || isOversized) {
|
|
1256
1287
|
return;
|
|
1257
1288
|
}
|
|
1258
1289
|
|
|
@@ -1267,7 +1298,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1267
1298
|
protected async validateAndStoreBlockProposal(
|
|
1268
1299
|
peerId: PeerId,
|
|
1269
1300
|
block: BlockProposal,
|
|
1270
|
-
): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean }>> {
|
|
1301
|
+
): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>> {
|
|
1271
1302
|
const validationResult = await this.blockProposalValidator.validate(block);
|
|
1272
1303
|
|
|
1273
1304
|
if (validationResult.result === 'reject') {
|
|
@@ -1282,6 +1313,12 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1282
1313
|
// Try to add the proposal: this handles existence check, cap check, and adding in one call
|
|
1283
1314
|
const { added, alreadyExists, count } = await this.mempools.attestationPool.tryAddBlockProposal(block);
|
|
1284
1315
|
const isEquivocated = count !== undefined && count > 1;
|
|
1316
|
+
// An oversized proposal (index at or beyond the consensus per-checkpoint limit) is structurally valid
|
|
1317
|
+
// proposer misbehavior: it is stored and re-broadcast as slashing evidence but never processed or
|
|
1318
|
+
// attested to. No-ops when maxBlocksPerCheckpoint is unset (local/test).
|
|
1319
|
+
const isOversized =
|
|
1320
|
+
this.config.maxBlocksPerCheckpoint !== undefined &&
|
|
1321
|
+
block.indexWithinCheckpoint >= this.config.maxBlocksPerCheckpoint;
|
|
1285
1322
|
|
|
1286
1323
|
// Duplicate proposal received, no need to re-broadcast
|
|
1287
1324
|
if (alreadyExists) {
|
|
@@ -1291,7 +1328,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1291
1328
|
proposer: block.getSender()?.toString(),
|
|
1292
1329
|
source: peerId.toString(),
|
|
1293
1330
|
});
|
|
1294
|
-
return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated } };
|
|
1331
|
+
return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated, isOversized } };
|
|
1295
1332
|
}
|
|
1296
1333
|
|
|
1297
1334
|
// Too many blocks received for this slot and index, penalize peer and do not re-broadcast
|
|
@@ -1305,11 +1342,27 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1305
1342
|
});
|
|
1306
1343
|
return {
|
|
1307
1344
|
result: TopicValidatorResult.Reject,
|
|
1308
|
-
metadata: { isEquivocated },
|
|
1345
|
+
metadata: { isEquivocated, isOversized },
|
|
1309
1346
|
severity: PeerErrorSeverity.HighToleranceError,
|
|
1310
1347
|
};
|
|
1311
1348
|
}
|
|
1312
1349
|
|
|
1350
|
+
// The proposal was stored: if oversized, invoke the oversized callback so the proposer can be
|
|
1351
|
+
// slashed. Fired alongside (not instead of) equivocation detection below.
|
|
1352
|
+
if (isOversized) {
|
|
1353
|
+
const proposer = block.getSender();
|
|
1354
|
+
if (proposer) {
|
|
1355
|
+
this.logger.warn(`Detected oversized block proposal at slot ${block.slotNumber}`, {
|
|
1356
|
+
...block.toBlockInfo(),
|
|
1357
|
+
indexWithinCheckpoint: block.indexWithinCheckpoint,
|
|
1358
|
+
maxBlocksPerCheckpoint: this.config.maxBlocksPerCheckpoint,
|
|
1359
|
+
source: peerId.toString(),
|
|
1360
|
+
proposer: proposer.toString(),
|
|
1361
|
+
});
|
|
1362
|
+
this.oversizedProposalCallback?.({ slot: block.slotNumber, proposer });
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1313
1366
|
// If this was a duplicate proposal, do not process it, but do invoke the duplicate callback,
|
|
1314
1367
|
// and do re-broadcast it so other nodes in the network know to slash the proposer
|
|
1315
1368
|
if (isEquivocated) {
|
|
@@ -1323,11 +1376,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1323
1376
|
if (proposer && count === 2) {
|
|
1324
1377
|
this.duplicateProposalCallback?.({ slot: block.slotNumber, proposer, type: 'block' });
|
|
1325
1378
|
}
|
|
1326
|
-
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated } };
|
|
1379
|
+
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated, isOversized } };
|
|
1327
1380
|
}
|
|
1328
1381
|
|
|
1329
1382
|
// Otherwise, we're good to go!
|
|
1330
|
-
return { result: TopicValidatorResult.Accept, obj: block };
|
|
1383
|
+
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated: false, isOversized } };
|
|
1331
1384
|
}
|
|
1332
1385
|
|
|
1333
1386
|
// REFACTOR(palla): This method should be moved to the p2p_client or to a separate component,
|
|
@@ -1367,17 +1420,21 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1367
1420
|
const {
|
|
1368
1421
|
result,
|
|
1369
1422
|
obj: checkpoint,
|
|
1370
|
-
metadata: { isEquivocated, processBlock } = {},
|
|
1371
|
-
} = await this.validateReceivedMessage<
|
|
1423
|
+
metadata: { isEquivocated, processBlock, isOversized } = {},
|
|
1424
|
+
} = await this.validateReceivedMessage<
|
|
1425
|
+
CheckpointProposal,
|
|
1426
|
+
{ isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
|
|
1427
|
+
>(
|
|
1372
1428
|
() => this.validateAndStoreCheckpointProposal(source, CheckpointProposal.fromBuffer(payloadData)),
|
|
1373
1429
|
msgId,
|
|
1374
1430
|
source,
|
|
1375
1431
|
TopicType.checkpoint_proposal,
|
|
1376
1432
|
);
|
|
1377
1433
|
|
|
1378
|
-
//
|
|
1434
|
+
// An oversized checkpoint is re-broadcast as slashing evidence but never attested or processed.
|
|
1435
|
+
// Process checkpoint proposal if valid and neither equivocated nor oversized.
|
|
1379
1436
|
const processCheckpointFn = () =>
|
|
1380
|
-
result === TopicValidatorResult.Accept && checkpoint && !isEquivocated
|
|
1437
|
+
result === TopicValidatorResult.Accept && checkpoint && !isEquivocated && !isOversized
|
|
1381
1438
|
? this.processValidCheckpointProposal(checkpoint.toCore(), source)
|
|
1382
1439
|
: Promise.resolve();
|
|
1383
1440
|
|
|
@@ -1414,7 +1471,12 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1414
1471
|
protected async validateAndStoreCheckpointProposal(
|
|
1415
1472
|
peerId: PeerId,
|
|
1416
1473
|
checkpoint: CheckpointProposal,
|
|
1417
|
-
): Promise<
|
|
1474
|
+
): Promise<
|
|
1475
|
+
ReceivedMessageValidationResult<
|
|
1476
|
+
CheckpointProposal,
|
|
1477
|
+
{ isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
|
|
1478
|
+
>
|
|
1479
|
+
> {
|
|
1418
1480
|
const validationResult = await this.checkpointProposalValidator.validate(checkpoint);
|
|
1419
1481
|
|
|
1420
1482
|
if (validationResult.result === 'reject') {
|
|
@@ -1429,13 +1491,16 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1429
1491
|
// Extract and try to add the block proposal first if present
|
|
1430
1492
|
const blockProposal = checkpoint.getBlockProposal();
|
|
1431
1493
|
let processBlock = false;
|
|
1494
|
+
let isOversized = false;
|
|
1432
1495
|
if (blockProposal) {
|
|
1433
1496
|
this.logger.debug(`Validating block proposal from propagated checkpoint`, {
|
|
1434
1497
|
[Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
|
|
1435
1498
|
[Attributes.P2P_ID]: peerId.toString(),
|
|
1436
1499
|
});
|
|
1437
1500
|
const blockProposalResult = await this.validateAndStoreBlockProposal(peerId, blockProposal);
|
|
1438
|
-
const { obj, metadata: { isEquivocated } = {} } = blockProposalResult;
|
|
1501
|
+
const { obj, metadata: { isEquivocated, isOversized: blockIsOversized } = {} } = blockProposalResult;
|
|
1502
|
+
isOversized = blockIsOversized ?? false;
|
|
1503
|
+
|
|
1439
1504
|
if (blockProposalResult.result === TopicValidatorResult.Reject || !obj || isEquivocated) {
|
|
1440
1505
|
this.logger.debug(`Rejecting checkpoint due to invalid last block proposal`, {
|
|
1441
1506
|
[Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
|
|
@@ -1448,7 +1513,8 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1448
1513
|
severity:
|
|
1449
1514
|
'severity' in blockProposalResult ? blockProposalResult.severity : PeerErrorSeverity.MidToleranceError,
|
|
1450
1515
|
};
|
|
1451
|
-
} else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated) {
|
|
1516
|
+
} else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated && !isOversized) {
|
|
1517
|
+
// An oversized terminal block is re-broadcast as slashing evidence but never processed.
|
|
1452
1518
|
processBlock = true;
|
|
1453
1519
|
}
|
|
1454
1520
|
}
|
|
@@ -1468,11 +1534,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1468
1534
|
return {
|
|
1469
1535
|
result: TopicValidatorResult.Ignore,
|
|
1470
1536
|
obj: checkpoint,
|
|
1471
|
-
metadata: { isEquivocated, processBlock },
|
|
1537
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1472
1538
|
};
|
|
1473
1539
|
}
|
|
1474
1540
|
|
|
1475
|
-
// Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast
|
|
1541
|
+
// Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast.
|
|
1476
1542
|
// Note: We still return the checkpoint obj so the lastBlock can be processed if valid
|
|
1477
1543
|
if (!added) {
|
|
1478
1544
|
this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
|
|
@@ -1483,7 +1549,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1483
1549
|
return {
|
|
1484
1550
|
result: TopicValidatorResult.Reject,
|
|
1485
1551
|
obj: checkpoint,
|
|
1486
|
-
metadata: { isEquivocated, processBlock },
|
|
1552
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1487
1553
|
severity: PeerErrorSeverity.HighToleranceError,
|
|
1488
1554
|
};
|
|
1489
1555
|
}
|
|
@@ -1504,12 +1570,16 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1504
1570
|
return {
|
|
1505
1571
|
result: TopicValidatorResult.Accept,
|
|
1506
1572
|
obj: checkpoint,
|
|
1507
|
-
metadata: { isEquivocated, processBlock },
|
|
1573
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1508
1574
|
};
|
|
1509
1575
|
}
|
|
1510
1576
|
|
|
1511
1577
|
// Otherwise, we're good to go!
|
|
1512
|
-
return {
|
|
1578
|
+
return {
|
|
1579
|
+
result: TopicValidatorResult.Accept,
|
|
1580
|
+
obj: checkpoint,
|
|
1581
|
+
metadata: { processBlock, isEquivocated, isOversized },
|
|
1582
|
+
};
|
|
1513
1583
|
}
|
|
1514
1584
|
|
|
1515
1585
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
|
+
import { compactArray } from '@aztec/foundation/collection';
|
|
2
3
|
import { makeEthSignDigest, tryRecoverAddress } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
3
4
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
5
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -199,21 +200,27 @@ export class PeerManager implements PeerManagerInterface {
|
|
|
199
200
|
.then(peerIds => peerIds.forEach(peerId => this.preferredPeers.add(peerId.toString())))
|
|
200
201
|
.catch(e => this.logger.error('Error initializing preferred peers', e));
|
|
201
202
|
|
|
202
|
-
const directPeers = (
|
|
203
|
+
const directPeers = compactArray(
|
|
203
204
|
await Promise.all(
|
|
204
205
|
preferredPeersEnrs.map(async enr => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
try {
|
|
207
|
+
const peerId = await enr.peerId();
|
|
208
|
+
const address = enr.getLocationMultiaddr('tcp');
|
|
209
|
+
if (address === undefined) {
|
|
210
|
+
throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: peerId,
|
|
214
|
+
addrs: [address],
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// A malformed configured ENR shouldn't abort preferred-peer setup — skip it and log.
|
|
218
|
+
this.logger.warn(`Skipping preferred peer with invalid ENR`, { err });
|
|
219
|
+
return undefined;
|
|
209
220
|
}
|
|
210
|
-
return {
|
|
211
|
-
id: peerId,
|
|
212
|
-
addrs: [address],
|
|
213
|
-
};
|
|
214
221
|
}),
|
|
215
|
-
)
|
|
216
|
-
)
|
|
222
|
+
),
|
|
223
|
+
);
|
|
217
224
|
|
|
218
225
|
await Promise.all(
|
|
219
226
|
directPeers.map(peer => {
|
|
@@ -14,9 +14,8 @@ Applied before the protocol handler runs.
|
|
|
14
14
|
| STATUS | 5/s | 10/s | same |
|
|
15
15
|
| AUTH | 5/s | 10/s | same |
|
|
16
16
|
| GOODBYE | 5/s | 10/s | same |
|
|
17
|
-
| BLOCK | 2/s | 5/s | same |
|
|
18
17
|
| BLOCK_TXS | 10/s | 200/s | same |
|
|
19
|
-
| TX |
|
|
18
|
+
| TX | 10/s | 200/s | same |
|
|
20
19
|
|
|
21
20
|
- Per-peer limit exceeded: `HighToleranceError` penalty + `RATE_LIMIT_EXCEEDED` status. Penalty fires inside `RequestResponseRateLimiter.allow()`, not the stream handler.
|
|
22
21
|
- Global limit exceeded: `RATE_LIMIT_EXCEEDED` status only (no peer penalty).
|
|
@@ -149,26 +148,6 @@ Protected peers (private/trusted/preferred) are always considered "authenticated
|
|
|
149
148
|
|
|
150
149
|
## Block Data Protocols
|
|
151
150
|
|
|
152
|
-
### BLOCK Protocol (`/aztec/req/block/1.0.0`)
|
|
153
|
-
|
|
154
|
-
**Server side**:
|
|
155
|
-
|
|
156
|
-
| Rule | Consequence | File |
|
|
157
|
-
|------|-------------|------|
|
|
158
|
-
| Request must parse as `Fr` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block.ts` |
|
|
159
|
-
| Block lookup throws | `INTERNAL_ERROR` status | same |
|
|
160
|
-
| Block not found | SUCCESS + empty buffer (design choice; no `NOT_FOUND` status used) | same |
|
|
161
|
-
|
|
162
|
-
**Requester side** (Snappy limit: 3 MB):
|
|
163
|
-
|
|
164
|
-
| Rule | Consequence | File |
|
|
165
|
-
|------|-------------|------|
|
|
166
|
-
| Response block number must match requested | LowToleranceError; rejected | `libp2p_service.ts` (`validateRequestedBlock`) |
|
|
167
|
-
| Local block must exist for hash verification | Rejected (no penalty) | same |
|
|
168
|
-
| Response block hash must equal local block hash | MidToleranceError; rejected | same |
|
|
169
|
-
|
|
170
|
-
**Limitation**: the local-block requirement means BLOCK req/resp is unusable for initial P2P-only sync (before L1 sync provides local copies for verification). A TODO in the code acknowledges this.
|
|
171
|
-
|
|
172
151
|
### BLOCK_TXS Protocol (`/aztec/req/block_txs/1.0.0`)
|
|
173
152
|
|
|
174
153
|
**Server side**:
|
|
@@ -178,7 +157,7 @@ Protected peers (private/trusted/preferred) are always considered "authenticated
|
|
|
178
157
|
| Request must parse as `BlockTxsRequest` (Fr + TxHashArray + BitVector) | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block_txs/block_txs_handler.ts` |
|
|
179
158
|
| BitVector length: non-negative and <= `MAX_TXS_PER_BLOCK` (65536) | Deserialization throws -> `BADLY_FORMED_REQUEST` | `protocols/block_txs/bitvector.ts` |
|
|
180
159
|
| Archive root not found and no explicit txHashes | `NOT_FOUND` status | handler |
|
|
181
|
-
| Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status
|
|
160
|
+
| Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status) | handler |
|
|
182
161
|
|
|
183
162
|
Conditional registration: BLOCK_TXS handler only registered when `config.disableTransactions` is false. Otherwise peers get `ERR_UNSUPPORTED_PROTOCOL`.
|
|
184
163
|
|
|
@@ -5,6 +5,23 @@ export const DEFAULT_OVERALL_REQUEST_TIMEOUT_MS = 10_000; // Not currently used
|
|
|
5
5
|
export const DEFAULT_REQRESP_DIAL_TIMEOUT_MS = 5_000;
|
|
6
6
|
export const DEFAULT_OPTIMISTIC_NEGOTIATION = false;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Max size of a yamux frame, header included. This is the library default, pinned explicitly when configuring the
|
|
10
|
+
* muxer in the libp2p service so a dependency upgrade cannot silently change it.
|
|
11
|
+
*/
|
|
12
|
+
export const YAMUX_MAX_MESSAGE_SIZE_BYTES = 64 * 1024;
|
|
13
|
+
|
|
14
|
+
/** Size of a yamux data frame header. */
|
|
15
|
+
const YAMUX_HEADER_LENGTH_BYTES = 12;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Max size of a reqresp request payload. A request must fit in a single muxer frame: the responder never reassembles
|
|
19
|
+
* a request from multiple chunks (see `ReqResp.processStream`), and yamux splits writes larger than one frame into
|
|
20
|
+
* multiple frames, each of which arrives as a separate chunk. mplex only splits at 1 MiB, so yamux is the binding
|
|
21
|
+
* constraint.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_REQRESP_REQUEST_SIZE_BYTES = YAMUX_MAX_MESSAGE_SIZE_BYTES - YAMUX_HEADER_LENGTH_BYTES;
|
|
24
|
+
|
|
8
25
|
// For use in tests.
|
|
9
26
|
export const DEFAULT_P2P_REQRESP_CONFIG: P2PReqRespConfig = {
|
|
10
27
|
overallRequestTimeoutMs: DEFAULT_OVERALL_REQUEST_TIMEOUT_MS,
|
|
@@ -9,7 +9,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
|
|
|
9
9
|
},
|
|
10
10
|
globalLimit: {
|
|
11
11
|
quotaTimeMs: 1000,
|
|
12
|
-
quotaCount:
|
|
12
|
+
quotaCount: 50,
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
[ReqRespSubProtocol.STATUS]: {
|
|
@@ -19,7 +19,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
|
|
|
19
19
|
},
|
|
20
20
|
globalLimit: {
|
|
21
21
|
quotaTimeMs: 1000,
|
|
22
|
-
quotaCount:
|
|
22
|
+
quotaCount: 50,
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
[ReqRespSubProtocol.AUTH]: {
|
|
@@ -49,7 +49,7 @@ export const DEFAULT_RATE_LIMITS: ReqRespSubProtocolRateLimits = {
|
|
|
49
49
|
},
|
|
50
50
|
globalLimit: {
|
|
51
51
|
quotaTimeMs: 1000,
|
|
52
|
-
quotaCount:
|
|
52
|
+
quotaCount: 50,
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
[ReqRespSubProtocol.BLOCK_TXS]: {
|