@aztec/p2p 5.0.0-rc.1 → 5.0.0-rc.2

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 (51) hide show
  1. package/dest/bootstrap/bootstrap.d.ts +1 -1
  2. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  3. package/dest/bootstrap/bootstrap.js +20 -12
  4. package/dest/client/interface.d.ts +9 -2
  5. package/dest/client/interface.d.ts.map +1 -1
  6. package/dest/client/p2p_client.d.ts +3 -2
  7. package/dest/client/p2p_client.d.ts.map +1 -1
  8. package/dest/client/p2p_client.js +5 -0
  9. package/dest/config.js +1 -1
  10. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
  11. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +1 -1
  12. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +10 -6
  13. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  14. package/dest/msg_validators/proposal_validator/proposal_validator.js +17 -11
  15. package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
  16. package/dest/services/discv5/discV5_service.d.ts +12 -2
  17. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  18. package/dest/services/discv5/discV5_service.js +84 -15
  19. package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
  20. package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
  21. package/dest/services/discv5/persisted_enr_store.js +145 -0
  22. package/dest/services/dummy_service.d.ts +6 -2
  23. package/dest/services/dummy_service.d.ts.map +1 -1
  24. package/dest/services/dummy_service.js +3 -0
  25. package/dest/services/encoding.d.ts +8 -8
  26. package/dest/services/encoding.d.ts.map +1 -1
  27. package/dest/services/encoding.js +11 -15
  28. package/dest/services/libp2p/libp2p_service.d.ts +10 -2
  29. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  30. package/dest/services/libp2p/libp2p_service.js +93 -33
  31. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  32. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  33. package/dest/services/peer-manager/peer_manager.js +21 -12
  34. package/dest/services/service.d.ts +17 -1
  35. package/dest/services/service.d.ts.map +1 -1
  36. package/package.json +16 -17
  37. package/src/bootstrap/bootstrap.ts +15 -9
  38. package/src/client/interface.ts +9 -0
  39. package/src/client/p2p_client.ts +7 -0
  40. package/src/config.ts +1 -1
  41. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
  42. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +1 -1
  43. package/src/msg_validators/proposal_validator/proposal_validator.ts +17 -14
  44. package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
  45. package/src/services/discv5/discV5_service.ts +88 -9
  46. package/src/services/discv5/persisted_enr_store.ts +158 -0
  47. package/src/services/dummy_service.ts +6 -0
  48. package/src/services/encoding.ts +11 -21
  49. package/src/services/libp2p/libp2p_service.ts +104 -35
  50. package/src/services/peer-manager/peer_manager.ts +18 -11
  51. package/src/services/service.ts +19 -0
@@ -2,12 +2,10 @@
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';
11
9
 
12
10
  /** Thrown when a Snappy-compressed response exceeds the allowed decompressed size. */
13
11
  export class OversizedSnappyResponseError extends Error {
@@ -17,26 +15,9 @@ export class OversizedSnappyResponseError extends Error {
17
15
  }
18
16
  }
19
17
 
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
18
  // Shared buffer to convert msgId to string
27
19
  const sharedMsgIdBuf = Buffer.alloc(20);
28
20
 
