@aztec-labs/p2p 6.0.0-rc.1 → 6.0.0-rc.3

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 (104) hide show
  1. package/dest/bootstrap/bootstrap.d.ts +2 -2
  2. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  3. package/dest/bootstrap/bootstrap.js +4 -2
  4. package/dest/client/factory.d.ts +3 -3
  5. package/dest/client/factory.d.ts.map +1 -1
  6. package/dest/client/factory.js +6 -6
  7. package/dest/client/interface.d.ts +1 -1
  8. package/dest/client/p2p_client.d.ts +1 -1
  9. package/dest/client/p2p_client.js +1 -1
  10. package/dest/config.d.ts +11 -1
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +16 -0
  13. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +15 -3
  14. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  15. package/dest/mem_pools/attestation_pool/attestation_pool.js +15 -0
  16. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  17. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  18. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +21 -0
  19. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts +6 -2
  20. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts.map +1 -1
  21. package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.js +9 -0
  22. package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.d.ts +10 -8
  23. package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.d.ts.map +1 -1
  24. package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.js +22 -11
  25. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +6 -2
  26. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  27. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.js +2 -1
  28. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +11 -5
  29. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  30. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
  31. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  32. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +111 -44
  33. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +1 -1
  34. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
  35. package/dest/msg_validators/attestation_validator/attestation_validator.js +3 -24
  36. package/dest/msg_validators/epoch_cache_lookup.d.ts +18 -0
  37. package/dest/msg_validators/epoch_cache_lookup.d.ts.map +1 -0
  38. package/dest/msg_validators/epoch_cache_lookup.js +29 -0
  39. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +1 -1
  40. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  41. package/dest/msg_validators/proposal_validator/proposal_validator.js +80 -82
  42. package/dest/msg_validators/tx_validator/data_validator.d.ts +1 -1
  43. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
  44. package/dest/msg_validators/tx_validator/data_validator.js +4 -3
  45. package/dest/msg_validators/tx_validator/factory.d.ts +40 -11
  46. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  47. package/dest/msg_validators/tx_validator/factory.js +36 -5
  48. package/dest/msg_validators/tx_validator/gas_validator.d.ts +18 -10
  49. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  50. package/dest/msg_validators/tx_validator/gas_validator.js +37 -27
  51. package/dest/services/discv5/discV5_service.d.ts +2 -2
  52. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  53. package/dest/services/discv5/discV5_service.js +3 -1
  54. package/dest/services/libp2p/libp2p_service.d.ts +18 -9
  55. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  56. package/dest/services/libp2p/libp2p_service.js +127 -29
  57. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  58. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  59. package/dest/services/peer-manager/peer_manager.js +14 -0
  60. package/dest/services/tx_provider.d.ts +1 -1
  61. package/dest/services/tx_provider.d.ts.map +1 -1
  62. package/dest/services/tx_provider.js +3 -0
  63. package/dest/test-helpers/make-test-p2p-clients.d.ts +1 -1
  64. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  65. package/dest/test-helpers/make-test-p2p-clients.js +3 -1
  66. package/dest/test-helpers/mock-pubsub.js +1 -1
  67. package/dest/test-helpers/reqresp-nodes.d.ts +1 -1
  68. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  69. package/dest/test-helpers/reqresp-nodes.js +3 -1
  70. package/dest/test-helpers/testbench-utils.d.ts +4 -2
  71. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  72. package/dest/test-helpers/testbench-utils.js +6 -0
  73. package/dest/testbench/p2p_client_testbench_worker.js +6 -2
  74. package/package.json +14 -14
  75. package/src/bootstrap/bootstrap.ts +3 -2
  76. package/src/client/factory.ts +7 -7
  77. package/src/client/interface.ts +1 -1
  78. package/src/client/p2p_client.ts +1 -1
  79. package/src/config.ts +11 -0
  80. package/src/mem_pools/attestation_pool/attestation_pool.ts +25 -0
  81. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +31 -0
  82. package/src/mem_pools/tx_pool_v2/README.md +1 -1
  83. package/src/mem_pools/tx_pool_v2/eviction/eviction_manager.ts +13 -1
  84. package/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts +17 -9
  85. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +6 -1
  86. package/src/mem_pools/tx_pool_v2/interfaces.ts +10 -4
  87. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +95 -40
  88. package/src/msg_validators/attestation_validator/attestation_validator.ts +3 -23
  89. package/src/msg_validators/epoch_cache_lookup.ts +33 -0
  90. package/src/msg_validators/proposal_validator/README.md +3 -3
  91. package/src/msg_validators/proposal_validator/proposal_validator.ts +78 -77
  92. package/src/msg_validators/tx_validator/README.md +12 -8
  93. package/src/msg_validators/tx_validator/data_validator.ts +4 -3
  94. package/src/msg_validators/tx_validator/factory.ts +60 -14
  95. package/src/msg_validators/tx_validator/gas_validator.ts +46 -34
  96. package/src/services/discv5/discV5_service.ts +2 -1
  97. package/src/services/libp2p/libp2p_service.ts +138 -34
  98. package/src/services/peer-manager/peer_manager.ts +14 -0
  99. package/src/services/tx_provider.ts +3 -0
  100. package/src/test-helpers/make-test-p2p-clients.ts +5 -1
  101. package/src/test-helpers/mock-pubsub.ts +3 -3
  102. package/src/test-helpers/reqresp-nodes.ts +5 -1
  103. package/src/test-helpers/testbench-utils.ts +13 -1
  104. package/src/testbench/p2p_client_testbench_worker.ts +10 -2
