@aztec/p2p 3.0.0-nightly.20251026 → 3.0.0-nightly.20251030-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 (40) hide show
  1. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +15 -0
  2. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  3. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  4. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +32 -0
  5. package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts +2 -0
  6. package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts.map +1 -1
  7. package/dest/mem_pools/attestation_pool/kv_attestation_pool.js +16 -0
  8. package/dest/mem_pools/attestation_pool/memory_attestation_pool.d.ts +2 -0
  9. package/dest/mem_pools/attestation_pool/memory_attestation_pool.d.ts.map +1 -1
  10. package/dest/mem_pools/attestation_pool/memory_attestation_pool.js +22 -0
  11. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts +1 -0
  12. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
  13. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.js +8 -2
  14. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts +1 -0
  15. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts.map +1 -1
  16. package/dest/mem_pools/tx_pool/memory_tx_pool.js +6 -0
  17. package/dest/mem_pools/tx_pool/tx_pool.d.ts +6 -0
  18. package/dest/mem_pools/tx_pool/tx_pool.d.ts.map +1 -1
  19. package/dest/services/encoding.d.ts +24 -3
  20. package/dest/services/encoding.d.ts.map +1 -1
  21. package/dest/services/encoding.js +73 -5
  22. package/dest/services/libp2p/libp2p_service.d.ts +15 -8
  23. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  24. package/dest/services/libp2p/libp2p_service.js +78 -26
  25. package/dest/services/reqresp/reqresp.js +2 -2
  26. package/dest/testbench/p2p_client_testbench_worker.js +4 -1
  27. package/dest/testbench/testbench.js +2 -2
  28. package/package.json +14 -14
  29. package/src/mem_pools/attestation_pool/attestation_pool.ts +17 -0
  30. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +37 -0
  31. package/src/mem_pools/attestation_pool/kv_attestation_pool.ts +21 -0
  32. package/src/mem_pools/attestation_pool/memory_attestation_pool.ts +28 -0
  33. package/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +7 -2
  34. package/src/mem_pools/tx_pool/memory_tx_pool.ts +5 -0
  35. package/src/mem_pools/tx_pool/tx_pool.ts +7 -0
  36. package/src/services/encoding.ts +80 -5
  37. package/src/services/libp2p/libp2p_service.ts +75 -24
  38. package/src/services/reqresp/reqresp.ts +2 -2
  39. package/src/testbench/p2p_client_testbench_worker.ts +3 -0
  40. package/src/testbench/testbench.ts +2 -2
@@ -430,7 +430,11 @@ import { P2PInstrumentation } from './instrumentation.js';
430
430
  const result = await this.node.services.pubsub.publish(topic, p2pMessage.toMessageData());
431
431
  return result.recipients.length;
432
432
  }
433
- preValidateReceivedMessage(msg, msgId, source) {
433
+ /**
434
+ * Checks if this message has already been seen, based on its msgId computed from hashing the message data.
435
+ * Note that we do not rely on the seenCache from gossipsub since we want to keep a longer history of seen
436
+ * messages to avoid tx echoes across the network.
437
+ */ preValidateReceivedMessage(msg, msgId, source) {
434
438
  let topicType;
435
439
  switch(msg.topic){
436
440
  case this.topicStrings[TopicType.tx]:
@@ -484,32 +488,52 @@ import { P2PInstrumentation } from './instrumentation.js';
484
488
  }
485
489
  async validateReceivedMessage(validationFunc, msgId, source, topicType) {
486
490
  let resultAndObj = {
487
- result: false,
488
- obj: undefined
491
+ result: TopicValidatorResult.Reject
489
492
  };
490
493
  const timer = new Timer();
491
494
  try {
492
495
  resultAndObj = await validationFunc();
493
496
  } catch (err) {
494
- this.logger.error(`Error deserializing and validating message `, err);
497
+ this.logger.error(`Error deserializing and validating gossipsub message`, err, {
498
+ msgId,
499
+ source: source.toString(),
500
+ topicType
501
+ });
495
502
  }
496
- if (resultAndObj.result) {
503
+ if (resultAndObj.result === TopicValidatorResult.Accept) {
497
504
  this.instrumentation.recordMessageValidation(topicType, timer);
498
505
  }
499
- this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), resultAndObj.result && resultAndObj.obj ? TopicValidatorResult.Accept : TopicValidatorResult.Reject);
506
+ this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), resultAndObj.result);
500
507
  return resultAndObj;
