@aztec/p2p 1.0.0 → 1.1.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 (59) hide show
  1. package/dest/bootstrap/bootstrap.js +1 -1
  2. package/dest/client/factory.js +1 -1
  3. package/dest/client/p2p_client.js +1 -1
  4. package/dest/config.d.ts +2 -2
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts +11 -4
  7. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
  8. package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.js +17 -10
  9. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts +2 -2
  10. package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts.map +1 -1
  11. package/dest/mem_pools/tx_pool/memory_tx_pool.js +2 -2
  12. package/dest/mem_pools/tx_pool/tx_pool.d.ts +3 -2
  13. package/dest/mem_pools/tx_pool/tx_pool.d.ts.map +1 -1
  14. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  15. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +10 -3
  16. package/dest/msg_validators/tx_validator/factory.d.ts +2 -1
  17. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  18. package/dest/msg_validators/tx_validator/factory.js +3 -2
  19. package/dest/msg_validators/tx_validator/metadata_validator.d.ts +2 -0
  20. package/dest/msg_validators/tx_validator/metadata_validator.d.ts.map +1 -1
  21. package/dest/msg_validators/tx_validator/metadata_validator.js +13 -7
  22. package/dest/msg_validators/tx_validator/phases_validator.d.ts +3 -2
  23. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  24. package/dest/msg_validators/tx_validator/phases_validator.js +4 -4
  25. package/dest/services/libp2p/instrumentation.d.ts +8 -1
  26. package/dest/services/libp2p/instrumentation.d.ts.map +1 -1
  27. package/dest/services/libp2p/instrumentation.js +130 -2
  28. package/dest/services/libp2p/libp2p_service.d.ts +8 -3
  29. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  30. package/dest/services/libp2p/libp2p_service.js +34 -14
  31. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  32. package/dest/test-helpers/make-test-p2p-clients.js +2 -1
  33. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  34. package/dest/test-helpers/reqresp-nodes.js +3 -2
  35. package/dest/testbench/p2p_client_testbench_worker.js +6 -0
  36. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  37. package/dest/testbench/worker_client_manager.js +2 -1
  38. package/dest/util.d.ts +3 -2
  39. package/dest/util.d.ts.map +1 -1
  40. package/dest/util.js +6 -5
  41. package/package.json +13 -13
  42. package/src/bootstrap/bootstrap.ts +1 -1
  43. package/src/client/factory.ts +1 -1
  44. package/src/client/p2p_client.ts +1 -1
  45. package/src/config.ts +2 -1
  46. package/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +22 -12
  47. package/src/mem_pools/tx_pool/memory_tx_pool.ts +3 -3
  48. package/src/mem_pools/tx_pool/tx_pool.ts +3 -2
  49. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +8 -4
  50. package/src/msg_validators/tx_validator/factory.ts +4 -1
  51. package/src/msg_validators/tx_validator/metadata_validator.ts +20 -9
  52. package/src/msg_validators/tx_validator/phases_validator.ts +3 -2
  53. package/src/services/libp2p/instrumentation.ts +122 -3
  54. package/src/services/libp2p/libp2p_service.ts +41 -15
  55. package/src/test-helpers/make-test-p2p-clients.ts +2 -1
  56. package/src/test-helpers/reqresp-nodes.ts +3 -2
  57. package/src/testbench/p2p_client_testbench_worker.ts +1 -0
  58. package/src/testbench/worker_client_manager.ts +2 -1
  59. package/src/util.ts +8 -7