@@ -36,7 +36,7 @@ import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'
36
36
  import { ProtocolContractAddress, protocolContractsHash } from '@aztec-labs/protocol-contracts';
37
37
  import type { BlockHash } from '@aztec-labs/stdlib/block';
38
38
  import type { ContractDataSource } from '@aztec-labs/stdlib/contract';
39
- import type { GasFees } from '@aztec-labs/stdlib/gas';
39
+ import { GasFees } from '@aztec-labs/stdlib/gas';
40
40
  import type {
41
41
  AllowedElement,
42
42
  ClientProtocolCircuitVerifier,
@@ -58,7 +58,7 @@ import { ContractInstanceTxValidator } from './contract_instance_validator.js';
58
58
  import { DataTxValidator } from './data_validator.js';
59
59
  import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js';
60
60
  import { MaxGasLimitsValidator, MinGasLimitsValidator } from './gas_limits_validator.js';
61
- import { GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js';
61
+ import { FeePayerBalanceValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js';
62
62
  import { MetadataTxValidator } from './metadata_validator.js';
63
63
  import { NullifierCache } from './nullifier_cache.js';
64
64
  import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js';
@@ -69,22 +69,55 @@ import { TxProofValidator } from './tx_proof_validator.js';
69
69
  import { TxValidationCache } from './tx_validation_cache.js';
70
70
 
71
71
  /**
72
- * A validator paired with a peer penalty severity.
73
- * Used for gossip validation where each validator's failure triggers a peer penalization
74
- * with the associated severity level.
72
+ * Marks a gossip validator whose failure is not attributable to the sending peer: the tx is dropped without
73
+ * being added to the pool or propagated, and the peer's score is left alone.
75
74
  */
76
- export interface TransactionValidator {
75
+ export const IgnoreWithoutPenalty = 'ignore-without-penalty';
76
+
77
+ /**
78
+ * Consequence of a gossip validator failing: the sending peer is penalized with this severity, unless the
79
+ * failure is local policy rather than peer misbehavior, in which case it is {@link IgnoreWithoutPenalty}.
80
+ */
81
+ export type GossipValidationFailure = PeerErrorSeverity | typeof IgnoreWithoutPenalty;
82
+
83
+ /**
84
+ * The fees a gossiped tx is checked against. A tx below `admission` is not relayed. A tx below `penaltyFloor` as
85
+ * well is priced lower than any view of the chain this node considers plausible, so its sender is penalized.
86
+ * `penaltyFloor` is never above `admission`.
87
+ */
88
+ export type GossipMinFees = { admission: GasFees; penaltyFloor: GasFees };
89
+
90
+ /**
91
+ * Fraction of the lower of the admission and L1-forward fees below which a gossiped tx gets its sender penalized.
92
+ * A peer can legitimately price the next block below both: one that has seen a checkpoint this node has not can
93
+ * see the congestion fee drop, and the L1 gas components move with every L1 block. Halving leaves room for
94
+ * several such steps while still penalizing zero-fee and negligible-fee spam.
95
+ */
96
+ export const GOSSIP_FEE_PENALTY_FLOOR_FRACTION = 0.5;
97
+
98
+ /** Derives the gossip fee thresholds from this node's admission fee and its L1-forward fee. */
99
+ export function computeGossipMinFees(admission: GasFees, l1Forward: GasFees): GossipMinFees {
100
+ return { admission, penaltyFloor: GasFees.min(admission, l1Forward).mul(GOSSIP_FEE_PENALTY_FLOOR_FRACTION) };
101
+ }
102
+
103
+ /**
104
+ * A validator paired with the consequence of its failure.
105
+ * Used for gossip validation where each validator's failure triggers a peer penalization with the associated
106
+ * severity level. Only stages that can also fail on local policy widen `F` to include
107
+ * {@link IgnoreWithoutPenalty}.
108
+ */
109
+ export interface TransactionValidator<F extends GossipValidationFailure = PeerErrorSeverity> {
77
110
  validator: {
78
111
  validateTx(tx: Tx): Promise<TxValidationResult>;
79
112
  };
80
- severity: PeerErrorSeverity;
113
+ severity: F;
81
114
  }
82
115
 
83
116
  /**
84
117
  * First stage of gossip validation — fast checks run before the pool pre-check.
85
118
  *
86
- * If any validator fails, the peer is penalized and the tx is rejected immediately,
87
- * without consulting the pool or running proof verification.
119
+ * If any validator fails, the tx is rejected immediately, without consulting the pool or running proof
120
+ * verification. The peer is penalized unless the only failures are marked {@link IgnoreWithoutPenalty}.
88
121
  *
89
122
  * The `doubleSpendValidator` failure is special-cased by the caller (`handleGossipedTx`)
90
123
  * to determine severity based on how recently the nullifier appeared. The caller reports the
@@ -94,7 +127,7 @@ export function createFirstStageTxValidationsForGossipedTransactions(
94
127
  timestamp: UInt64,
95
128
  blockNumber: BlockNumber,
96
129
  worldStateSynchronizer: WorldStateSynchronizer,
97
- gasFees: GasFees,
130
+ gasFees: GossipMinFees,
98
131
  l1ChainId: number,
99
132
  rollupVersion: number,
100
133
  protocolContractsHash: Fr,
@@ -103,7 +136,7 @@ export function createFirstStageTxValidationsForGossipedTransactions(
103
136
  allowedInSetup: AllowedElement[] = [],
104
137
  bindings?: LoggerBindings,
105
138
  gasLimitOpts?: { maxTxL2Gas?: number; maxTxDAGas?: number },
106
- ): Record<string, TransactionValidator> {
139
+ ): Record<string, TransactionValidator<GossipValidationFailure>> {
107
140
  const merkleTree = worldStateSynchronizer.getCommitted();
108
141
 
109
142
  return {
@@ -166,11 +199,24 @@ export function createFirstStageTxValidationsForGossipedTransactions(
166
199
  validator: new MaxGasLimitsValidator<Tx>({ ...gasLimitOpts, bindings }),
167
200
  severity: PeerErrorSeverity.MidToleranceError,
168
201
  },
169
- gasValidator: {
170
- validator: new GasTxValidator(
202
+ // The max-fee comparisons and the fee-payer balance check are separate entries because their failures mean
203
+ // different things. A tx below our admission fee is only invalid against a fee this node resolved: a peer
204
+ // ahead of us may be relaying against a lower checkpoint fee it legitimately sees, so the tx is dropped
205
+ // without penalizing the sender. That excuse only stretches down to the penalty floor, so a tx below it fails
206
+ // both entries and the harshest failure wins. An underfunded fee payer is invalid against shared state, so
207
+ // it stays penalized.
208
+ maxFeePerGasValidator: {
209
+ validator: new MaxFeePerGasValidator<Tx>(gasFees.admission, bindings),
210
+ severity: IgnoreWithoutPenalty,
211
+ },
212
+ maxFeePerGasFloorValidator: {
213
+ validator: new MaxFeePerGasValidator<Tx>(gasFees.penaltyFloor, bindings),
214
+ severity: PeerErrorSeverity.MidToleranceError,
215
+ },
216
+ feePayerBalanceValidator: {
217
+ validator: new FeePayerBalanceValidator(
171
218
  new DatabasePublicStateSource(merkleTree),
172
219
  ProtocolContractAddress.FeeJuice,
173
- gasFees,
174
220
  bindings,
175
221
  ),
176
222
  severity: PeerErrorSeverity.MidToleranceError,
@@ -72,52 +72,25 @@ export class MaxFeePerGasValidator<T extends HasMaxFeePerGasData> implements TxV
72
72
  }
73
73
 
74
74
  /**
75
- * Validates that a transaction can pay its gas fees.
76
- *
77
- * Runs two checks in order:
78
- * 1. **Max fee per gas** (delegates to {@link MaxFeePerGasValidator}) — rejects the tx if its
79
- * maxFeesPerGas is below the current block's gas fees.
80
- * 2. **Fee payer balance** — reads the fee payer's FeeJuice balance from public state,
81
- * adds any pending claim from a setup-phase `_increase_public_balance` call, and
82
- * rejects if the total is less than the tx's fee limit (gasLimits * maxFeePerGas).
83
- *
84
- * Gas limits are deliberately not checked here: they are owned by {@link MinGasLimitsValidator} and
85
- * {@link MaxGasLimitsValidator}, which factories include separately so that exemptions (e.g. gas estimation)
86
- * don't change fee enforcement.
75
+ * Validates that a transaction's fee payer can cover its fee limit: reads the fee payer's FeeJuice balance from
76
+ * public state, adds any pending claim from a setup-phase `_increase_public_balance` call, and rejects if the total
77
+ * is less than the tx's fee limit (gasLimits * maxFeePerGas).
87
78
  *
88
- * Used by: gossip (stage 1), RPC, and block building validators.
79
+ * Does not check max fee per gas against any fee: a tx with zero max fees has a zero fee limit and passes here with
80
+ * no balance, so every caller must also run a {@link MaxFeePerGasValidator}, as {@link GasTxValidator} does.
89
81
  */
90
- export class GasTxValidator implements TxValidator<Tx> {
82
+ export class FeePayerBalanceValidator implements TxValidator<Tx> {
91
83
  #log: Logger;
92
84
  #publicDataSource: PublicStateSource;
93
85
  #feeJuiceAddress: AztecAddress;
94
- #maxFeePerGasValidator: MaxFeePerGasValidator<Tx>;
95
86
 
96
- constructor(
97
- publicDataSource: PublicStateSource,
98
- feeJuiceAddress: AztecAddress,
99
- gasFees: GasFees,
100
- bindings?: LoggerBindings,
101
- ) {
87
+ constructor(publicDataSource: PublicStateSource, feeJuiceAddress: AztecAddress, bindings?: LoggerBindings) {
102
88
  this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings);
103
89
  this.#publicDataSource = publicDataSource;
104
90
  this.#feeJuiceAddress = feeJuiceAddress;
105
- this.#maxFeePerGasValidator = new MaxFeePerGasValidator(gasFees, bindings);
106
91
  }
107
92
 
108
93
  async validateTx(tx: Tx): Promise<TxValidationResult> {
109
- const maxFeeValidation = this.#maxFeePerGasValidator.validateMaxFeePerGas(tx);
110
- if (maxFeeValidation.result === 'invalid') {
111
- return maxFeeValidation;
112
- }
113
- return await this.validateTxFee(tx);
114
- }
115
-
116
- /**
117
- * Checks the fee payer has enough FeeJuice balance to cover the tx's fee limit.
118
- * Accounts for any pending claim from a setup-phase `_increase_public_balance` call.
119
- */
120
- public async validateTxFee(tx: Tx): Promise<TxValidationResult> {
121
94
  const feePayer = tx.data.feePayer;
122
95
 
123
96
  // Compute the maximum fee that this tx may pay, based on its gasLimits and maxFeePerGas
@@ -147,3 +120,42 @@ export class GasTxValidator implements TxValidator<Tx> {
147
120
  return { result: 'valid' };
148
121
  }
149
122
  }
123
+
124
+ /**
125
+ * Validates that a transaction can pay its gas fees.
126
+ *
127
+ * Runs two checks in order:
128
+ * 1. **Max fee per gas** (delegates to {@link MaxFeePerGasValidator}) — rejects the tx if its
129
+ * maxFeesPerGas is below the current block's gas fees.
130
+ * 2. **Fee payer balance** (delegates to {@link FeePayerBalanceValidator}) — rejects if the fee payer cannot
131
+ * cover the tx's fee limit (gasLimits * maxFeePerGas).
132
+ *
133
+ * Gas limits are deliberately not checked here: they are owned by {@link MinGasLimitsValidator} and
134
+ * {@link MaxGasLimitsValidator}, which factories include separately so that exemptions (e.g. gas estimation)
135
+ * don't change fee enforcement.
136
+ *
137
+ * Used by: RPC and block building validators. Gossip runs the two checks as separate entries, since their
138
+ * failures carry different peer penalties.
139
+ */
140
+ export class GasTxValidator implements TxValidator<Tx> {
141
+ #maxFeePerGasValidator: MaxFeePerGasValidator<Tx>;
142
+ #feePayerBalanceValidator: FeePayerBalanceValidator;
143
+
144
+ constructor(
145
+ publicDataSource: PublicStateSource,
146
+ feeJuiceAddress: AztecAddress,
147
+ gasFees: GasFees,
148
+ bindings?: LoggerBindings,
149
+ ) {
150
+ this.#maxFeePerGasValidator = new MaxFeePerGasValidator(gasFees, bindings);
151
+ this.#feePayerBalanceValidator = new FeePayerBalanceValidator(publicDataSource, feeJuiceAddress, bindings);
152
+ }
153
+
154
+ async validateTx(tx: Tx): Promise<TxValidationResult> {
155
+ const maxFeeValidation = this.#maxFeePerGasValidator.validateMaxFeePerGas(tx);
156
+ if (maxFeeValidation.result === 'invalid') {
157
+ return maxFeeValidation;
158
+ }
159
+ return await this.#feePayerBalanceValidator.validateTx(tx);
160
+ }
161
+ }
@@ -10,7 +10,7 @@ import { Discv5, type Discv5EventEmitter, type IDiscv5CreateOptions } from '@net
10
10
  import { ENR, SignableENR } from '@nethermindeth/enr';
11
11
  import EventEmitter from 'events';
12
12
 
13
- import type { P2PConfig } from '../../config.js';
13
+ import { DISCV5_RATE_LIMITER_OPTS, type P2PConfig } from '../../config.js';
14
14
  import { createNodeENR } from '../../enr/generate-enr.js';
15
15
  import { AZTEC_ENR_KEY, Discv5Event, PeerEvent } from '../../types/index.js';
16
16
  import { convertToMultiaddr } from '../../util.js';
@@ -126,6 +126,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
126
126
  ...configOverrides.config,
127
127
  },
128
128
  metricsRegistry,
129
+ rateLimiterOpts: DISCV5_RATE_LIMITER_OPTS,
129
130
  });
130
131
 
131
132
  // Hook onto the onEstablished method to check the peer's version from the ENR,
@@ -9,7 +9,7 @@ import { protocolContractsHash } from '@aztec-labs/protocol-contracts';
9
9
  import type { EthAddress, L2BlockSource } from '@aztec-labs/stdlib/block';
10
10
  import { DEFAULT_MAX_BLOCKS_PER_CHECKPOINT } from '@aztec-labs/stdlib/config';
11
11
  import type { ContractDataSource } from '@aztec-labs/stdlib/contract';
12
- import { type BlockMinFeesProvider, GasFees, getNetworkTxGasLimits } from '@aztec-labs/stdlib/gas';
12
+ import { type TxAdmissionMinFeesProvider, getNetworkTxGasLimits } from '@aztec-labs/stdlib/gas';
13
13
  import type {
14
14
  ClientProtocolCircuitVerifier,
15
15
  P2PConnectivity,
@@ -77,7 +77,11 @@ import {
77
77
  } from '../../msg_validators/index.js';
78
78
  import { MessageSeenValidator } from '../../msg_validators/msg_seen_validator/msg_seen_validator.js';
79
79
  import {
80
+ type GossipMinFees,
81
+ type GossipValidationFailure,
82
+ IgnoreWithoutPenalty,
80
83
  type TransactionValidator,
84
+ computeGossipMinFees,
81
85
  createFirstStageTxValidationsForGossipedTransactions,
82
86
  createSecondStageTxValidationsForGossipedTransactions,
83
87
  createTxValidatorForBlockProposalReceivedTxs,
@@ -143,13 +147,21 @@ function buildConsensusTimetable(
143
147
  });
144
148
  }
145
149
 
146
- interface ValidationResult {
150
+ interface ValidationResult<F extends GossipValidationFailure> {
147
151
  name: string;
148
152
  isValid: TxValidationResult;
149
- severity: PeerErrorSeverity;
153
+ severity: F;
150
154
  }
151
155
 
152
- type ValidationOutcome = { allPassed: true } | { allPassed: false; failure: ValidationResult };
156
+ /**
157
+ * Failure consequences ordered from mildest to harshest, so the outcome of a stage is the harshest failure it
158
+ * produced. A penalty-free ignore is the mildest: any penalized failure on the same tx wins over it.
159
+ */
160
+ const GossipValidationFailureByHarshness = [IgnoreWithoutPenalty, ...PeerErrorSeverityByHarshness] as const;
161
+
162
+ type ValidationOutcome<F extends GossipValidationFailure> =
163
+ | { allPassed: true }
164
+ | { allPassed: false; failure: ValidationResult<F> };
153
165
 
154
166
  // REFACTOR: Unify with the type above
155
167
  type ReceivedMessageValidationResult<T, M = undefined> =
@@ -229,7 +241,7 @@ export class LibP2PService extends WithTracer implements P2PService {
229
241
  private epochCache: EpochCacheInterface,
230
242
  private proofVerifier: ClientProtocolCircuitVerifier,
231
243
  private worldStateSynchronizer: WorldStateSynchronizer,
232
- private blockMinFeesProvider: BlockMinFeesProvider,
244
+ private nextBlockMinFeesProvider: TxAdmissionMinFeesProvider,
233
245
  telemetry: TelemetryClient,
234
246
  logger: Logger = createLogger('p2p:libp2p_service'),
235
247
  private txValidationCache?: TxValidationCache,
@@ -345,7 +357,7 @@ export class LibP2PService extends WithTracer implements P2PService {
345
357
  proofVerifier: ClientProtocolCircuitVerifier;
346
358
  worldStateSynchronizer: WorldStateSynchronizer;
347
359
  peerStore: AztecAsyncKVStore;
348
- blockMinFeesProvider: BlockMinFeesProvider;
360
+ nextBlockMinFeesProvider: TxAdmissionMinFeesProvider;
349
361
  telemetry: TelemetryClient;
350
362
  logger: Logger;
351
363
  packageVersion: string;
@@ -359,7 +371,7 @@ export class LibP2PService extends WithTracer implements P2PService {
359
371
  mempools,
360
372
  proofVerifier,
361
373
  peerStore,
362
- blockMinFeesProvider,
374
+ nextBlockMinFeesProvider,
363
375
  telemetry,
364
376
  logger,
365
377
  packageVersion,
@@ -587,7 +599,7 @@ export class LibP2PService extends WithTracer implements P2PService {
587
599
  epochCache,
588
600
  proofVerifier,
589
601
  worldStateSynchronizer,
590
- blockMinFeesProvider,
602
+ nextBlockMinFeesProvider,
591
603
  telemetry,
592
604
  logger,
593
605
  txValidationCache,
@@ -1121,6 +1133,12 @@ export class LibP2PService extends WithTracer implements P2PService {
1121
1133
  return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.LowToleranceError };
1122
1134
  }
1123
1135
 
1136
+ // Dedup on already-accepted content before validation: a tx hash already in the pool has been
1137
+ // validated, so a re-encoded copy carries no new work and is ignored without running the stages.
1138
+ if ((await this.mempools.txPool.hasTxs([tx.getTxHash()]))[0]) {
1139
+ return { result: TopicValidatorResult.Ignore, obj: tx };
1140
+ }
1141
+
1124
1142
  // Stage 1 setup: chain tip, epoch info, gas fees, and L1 constants reads
1125
1143
  const { currentBlockNumber, firstStageValidators } = await timed('fast_validation_setup', async () => {
1126
1144
  const currentBlockNumber = await this.archiver.getBlockNumber();
@@ -1138,6 +1156,16 @@ export class LibP2PService extends WithTracer implements P2PService {
1138
1156
  const { name } = firstStageOutcome.failure;
1139
1157
  let { severity } = firstStageOutcome.failure;
1140
1158
 
1159
+ // The tx failed a check that only holds against this node's own view (it pays less than the fee we
1160
+ // resolved for the next block). Drop it without propagating, but leave the sender's score alone.
1161
+ if (severity === IgnoreWithoutPenalty) {
1162
+ this.logger.verbose(`Ignoring gossiped tx ${tx.getTxHash().toString()}: stage 1 local check failed`, {
1163
+ validator: name,
1164
+ source: source.toString(),
1165
+ });
1166
+ return { result: TopicValidatorResult.Ignore, obj: tx };
1167
+ }
1168
+
1141
1169
  // Double spend validator has a special case handler. We perform more detailed examination
1142
1170
  // as to how recently the nullifier was entered into the tree and if the transaction should
1143
1171
  // have 'known' the nullifier existed. This determines the severity of the penalty applied to the peer.
@@ -1228,7 +1256,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1228
1256
  * Process a checkpoint attestation from a peer.
1229
1257
  * Validates the attestation and adds it to the pool.
1230
1258
  */
1231
- private async processCheckpointAttestationFromPeer(
1259
+ protected async processCheckpointAttestationFromPeer(
1232
1260
  payloadData: Buffer,
1233
1261
  msgId: string,
1234
1262
  source: PeerId,
@@ -1264,6 +1292,24 @@ export class LibP2PService extends WithTracer implements P2PService {
1264
1292
  );
1265
1293
  }
1266
1294
 
1295
+ // Bounded set of accepted checkpoint-attestation identities (slot, payload hash, signature bytes), so a
1296
+ // re-encoded copy of an accepted attestation is ignored before the expensive signature recovery.
1297
+ private readonly recentAcceptedAttestations = new Set<string>();
1298
+ private static readonly MAX_RECENT_ATTESTATIONS = 8192;
1299
+
1300
+ private rememberAcceptedAttestation(key: string): void {
1301
+ if (this.recentAcceptedAttestations.has(key)) {
1302
+ return;
1303
+ }
1304
+ this.recentAcceptedAttestations.add(key);
1305
+ if (this.recentAcceptedAttestations.size > LibP2PService.MAX_RECENT_ATTESTATIONS) {
1306
+ const oldest = this.recentAcceptedAttestations.keys().next().value;
1307
+ if (oldest !== undefined) {
1308
+ this.recentAcceptedAttestations.delete(oldest);
1309
+ }
1310
+ }
1311
+ }
1312
+
1267
1313
  /** Validates a checkpoint attestation and adds it to the pool. Penalizes the peer if validation fails. */
1268
1314
  @trackSpan('Libp2pService.validateAndStoreCheckpointAttestation', (_peerId, attestation) => ({
1269
1315
  [Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber.toString(),
@@ -1272,6 +1318,15 @@ export class LibP2PService extends WithTracer implements P2PService {
1272
1318
  peerId: PeerId,
1273
1319
  attestation: CheckpointAttestation,
1274
1320
  ): Promise<ReceivedMessageValidationResult<CheckpointAttestation>> {
1321
+ const attestationKey = `${attestation.payload.header.slotNumber}:${attestation
1322
+ .getPayloadHash()
1323
+ .toString()}:${attestation.signature.toBuffer().toString('hex')}`;
1324
+ // Dedup on already-accepted content before validation: a re-encoded copy of an accepted attestation
1325
+ // is ignored here, before the expensive signature recovery in validate().
1326
+ if (this.recentAcceptedAttestations.has(attestationKey)) {
1327
+ return { result: TopicValidatorResult.Ignore, obj: attestation };
1328
+ }
1329
+
1275
1330
  const validationResult = await this.checkpointAttestationValidator.validate(attestation);
1276
1331
 
1277
1332
  if (validationResult.result === 'reject') {
@@ -1302,16 +1357,18 @@ export class LibP2PService extends WithTracer implements P2PService {
1302
1357
  return { result: TopicValidatorResult.Ignore, obj: attestation };
1303
1358
  }
1304
1359
 
1305
- // Could not add (cap reached for signer), penalize and do not re-broadcast
1360
+ // Local per-(slot, signer) retention cap is full. That is receiver-local state, not evidence the sender
1361
+ // forwarded invalid data (it could not know our cache was full), so drop the extra payload without
1362
+ // penalizing the peer. Do not re-broadcast.
1306
1363
  if (!added) {
1307
- this.logger.warn(`Rejecting checkpoint attestation due to cap`, {
1364
+ this.logger.debug(`Ignoring checkpoint attestation exceeding per-signer cap`, {
1308
1365
  slot: slot.toString(),
1309
1366
  archive: attestation.archive.toString(),
1310
1367
  source: peerId.toString(),
1311
1368
  attester: attestation.getSender()?.toString(),
1312
1369
  count,
1313
1370
  });
1314
- return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.HighToleranceError };
1371
+ return { result: TopicValidatorResult.Ignore, obj: attestation };
1315
1372
  }
1316
1373
 
1317
1374
  // Check if this is a duplicate attestation (signer attested to a different proposal at the same slot)
@@ -1330,6 +1387,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1330
1387
  }
1331
1388
 
1332
1389
  // Attestation was added successfully - accept it so other nodes can also detect the equivocation
1390
+ this.rememberAcceptedAttestation(attestationKey);
1333
1391
  this.checkpointAttestationCallback?.(attestation);
1334
1392
  return { result: TopicValidatorResult.Accept, obj: attestation };
1335
1393
  }
@@ -1339,7 +1397,10 @@ export class LibP2PService extends WithTracer implements P2PService {
1339
1397
  result,
1340
1398
  obj: block,
1341
1399
  metadata: { isEquivocated, isOversized } = {},
1342
- } = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>(
1400
+ } = await this.validateReceivedMessage<
1401
+ BlockProposal,
1402
+ { isEquivocated: boolean; isOversized: boolean; capFull?: boolean }
1403
+ >(
1343
1404
  () => this.validateAndStoreBlockProposal(source, BlockProposal.fromBuffer(payloadData)),
1344
1405
  msgId,
1345
1406
  source,
@@ -1363,7 +1424,22 @@ export class LibP2PService extends WithTracer implements P2PService {
1363
1424
  protected async validateAndStoreBlockProposal(
1364
1425
  peerId: PeerId,
1365
1426
  block: BlockProposal,
1366
- ): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>> {
1427
+ ): Promise<
1428
+ ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean; isOversized: boolean; capFull?: boolean }>
1429
+ > {
1430
+ // Dedup on already-accepted content before validation: an exact signed payload already stored at
1431
+ // this (slot, index) has been validated, so a re-encoded copy is ignored without re-running it. A
1432
+ // different payload hash at the same position is not stored, so equivocation still takes the full path.
1433
+ if (
1434
+ await this.mempools.attestationPool.hasBlockProposal(
1435
+ block.slotNumber,
1436
+ block.indexWithinCheckpoint,
1437
+ block.getPayloadHash(),
1438
+ )
1439
+ ) {
1440
+ return { result: TopicValidatorResult.Ignore, obj: block };
1441
+ }
1442
+
1367
1443
  const validationResult = await this.blockProposalValidator.validate(block);
1368
1444
 
1369
1445
  if (validationResult.result === 'reject') {
@@ -1396,9 +1472,11 @@ export class LibP2PService extends WithTracer implements P2PService {
1396
1472
  return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated, isOversized } };
1397
1473
  }
1398
1474
 
1399
- // Too many blocks received for this slot and index, penalize peer and do not re-broadcast
1475
+ // Local per-position retention cap is full. That is receiver-local state, not evidence the
1476
+ // sender forwarded invalid data (it could not know our cache was full), so drop the extra
1477
+ // payload without penalizing the peer. Do not re-broadcast.
1400
1478
  if (!added) {
1401
- this.logger.warn(`Penalizing peer for block proposal exceeding per-position cap`, {
1479
+ this.logger.debug(`Ignoring block proposal exceeding per-position cap`, {
1402
1480
  ...block.toBlockInfo(),
1403
1481
  indexWithinCheckpoint: block.indexWithinCheckpoint,
1404
1482
  count,
@@ -1406,9 +1484,14 @@ export class LibP2PService extends WithTracer implements P2PService {
1406
1484
  source: peerId.toString(),
1407
1485
  });
1408
1486
  return {
1409
- result: TopicValidatorResult.Reject,
1410
- metadata: { isEquivocated, isOversized },
1411
- severity: PeerErrorSeverity.HighToleranceError,
1487
+ // isEquivocated is false here even when count > 1: a full cache is a receiver-local drop, not
1488
+ // a fresh equivocation by this block. Genuine equivocation is already captured on the add
1489
+ // (duplicateProposalCallback below). capFull tells the checkpoint path, whose terminal block
1490
+ // this may be, to ignore the whole checkpoint too, rather than store and re-broadcast it while
1491
+ // its terminal block was dropped.
1492
+ result: TopicValidatorResult.Ignore,
1493
+ obj: block,
1494
+ metadata: { isEquivocated: false, isOversized, capFull: true },
1412
1495
  };
1413
1496
  }
1414
1497
 
@@ -1558,6 +1641,13 @@ export class LibP2PService extends WithTracer implements P2PService {
1558
1641
  { isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
1559
1642
  >
1560
1643
  > {
1644
+ // Dedup on already-accepted content before validation: an exact signed checkpoint payload already
1645
+ // stored for this slot has been validated, so a re-encoded copy is ignored without re-running it. A
1646
+ // different payload hash at the same slot is not stored, so equivocation still takes the full path.
1647
+ if (await this.mempools.attestationPool.hasCheckpointProposal(checkpoint.slotNumber, checkpoint.getPayloadHash())) {
1648
+ return { result: TopicValidatorResult.Ignore, obj: checkpoint };
1649
+ }
1650
+
1561
1651
  const validationResult = await this.checkpointProposalValidator.validate(checkpoint);
1562
1652
 
1563
1653
  if (validationResult.result === 'reject') {
@@ -1579,14 +1669,14 @@ export class LibP2PService extends WithTracer implements P2PService {
1579
1669
  [Attributes.P2P_ID]: peerId.toString(),
1580
1670
  });
1581
1671
  const blockProposalResult = await this.validateAndStoreBlockProposal(peerId, blockProposal);
1582
- const { obj, metadata: { isEquivocated, isOversized: blockIsOversized } = {} } = blockProposalResult;
1672
+ const { obj, metadata: { isEquivocated, isOversized: blockIsOversized, capFull: blockCapFull } = {} } =
1673
+ blockProposalResult;
1583
1674
  isOversized = blockIsOversized ?? false;
1584
1675
 
1585
- if (blockProposalResult.result === TopicValidatorResult.Reject || !obj || isEquivocated) {
1676
+ if (blockProposalResult.result === TopicValidatorResult.Reject || !obj) {
1586
1677
  this.logger.debug(`Rejecting checkpoint due to invalid last block proposal`, {
1587
1678
  [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
1588
1679
  [Attributes.P2P_ID]: peerId.toString(),
1589
- isEquivocated,
1590
1680
  result: blockProposalResult.result,
1591
1681
  });
1592
1682
  return {
@@ -1594,6 +1684,15 @@ export class LibP2PService extends WithTracer implements P2PService {
1594
1684
  severity:
1595
1685
  'severity' in blockProposalResult ? blockProposalResult.severity : PeerErrorSeverity.MidToleranceError,
1596
1686
  };
1687
+ } else if (blockCapFull || isEquivocated) {
1688
+ // Both cases depend on our local cache, not on anything the relaying peer could check, so do not
1689
+ // penalize it. Genuine equivocation was already reported at the block level. Drop the checkpoint:
1690
+ // no store, no re-broadcast, no processing or attestation.
1691
+ this.logger.debug(`Ignoring checkpoint whose terminal block was a receiver-local drop`, {
1692
+ [Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
1693
+ [Attributes.P2P_ID]: peerId.toString(),
1694
+ });
1695
+ return { result: TopicValidatorResult.Ignore, obj: checkpoint };
1597
1696
  } else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated && !isOversized) {
1598
1697
  // An oversized terminal block is re-broadcast as slashing evidence but never processed.
1599
1698
  processBlock = true;
@@ -1619,19 +1718,19 @@ export class LibP2PService extends WithTracer implements P2PService {
1619
1718
  };
1620
1719
  }
1621
1720
 
1622
- // Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast.
1623
- // Note: We still return the checkpoint obj so the lastBlock can be processed if valid
1721
+ // Local per-slot retention cap is full. That is receiver-local state, not evidence the sender
1722
+ // forwarded invalid data, so drop the extra payload without penalizing the peer. Do not
1723
+ // re-broadcast. We still return the checkpoint obj so the lastBlock can be processed if valid.
1624
1724
  if (!added) {
1625
- this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
1725
+ this.logger.debug(`Ignoring checkpoint proposal exceeding per-slot cap`, {
1626
1726
  ...checkpoint.toCheckpointInfo(),
1627
1727
  count,
1628
1728
  source: peerId.toString(),
1629
1729
  });
1630
1730
  return {
1631
- result: TopicValidatorResult.Reject,
1731
+ result: TopicValidatorResult.Ignore,
1632
1732
  obj: checkpoint,
1633
1733
  metadata: { isEquivocated, processBlock, isOversized },
1634
- severity: PeerErrorSeverity.HighToleranceError,
1635
1734
  };
1636
1735
  }
1637
1736
 
@@ -1805,8 +1904,13 @@ export class LibP2PService extends WithTracer implements P2PService {
1805
1904
  }
1806
1905
  }
1807
1906
 
1808
- private getGasFees(): Promise<GasFees> {
1809
- return this.blockMinFeesProvider.getCurrentMinFees();
1907
+ /** The fees a gossiped transaction is checked against (see {@link computeGossipMinFees}). */
1908
+ protected async getGasFees(): Promise<GossipMinFees> {
1909
+ const [admission, l1Forward] = await Promise.all([
1910
+ this.nextBlockMinFeesProvider.getAdmissionMinFees(),
1911
+ this.nextBlockMinFeesProvider.getL1ForwardMinFees(),
1912
+ ]);
1913
+ return computeGossipMinFees(admission, l1Forward);
1810
1914
  }
1811
1915
 
1812
1916
  /**
@@ -1852,7 +1956,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1852
1956
  protected async createFirstStageMessageValidators(
1853
1957
  currentBlockNumber: BlockNumber,
1854
1958
  nextSlotTimestamp: UInt64,
1855
- ): Promise<Record<string, TransactionValidator>> {
1959
+ ): Promise<Record<string, TransactionValidator<GossipValidationFailure>>> {
1856
1960
  const gasFees = await this.getGasFees();
1857
1961
  const allowedInSetup = [
1858
1962
  ...(await getDefaultAllowedSetupFunctions()),
@@ -1892,10 +1996,10 @@ export class LibP2PService extends WithTracer implements P2PService {
1892
1996
  * @param messageValidators - The message validators to run.
1893
1997
  * @returns The validation outcome.
1894
1998
  */
1895
- private async runValidations(
1999
+ private async runValidations<F extends GossipValidationFailure>(
1896
2000
  tx: Tx,
1897
- messageValidators: Record<string, TransactionValidator>,
1898
- ): Promise<ValidationOutcome> {
2001
+ messageValidators: Record<string, TransactionValidator<F>>,
2002
+ ): Promise<ValidationOutcome<F>> {
1899
2003
  // Gossip validation stays exhaustive within each stage because peer scoring uses the harshest failure severity;
1900
2004
  // failing fast would make the penalty depend on validator order. The stage boundaries themselves fail fast.
1901
2005
  const validationPromises = Object.entries(messageValidators).map(async ([name, { validator, severity }]) => {
@@ -1908,7 +2012,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1908
2012
  const failures = allValidations.filter(x => !x.isValid);
1909
2013
  if (failures.length > 0) {
1910
2014
  // Pick the most severe failure (lowest tolerance = harshest penalty)
1911
- const failed = maxBy(failures, f => PeerErrorSeverityByHarshness.indexOf(f.severity))!;
2015
+ const failed = maxBy(failures, f => GossipValidationFailureByHarshness.indexOf(f.severity))!;
1912
2016
  return {
1913
2017
  allPassed: false,
1914
2018
  failure: {
@@ -923,6 +923,20 @@ export class PeerManager implements PeerManagerInterface {
923
923
  timeoutUntilMs:
924
924
  this.dateProvider.now() + (this.config.peerFailedBanTimeMs ?? DEFAULT_FAILED_PEER_BAN_TIME_MS),
925
925
  });
926
+ // Drop the address-book record written before the dial. Nothing else deletes /peers/
927
+ // rows, so a stream of unreachable discovered peers would otherwise grow the durable
928
+ // peerstore without bound. Keep operator-configured peers (trusted, private, preferred),
929
+ // and keep any peer we currently hold a connection to: an inbound connect or a concurrent
930
+ // rediscovery may have refreshed the record while this dial was failing.
931
+ if (!this.isProtectedPeer(peer.peerId) && this.libP2PNode.getConnections(peer.peerId).length === 0) {
932
+ try {
933
+ await this.libP2PNode.peerStore.delete(peer.peerId);
934
+ } catch (deleteError) {
935
+ this.logger.trace(`Failed to remove dropped peer ${id} from the peer store`, {
936
+ error: inspect(deleteError),
937
+ });
938
+ }
939
+ }
926
940
  }
927
941
  }
928
942
  }
@@ -189,6 +189,9 @@ export class TxProvider implements ITxProvider {
189
189
  const moreTxsFromPool = compactArray(await this.txPool.getTxsByHash([...missingTxHashes].map(TxHash.fromString)));
190
190
 
191
191
  if (moreTxsFromPool.length > 0) {
192
+ for (const tx of moreTxsFromPool) {
193
+ missingTxHashes.delete(tx.getTxHash().toString());
194
+ }
192
195
  this.instrumentation.incTxsFromMempool(moreTxsFromPool.length);
193
196
  this.log.debug(
194
197
  `Retrieved ${moreTxsFromPool.length} txs from pool retry for block proposal (${missingTxHashes.size} pending)`,