501
508
  }
502
509
  async handleGossipedTx(payloadData, msgId, source) {
503
510
  const validationFunc = async ()=>{
504
511
  const tx = Tx.fromBuffer(payloadData);
505
- const result = await this.validatePropagatedTx(tx, source);
506
- return {
507
- result,
508
- obj: tx
509
- };
512
+ const isValid = await this.validatePropagatedTx(tx, source);
513
+ const exists = isValid && await this.mempools.txPool.hasTx(tx.getTxHash());
514
+ this.logger.trace(`Validate propagated tx`, {
515
+ isValid,
516
+ exists,
517
+ [Attributes.P2P_ID]: source.toString()
518
+ });
519
+ if (!isValid) {
520
+ return {
521
+ result: TopicValidatorResult.Reject
522
+ };
523
+ } else if (exists) {
524
+ return {
525
+ result: TopicValidatorResult.Ignore,
526
+ obj: tx
527
+ };
528
+ } else {
529
+ return {
530
+ result: TopicValidatorResult.Accept,
531
+ obj: tx
532
+ };
533
+ }
510
534
  };
511
535
  const { result, obj: tx } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.tx);
512
- if (!result || !tx) {
536
+ if (result !== TopicValidatorResult.Accept || !tx) {
513
537
  return;
514
538
  }
515
539
  const txHash = tx.getTxHash();
@@ -519,7 +543,7 @@ import { P2PInstrumentation } from './instrumentation.js';
519
543
  txHash: txHashString
520
544
  });
521
545
  if (this.config.dropTransactions && randomInt(1000) < this.config.dropTransactionsProbability * 1000) {
522
- this.logger.debug(`Intentionally dropping tx ${txHashString} (probability rule)`);
546
+ this.logger.warn(`Intentionally dropping tx ${txHashString} (probability rule)`);
523
547
  return;
524
548
  }