@@ -8,7 +8,7 @@ import type { MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/st
8
8
  import { ClientIvcProof } from '@aztec/stdlib/proofs';
9
9
  import type { TxAddedToPoolStats } from '@aztec/stdlib/stats';
10
10
  import { DatabasePublicStateSource } from '@aztec/stdlib/trees';
11
- import { Tx, TxHash } from '@aztec/stdlib/tx';
11
+ import { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
12
12
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
13
13
 
14
14
  import assert from 'assert';
@@ -129,8 +129,14 @@ export class AztecKVTxPool implements TxPool {
129
129
  }
130
130
  return true;
131
131
  }
132
-
133
- public async markAsMined(txHashes: TxHash[], blockNumber: number): Promise<void> {
132
+ /**
133
+ * Marks transactions as mined in a block and updates the pool state accordingly.
134
+ * Removes the transactions from the pending set and adds them to the mined set.
135
+ * Also evicts any transactions that become invalid after the block is mined.
136
+ * @param txHashes - Array of transaction hashes that were mined
137
+ * @param blockHeader - The header of the block the transactions were mined in
138
+ */
139
+ public async markAsMined(txHashes: TxHash[], blockHeader: BlockHeader): Promise<void> {
134
140
  if (txHashes.length === 0) {
135
141
  return Promise.resolve();
136
142
  }
@@ -142,7 +148,7 @@ export class AztecKVTxPool implements TxPool {
142
148
  let pendingTxSize = (await this.#pendingTxSize.getAsync()) ?? 0;
143
149
  for (const hash of txHashes) {
144
150
  const key = hash.toString();
145
- await this.#minedTxHashToBlock.set(key, blockNumber);
151
+ await this.#minedTxHashToBlock.set(key, blockHeader.globalVariables.blockNumber);
146
152
 
147
153
  const tx = await this.getPendingTxByHash(hash);
148
154
  if (tx) {
@@ -155,7 +161,7 @@ export class AztecKVTxPool implements TxPool {
155
161
  }
156
162
  await this.#pendingTxSize.set(pendingTxSize);
157
163
 
158
- await this.evictInvalidTxsAfterMining(txHashes, blockNumber, minedNullifiers, minedFeePayers);
164
+ await this.evictInvalidTxsAfterMining(txHashes, blockHeader, minedNullifiers, minedFeePayers);
159
165
  });
160
166
  // We update this after the transaction above. This ensures that the non-evictable transactions are not evicted
161
167
  // until any that have been mined are marked as such.
@@ -535,15 +541,15 @@ export class AztecKVTxPool implements TxPool {
535
541
  * Eviction criteria includes:
536
542
  * - txs with nullifiers that are already included in the mined block
537
543
  * - txs with an insufficient fee payer balance
538
- * - txs with a max block number lower than the mined block
544
+ * - txs with an expiration timestamp lower than that of the mined block
539
545
  *
540
546
  * @param minedTxHashes - The tx hashes of the txs mined in the block.
541
- * @param blockNumber - The block number of the mined block.
547
+ * @param blockHeader - The header of the mined block.
542
548
  * @returns The total number of txs evicted from the pool.
543
549
  */
544
550
  private async evictInvalidTxsAfterMining(
545
551
  minedTxHashes: TxHash[],
546
- blockNumber: number,
552
+ blockHeader: BlockHeader,
547
553
  minedNullifiers: Set<string>,
548
554
  minedFeePayers: Set<string>,
549
555
  ): Promise<number> {
@@ -551,6 +557,8 @@ export class AztecKVTxPool implements TxPool {
551
557
  return 0;
552
558
  }
553
559
 
560
+ const { blockNumber, timestamp } = blockHeader.globalVariables;
561
+
554
562
  // Wait for world state to be synced to at least the mined block number
555
563
  await this.#worldStateSynchronizer.syncImmediate(blockNumber);
556
564
 
@@ -582,10 +590,12 @@ export class AztecKVTxPool implements TxPool {
582
590
  continue;
583
591
  }
584
592
 
585
- // Evict pending txs with a max block number less than or equal to the mined block
586
- const maxBlockNumber = tx.data.rollupValidationRequests.maxBlockNumber;
587
- if (maxBlockNumber.isSome && maxBlockNumber.value <= blockNumber) {
588
- this.#log.verbose(`Evicting tx ${txHash} from pool due to an invalid max block number`);
593
+ // Evict pending txs with an expiration timestamp less than or equal to the mined block timestamp
594
+ const includeByTimestamp = tx.data.rollupValidationRequests.includeByTimestamp;
595
+ if (includeByTimestamp.isSome && includeByTimestamp.value <= timestamp) {
596
+ this.#log.verbose(
597
+ `Evicting tx ${txHash} from pool due to the tx being expired (includeByTimestamp: ${includeByTimestamp.value}, mined block timestamp: ${timestamp})`,
598
+ );
589
599
  txsToEvict.push(TxHash.fromString(txHash));
590
600
  continue;
591
601
  }
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
  import type { TxAddedToPoolStats } from '@aztec/stdlib/stats';
3
- import { Tx, TxHash } from '@aztec/stdlib/tx';
3
+ import { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
4
4
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
5
5
 
6
6
  import { PoolInstrumentation, PoolName, type PoolStatsCallback } from '../instrumentation.js';
@@ -47,10 +47,10 @@ export class InMemoryTxPool implements TxPool {
47
47
  return Promise.resolve(this.txs.size === 0);
48
48
  }
49
49
 
50
- public markAsMined(txHashes: TxHash[], blockNumber: number): Promise<void> {
50
+ public markAsMined(txHashes: TxHash[], blockHeader: BlockHeader): Promise<void> {
51
51
  const keys = txHashes.map(x => x.toBigInt());
52
52
  for (const key of keys) {
53
- this.minedTxs.set(key, blockNumber);
53
+ this.minedTxs.set(key, blockHeader.globalVariables.blockNumber);
54
54
  this.pendingTxs.delete(key);
55
55
  }
56
56
  return Promise.resolve();
@@ -1,4 +1,4 @@
1
- import type { Tx, TxHash } from '@aztec/stdlib/tx';
1
+ import type { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
2
2
 
3
3
  export type TxPoolOptions = {
4
4
  maxTxPoolSize?: number;
@@ -48,8 +48,9 @@ export interface TxPool {
48
48
  /**
49
49
  * Marks the set of txs as mined, as opposed to pending.
50
50
  * @param txHashes - Hashes of the txs to flag as mined.
51
+ * @param blockHeader - The header of the mined block.
51
52
  */
52
- markAsMined(txHashes: TxHash[], blockNumber: number): Promise<void>;
53
+ markAsMined(txHashes: TxHash[], blockHeader: BlockHeader): Promise<void>;
53
54
 
54
55
  /**
55
56
  * Moves mined txs back to the pending set in the case of a reorg.
@@ -1,7 +1,7 @@
1
1
  import { unfreeze } from '@aztec/foundation/types';
2
2
  import { GasFees } from '@aztec/stdlib/gas';
3
3
  import { mockTx } from '@aztec/stdlib/testing';
4
- import type { Tx } from '@aztec/stdlib/tx';
4
+ import { BlockHeader, GlobalVariables, type Tx } from '@aztec/stdlib/tx';
5
5
 
6
6
  import type { TxPool } from './tx_pool.js';
7
7
 
@@ -12,6 +12,10 @@ import type { TxPool } from './tx_pool.js';
12
12
  export function describeTxPool(getTxPool: () => TxPool) {
13
13
  let pool: TxPool;
14
14
 
15
+ const minedBlockHeader = BlockHeader.empty({
16
+ globalVariables: GlobalVariables.empty({ blockNumber: 1, timestamp: 0n }),
17
+ });
18
+
15
19
  beforeEach(() => {
16
20
  pool = getTxPool();
17
21
  });
@@ -43,7 +47,7 @@ export function describeTxPool(getTxPool: () => TxPool) {
43
47
  const tx2 = await mockTx(2);
44
48
 
45
49
  await pool.addTxs([tx1, tx2]);
46
- await pool.markAsMined([await tx1.getTxHash()], 1);
50
+ await pool.markAsMined([await tx1.getTxHash()], minedBlockHeader);
47
51
 
48
52
  await expect(pool.getTxByHash(await tx1.getTxHash())).resolves.toEqual(tx1);
49
53
  await expect(pool.getTxStatus(await tx1.getTxHash())).resolves.toEqual('mined');
@@ -57,7 +61,7 @@ export function describeTxPool(getTxPool: () => TxPool) {
57
61
  const tx2 = await mockTx(2);
58
62
 
59
63
  await pool.addTxs([tx1, tx2]);
60
- await pool.markAsMined([await tx1.getTxHash()], 1);
64
+ await pool.markAsMined([await tx1.getTxHash()], minedBlockHeader);
61
65
 
62
66
  await pool.markMinedAsPending([await tx1.getTxHash()]);
63
67
  await expect(pool.getMinedTxHashes()).resolves.toEqual([]);
@@ -74,7 +78,7 @@ export function describeTxPool(getTxPool: () => TxPool) {
74
78
  const someTxHashThatThisPeerDidNotSee = await tx2.getTxHash();
75
79
  await pool.addTxs([tx1]);
76
80
  // this peer knows that tx2 was mined, but it does not have the tx object
77
- await pool.markAsMined([await tx1.getTxHash(), someTxHashThatThisPeerDidNotSee], 1);
81
+ await pool.markAsMined([await tx1.getTxHash(), someTxHashThatThisPeerDidNotSee], minedBlockHeader);
78
82
  expect(await pool.getMinedTxHashes()).toEqual(
79
83
  expect.arrayContaining([
80
84
  [await tx1.getTxHash(), 1],
@@ -11,6 +11,7 @@ import type {
11
11
  import { PeerErrorSeverity } from '@aztec/stdlib/p2p';
12
12
  import { DatabasePublicStateSource, MerkleTreeId } from '@aztec/stdlib/trees';
13
13
  import type { Tx, TxValidationResult } from '@aztec/stdlib/tx';
14
+ import type { UInt64 } from '@aztec/stdlib/types';
14
15
 
15
16
  import { ArchiveCache } from './archive_cache.js';
16
17
  import { BlockHeaderTxValidator } from './block_header_validator.js';
@@ -29,6 +30,7 @@ export interface MessageValidator {
29
30
  }
30
31
 
31
32
  export function createTxMessageValidators(
33
+ timestamp: UInt64,
32
34
  blockNumber: number,
33
35
  worldStateSynchronizer: WorldStateSynchronizer,
34
36
  gasFees: GasFees,
@@ -51,6 +53,7 @@ export function createTxMessageValidators(
51
53
  validator: new MetadataTxValidator({
52
54
  l1ChainId: new Fr(l1ChainId),
53
55
  rollupVersion: new Fr(rollupVersion),
56
+ timestamp,
54
57
  blockNumber,
55
58
  protocolContractTreeRoot,
56
59
  vkTreeRoot: getVKTreeRoot(),
@@ -76,7 +79,7 @@ export function createTxMessageValidators(
76
79
  severity: PeerErrorSeverity.HighToleranceError,
77
80
  },
78
81
  phasesValidator: {
79
- validator: new PhasesTxValidator(contractDataSource, allowedInSetup, blockNumber),
82
+ validator: new PhasesTxValidator(contractDataSource, allowedInSetup, timestamp),
80
83
  severity: PeerErrorSeverity.MidToleranceError,
81
84
  },
82
85
  blockHeaderValidator: {
@@ -6,11 +6,12 @@ import {
6
6
  TX_ERROR_INCORRECT_PROTOCOL_CONTRACT_TREE_ROOT,
7
7
  TX_ERROR_INCORRECT_ROLLUP_VERSION,
8
8
  TX_ERROR_INCORRECT_VK_TREE_ROOT,
9
- TX_ERROR_INVALID_MAX_BLOCK_NUMBER,
9
+ TX_ERROR_INVALID_INCLUDE_BY_TIMESTAMP,
10
10
  Tx,
11
11
  type TxValidationResult,
12
12
  type TxValidator,
13
13
  } from '@aztec/stdlib/tx';
14
+ import type { UInt64 } from '@aztec/stdlib/types';
14
15
 
15
16
  export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
16
17
  #log = createLogger('p2p:tx_validator:tx_metadata');
@@ -19,6 +20,10 @@ export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
19
20
  private values: {
20
21
  l1ChainId: Fr;
21
22
  rollupVersion: Fr;
23
+ // Timestamp at which we will validate that the tx is not expired. This is typically the timestamp of the block
24
+ // being built.
25
+ timestamp: UInt64;
26
+ // Block number in which the tx is considered to be included.
22
27
  blockNumber: number;
23
28
  vkTreeRoot: Fr;
24
29
  protocolContractTreeRoot: Fr;
@@ -33,8 +38,8 @@ export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
33
38
  if (!(await this.#hasCorrectRollupVersion(tx))) {
34
39
  errors.push(TX_ERROR_INCORRECT_ROLLUP_VERSION);
35
40
  }
36
- if (!(await this.#isValidForBlockNumber(tx))) {
37
- errors.push(TX_ERROR_INVALID_MAX_BLOCK_NUMBER);
41
+ if (!(await this.#isValidForTimestamp(tx))) {
42
+ errors.push(TX_ERROR_INVALID_INCLUDE_BY_TIMESTAMP);
38
43
  }
39
44
  if (!(await this.#hasCorrectVkTreeRoot(tx))) {
40
45
  errors.push(TX_ERROR_INCORRECT_VK_TREE_ROOT);
@@ -84,14 +89,20 @@ export class MetadataTxValidator<T extends AnyTx> implements TxValidator<T> {
84
89
  }
85
90
  }
86
91
 
87
- async #isValidForBlockNumber(tx: T): Promise<boolean> {
88
- const maxBlockNumber = tx.data.rollupValidationRequests.maxBlockNumber;
92
+ async #isValidForTimestamp(tx: T): Promise<boolean> {
93
+ const includeByTimestamp = tx.data.rollupValidationRequests.includeByTimestamp;
94
+ // If building block 1, we skip the expiration check. For details on why see the `validate_include_by_timestamp`
95
+ // function in `noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/components/validation_requests.nr`.
96
+ const buildingBlock1 = this.values.blockNumber === 1;
89
97
 
90
- if (maxBlockNumber.isSome && maxBlockNumber.value < this.values.blockNumber) {
98
+ if (!buildingBlock1 && includeByTimestamp.isSome && includeByTimestamp.value < this.values.timestamp) {
99
+ if (tx.data.constants.historicalHeader.globalVariables.blockNumber === 0) {
100
+ this.#log.warn(
101
+ `A tx built against a genesis block failed to be included in block 1 which is the only block in which txs built against a genesis block are allowed to be included.`,
102
+ );
103
+ }
91
104
  this.#log.verbose(
92
- `Rejecting tx ${await Tx.getHash(tx)} for low max block number. Tx max block number: ${
93
- maxBlockNumber.value
94
- }, current block number: ${this.values.blockNumber}.`,
105
+ `Rejecting tx ${await Tx.getHash(tx)} for low expiration timestamp. Tx expiration timestamp: ${includeByTimestamp.value}, timestamp: ${this.values.timestamp}.`,
95
106
  );
96
107
  return false;
97
108
  } else {
@@ -11,6 +11,7 @@ import {
11
11
  type TxValidationResult,
12
12
  type TxValidator,
13
13
  } from '@aztec/stdlib/tx';
14
+ import type { UInt64 } from '@aztec/stdlib/types';
14
15
 
15
16
  export class PhasesTxValidator implements TxValidator<Tx> {
16
17
  #log = createLogger('sequencer:tx_validator:tx_phases');
@@ -19,7 +20,7 @@ export class PhasesTxValidator implements TxValidator<Tx> {
19
20
  constructor(
20
21
  contracts: ContractDataSource,
21
22
  private setupAllowList: AllowedElement[],
22
- private blockNumber: number,
23
+ private timestamp: UInt64,
23
24
  ) {
24
25
  this.contractsDB = new PublicContractsDB(contracts);
25
26
  }
@@ -86,7 +87,7 @@ export class PhasesTxValidator implements TxValidator<Tx> {
86
87
  }
87
88
  }
88
89
 
89
- const contractClass = await this.contractsDB.getContractInstance(contractAddress, this.blockNumber);
90
+ const contractClass = await this.contractsDB.getContractInstance(contractAddress, this.timestamp);
90
91
 
91
92
  if (!contractClass) {
92
93
  throw new Error(`Contract not found: ${contractAddress}`);
@@ -1,17 +1,28 @@
1
1
  import type { Timer } from '@aztec/foundation/timer';
2
- import type { TopicType } from '@aztec/stdlib/p2p';
2
+ import { TopicType } from '@aztec/stdlib/p2p';
3
3
  import {
4
4
  Attributes,
5
+ type BatchObservableResult,
5
6
  type Histogram,
6
7
  Metrics,
8
+ type ObservableGauge,
7
9
  type TelemetryClient,
8
10
  type UpDownCounter,
9
11
  ValueType,
10
12
  } from '@aztec/telemetry-client';
11
13
 
14
+ import { type RecordableHistogram, createHistogram } from 'node:perf_hooks';
15
+
12
16
  export class P2PInstrumentation {
13
17
  private messageValidationDuration: Histogram;
14
18
  private messagePrevalidationCount: UpDownCounter;
19
+ private messageLatency: Histogram;
20
+
21
+ private aggLatencyHisto = new Map<TopicType, RecordableHistogram>();
22
+ private aggValidationHisto = new Map<TopicType, RecordableHistogram>();
23
+
24
+ private aggLatencyMetrics: Record<'min' | 'max' | 'p50' | 'p90' | 'avg', ObservableGauge>;
25
+ private aggValidationMetrics: Record<'min' | 'max' | 'p50' | 'p90' | 'avg', ObservableGauge>;
15
26
 
16
27
  constructor(client: TelemetryClient, name: string) {
17
28
  const meter = client.getMeter(name);
@@ -26,14 +37,122 @@ export class P2PInstrumentation {
26
37
  description: 'How many message pass/fail prevalidation',
27
38
  valueType: ValueType.INT,
28
39
  });
40
+
41
+ this.messageLatency = meter.createHistogram(Metrics.P2P_GOSSIP_MESSAGE_LATENCY, {
42
+ unit: 'ms',
43
+ description: 'P2P message latency',
44
+ valueType: ValueType.INT,
45
+ });
46
+
47
+ this.aggLatencyMetrics = {
48
+ avg: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_LATENCY_AVG, {
49
+ valueType: ValueType.DOUBLE,
50
+ description: 'AVG msg latency',
51
+ unit: 'ms',
52
+ }),
53
+ max: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_LATENCY_MAX, {
54
+ valueType: ValueType.DOUBLE,
55
+ description: 'MAX msg latency',
56
+ unit: 'ms',
57
+ }),
58
+ min: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_LATENCY_MIN, {
59
+ valueType: ValueType.DOUBLE,
60
+ description: 'MIN msg latency',
61
+ unit: 'ms',
62
+ }),
63
+ p50: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_LATENCY_P50, {
64
+ valueType: ValueType.DOUBLE,
65
+ description: 'P50 msg latency',
66
+ unit: 'ms',
67
+ }),
68
+ p90: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_LATENCY_P90, {
69
+ valueType: ValueType.DOUBLE,
70
+ description: 'P90 msg latency',
71
+ unit: 'ms',
72
+ }),
73
+ };
74
+
75
+ this.aggValidationMetrics = {
76
+ avg: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_VALIDATION_DURATION_AVG, {
77
+ valueType: ValueType.DOUBLE,
78
+ description: 'AVG msg validation',
79
+ unit: 'ms',
80
+ }),
81
+ max: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_VALIDATION_DURATION_MAX, {
82
+ valueType: ValueType.DOUBLE,
83
+ description: 'MAX msg validation',
84
+ unit: 'ms',
85
+ }),
86
+ min: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_VALIDATION_DURATION_MIN, {
87
+ valueType: ValueType.DOUBLE,
88
+ description: 'MIN msg validation',
89
+ unit: 'ms',
90
+ }),
91
+ p50: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_VALIDATION_DURATION_P50, {
92
+ valueType: ValueType.DOUBLE,
93
+ description: 'P50 msg validation',
94
+ unit: 'ms',
95
+ }),
96
+ p90: meter.createObservableGauge(Metrics.P2P_GOSSIP_AGG_MESSAGE_VALIDATION_DURATION_P90, {
97
+ valueType: ValueType.DOUBLE,
98
+ description: 'P90 msg validation',
99
+ unit: 'ms',
100
+ }),
101
+ };
102
+
103
+ meter.addBatchObservableCallback(this.aggregate, [
104
+ ...Object.values(this.aggValidationMetrics),
105
+ ...Object.values(this.aggLatencyMetrics),
106
+ ]);
29
107
  }
30
108
 
31
109
  public recordMessageValidation(topicName: TopicType, timerOrMs: Timer | number) {
32
- const ms = typeof timerOrMs === 'number' ? timerOrMs : timerOrMs.ms();
33
- this.messageValidationDuration.record(Math.ceil(ms), { [Attributes.TOPIC_NAME]: topicName });
110
+ const ms = Math.ceil(typeof timerOrMs === 'number' ? timerOrMs : timerOrMs.ms());
111
+ this.messageValidationDuration.record(ms, { [Attributes.TOPIC_NAME]: topicName });
112
+
113
+ let validationHistogram = this.aggValidationHisto.get(topicName);
114
+ if (!validationHistogram) {
115
+ validationHistogram = createHistogram({ min: 1, max: 5 * 60 * 1000 }); // 5 mins
116
+ this.aggValidationHisto.set(topicName, validationHistogram);
117
+ }
118
+
119
+ validationHistogram.record(Math.max(ms, 1));
34
120
  }
35
121
 
36
122
  public incMessagePrevalidationStatus(passed: boolean, topicName: TopicType | undefined) {
37
123
  this.messagePrevalidationCount.add(1, { [Attributes.TOPIC_NAME]: topicName, [Attributes.OK]: passed });
38
124
  }
125
+
126
+ public recordMessageLatency(topicName: TopicType, timerOrMs: Timer | number) {
127
+ const ms = Math.ceil(typeof timerOrMs === 'number' ? timerOrMs : timerOrMs.ms());
128
+ this.messageLatency.record(ms, { [Attributes.TOPIC_NAME]: topicName });
129
+
130
+ let latencyHistogram = this.aggLatencyHisto.get(topicName);
131
+ if (!latencyHistogram) {
132
+ latencyHistogram = createHistogram({ min: 1, max: 24 * 60 * 60 * 1000 }); // 24hrs
133
+ this.aggLatencyHisto.set(topicName, latencyHistogram);
134
+ }
135
+
136
+ latencyHistogram.record(Math.max(ms, 1));
137
+ }
138
+
139
+ private aggregate = (res: BatchObservableResult) => {
140
+ for (const [metrics, histograms] of [
141
+ [this.aggLatencyMetrics, this.aggLatencyHisto],
142
+ [this.aggValidationMetrics, this.aggValidationHisto],
143
+ ] as const) {
144
+ for (const topicName of Object.values(TopicType)) {
145
+ const histogram = histograms.get(topicName);
146
+ if (!histogram || histogram.count === 0) {
147
+ continue;
148
+ }
149
+
150
+ res.observe(metrics.avg, histogram.mean, { [Attributes.TOPIC_NAME]: topicName });
151
+ res.observe(metrics.max, histogram.max, { [Attributes.TOPIC_NAME]: topicName });
152
+ res.observe(metrics.min, histogram.min, { [Attributes.TOPIC_NAME]: topicName });
153
+ res.observe(metrics.p50, histogram.percentile(50), { [Attributes.TOPIC_NAME]: topicName });
154
+ res.observe(metrics.p90, histogram.percentile(90), { [Attributes.TOPIC_NAME]: topicName });
155
+ }
156
+ }
157
+ };
39
158
  }
@@ -23,6 +23,7 @@ import {
23
23
  } from '@aztec/stdlib/p2p';
24
24
  import { MerkleTreeId } from '@aztec/stdlib/trees';
25
25
  import { Tx, type TxHash, type TxValidationResult } from '@aztec/stdlib/tx';
26
+ import type { UInt64 } from '@aztec/stdlib/types';
26
27
  import { compressComponentVersions } from '@aztec/stdlib/versioning';
27
28
  import { Attributes, OtelMetricsAdapter, type TelemetryClient, WithTracer, trackSpan } from '@aztec/telemetry-client';
28
29
 
@@ -125,7 +126,7 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
125
126
  private peerManager: PeerManagerInterface,
126
127
  protected mempools: MemPools<T>,
127
128
  private archiver: L2BlockSource & ContractDataSource,
128
- epochCache: EpochCacheInterface,
129
+ private epochCache: EpochCacheInterface,
129
130
  private proofVerifier: ClientProtocolCircuitVerifier,
130
131
  private worldStateSynchronizer: WorldStateSynchronizer,
131
132
  telemetry: TelemetryClient,
@@ -536,7 +537,11 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
536
537
  return result.recipients.length;
537
538
  }
538
539
 
539
- protected preValidateReceivedMessage(msg: Message, msgId: string, source: PeerId) {
540
+ protected preValidateReceivedMessage(
541
+ msg: Message,
542
+ msgId: string,
543
+ source: PeerId,
544
+ ): { result: boolean; topicType?: TopicType } {
540
545
  let topicType: TopicType | undefined;
541
546
 
542
547
  switch (msg.topic) {
@@ -559,12 +564,12 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
559
564
  if (!validator || !validator.addMessage(msgId)) {
560
565
  this.instrumentation.incMessagePrevalidationStatus(false, topicType);
561
566
  this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), TopicValidatorResult.Ignore);
562
- return false;
567
+ return { result: false, topicType };
563
568
  }
564
569
 
565
570
  this.instrumentation.incMessagePrevalidationStatus(true, topicType);
566
571
 
567
- return true;
572
+ return { result: true, topicType };
568
573
  }
569
574
 
570
575
  /**
@@ -582,8 +587,15 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
582
587
  messageLatency,
583
588
  });
584
589
 
585
- if (!this.preValidateReceivedMessage(msg, msgId, source)) {
590
+ const preValidationResult = this.preValidateReceivedMessage(msg, msgId, source);
591
+
592
+ if (!preValidationResult.result) {
586
593
  return;
594
+ } else if (preValidationResult.topicType !== undefined) {
595
+ // guard against clock skew & DST
596
+ if (messageLatency > 0) {
597
+ this.instrumentation.recordMessageLatency(preValidationResult.topicType, messageLatency);
598
+ }
587
599
  }
588
600
 
589
601
  if (msg.topic === this.topicStrings[TopicType.tx]) {
@@ -827,8 +839,11 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
827
839
  [Attributes.TX_HASH]: (await tx.getTxHash()).toString(),
828
840
  }))
829
841
  private async validatePropagatedTx(tx: Tx, peerId: PeerId): Promise<boolean> {
830
- const blockNumber = (await this.archiver.getBlockNumber()) + 1;
831
- const messageValidators = await this.createMessageValidators(blockNumber);
842
+ const currentBlockNumber = await this.archiver.getBlockNumber();
843
+
844
+ // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
845
+ const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
846
+ const messageValidators = await this.createMessageValidators(currentBlockNumber, nextSlotTimestamp);
832
847
 
833
848
  for (const validator of messageValidators) {
834
849
  const outcome = await this.runValidations(tx, validator);
@@ -841,7 +856,8 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
841
856
 
842
857
  // Double spend validator has a special case handler
843
858
  if (name === 'doubleSpendValidator') {
844
- severity = await this.handleDoubleSpendFailure(tx, blockNumber);
859
+ const txBlockNumber = currentBlockNumber + 1; // tx is expected to be in the next block
860
+ severity = await this.handleDoubleSpendFailure(tx, txBlockNumber);
845
861
  }
846
862
 
847
863
  this.peerManager.penalizePeer(peerId, severity);
@@ -862,8 +878,11 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
862
878
  }
863
879
 
864
880
  public async validate(txs: Tx[]): Promise<void> {
865
- const blockNumber = (await this.archiver.getBlockNumber()) + 1;
866
- const messageValidators = await this.createMessageValidators(blockNumber);
881
+ const currentBlockNumber = await this.archiver.getBlockNumber();
882
+
883
+ // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
884
+ const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
885
+ const messageValidators = await this.createMessageValidators(currentBlockNumber, nextSlotTimestamp);
867
886
 
868
887
  await Promise.all(
869
888
  txs.map(async tx => {
@@ -878,20 +897,27 @@ export class LibP2PService<T extends P2PClientType = P2PClientType.Full> extends
878
897
  }
879
898
 
880
899
  /**
881
- * Create message validators for the given block number.
900
+ * Create message validators for the given block number and timestamp.
882
901
  *
883
902
  * Each validator is a pair of a validator and a severity.
884
903
  * If a validator fails, the peer is penalized with the severity of the validator.
885
904
  *
886
- * @param blockNumber - The block number to create validators for.
905
+ * @param currentBlockNumber - The current synced block number.
906
+ * @param nextSlotTimestamp - The timestamp of the next slot (used to validate txs are not expired).
887
907
  * @returns The message validators.
888
908
  */
889
- private async createMessageValidators(blockNumber: number): Promise<Record<string, MessageValidator>[]> {
890
- const gasFees = await this.getGasFees(blockNumber - 1);
909
+ private async createMessageValidators(
910
+ currentBlockNumber: number,
911
+ nextSlotTimestamp: UInt64,
912
+ ): Promise<Record<string, MessageValidator>[]> {
913
+ const gasFees = await this.getGasFees(currentBlockNumber);
891
914
  const allowedInSetup = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
892
915
 
916
+ const blockNumberInWhichTheTxIsConsideredToBeIncluded = currentBlockNumber + 1;
917
+
893
918
  return createTxMessageValidators(
894
- blockNumber,
919
+ nextSlotTimestamp,
920
+ blockNumberInWhichTheTxIsConsideredToBeIncluded,
895
921
  this.worldStateSynchronizer,
896
922
  gasFees,
897
923
  this.config.l1ChainId,
@@ -1,5 +1,6 @@
1
1
  import { MockL2BlockSource } from '@aztec/archiver/test';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
+ import { SecretValue } from '@aztec/foundation/config';
3
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
5
  import { sleep } from '@aztec/foundation/sleep';
5
6
  import type { DataStoreConfig } from '@aztec/kv-store/config';
@@ -78,7 +79,7 @@ export async function makeTestP2PClient(
78
79
  const config: P2PConfig & DataStoreConfig = {
79
80
  ...p2pBaseConfig,
80
81
  p2pEnabled: true,
81
- peerIdPrivateKey,
82
+ peerIdPrivateKey: new SecretValue(peerIdPrivateKey),
82
83
  p2pIp: `127.0.0.1`,
83
84
  listenAddress: `127.0.0.1`,
84
85
  p2pPort: port,
@@ -1,5 +1,6 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
2
  import { timesParallel } from '@aztec/foundation/collection';
3
+ import { SecretValue } from '@aztec/foundation/config';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import type { DataStoreConfig } from '@aztec/kv-store/config';
5
6
  import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
@@ -125,7 +126,7 @@ export async function createTestLibP2PService<T extends P2PClientType>(
125
126
  peerCheckIntervalMS: 1000,
126
127
  maxPeerCount: 5,
127
128
  p2pEnabled: true,
128
- peerIdPrivateKey: Buffer.from(peerId.privateKey!).toString('hex'),
129
+ peerIdPrivateKey: new SecretValue(Buffer.from(peerId.privateKey!).toString('hex')),
129
130
  bootstrapNodeEnrVersionCheck: false,
130
131
  ...chainConfig,
131
132
  } as P2PConfig & DataStoreConfig;
@@ -278,7 +279,7 @@ export function createBootstrapNodeConfig(privateKey: string, port: number, chai
278
279
  l1ChainId: chainConfig.l1ChainId,
279
280
  p2pIp: '127.0.0.1',
280
281
  p2pPort: port,
281
- peerIdPrivateKey: privateKey,
282
+ peerIdPrivateKey: new SecretValue(privateKey),
282
283
  dataDirectory: undefined,
283
284
  dataStoreMapSizeKB: 0,
284
285
  bootstrapNodes: [],
@@ -82,6 +82,7 @@ function mockEpochCache(): EpochCacheInterface {
82
82
  currentSlot: 0n,
83
83
  nextSlot: 0n,
84
84
  }),
85
+ getEpochAndSlotInNextL1Slot: () => ({ epoch: 0n, slot: 0n, ts: 0n, now: 0n }),
85
86
  isInCommittee: () => Promise.resolve(false),
86
87
  };
87
88
  }
@@ -1,3 +1,4 @@
1
+ import { SecretValue } from '@aztec/foundation/config';
1
2
  import { EthAddress } from '@aztec/foundation/eth-address';
2
3
  import type { Logger } from '@aztec/foundation/log';
3
4
  import { sleep } from '@aztec/foundation/sleep';
@@ -55,7 +56,7 @@ class WorkerClientManager {
55
56
  return {
56
57
  ...getP2PDefaultConfig(),
57
58
  p2pEnabled: true,
58
- peerIdPrivateKey: this.peerIdPrivateKeys[clientIndex],
59
+ peerIdPrivateKey: new SecretValue(this.peerIdPrivateKeys[clientIndex]),
59
60
  listenAddress: '127.0.0.1',
60
61
  p2pIp: '127.0.0.1',
61
62
  p2pPort: port,