29
- /**
30
- * The function used to generate a gossipsub message id
31
- * We use the first 8 bytes of SHA256(data) for content addressing
32
- */
33
- export function fastMsgIdFn(rpcMsg: RPC.Message): string {
34
- if (rpcMsg.data) {
35
- return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16);
36
- }
37
- return '0000000000000000';
38
- }
39
-
40
21
  export function msgIdToStrFn(msgId: Uint8Array): string {
41
22
  // This happens serially, no need to reallocate the buffer
42
23
  sharedMsgIdBuf.set(msgId);
@@ -49,11 +30,20 @@ export function msgIdToStrFn(msgId: Uint8Array): string {
49
30
  * Follows similarly to:
50
31
  * https://github.com/ethereum/consensus-specs/blob/v1.1.0-alpha.7/specs/altair/p2p-interface.md#topics-and-messages
51
32
  *
33
+ * The topic length is framed into the hash input (`uint32be(topicLen) || topic || data`) so the
34
+ * `(topic, data)` boundary is unambiguous. A raw `topic || data` concatenation is not injective —
35
+ * shifting bytes across the boundary (e.g. `(topic + data[0], data[1:])`) yields the same bytes and
36
+ * thus the same id, letting an unsubscribed-topic message pre-occupy a real message's seenCache slot
37
+ * and suppress it as a duplicate.
38
+ *
52
39
  * @param message - The libp2p message
53
40
  * @returns The message identifier
54
41
  */
55
- export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
56
- const buffer = Buffer.concat([Buffer.from(topic), data]);
42
+ export async function getMsgIdFn({ topic, data }: Pick<Message, 'topic' | 'data'>): Promise<Uint8Array> {
43
+ const topicBytes = Buffer.from(topic);
44
+ const framedTopicLength = Buffer.allocUnsafe(4);
45
+ framedTopicLength.writeUInt32BE(topicBytes.length);
46
+ const buffer = Buffer.concat([framedTopicLength, topicBytes, data]);
57
47
  const hash = await webcrypto.subtle.digest('SHA-256', buffer);
58
48
  return Buffer.from(hash.slice(0, 20));
59
49
  }
@@ -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,7 +83,7 @@ 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';
@@ -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,
@@ -492,9 +510,13 @@ export class LibP2PService extends WithTracer implements P2PService {
492
510
  mcacheLength: config.gossipsubMcacheLength,
493
511
  mcacheGossip: config.gossipsubMcacheGossip,
494
512
  seenTTL: config.gossipsubSeenTTL,
513
+ allowedTopics,
514
+ // No fastMsgIdFn: the fast-path dedup cache keys on a non-cryptographic 64-bit hash of the
515
+ // raw data only (no topic), so a collision — accidental or engineered via a weak seed — drops
516
+ // a different message with no fallback to the full id. Dedup instead rests solely on the
517
+ // cryptographic, topic-framed msgIdFn below.
495
518
  msgIdFn: getMsgIdFn,
496
519
  msgIdToStrFn: msgIdToStrFn,
497
- fastMsgIdFn: fastMsgIdFn,
498
520
  dataTransform: new SnappyTransform(),
499
521
  metricsRegister: otelMetricsAdapter,
500
522
  metricsTopicStrToLabel: metricsTopicStrToLabels(protocolVersion),
@@ -762,6 +784,13 @@ export class LibP2PService extends WithTracer implements P2PService {
762
784
  this.duplicateProposalCallback = callback;
763
785
  }
764
786
 
787
+ /**
788
+ * Registers a callback to be invoked when an oversized block proposal is stored as slashing evidence.
789
+ */
790
+ public registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void {
791
+ this.oversizedProposalCallback = callback;
792
+ }
793
+
765
794
  /**
766
795
  * Registers a callback to be invoked when a duplicate attestation is detected.
767
796
  * A validator signing attestations for different proposals at the same slot.
@@ -1243,16 +1272,17 @@ export class LibP2PService extends WithTracer implements P2PService {
1243
1272
  const {
1244
1273
  result,
1245
1274
  obj: block,
1246
- metadata: { isEquivocated } = {},
1247
- } = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean }>(
1275
+ metadata: { isEquivocated, isOversized } = {},
1276
+ } = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>(
1248
1277
  () => this.validateAndStoreBlockProposal(source, BlockProposal.fromBuffer(payloadData)),
1249
1278
  msgId,
1250
1279
  source,
1251
1280
  TopicType.block_proposal,
1252
1281
  );
1253
1282
 
1254
- // If not accepted or equivocated, return
1255
- if (result !== TopicValidatorResult.Accept || !block || isEquivocated) {
1283
+ // If not accepted, equivocated, or oversized, return. Oversized proposals are re-broadcast as
1284
+ // slashing evidence but never attested or processed.
1285
+ if (result !== TopicValidatorResult.Accept || !block || isEquivocated || isOversized) {
1256
1286
  return;
1257
1287
  }
1258
1288
 
@@ -1267,7 +1297,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1267
1297
  protected async validateAndStoreBlockProposal(
1268
1298
  peerId: PeerId,
1269
1299
  block: BlockProposal,
1270
- ): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean }>> {
1300
+ ): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>> {
1271
1301
  const validationResult = await this.blockProposalValidator.validate(block);
1272
1302
 
1273
1303
  if (validationResult.result === 'reject') {
@@ -1282,6 +1312,12 @@ export class LibP2PService extends WithTracer implements P2PService {
1282
1312
  // Try to add the proposal: this handles existence check, cap check, and adding in one call
1283
1313
  const { added, alreadyExists, count } = await this.mempools.attestationPool.tryAddBlockProposal(block);
1284
1314
  const isEquivocated = count !== undefined && count > 1;
1315
+ // An oversized proposal (index at or beyond the consensus per-checkpoint limit) is structurally valid
1316
+ // proposer misbehavior: it is stored and re-broadcast as slashing evidence but never processed or
1317
+ // attested to. No-ops when maxBlocksPerCheckpoint is unset (local/test).
1318
+ const isOversized =
1319
+ this.config.maxBlocksPerCheckpoint !== undefined &&
1320
+ block.indexWithinCheckpoint >= this.config.maxBlocksPerCheckpoint;
1285
1321
 
1286
1322
  // Duplicate proposal received, no need to re-broadcast
1287
1323
  if (alreadyExists) {
@@ -1291,7 +1327,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1291
1327
  proposer: block.getSender()?.toString(),
1292
1328
  source: peerId.toString(),
1293
1329
  });
1294
- return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated } };
1330
+ return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated, isOversized } };
1295
1331
  }
1296
1332
 
1297
1333
  // Too many blocks received for this slot and index, penalize peer and do not re-broadcast
@@ -1305,11 +1341,27 @@ export class LibP2PService extends WithTracer implements P2PService {
1305
1341
  });
1306
1342
  return {
1307
1343
  result: TopicValidatorResult.Reject,
1308
- metadata: { isEquivocated },
1344
+ metadata: { isEquivocated, isOversized },
1309
1345
  severity: PeerErrorSeverity.HighToleranceError,
1310
1346
  };
1311
1347
  }
1312
1348
 
1349
+ // The proposal was stored: if oversized, invoke the oversized callback so the proposer can be
1350
+ // slashed. Fired alongside (not instead of) equivocation detection below.
1351
+ if (isOversized) {
1352
+ const proposer = block.getSender();
1353
+ if (proposer) {
1354
+ this.logger.warn(`Detected oversized block proposal at slot ${block.slotNumber}`, {
1355
+ ...block.toBlockInfo(),
1356
+ indexWithinCheckpoint: block.indexWithinCheckpoint,
1357
+ maxBlocksPerCheckpoint: this.config.maxBlocksPerCheckpoint,
1358
+ source: peerId.toString(),
1359
+ proposer: proposer.toString(),
1360
+ });
1361
+ this.oversizedProposalCallback?.({ slot: block.slotNumber, proposer });
1362
+ }
1363
+ }
1364
+
1313
1365
  // If this was a duplicate proposal, do not process it, but do invoke the duplicate callback,
1314
1366
  // and do re-broadcast it so other nodes in the network know to slash the proposer
1315
1367
  if (isEquivocated) {
@@ -1323,11 +1375,11 @@ export class LibP2PService extends WithTracer implements P2PService {
1323
1375
  if (proposer && count === 2) {
1324
1376
  this.duplicateProposalCallback?.({ slot: block.slotNumber, proposer, type: 'block' });
1325
1377
  }
1326
- return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated } };
1378
+ return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated, isOversized } };
1327
1379
  }
1328
1380
 
1329
1381
  // Otherwise, we're good to go!
1330
- return { result: TopicValidatorResult.Accept, obj: block };
1382
+ return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated: false, isOversized } };
1331
1383
  }
1332
1384
 
1333
1385
  // REFACTOR(palla): This method should be moved to the p2p_client or to a separate component,
@@ -1367,17 +1419,21 @@ export class LibP2PService extends WithTracer implements P2PService {
1367
1419
  const {
1368
1420
  result,
1369
1421
  obj: checkpoint,
1370
- metadata: { isEquivocated, processBlock } = {},
1371
- } = await this.validateReceivedMessage<CheckpointProposal, { isEquivocated: boolean; processBlock: boolean }>(
1422
+ metadata: { isEquivocated, processBlock, isOversized } = {},
1423
+ } = await this.validateReceivedMessage<
1424
+ CheckpointProposal,
1425
+ { isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
1426
+ >(
1372
1427
  () => this.validateAndStoreCheckpointProposal(source, CheckpointProposal.fromBuffer(payloadData)),
1373
1428
  msgId,
1374
1429
  source,
1375
1430
  TopicType.checkpoint_proposal,
1376
1431
  );
1377
1432
 
1378
- // Process checkpoint proposal if valid and not equivocated.
1433
+ // An oversized checkpoint is re-broadcast as slashing evidence but never attested or processed.
1434
+ // Process checkpoint proposal if valid and neither equivocated nor oversized.
1379
1435
  const processCheckpointFn = () =>
1380
- result === TopicValidatorResult.Accept && checkpoint && !isEquivocated
1436
+ result === TopicValidatorResult.Accept && checkpoint && !isEquivocated && !isOversized
1381
1437
  ? this.processValidCheckpointProposal(checkpoint.toCore(), source)
1382
1438
  : Promise.resolve();
1383
1439
 
@@ -1414,7 +1470,12 @@ export class LibP2PService extends WithTracer implements P2PService {
1414
1470
  protected async validateAndStoreCheckpointProposal(
1415
1471
  peerId: PeerId,
1416
1472
  checkpoint: CheckpointProposal,
1417
- ): Promise<ReceivedMessageValidationResult<CheckpointProposal, { isEquivocated: boolean; processBlock: boolean }>> {
1473
+ ): Promise<
1474
+ ReceivedMessageValidationResult<
1475
+ CheckpointProposal,
1476
+ { isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
1477
+ >
1478
+ > {
1418
1479
  const validationResult = await this.checkpointProposalValidator.validate(checkpoint);
1419
1480
 
1420
1481
  if (validationResult.result === 'reject') {
@@ -1429,13 +1490,16 @@ export class LibP2PService extends WithTracer implements P2PService {
1429
1490
  // Extract and try to add the block proposal first if present
1430
1491
  const blockProposal = checkpoint.getBlockProposal();
1431
1492
  let processBlock = false;
1493
+ let isOversized = false;
1432
1494
  if (blockProposal) {
1433
1495
  this.logger.debug(`Validating block proposal from propagated checkpoint`, {
1434
1496
  [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
1435
1497
  [Attributes.P2P_ID]: peerId.toString(),
1436
1498
  });
1437
1499
  const blockProposalResult = await this.validateAndStoreBlockProposal(peerId, blockProposal);
1438
- const { obj, metadata: { isEquivocated } = {} } = blockProposalResult;
1500
+ const { obj, metadata: { isEquivocated, isOversized: blockIsOversized } = {} } = blockProposalResult;
1501
+ isOversized = blockIsOversized ?? false;
1502
+
1439
1503
  if (blockProposalResult.result === TopicValidatorResult.Reject || !obj || isEquivocated) {
1440
1504
  this.logger.debug(`Rejecting checkpoint due to invalid last block proposal`, {
1441
1505
  [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
@@ -1448,7 +1512,8 @@ export class LibP2PService extends WithTracer implements P2PService {
1448
1512
  severity:
1449
1513
  'severity' in blockProposalResult ? blockProposalResult.severity : PeerErrorSeverity.MidToleranceError,
1450
1514
  };
1451
- } else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated) {
1515
+ } else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated && !isOversized) {
1516
+ // An oversized terminal block is re-broadcast as slashing evidence but never processed.
1452
1517
  processBlock = true;
1453
1518
  }
1454
1519
  }
@@ -1468,11 +1533,11 @@ export class LibP2PService extends WithTracer implements P2PService {
1468
1533
  return {
1469
1534
  result: TopicValidatorResult.Ignore,
1470
1535
  obj: checkpoint,
1471
- metadata: { isEquivocated, processBlock },
1536
+ metadata: { isEquivocated, processBlock, isOversized },
1472
1537
  };
1473
1538
  }
1474
1539
 
1475
- // Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast
1540
+ // Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast.
1476
1541
  // Note: We still return the checkpoint obj so the lastBlock can be processed if valid
1477
1542
  if (!added) {
1478
1543
  this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
@@ -1483,7 +1548,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1483
1548
  return {
1484
1549
  result: TopicValidatorResult.Reject,
1485
1550
  obj: checkpoint,
1486
- metadata: { isEquivocated, processBlock },
1551
+ metadata: { isEquivocated, processBlock, isOversized },
1487
1552
  severity: PeerErrorSeverity.HighToleranceError,
1488
1553
  };
1489
1554
  }
@@ -1504,12 +1569,16 @@ export class LibP2PService extends WithTracer implements P2PService {
1504
1569
  return {
1505
1570
  result: TopicValidatorResult.Accept,
1506
1571
  obj: checkpoint,
1507
- metadata: { isEquivocated, processBlock },
1572
+ metadata: { isEquivocated, processBlock, isOversized },
1508
1573
  };
1509
1574
  }
1510
1575
 
1511
1576
  // Otherwise, we're good to go!
1512
- return { result: TopicValidatorResult.Accept, obj: checkpoint, metadata: { processBlock, isEquivocated } };
1577
+ return {
1578
+ result: TopicValidatorResult.Accept,
1579
+ obj: checkpoint,
1580
+ metadata: { processBlock, isEquivocated, isOversized },
1581
+ };
1513
1582
  }
1514
1583
 
1515
1584
  /**
@@ -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 => {
@@ -58,6 +58,19 @@ export type DuplicateProposalInfo = {
58
58
  */
59
59
  export type P2PDuplicateProposalCallback = (info: DuplicateProposalInfo) => void;
60
60
 
61
+ /** Minimal info passed to the oversized proposal callback. */
62
+ export type OversizedProposalInfo = {
63
+ slot: SlotNumber;
64
+ proposer: EthAddress;
65
+ };
66
+
67
+ /**
68
+ * Callback for when a block proposal whose index lands at or beyond the consensus per-checkpoint block
69
+ * limit is stored and re-broadcast as slashing evidence. May fire multiple times per (slot, proposer)
70
+ * if the proposer signed several oversized proposals; consumers are expected to dedup.
71
+ */
72
+ export type P2POversizedProposalCallback = (info: OversizedProposalInfo) => void;
73
+
61
74
  /** Minimal info passed to the duplicate attestation callback. */
62
75
  export type DuplicateAttestationInfo = {
63
76
  slot: SlotNumber;
@@ -108,6 +121,12 @@ export interface P2PService {
108
121
  */
109
122
  registerDuplicateProposalCallback(callback: P2PDuplicateProposalCallback): void;
110
123
 
124
+ /**
125
+ * Registers a callback invoked when an oversized block proposal (index at or beyond the consensus
126
+ * per-checkpoint block limit) is stored and re-broadcast as slashing evidence.
127
+ */
128
+ registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void;
129
+
111
130
  /**
112
131
  * Registers a callback invoked when a duplicate attestation is detected (equivocation).
113
132
  * A validator signing attestations for different proposals at the same slot.