525
549
  await this.mempools.txPool.addTxs([
@@ -534,18 +558,32 @@ import { P2PInstrumentation } from './instrumentation.js';
534
558
  */ async processAttestationFromPeer(payloadData, msgId, source) {
535
559
  const validationFunc = async ()=>{
536
560
  const attestation = BlockAttestation.fromBuffer(payloadData);
537
- const result = await this.validateAttestation(source, attestation);
538
- this.logger.trace(`validatePropagatedAttestation: ${result}`, {
561
+ const isValid = await this.validateAttestation(source, attestation);
562
+ const exists = isValid && await this.mempools.attestationPool.hasAttestation(attestation);
563
+ this.logger.trace(`Validate propagated block attestation`, {
564
+ isValid,
565
+ exists,
539
566
  [Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber.toString(),
540
567
  [Attributes.P2P_ID]: source.toString()
541
568
  });
542
- return {
543
- result,
544
- obj: attestation
545
- };
569
+ if (!isValid) {
570
+ return {
571
+ result: TopicValidatorResult.Reject
572
+ };
573
+ } else if (exists) {
574
+ return {
575
+ result: TopicValidatorResult.Ignore,
576
+ obj: attestation
577
+ };
578
+ } else {
579
+ return {
580
+ result: TopicValidatorResult.Accept,
581
+ obj: attestation
582
+ };
583
+ }
546
584
  };
547
585
  const { result, obj: attestation } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.block_attestation);
548
- if (!result || !attestation) {
586
+ if (result !== TopicValidatorResult.Accept || !attestation) {
549
587
  return;
550
588
  }
551
589
  this.logger.debug(`Received attestation for slot ${attestation.slotNumber.toNumber()} from external peer ${source.toString()}`, {
@@ -561,15 +599,29 @@ import { P2PInstrumentation } from './instrumentation.js';
561
599
  async processBlockFromPeer(payloadData, msgId, source) {
562
600
  const validationFunc = async ()=>{
563
601
  const block = BlockProposal.fromBuffer(payloadData);
564
- const result = await this.validateBlockProposal(source, block);
565
- this.logger.trace(`validatePropagatedBlock: ${result}`, {
602
+ const isValid = await this.validateBlockProposal(source, block);
603
+ const exists = isValid && await this.mempools.attestationPool.hasBlockProposal(block);
604
+ this.logger.trace(`Validate propagated block proposal`, {
605
+ isValid,
606
+ exists,
566
607
  [Attributes.SLOT_NUMBER]: block.payload.header.slotNumber.toString(),
567
608
  [Attributes.P2P_ID]: source.toString()
568
609
  });
569
- return {
570
- result,
571
- obj: block
572
- };
610
+ if (!isValid) {
611
+ return {
612
+ result: TopicValidatorResult.Reject
613
+ };
614
+ } else if (exists) {
615
+ return {
616
+ result: TopicValidatorResult.Ignore,
617
+ obj: block
618
+ };
619
+ } else {
620
+ return {
621
+ result: TopicValidatorResult.Accept,
622
+ obj: block
623
+ };
624
+ }
573
625
  };
574
626
  const { result, obj: block } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.block_proposal);
575
627
  if (!result || !block) {
@@ -362,7 +362,7 @@ import { ReqRespStatus, ReqRespStatusError, parseStatusChunk, prettyPrintReqResp
362
362
  }
363
363
  }
364
364
  const messageData = Buffer.concat(chunks);
365
- const message = this.snappyTransform.inboundTransformNoTopic(messageData);
365
+ const message = this.snappyTransform.inboundTransformData(messageData);
366
366
  return {
367
367
  status: status ?? ReqRespStatus.UNKNOWN,
368
368
  data: message
@@ -462,7 +462,7 @@ import { ReqRespStatus, ReqRespStatusError, parseStatusChunk, prettyPrintReqResp
462
462
  }
463
463
  stream.metadata.written = true; // Mark the stream as written to;
464
464
  yield SUCCESS;
465
- yield snappy.outboundTransformNoTopic(response);
465
+ yield snappy.outboundTransformData(response);
466
466
  }
467
467
  }, stream.sink);
468
468
  }
@@ -34,6 +34,7 @@ function mockTxPool() {
34
34
  getTxStatus: ()=>Promise.resolve(TxStatus.PENDING),
35
35
  getTxsByHash: ()=>Promise.resolve([]),
36
36
  hasTxs: ()=>Promise.resolve([]),
37
+ hasTx: ()=>Promise.resolve(false),
37
38
  updateConfig: ()=>{},
38
39
  markTxsAsNonEvictable: ()=>Promise.resolve(),
39
40
  cleanupDeletedMinedTxs: ()=>Promise.resolve(0)
@@ -51,7 +52,9 @@ function mockAttestationPool() {
51
52
  getAttestationsForSlot: ()=>Promise.resolve([]),
52
53
  getAttestationsForSlotAndProposal: ()=>Promise.resolve([]),
53
54
  addBlockProposal: ()=>Promise.resolve(),
54
- getBlockProposal: ()=>Promise.resolve(undefined)
55
+ getBlockProposal: ()=>Promise.resolve(undefined),
56
+ hasBlockProposal: ()=>Promise.resolve(false),
57
+ hasAttestation: ()=>Promise.resolve(false)
55
58
  };
56
59
  }
57
60
  function mockEpochCache() {
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
  import { sleep } from '@aztec/foundation/sleep';
3
- import { ClientIvcProof } from '@aztec/stdlib/proofs';
3
+ import { ChonkProof } from '@aztec/stdlib/proofs';
4
4
  import { mockTx } from '@aztec/stdlib/testing';
5
5
  import assert from 'assert';
6
6
  import path from 'path';
@@ -34,7 +34,7 @@ async function main() {
34
34
  logger.info('Workers Ready');
35
35
  // Send tx from client 0
36
36
  const tx = await mockTx(1, {
37
- clientIvcProof: ClientIvcProof.random()
37
+ chonkProof: ChonkProof.random()
38
38
  });
39
39
  workerClientManager.processes[0].send({
40
40
  type: 'SEND_TX',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/p2p",
3
- "version": "3.0.0-nightly.20251026",
3
+ "version": "3.0.0-nightly.20251030-2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -67,17 +67,17 @@
67
67
  ]
68
68
  },
69
69
  "dependencies": {
70
- "@aztec/constants": "3.0.0-nightly.20251026",
71
- "@aztec/epoch-cache": "3.0.0-nightly.20251026",
72
- "@aztec/ethereum": "3.0.0-nightly.20251026",
73
- "@aztec/foundation": "3.0.0-nightly.20251026",
74
- "@aztec/kv-store": "3.0.0-nightly.20251026",
75
- "@aztec/noir-contracts.js": "3.0.0-nightly.20251026",
76
- "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251026",
77
- "@aztec/protocol-contracts": "3.0.0-nightly.20251026",
78
- "@aztec/simulator": "3.0.0-nightly.20251026",
79
- "@aztec/stdlib": "3.0.0-nightly.20251026",
80
- "@aztec/telemetry-client": "3.0.0-nightly.20251026",
70
+ "@aztec/constants": "3.0.0-nightly.20251030-2",
71
+ "@aztec/epoch-cache": "3.0.0-nightly.20251030-2",
72
+ "@aztec/ethereum": "3.0.0-nightly.20251030-2",
73
+ "@aztec/foundation": "3.0.0-nightly.20251030-2",
74
+ "@aztec/kv-store": "3.0.0-nightly.20251030-2",
75
+ "@aztec/noir-contracts.js": "3.0.0-nightly.20251030-2",
76
+ "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251030-2",
77
+ "@aztec/protocol-contracts": "3.0.0-nightly.20251030-2",
78
+ "@aztec/simulator": "3.0.0-nightly.20251030-2",
79
+ "@aztec/stdlib": "3.0.0-nightly.20251030-2",
80
+ "@aztec/telemetry-client": "3.0.0-nightly.20251030-2",
81
81
  "@chainsafe/libp2p-gossipsub": "13.0.0",
82
82
  "@chainsafe/libp2p-noise": "^15.0.0",
83
83
  "@chainsafe/libp2p-yamux": "^6.0.2",
@@ -104,8 +104,8 @@
104
104
  "xxhash-wasm": "^1.1.0"
105
105
  },
106
106
  "devDependencies": {
107
- "@aztec/archiver": "3.0.0-nightly.20251026",
108
- "@aztec/world-state": "3.0.0-nightly.20251026",
107
+ "@aztec/archiver": "3.0.0-nightly.20251030-2",
108
+ "@aztec/world-state": "3.0.0-nightly.20251030-2",
109
109
  "@jest/globals": "^30.0.0",
110
110
  "@types/jest": "^30.0.0",
111
111
  "@types/node": "^22.15.17",
@@ -21,6 +21,15 @@ export interface AttestationPool {
21
21
  */
22
22
  getBlockProposal(id: string): Promise<BlockProposal | undefined>;
23
23
 
24
+ /**
25
+ * Check if a block proposal exists in the pool
26
+ *
27
+ * @param idOrProposal - The ID of the block proposal or the block proposal itself to check. The ID is proposal.payload.archive
28
+ *
29
+ * @return True if the block proposal exists, false otherwise.
30
+ */
31
+ hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean>;
32
+
24
33
  /**
25
34
  * AddAttestations
26
35
  *
@@ -84,6 +93,14 @@ export interface AttestationPool {
84
93
  */
85
94
  getAttestationsForSlotAndProposal(slot: bigint, proposalId: string): Promise<BlockAttestation[]>;
86
95
 
96
+ /**
97
+ * Check if a specific attestation exists in the pool
98
+ *
99
+ * @param attestation - The attestation to check
100
+ * @return True if the attestation exists, false otherwise
101
+ */
102
+ hasAttestation(attestation: BlockAttestation): Promise<boolean>;
103
+
87
104
  /** Returns whether the pool is empty. */
88
105
  isEmpty(): Promise<boolean>;
89
106
  }
@@ -72,6 +72,11 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
72
72
  expect(retrievedAttestations.length).toBe(attestations.length);
73
73
  compareAttestations(retrievedAttestations, attestations);
74
74
 
75
+ // Check hasAttestation for added attestations
76
+ for (const attestation of attestations) {
77
+ expect(await ap.hasAttestation(attestation)).toBe(true);
78
+ }
79
+
75
80
  const retrievedAttestationsForSlot = await ap.getAttestationsForSlot(BigInt(slotNumber));
76
81
  expect(retrievedAttestationsForSlot.length).toBe(attestations.length);
77
82
  compareAttestations(retrievedAttestationsForSlot, attestations);
@@ -85,6 +90,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
85
90
  );
86
91
  expect(retrievedAttestationsAfterAdd.length).toBe(attestations.length + 1);
87
92
  compareAttestations(retrievedAttestationsAfterAdd, [...attestations, newAttestation]);
93
+ expect(await ap.hasAttestation(newAttestation)).toBe(true);
88
94
  const retrievedAttestationsForSlotAfterAdd = await ap.getAttestationsForSlot(BigInt(slotNumber));
89
95
  expect(retrievedAttestationsForSlotAfterAdd.length).toBe(attestations.length + 1);
90
96
  compareAttestations(retrievedAttestationsForSlotAfterAdd, [...attestations, newAttestation]);
@@ -97,6 +103,11 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
97
103
  archive.toString(),
98
104
  );
99
105
  expect(retreivedAttestationsAfterDelete.length).toBe(0);
106
+ // Check hasAttestation after deletion
107
+ for (const attestation of attestations) {
108
+ expect(await ap.hasAttestation(attestation)).toBe(false);
109
+ }
110
+ expect(await ap.hasAttestation(newAttestation)).toBe(false);
100
111
  });
101
112
 
102
113
  it('should handle duplicate proposals in a slot', async () => {
@@ -170,10 +181,20 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
170
181
  expect(retreivedAttestations.length).toBe(NUMBER_OF_SIGNERS_PER_TEST);
171
182
  compareAttestations(retreivedAttestations, attestations);
172
183
 
184
+ // Check hasAttestation before deletion
185
+ for (const attestation of attestations) {
186
+ expect(await ap.hasAttestation(attestation)).toBe(true);
187
+ }
188
+
173
189
  await ap.deleteAttestations(attestations);
174
190
 
175
191
  const gottenAfterDelete = await ap.getAttestationsForSlotAndProposal(BigInt(slotNumber), proposalId);
176
192
  expect(gottenAfterDelete.length).toBe(0);
193
+
194
+ // Check hasAttestation after deletion
195
+ for (const attestation of attestations) {
196
+ expect(await ap.hasAttestation(attestation)).toBe(false);
197
+ }
177
198
  });
178
199
 
179
200
  it('should blanket delete attestations per slot', async () => {
@@ -265,12 +286,19 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
265
286
 
266
287
  expect(retrievedProposal).toBeDefined();
267
288
  expect(retrievedProposal!).toEqual(proposal);
289
+
290
+ // Check hasBlockProposal with both id and object
291
+ expect(await ap.hasBlockProposal(proposalId)).toBe(true);
292
+ expect(await ap.hasBlockProposal(proposal)).toBe(true);
268
293
  });
269
294
 
270
295
  it('should return undefined for non-existent block proposal', async () => {
271
296
  const nonExistentId = Fr.random().toString();
272
297
  const retrievedProposal = await ap.getBlockProposal(nonExistentId);
273
298
  expect(retrievedProposal).toBeUndefined();
299
+
300
+ // Check hasBlockProposal returns false for non-existent proposal
301
+ expect(await ap.hasBlockProposal(nonExistentId)).toBe(false);
274
302
  });
275
303
 
276
304
  it('should update block proposal if added twice with same id', async () => {
@@ -323,6 +351,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
323
351
  // Verify proposal exists
324
352
  let retrievedProposal = await ap.getBlockProposal(proposalId);
325
353
  expect(retrievedProposal).toBeDefined();
354
+ expect(await ap.hasBlockProposal(proposalId)).toBe(true);
326
355
 
327
356
  // Delete attestations for slot and proposal
328
357
  await ap.deleteAttestationsForSlotAndProposal(BigInt(slotNumber), proposalId);
@@ -330,6 +359,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
330
359
  // Proposal should be deleted
331
360
  retrievedProposal = await ap.getBlockProposal(proposalId);
332
361
  expect(retrievedProposal).toBeUndefined();
362
+ expect(await ap.hasBlockProposal(proposalId)).toBe(false);
333
363
  });
334
364
 
335
365
  it('should delete block proposal when deleting attestations for slot', async () => {
@@ -344,6 +374,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
344
374
  // Verify proposal exists
345
375
  let retrievedProposal = await ap.getBlockProposal(proposalId);
346
376
  expect(retrievedProposal).toBeDefined();
377
+ expect(await ap.hasBlockProposal(proposal)).toBe(true);
347
378
 
348
379
  // Delete attestations for slot
349
380
  await ap.deleteAttestationsForSlot(BigInt(slotNumber));
@@ -351,6 +382,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
351
382
  // Proposal should be deleted
352
383
  retrievedProposal = await ap.getBlockProposal(proposalId);
353
384
  expect(retrievedProposal).toBeUndefined();
385
+ expect(await ap.hasBlockProposal(proposal)).toBe(false);
354
386
  });
355
387
 
356
388
  it('should be able to fetch both block proposal and attestations', async () => {
@@ -372,8 +404,13 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
372
404
 
373
405
  expect(retrievedProposal).toBeDefined();
374
406
  expect(retrievedProposal).toEqual(proposal);
407
+ expect(await ap.hasBlockProposal(proposalId)).toBe(true);
375
408
 
376
409
  compareAttestations(retrievedAttestations, attestations);
410
+ // Check hasAttestation for all attestations
411
+ for (const attestation of attestations) {
412
+ expect(await ap.hasAttestation(attestation)).toBe(true);
413
+ }
377
414
  });
378
415
  });
379
416
  }
@@ -213,6 +213,22 @@ export class KvAttestationPool implements AttestationPool {
213
213
  });
214
214
  }
215
215
 
216
+ public async hasAttestation(attestation: BlockAttestation): Promise<boolean> {
217
+ const slotNumber = attestation.payload.header.slotNumber;
218
+ const proposalId = attestation.archive;
219
+ const sender = attestation.getSender();
220
+
221
+ // Attestations with invalid signatures are never in the pool
222
+ if (!sender) {
223
+ return false;
224
+ }
225
+
226
+ const address = sender.toString();
227
+ const key = this.getAttestationKey(slotNumber, proposalId, address);
228
+
229
+ return await this.attestations.hasAsync(key);
230
+ }
231
+
216
232
  public async getBlockProposal(id: string): Promise<BlockProposal | undefined> {
217
233
  const buffer = await this.proposals.getAsync(id);
218
234
  try {
@@ -226,6 +242,11 @@ export class KvAttestationPool implements AttestationPool {
226
242
  return Promise.resolve(undefined);
227
243
  }
228
244
 
245
+ public async hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean> {
246
+ const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.payload.archive.toString();
247
+ return await this.proposals.hasAsync(id);
248
+ }
249
+
229
250
  public async addBlockProposal(blockProposal: BlockProposal): Promise<void> {
230
251
  await this.store.transactionAsync(async () => {
231
252
  await this.proposalsForSlot.set(blockProposal.slotNumber.toString(), blockProposal.archive.toString());
@@ -173,6 +173,29 @@ export class InMemoryAttestationPool implements AttestationPool {
173
173
  return Promise.resolve();
174
174
  }
175
175
 
176
+ public hasAttestation(attestation: BlockAttestation): Promise<boolean> {
177
+ const slotNumber = attestation.payload.header.slotNumber;
178
+ const proposalId = attestation.archive.toString();
179
+ const sender = attestation.getSender();
180
+
181
+ // Attestations with invalid signatures are never in the pool
182
+ if (!sender) {
183
+ return Promise.resolve(false);
184
+ }
185
+
186
+ const slotAttestationMap = this.attestations.get(slotNumber.toBigInt());
187
+ if (!slotAttestationMap) {
188
+ return Promise.resolve(false);
189
+ }
190
+
191
+ const proposalAttestationMap = slotAttestationMap.get(proposalId);
192
+ if (!proposalAttestationMap) {
193
+ return Promise.resolve(false);
194
+ }
195
+
196
+ return Promise.resolve(proposalAttestationMap.has(sender.toString()));
197
+ }
198
+
176
199
  public addBlockProposal(blockProposal: BlockProposal): Promise<void> {
177
200
  // We initialize slot-proposal mapping if it does not exist
178
201
  // This is important to ensure we can delete this proposal if there were not attestations for it
@@ -186,6 +209,11 @@ export class InMemoryAttestationPool implements AttestationPool {
186
209
  public getBlockProposal(id: string): Promise<BlockProposal | undefined> {
187
210
  return Promise.resolve(this.proposals.get(id));
188
211
  }
212
+
213
+ public hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean> {
214
+ const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.payload.archive.toString();
215
+ return Promise.resolve(this.proposals.has(id));
216
+ }
189
217
  }
190
218
 
191
219
  /**
@@ -6,7 +6,7 @@ import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap, AztecAsyncSi
6
6
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
7
7
  import { GasFees } from '@aztec/stdlib/gas';
8
8
  import type { MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
9
- import { ClientIvcProof } from '@aztec/stdlib/proofs';
9
+ import { ChonkProof } from '@aztec/stdlib/proofs';
10
10
  import type { TxAddedToPoolStats } from '@aztec/stdlib/stats';
11
11
  import { DatabasePublicStateSource } from '@aztec/stdlib/trees';
12
12
  import { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
@@ -273,6 +273,11 @@ export class AztecKVTxPool extends (EventEmitter as new () => TypedEventEmitter<
273
273
  return await Promise.all(txHashes.map(txHash => this.#txs.hasAsync(txHash.toString())));
274
274
  }
275
275
 
276
+ async hasTx(txHash: TxHash): Promise<boolean> {
277
+ const result = await this.hasTxs([txHash]);
278
+ return result[0];
279
+ }
280
+
276
281
  /**
277
282
  * Checks if an archived tx exists and returns it.
278
283
  * @param txHash - The tx hash.
@@ -546,7 +551,7 @@ export class AztecKVTxPool extends (EventEmitter as new () => TypedEventEmitter<
546
551
  const archivedTx: Tx = new Tx(
547
552
  tx.txHash,
548
553
  tx.data,
549
- ClientIvcProof.empty(),
554
+ ChonkProof.empty(),
550
555
  tx.contractClassLogFields,
551
556
  tx.publicFunctionCalldata,
552
557
  );
@@ -151,6 +151,11 @@ export class InMemoryTxPool extends (EventEmitter as new () => TypedEventEmitter
151
151
  return Promise.resolve(txHashes.map(txHash => this.txs.has(txHash.toBigInt())));
152
152
  }
153
153
 
154
+ async hasTx(txHash: TxHash): Promise<boolean> {
155
+ const result = await this.hasTxs([txHash]);
156
+ return result[0];
157
+ }
158
+
154
159
  public getArchivedTxByHash(): Promise<Tx | undefined> {
155
160
  return Promise.resolve(undefined);
156
161
  }
@@ -43,6 +43,13 @@ export interface TxPool extends TypedEventEmitter<TxPoolEvents> {
43
43
  */
44
44
  hasTxs(txHashes: TxHash[]): Promise<boolean[]>;
45
45
 
46
+ /**
47
+ * Checks if a transaction exists in the pool
48
+ * @param txHash - The hash of the transaction to check for
49
+ * @returns True if the transaction exists, false otherwise
50
+ */
51
+ hasTx(txHash: TxHash): Promise<boolean>;
52
+
46
53
  /**
47
54
  * Checks if an archived transaction exists in the pool and returns it.
48
55
  * @param txHash - The hash of the transaction, used as an ID.
@@ -1,5 +1,7 @@
1
1
  // Taken from lodestar: https://github.com/ChainSafe/lodestar
2
2
  import { sha256 } from '@aztec/foundation/crypto';
3
+ import { createLogger } from '@aztec/foundation/log';
4
+ import { TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
3
5
 
4
6
  import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
5
7
  import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
@@ -49,31 +51,104 @@ export function getMsgIdFn(message: Message) {
49
51
  return sha256(Buffer.concat(vec)).subarray(0, 20);
50
52
  }
51
53
 
54
+ const DefaultMaxSizesKb: Record<TopicType, number> = {
55
+ // Tx effects should not exceed 128kb, so 512kb for the full tx obj should be sufficient
56
+ [TopicType.tx]: 512,
57
+ // An attestation has roughly 30 fields, which is 1kb, so 5x is plenty
58
+ [TopicType.block_attestation]: 5,
59
+ // Proposals may carry some tx objects, so we allow a larger size capped at 10mb
60
+ // Note this may not be enough for carrying all tx objects in a block
61
+ [TopicType.block_proposal]: 1024 * 10,
62
+ };
63
+
52
64
  /**
53
65
  * Snappy transform for libp2p gossipsub
54
66
  */
55
67
  export class SnappyTransform implements DataTransform {
68
+ constructor(
69
+ private maxSizesKb: Record<TopicType, number> = DefaultMaxSizesKb,
70
+ private defaultMaxSizeKb: number = 10 * 1024,
71
+ private logger = createLogger('p2p:snappy-transform'),
72
+ ) {}
73
+
56
74
  // Topic string included to satisfy DataTransform interface
57
- inboundTransform(_topicStr: string, data: Uint8Array): Uint8Array {
58
- return this.inboundTransformNoTopic(Buffer.from(data));
75
+ inboundTransform(topicStr: string, data: Uint8Array): Uint8Array {
76
+ const topic = getTopicFromString(topicStr);
77
+ return this.inboundTransformData(Buffer.from(data), topic);
59
78
  }
60
79
 
61
- public inboundTransformNoTopic(data: Buffer): Buffer {
80
+ public inboundTransformData(data: Buffer, topic?: TopicType): Buffer {
62
81
  if (data.length === 0) {
63
82
  return data;
64
83
  }
84
+ const maxSizeKb = this.maxSizesKb[topic!] ?? this.defaultMaxSizeKb;
85
+ const { decompressedSize } = readSnappyPreamble(data);
86
+ if (decompressedSize > maxSizeKb * 1024) {
87
+ this.logger.warn(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
88
+ throw new Error(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
89
+ }
90
+
65
91
  return Buffer.from(uncompressSync(data, { asBuffer: true }));
66
92
  }
67
93
 
68
94
  // Topic string included to satisfy DataTransform interface
69
95
  outboundTransform(_topicStr: string, data: Uint8Array): Uint8Array {
70
- return this.outboundTransformNoTopic(Buffer.from(data));
96
+ return this.outboundTransformData(Buffer.from(data));
71
97
  }
72
98
 
73
- public outboundTransformNoTopic(data: Buffer): Buffer {
99
+ public outboundTransformData(data: Buffer): Buffer {
74
100
  if (data.length === 0) {
75
101
  return data;
76
102
  }
77
103
  return Buffer.from(compressSync(data));
78
104
  }
79
105
  }
106
+
107
+ /**
108
+ * Reads the Snappy preamble from compressed data and returns the expected decompressed size.
109
+ *
110
+ * The Snappy format starts with a little-endian varint encoding the uncompressed length.
111
+ * Varints consist of a series of bytes where:
112
+ * - Lower 7 bits contain data
113
+ * - Upper bit (0x80) is set if more bytes follow
114
+ *
115
+ * @param data - The compressed data starting with the Snappy preamble
116
+ * @returns Object containing the decompressed size and the number of bytes read from the preamble
117
+ * @throws Error if the data is too short or the varint is invalid
118
+ */
119
+ export function readSnappyPreamble(data: Uint8Array): { decompressedSize: number; bytesRead: number } {
120
+ if (data.length === 0) {
121
+ throw new Error('Cannot read preamble from empty data');
122
+ }
123
+
124
+ let result = 0;
125
+ let shift = 0;
126
+ let bytesRead = 0;
127
+
128
+ // Maximum varint length for 32-bit value is 5 bytes
129
+ // (7 bits per byte, so 5 bytes = 35 bits, enough for 2^32 - 1)
130
+ const maxBytes = 5;
131
+
132
+ for (let i = 0; i < Math.min(data.length, maxBytes); i++) {
133
+ const byte = data[i];
134
+ bytesRead++;
135
+
136
+ // Extract lower 7 bits and add to result with appropriate shift
137
+ // Use >>> 0 to convert to unsigned 32-bit integer to avoid sign issues
138
+ result = (result | ((byte & 0x7f) << shift)) >>> 0;
139
+
140
+ // If upper bit is not set, we're done
141
+ if ((byte & 0x80) === 0) {
142
+ return { decompressedSize: result, bytesRead };
143
+ }
144
+
145
+ shift += 7;
146
+ }
147
+
148
+ // If we get here, either we ran out of data or the varint is too long
149
+ if (bytesRead >= maxBytes) {
150
+ throw new Error('Varint is too long (max 5 bytes for 32-bit value)');
151
+ }
152
+
153
+ throw new Error('Incomplete varint: data ended before varint termination');
154
+ }