@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.
Files changed (103) hide show
  1. package/README.md +3 -5
  2. package/dest/bootstrap/bootstrap.d.ts +1 -1
  3. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  4. package/dest/bootstrap/bootstrap.js +20 -12
  5. package/dest/client/factory.d.ts +1 -1
  6. package/dest/client/factory.d.ts.map +1 -1
  7. package/dest/client/factory.js +2 -1
  8. package/dest/client/interface.d.ts +14 -2
  9. package/dest/client/interface.d.ts.map +1 -1
  10. package/dest/client/p2p_client.d.ts +4 -2
  11. package/dest/client/p2p_client.d.ts.map +1 -1
  12. package/dest/client/p2p_client.js +10 -2
  13. package/dest/config.d.ts +7 -1
  14. package/dest/config.d.ts.map +1 -1
  15. package/dest/config.js +10 -5
  16. package/dest/errors/reqresp.error.d.ts +13 -1
  17. package/dest/errors/reqresp.error.d.ts.map +1 -1
  18. package/dest/errors/reqresp.error.js +14 -0
  19. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +1 -1
  20. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  21. package/dest/mem_pools/attestation_pool/attestation_pool.js +6 -2
  22. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  23. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  24. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +43 -0
  25. package/dest/mem_pools/instrumentation.d.ts +9 -4
  26. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  27. package/dest/mem_pools/instrumentation.js +11 -8
  28. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
  29. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +12 -1
  30. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  31. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  32. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts +9 -1
  33. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.d.ts.map +1 -1
  34. package/dest/mem_pools/tx_pool_v2/tx_pool_indices.js +21 -0
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +2 -1
  36. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  37. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +12 -4
  38. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +10 -2
  39. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  40. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +56 -8
  41. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +10 -6
  42. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  43. package/dest/msg_validators/proposal_validator/proposal_validator.js +25 -12
  44. package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
  45. package/dest/services/discv5/discV5_service.d.ts +12 -2
  46. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  47. package/dest/services/discv5/discV5_service.js +84 -15
  48. package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
  49. package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
  50. package/dest/services/discv5/persisted_enr_store.js +145 -0
  51. package/dest/services/dummy_service.d.ts +6 -2
  52. package/dest/services/dummy_service.d.ts.map +1 -1
  53. package/dest/services/dummy_service.js +3 -0
  54. package/dest/services/encoding.d.ts +19 -6
  55. package/dest/services/encoding.d.ts.map +1 -1
  56. package/dest/services/encoding.js +25 -15
  57. package/dest/services/libp2p/libp2p_service.d.ts +11 -3
  58. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  59. package/dest/services/libp2p/libp2p_service.js +98 -34
  60. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  61. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  62. package/dest/services/peer-manager/peer_manager.js +21 -12
  63. package/dest/services/reqresp/config.d.ts +13 -1
  64. package/dest/services/reqresp/config.d.ts.map +1 -1
  65. package/dest/services/reqresp/config.js +11 -0
  66. package/dest/services/reqresp/rate-limiter/rate_limits.js +3 -3
  67. package/dest/services/reqresp/reqresp.d.ts +1 -1
  68. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  69. package/dest/services/reqresp/reqresp.js +34 -3
  70. package/dest/services/service.d.ts +17 -1
  71. package/dest/services/service.d.ts.map +1 -1
  72. package/dest/test-helpers/testbench-utils.d.ts +2 -1
  73. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  74. package/dest/test-helpers/testbench-utils.js +3 -0
  75. package/package.json +16 -17
  76. package/src/bootstrap/bootstrap.ts +15 -9
  77. package/src/client/factory.ts +1 -0
  78. package/src/client/interface.ts +15 -0
  79. package/src/client/p2p_client.ts +14 -3
  80. package/src/config.ts +17 -4
  81. package/src/errors/reqresp.error.ts +19 -0
  82. package/src/mem_pools/attestation_pool/attestation_pool.ts +6 -2
  83. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +40 -1
  84. package/src/mem_pools/instrumentation.ts +13 -11
  85. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
  86. package/src/mem_pools/tx_pool_v2/interfaces.ts +13 -0
  87. package/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +23 -0
  88. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +14 -4
  89. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +71 -7
  90. package/src/msg_validators/proposal_validator/proposal_validator.ts +22 -15
  91. package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
  92. package/src/services/discv5/discV5_service.ts +88 -9
  93. package/src/services/discv5/persisted_enr_store.ts +158 -0
  94. package/src/services/dummy_service.ts +6 -0
  95. package/src/services/encoding.ts +28 -20
  96. package/src/services/libp2p/libp2p_service.ts +107 -37
  97. package/src/services/peer-manager/peer_manager.ts +18 -11
  98. package/src/services/reqresp/README.md +2 -23
  99. package/src/services/reqresp/config.ts +17 -0
  100. package/src/services/reqresp/rate-limiter/rate_limits.ts +3 -3
  101. package/src/services/reqresp/reqresp.ts +42 -2
  102. package/src/services/service.ts +19 -0
  103. package/src/test-helpers/testbench-utils.ts +4 -0
@@ -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
- import xxhashFactory from 'xxhash-wasm';
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
- * The function used to generate a gossipsub message id
31
- * We use the first 8 bytes of SHA256(data) for content addressing
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 function fastMsgIdFn(rpcMsg: RPC.Message): string {
34
- if (rpcMsg.data) {
35
- return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16);
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 buffer = Buffer.concat([Buffer.from(topic), data]);
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 = 10 * 1024,
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, fastMsgIdFn, getMsgIdFn, msgIdToStrFn } from '../encoding.js';
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 { P2PReqRespConfig } from '../reqresp/config.js';
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
- const peerId = await enr.peerId();
385
- const address = enr.getLocationMultiaddr('tcp');
386
- if (address === undefined) {
387
- throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
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
- ).filter(peer => peer !== undefined);
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
- streamMuxers: [yamux(), mplex()],
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 equivocated, return
1255
- if (result !== TopicValidatorResult.Accept || !block || isEquivocated) {
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<CheckpointProposal, { isEquivocated: boolean; processBlock: boolean }>(
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
- // Process checkpoint proposal if valid and not equivocated.
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<ReceivedMessageValidationResult<CheckpointProposal, { isEquivocated: boolean; processBlock: boolean }>> {
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 { result: TopicValidatorResult.Accept, obj: checkpoint, metadata: { processBlock, isEquivocated } };
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
- const peerId = await enr.peerId();
206
- const address = enr.getLocationMultiaddr('tcp');
207
- if (address === undefined) {
208
- throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
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
- ).filter(peer => peer !== undefined);
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 | (see rate limits file) | (see rate limits file) | same |
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, unlike BLOCK) | handler |
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: 10,
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: 10,
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: 10,
52
+ quotaCount: 50,
53
53
  },
54
54
  },
55
55
  [ReqRespSubProtocol.BLOCK_TXS]: {