@aztec/p2p 4.0.4 → 4.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dest/client/factory.d.ts +1 -1
  2. package/dest/client/factory.d.ts.map +1 -1
  3. package/dest/client/factory.js +2 -1
  4. package/dest/client/p2p_client.d.ts +1 -1
  5. package/dest/client/p2p_client.d.ts.map +1 -1
  6. package/dest/client/p2p_client.js +0 -24
  7. package/dest/config.d.ts +17 -9
  8. package/dest/config.d.ts.map +1 -1
  9. package/dest/config.js +65 -31
  10. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +7 -1
  11. package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
  12. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +1 -1
  13. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
  14. package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +8 -6
  15. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts +2 -2
  16. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.d.ts.map +1 -1
  17. package/dest/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.js +2 -2
  18. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -1
  19. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  20. package/dest/mem_pools/tx_pool_v2/interfaces.js +2 -1
  21. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +13 -4
  22. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  23. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +26 -9
  24. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
  25. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  26. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +2 -1
  27. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +5 -4
  28. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  29. package/dest/msg_validators/proposal_validator/block_proposal_validator.js +10 -2
  30. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +5 -4
  31. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  32. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.js +16 -2
  33. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +12 -9
  34. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  35. package/dest/msg_validators/proposal_validator/proposal_validator.js +46 -44
  36. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts +2 -1
  37. package/dest/msg_validators/tx_validator/allowed_public_setup.d.ts.map +1 -1
  38. package/dest/msg_validators/tx_validator/allowed_public_setup.js +32 -14
  39. package/dest/msg_validators/tx_validator/phases_validator.d.ts +2 -2
  40. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  41. package/dest/msg_validators/tx_validator/phases_validator.js +44 -23
  42. package/dest/services/libp2p/libp2p_service.d.ts +1 -1
  43. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  44. package/dest/services/libp2p/libp2p_service.js +9 -8
  45. package/dest/testbench/p2p_client_testbench_worker.js +2 -1
  46. package/dest/testbench/worker_client_manager.d.ts +3 -1
  47. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  48. package/dest/testbench/worker_client_manager.js +4 -1
  49. package/package.json +14 -14
  50. package/src/client/factory.ts +1 -0
  51. package/src/client/p2p_client.ts +0 -22
  52. package/src/config.ts +89 -32
  53. package/src/mem_pools/tx_pool_v2/README.md +9 -1
  54. package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +11 -1
  55. package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +15 -6
  56. package/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.ts +2 -1
  57. package/src/mem_pools/tx_pool_v2/interfaces.ts +3 -0
  58. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +37 -7
  59. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +3 -1
  60. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +13 -3
  61. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +19 -6
  62. package/src/msg_validators/proposal_validator/proposal_validator.ts +57 -48
  63. package/src/msg_validators/tx_validator/allowed_public_setup.ts +38 -18
  64. package/src/msg_validators/tx_validator/phases_validator.ts +51 -26
  65. package/src/services/libp2p/libp2p_service.ts +9 -8
  66. package/src/testbench/p2p_client_testbench_worker.ts +1 -0
  67. package/src/testbench/worker_client_manager.ts +11 -4
  68. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts +0 -24
  69. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.d.ts.map +0 -1
  70. package/dest/msg_validators/proposal_validator/proposal_validator_test_suite.js +0 -378
  71. package/src/msg_validators/proposal_validator/proposal_validator_test_suite.ts +0 -373
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
 
3
- import { type TxMetaData, comparePriority } from '../tx_metadata.js';
3
+ import { type TxMetaData, comparePriority, getMinimumPriceBumpFee } from '../tx_metadata.js';
4
4
  import {
5
5
  type EvictionConfig,
6
6
  type PreAddContext,
@@ -48,10 +48,14 @@ export class LowPriorityPreAddRule implements PreAddRule {
48
48
  }
49
49
 
50
50
  // Compare incoming tx against lowest priority tx.
51
- // feeOnly mode (RPC): use strict fee comparison only — avoids churn from hash ordering
52
- // Default (gossip): use full comparePriority (fee + tx hash tiebreaker) for determinism
51
+ // feeOnly mode (RPC): use strict fee comparison only — avoids churn from hash ordering.
52
+ // When price bump is also set, require the bumped fee threshold.
53
+ // Default (gossip): use full comparePriority (fee + tx hash tiebreaker) for determinism.
53
54
  const isHigherPriority = context?.feeComparisonOnly
54
- ? incomingMeta.priorityFee > lowestPriorityMeta.priorityFee
55
+ ? context.priceBumpPercentage !== undefined
56
+ ? incomingMeta.priorityFee >=
57
+ getMinimumPriceBumpFee(lowestPriorityMeta.priorityFee, context.priceBumpPercentage)
58
+ : incomingMeta.priorityFee > lowestPriorityMeta.priorityFee
55
59
  : comparePriority(incomingMeta, lowestPriorityMeta) > 0;
56
60
 
57
61
  if (isHigherPriority) {
@@ -66,6 +70,11 @@ export class LowPriorityPreAddRule implements PreAddRule {
66
70
  }
67
71
 
68
72
  // Incoming tx has equal or lower priority - ignore it (it would be evicted anyway)
73
+ const minimumFee =
74
+ context?.feeComparisonOnly && context.priceBumpPercentage !== undefined
75
+ ? getMinimumPriceBumpFee(lowestPriorityMeta.priorityFee, context.priceBumpPercentage)
76
+ : lowestPriorityMeta.priorityFee + 1n;
77
+
69
78
  this.log.debug(
70
79
  `Pool at capacity (${currentCount}/${this.maxPoolSize}), ignoring ${incomingMeta.txHash} ` +
71
80
  `(priority ${incomingMeta.priorityFee}) - lower than existing minimum (priority ${lowestPriorityMeta.priorityFee})`,
@@ -75,8 +84,8 @@ export class LowPriorityPreAddRule implements PreAddRule {
75
84
  txHashesToEvict: [],
76
85
  reason: {
77
86
  code: TxPoolRejectionCode.LOW_PRIORITY_FEE,
78
- message: `Tx does not meet minimum priority fee. Required: ${lowestPriorityMeta.priorityFee + 1n}, got: ${incomingMeta.priorityFee}`,
79
- minimumPriorityFee: lowestPriorityMeta.priorityFee + 1n,
87
+ message: `Tx does not meet minimum priority fee. Required: ${minimumFee}, got: ${incomingMeta.priorityFee}`,
88
+ minimumPriorityFee: minimumFee,
80
89
  txPriorityFee: incomingMeta.priorityFee,
81
90
  },
82
91
  });
@@ -15,11 +15,12 @@ export class NullifierConflictRule implements PreAddRule {
15
15
 
16
16
  private log = createLogger('p2p:tx_pool_v2:nullifier_conflict_rule');
17
17
 
18
- check(incomingMeta: TxMetaData, poolAccess: PreAddPoolAccess, _context?: PreAddContext): Promise<PreAddResult> {
18
+ check(incomingMeta: TxMetaData, poolAccess: PreAddPoolAccess, context?: PreAddContext): Promise<PreAddResult> {
19
19
  const result = checkNullifierConflict(
20
20
  incomingMeta,
21
21
  nullifier => poolAccess.getTxHashByNullifier(nullifier),
22
22
  txHash => poolAccess.getMetadata(txHash),
23
+ context?.priceBumpPercentage,
23
24
  );
24
25
 
25
26
  if (result.shouldIgnore) {
@@ -44,6 +44,8 @@ export type TxPoolV2Config = {
44
44
  minTxPoolAgeMs: number;
45
45
  /** Maximum number of evicted tx hashes to remember for metrics tracking */
46
46
  evictedTxCacheSize: number;
47
+ /** Minimum percentage fee increase required to replace an existing tx via RPC (0 = no bump). */
48
+ priceBumpPercentage: bigint;
47
49
  };
48
50
 
49
51
  /**
@@ -54,6 +56,7 @@ export const DEFAULT_TX_POOL_V2_CONFIG: TxPoolV2Config = {
54
56
  archivedTxLimit: 0, // 0 = disabled
55
57
  minTxPoolAgeMs: 2_000,
56
58
  evictedTxCacheSize: 10_000,
59
+ priceBumpPercentage: 10n,
57
60
  };
58
61
 
59
62
  /**
@@ -190,21 +190,38 @@ export function comparePriority(a: PriorityComparable, b: PriorityComparable): n
190
190
  return compareTxHash(a.txHashBigInt, b.txHashBigInt);
191
191
  }
192
192
 
193
+ /**
194
+ * Returns the minimum fee required to replace an existing tx with the given price bump percentage.
195
+ * Uses integer arithmetic: `existingFee + existingFee * priceBumpPercentage / 100`.
196
+ */
197
+ export function getMinimumPriceBumpFee(existingFee: bigint, priceBumpPercentage: bigint): bigint {
198
+ const bump = (existingFee * priceBumpPercentage) / 100n;
199
+ // Ensure the minimum bump is at least 1, so that replacement always requires
200
+ // paying strictly more — even with 0% bump or zero existing fee.
201
+ const effectiveBump = bump > 0n ? bump : 1n;
202
+ return existingFee + effectiveBump;
203
+ }
204
+
193
205
  /**
194
206
  * Checks for nullifier conflicts between an incoming transaction and existing pool state.
195
207
  *
196
208
  * When the incoming tx shares nullifiers with existing pending txs:
197
- * - If the incoming tx has strictly higher priority, mark conflicting txs for eviction
198
- * - If any conflicting tx has equal or higher priority, ignore the incoming tx
209
+ * - If the incoming tx meets or exceeds the required priority, mark conflicting txs for eviction
210
+ * - Otherwise, ignore the incoming tx
211
+ *
212
+ * When `priceBumpPercentage` is provided (RPC path), uses fee-only comparison with the
213
+ * percentage bump instead of `comparePriority`.
199
214
  *
200
215
  * @param incomingMeta - Metadata for the incoming transaction
201
216
  * @param getTxHashByNullifier - Accessor to find which tx uses a nullifier
202
217
  * @param getMetadata - Accessor to get metadata for a tx hash
218
+ * @param priceBumpPercentage - Optional percentage bump required for fee-based replacement
203
219
  */
204
220
  export function checkNullifierConflict(
205
221
  incomingMeta: TxMetaData,
206
222
  getTxHashByNullifier: (nullifier: string) => string | undefined,
207
223
  getMetadata: (txHash: string) => TxMetaData | undefined,
224
+ priceBumpPercentage?: bigint,
208
225
  ): PreAddResult {
209
226
  const txHashesToEvict: string[] = [];
210
227
 
@@ -225,19 +242,32 @@ export function checkNullifierConflict(
225
242
  continue;
226
243
  }
227
244
 
228
- // If incoming tx has strictly higher priority, mark for eviction
229
- // Otherwise, ignore incoming tx (ties go to existing tx)
230
- // Use comparePriority for deterministic ordering (includes txHash as tiebreaker)
231
- if (comparePriority(incomingMeta, conflictingMeta) > 0) {
245
+ // When price bump is set (RPC path), require the incoming fee to meet the bumped threshold.
246
+ // Otherwise (P2P path), use full comparePriority with tx hash tiebreaker.
247
+ const isHigherPriority =
248
+ priceBumpPercentage !== undefined
249
+ ? incomingMeta.priorityFee >= getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
250
+ : comparePriority(incomingMeta, conflictingMeta) > 0;
251
+
252
+ if (isHigherPriority) {
232
253
  txHashesToEvict.push(conflictingHashStr);
233
254
  } else {
255
+ const minimumFee =
256
+ priceBumpPercentage !== undefined
257
+ ? getMinimumPriceBumpFee(conflictingMeta.priorityFee, priceBumpPercentage)
258
+ : undefined;
234
259
  return {
235
260
  shouldIgnore: true,
236
261
  txHashesToEvict: [],
237
262
  reason: {
238
263
  code: TxPoolRejectionCode.NULLIFIER_CONFLICT,
239
- message: `Nullifier conflict with existing tx ${conflictingHashStr}`,
264
+ message:
265
+ minimumFee !== undefined
266
+ ? `Nullifier conflict with existing tx ${conflictingHashStr}. Minimum required fee: ${minimumFee}, got: ${incomingMeta.priorityFee}`
267
+ : `Nullifier conflict with existing tx ${conflictingHashStr}`,
240
268
  conflictingTxHash: conflictingHashStr,
269
+ minimumPriceBumpFee: minimumFee,
270
+ txPriorityFee: minimumFee !== undefined ? incomingMeta.priorityFee : undefined,
241
271
  },
242
272
  };
243
273
  }
@@ -213,7 +213,9 @@ export class TxPoolV2Impl {
213
213
  // in-memory reads, and buffered DB writes. Nothing here can throw an unhandled exception.
214
214
  const poolAccess = this.#createPreAddPoolAccess();
215
215
  const preAddContext: PreAddContext | undefined =
216
- opts.feeComparisonOnly !== undefined ? { feeComparisonOnly: opts.feeComparisonOnly } : undefined;
216
+ opts.feeComparisonOnly !== undefined
217
+ ? { feeComparisonOnly: opts.feeComparisonOnly, priceBumpPercentage: this.#config.priceBumpPercentage }
218
+ : undefined;
217
219
 
218
220
  await this.#store.transactionAsync(async () => {
219
221
  for (const tx of txs) {
@@ -1,10 +1,20 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
- import type { BlockProposal, P2PValidator } from '@aztec/stdlib/p2p';
2
+ import type { BlockProposal, P2PValidator, ValidationResult } from '@aztec/stdlib/p2p';
3
3
 
4
4
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
5
 
6
- export class BlockProposalValidator extends ProposalValidator<BlockProposal> implements P2PValidator<BlockProposal> {
6
+ export class BlockProposalValidator implements P2PValidator<BlockProposal> {
7
+ private proposalValidator: ProposalValidator;
8
+
7
9
  constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
8
- super(epochCache, opts, 'p2p:block_proposal_validator');
10
+ this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:block_proposal_validator');
11
+ }
12
+
13
+ async validate(proposal: BlockProposal): Promise<ValidationResult> {
14
+ const headerResult = await this.proposalValidator.validate(proposal);
15
+ if (headerResult.result !== 'accept') {
16
+ return headerResult;
17
+ }
18
+ return this.proposalValidator.validateTxs(proposal);
9
19
  }
10
20
  }
@@ -1,13 +1,26 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
- import type { CheckpointProposal, P2PValidator } from '@aztec/stdlib/p2p';
2
+ import type { CheckpointProposal, P2PValidator, ValidationResult } from '@aztec/stdlib/p2p';
3
3
 
4
4
  import { ProposalValidator } from '../proposal_validator/proposal_validator.js';
5
5
 
6
- export class CheckpointProposalValidator
7
- extends ProposalValidator<CheckpointProposal>
8
- implements P2PValidator<CheckpointProposal>
9
- {
6
+ export class CheckpointProposalValidator implements P2PValidator<CheckpointProposal> {
7
+ private proposalValidator: ProposalValidator;
8
+
10
9
  constructor(epochCache: EpochCacheInterface, opts: { txsPermitted: boolean; maxTxsPerBlock?: number }) {
11
- super(epochCache, opts, 'p2p:checkpoint_proposal_validator');
10
+ this.proposalValidator = new ProposalValidator(epochCache, opts, 'p2p:checkpoint_proposal_validator');
11
+ }
12
+
13
+ async validate(proposal: CheckpointProposal): Promise<ValidationResult> {
14
+ const headerResult = await this.proposalValidator.validate(proposal);
15
+ if (headerResult.result !== 'accept') {
16
+ return headerResult;
17
+ }
18
+
19
+ const blockProposal = proposal.getBlockProposal();
20
+ if (blockProposal) {
21
+ return this.proposalValidator.validateTxs(blockProposal);
22
+ }
23
+
24
+ return { result: 'accept' };
12
25
  }
13
26
  }
@@ -1,15 +1,21 @@
1
1
  import type { EpochCacheInterface } from '@aztec/epoch-cache';
2
2
  import { NoCommitteeError } from '@aztec/ethereum/contracts';
3
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
- import { BlockProposal, CheckpointProposal, PeerErrorSeverity, type ValidationResult } from '@aztec/stdlib/p2p';
4
+ import {
5
+ type BlockProposal,
6
+ type CheckpointProposalCore,
7
+ PeerErrorSeverity,
8
+ type ValidationResult,
9
+ } from '@aztec/stdlib/p2p';
5
10
 
6
11
  import { isWithinClockTolerance } from '../clock_tolerance.js';
7
12
 
8
- export abstract class ProposalValidator<TProposal extends BlockProposal | CheckpointProposal> {
9
- protected epochCache: EpochCacheInterface;
10
- protected logger: Logger;
11
- protected txsPermitted: boolean;
12
- protected maxTxsPerBlock?: number;
13
+ /** Validates header-level and tx-level fields of block and checkpoint proposals. */
14
+ export class ProposalValidator {
15
+ private epochCache: EpochCacheInterface;
16
+ private logger: Logger;
17
+ private txsPermitted: boolean;
18
+ private maxTxsPerBlock?: number;
13
19
 
14
20
  constructor(
15
21
  epochCache: EpochCacheInterface,
@@ -22,7 +28,8 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
22
28
  this.logger = createLogger(loggerName);
23
29
  }
24
30
 
25
- public async validate(proposal: TProposal): Promise<ValidationResult> {
31
+ /** Validates header-level fields: slot, signature, and proposer. */
32
+ public async validate(proposal: BlockProposal | CheckpointProposalCore): Promise<ValidationResult> {
26
33
  try {
27
34
  // Slot check
28
35
  const { currentSlot, nextSlot } = this.epochCache.getCurrentAndNextSlot();
@@ -44,38 +51,6 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
44
51
  return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
45
52
  }
46
53
 
47
- // Transactions permitted check
48
- const embeddedTxCount = proposal.txs?.length ?? 0;
49
- if (!this.txsPermitted && (proposal.txHashes.length > 0 || embeddedTxCount > 0)) {
50
- this.logger.warn(
51
- `Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when transactions are not permitted`,
52
- );
53
- return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
54
- }
55
-
56
- // Max txs per block check
57
- if (this.maxTxsPerBlock !== undefined && proposal.txHashes.length > this.maxTxsPerBlock) {
58
- this.logger.warn(
59
- `Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when max is ${this.maxTxsPerBlock}`,
60
- );
61
- return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
62
- }
63
-
64
- // Embedded txs must be listed in txHashes
65
- const hashSet = new Set(proposal.txHashes.map(h => h.toString()));
66
- const missingTxHashes =
67
- embeddedTxCount > 0
68
- ? proposal.txs!.filter(tx => !hashSet.has(tx.getTxHash().toString())).map(tx => tx.getTxHash().toString())
69
- : [];
70
- if (embeddedTxCount > 0 && missingTxHashes.length > 0) {
71
- this.logger.warn('Penalizing peer for embedded transaction(s) not included in txHashes', {
72
- embeddedTxCount,
73
- txHashesLength: proposal.txHashes.length,
74
- missingTxHashes,
75
- });
76
- return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
77
- }
78
-
79
54
  // Proposer check
80
55
  const expectedProposer = await this.epochCache.getProposerAttesterAddressInSlot(slotNumber);
81
56
  if (expectedProposer !== undefined && !proposer.equals(expectedProposer)) {
@@ -86,15 +61,6 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
86
61
  return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
87
62
  }
88
63
 
89
- // Validate tx hashes for all txs embedded in the proposal
90
- if (!(await Promise.all(proposal.txs?.map(tx => tx.validateTxHash()) ?? [])).every(v => v)) {
91
- this.logger.warn(`Penalizing peer for invalid tx hashes in proposal`, {
92
- proposer,
93
- slotNumber,
94
- });
95
- return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
96
- }
97
-
98
64
  return { result: 'accept' };
99
65
  } catch (e) {
100
66
  if (e instanceof NoCommitteeError) {
@@ -103,4 +69,47 @@ export abstract class ProposalValidator<TProposal extends BlockProposal | Checkp
103
69
  throw e;
104
70
  }
105
71
  }
72
+
73
+ /** Validates transaction-related fields of a block proposal. */
74
+ public async validateTxs(proposal: BlockProposal): Promise<ValidationResult> {
75
+ // Transactions permitted check
76
+ const embeddedTxCount = proposal.txs?.length ?? 0;
77
+ if (!this.txsPermitted && (proposal.txHashes.length > 0 || embeddedTxCount > 0)) {
78
+ this.logger.warn(
79
+ `Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when transactions are not permitted`,
80
+ );
81
+ return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
82
+ }
83
+
84
+ // Max txs per block check
85
+ if (this.maxTxsPerBlock !== undefined && proposal.txHashes.length > this.maxTxsPerBlock) {
86
+ this.logger.warn(
87
+ `Penalizing peer for proposal with ${proposal.txHashes.length} transaction(s) when max is ${this.maxTxsPerBlock}`,
88
+ );
89
+ return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
90
+ }
91
+
92
+ // Embedded txs must be listed in txHashes
93
+ const hashSet = new Set(proposal.txHashes.map(h => h.toString()));
94
+ const missingTxHashes =
95
+ embeddedTxCount > 0
96
+ ? proposal.txs!.filter(tx => !hashSet.has(tx.getTxHash().toString())).map(tx => tx.getTxHash().toString())
97
+ : [];
98
+ if (embeddedTxCount > 0 && missingTxHashes.length > 0) {
99
+ this.logger.warn('Penalizing peer for embedded transaction(s) not included in txHashes', {
100
+ embeddedTxCount,
101
+ txHashesLength: proposal.txHashes.length,
102
+ missingTxHashes,
103
+ });
104
+ return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
105
+ }
106
+
107
+ // Validate tx hashes for all txs embedded in the proposal
108
+ if (!(await Promise.all(proposal.txs?.map(tx => tx.validateTxHash()) ?? [])).every(v => v)) {
109
+ this.logger.warn(`Penalizing peer for invalid tx hashes in proposal`);
110
+ return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
111
+ }
112
+
113
+ return { result: 'accept' };
114
+ }
106
115
  }
@@ -1,33 +1,53 @@
1
- import { FPCContract } from '@aztec/noir-contracts.js/FPC';
2
- import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token';
3
1
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
4
- import { getContractClassFromArtifact } from '@aztec/stdlib/contract';
2
+ import { AuthRegistryArtifact } from '@aztec/protocol-contracts/auth-registry';
3
+ import { FeeJuiceArtifact } from '@aztec/protocol-contracts/fee-juice';
4
+ import { FunctionSelector, countArgumentsSize } from '@aztec/stdlib/abi';
5
+ import type { ContractArtifact, FunctionAbi } from '@aztec/stdlib/abi';
5
6
  import type { AllowedElement } from '@aztec/stdlib/interfaces/server';
6
7
 
7
- let defaultAllowedSetupFunctions: AllowedElement[] | undefined = undefined;
8
+ /** Returns the expected calldata length for a function: 1 (selector) + arguments size. */
9
+ function getCalldataLength(artifact: ContractArtifact, functionName: string): number {
10
+ const allFunctions: FunctionAbi[] = (artifact.functions as FunctionAbi[]).concat(
11
+ artifact.nonDispatchPublicFunctions || [],
12
+ );
13
+ const fn = allFunctions.find(f => f.name === functionName);
14
+ if (!fn) {
15
+ throw new Error(`Unknown function ${functionName} in artifact ${artifact.name}`);
16
+ }
17
+ return 1 + countArgumentsSize(fn);
18
+ }
19
+
20
+ let defaultAllowedSetupFunctions: AllowedElement[] | undefined;
21
+
22
+ /** Returns the default list of functions allowed to run in the setup phase of a transaction. */
8
23
  export async function getDefaultAllowedSetupFunctions(): Promise<AllowedElement[]> {
9
24
  if (defaultAllowedSetupFunctions === undefined) {
25
+ const setAuthorizedInternalSelector = await FunctionSelector.fromSignature('_set_authorized((Field),Field,bool)');
26
+ const setAuthorizedSelector = await FunctionSelector.fromSignature('set_authorized(Field,bool)');
27
+ const increaseBalanceSelector = await FunctionSelector.fromSignature('_increase_public_balance((Field),u128)');
28
+
10
29
  defaultAllowedSetupFunctions = [
11
- // needed for authwit support
30
+ // AuthRegistry: needed for authwit support via private path (set_authorized_private enqueues _set_authorized)
12
31
  {
13
32
  address: ProtocolContractAddress.AuthRegistry,
33
+ selector: setAuthorizedInternalSelector,
34
+ calldataLength: getCalldataLength(AuthRegistryArtifact, '_set_authorized'),
35
+ onlySelf: true,
36
+ rejectNullMsgSender: true,
14
37
  },
15
- // needed for claiming on the same tx as a spend
38
+ // AuthRegistry: needed for authwit support via public path (PublicFeePaymentMethod calls set_authorized directly)
16
39
  {
17
- address: ProtocolContractAddress.FeeJuice,
18
- // We can't restrict the selector because public functions get routed via dispatch.
19
- // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),u128)'),
20
- },
21
- // needed for private transfers via FPC
22
- {
23
- classId: (await getContractClassFromArtifact(TokenContractArtifact)).id,
24
- // We can't restrict the selector because public functions get routed via dispatch.
25
- // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),u128)'),
40
+ address: ProtocolContractAddress.AuthRegistry,
41
+ selector: setAuthorizedSelector,
42
+ calldataLength: getCalldataLength(AuthRegistryArtifact, 'set_authorized'),
43
+ rejectNullMsgSender: true,
26
44
  },
45
+ // FeeJuice: needed for claiming on the same tx as a spend (claim_and_end_setup enqueues this)
27
46
  {
28
- classId: (await getContractClassFromArtifact(FPCContract.artifact)).id,
29
- // We can't restrict the selector because public functions get routed via dispatch.
30
- // selector: FunctionSelector.fromSignature('prepare_fee((Field),Field,(Field),Field)'),
47
+ address: ProtocolContractAddress.FeeJuice,
48
+ selector: increaseBalanceSelector,
49
+ calldataLength: getCalldataLength(FeeJuiceArtifact, '_increase_public_balance'),
50
+ onlySelf: true,
31
51
  },
32
52
  ];
33
53
  }
@@ -1,11 +1,17 @@
1
+ import { NULL_MSG_SENDER_CONTRACT_ADDRESS } from '@aztec/constants';
1
2
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
2
3
  import { PublicContractsDB, getCallRequestsWithCalldataByPhase } from '@aztec/simulator/server';
4
+ import { AztecAddress } from '@aztec/stdlib/aztec-address';
3
5
  import type { ContractDataSource } from '@aztec/stdlib/contract';
4
6
  import type { AllowedElement } from '@aztec/stdlib/interfaces/server';
5
7
  import {
6
8
  type PublicCallRequestWithCalldata,
7
9
  TX_ERROR_DURING_VALIDATION,
8
10
  TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED,
11
+ TX_ERROR_SETUP_FUNCTION_UNKNOWN_CONTRACT,
12
+ TX_ERROR_SETUP_NULL_MSG_SENDER,
13
+ TX_ERROR_SETUP_ONLY_SELF_WRONG_SENDER,
14
+ TX_ERROR_SETUP_WRONG_CALLDATA_LENGTH,
9
15
  Tx,
10
16
  TxExecutionPhase,
11
17
  type TxValidationResult,
@@ -45,7 +51,8 @@ export class PhasesTxValidator implements TxValidator<Tx> {
45
51
 
46
52
  const setupFns = getCallRequestsWithCalldataByPhase(tx, TxExecutionPhase.SETUP);
47
53
  for (const setupFn of setupFns) {
48
- if (!(await this.isOnAllowList(setupFn, this.setupAllowList))) {
54
+ const rejectionReason = await this.checkAllowList(setupFn, this.setupAllowList);
55
+ if (rejectionReason) {
49
56
  this.#log.verbose(
50
57
  `Rejecting tx ${tx.getTxHash().toString()} because it calls setup function not on allow list: ${
51
58
  setupFn.request.contractAddress
@@ -53,7 +60,7 @@ export class PhasesTxValidator implements TxValidator<Tx> {
53
60
  { allowList: this.setupAllowList },
54
61
  );
55
62
 
56
- return { result: 'invalid', reason: [TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED] };
63
+ return { result: 'invalid', reason: [rejectionReason] };
57
64
  }
58
65
  }
59
66
 
@@ -66,53 +73,71 @@ export class PhasesTxValidator implements TxValidator<Tx> {
66
73
  }
67
74
  }
68
75
 
69
- private async isOnAllowList(
76
+ /** Returns a rejection reason if the call is not on the allow list, or undefined if it is allowed. */
77
+ private async checkAllowList(
70
78
  publicCall: PublicCallRequestWithCalldata,
71
79
  allowList: AllowedElement[],
72
- ): Promise<boolean> {
80
+ ): Promise<string | undefined> {
73
81
  if (publicCall.isEmpty()) {
74
- return true;
82
+ return undefined;
75
83
  }
76
84
 
77
85
  const contractAddress = publicCall.request.contractAddress;
78
86
  const functionSelector = publicCall.functionSelector;
79
87
 
80
- // do these checks first since they don't require the contract class
88
+ // Check address-based entries first since they don't require the contract class.
81
89
  for (const entry of allowList) {
82
- if ('address' in entry && !('selector' in entry)) {
83
- if (contractAddress.equals(entry.address)) {
84
- return true;
85
- }
86
- }
87
-
88
- if ('address' in entry && 'selector' in entry) {
90
+ if ('address' in entry) {
89
91
  if (contractAddress.equals(entry.address) && entry.selector.equals(functionSelector)) {
90
- return true;
92
+ if (entry.calldataLength !== undefined && publicCall.calldata.length !== entry.calldataLength) {
93
+ return TX_ERROR_SETUP_WRONG_CALLDATA_LENGTH;
94
+ }
95
+ if (entry.onlySelf && !publicCall.request.msgSender.equals(contractAddress)) {
96
+ return TX_ERROR_SETUP_ONLY_SELF_WRONG_SENDER;
97
+ }
98
+ if (
99
+ entry.rejectNullMsgSender &&
100
+ publicCall.request.msgSender.equals(AztecAddress.fromBigInt(NULL_MSG_SENDER_CONTRACT_ADDRESS))
101
+ ) {
102
+ return TX_ERROR_SETUP_NULL_MSG_SENDER;
103
+ }
104
+ return undefined;
91
105
  }
92
106
  }
107
+ }
93
108
 
94
- const contractClass = await this.contractsDB.getContractInstance(contractAddress, this.timestamp);
95
-
96
- if (!contractClass) {
97
- throw new Error(`Contract not found: ${contractAddress}`);
109
+ // Check class-based entries. Fetch the contract instance lazily (only once).
110
+ let contractClassId: undefined | { value: string | undefined };
111
+ for (const entry of allowList) {
112
+ if (!('classId' in entry)) {
113
+ continue;
98
114
  }
99
115
 
100
- if ('classId' in entry && !('selector' in entry)) {
101
- if (contractClass.currentContractClassId.equals(entry.classId)) {
102
- return true;
116
+ if (contractClassId === undefined) {
117
+ const instance = await this.contractsDB.getContractInstance(contractAddress, this.timestamp);
118
+ contractClassId = { value: instance?.currentContractClassId.toString() };
119
+ if (!contractClassId.value) {
120
+ return TX_ERROR_SETUP_FUNCTION_UNKNOWN_CONTRACT;
103
121
  }
104
122
  }
105
123
 
106
- if ('classId' in entry && 'selector' in entry) {
124
+ if (contractClassId.value === entry.classId.toString() && entry.selector.equals(functionSelector)) {
125
+ if (entry.calldataLength !== undefined && publicCall.calldata.length !== entry.calldataLength) {
126
+ return TX_ERROR_SETUP_WRONG_CALLDATA_LENGTH;
127
+ }
128
+ if (entry.onlySelf && !publicCall.request.msgSender.equals(contractAddress)) {
129
+ return TX_ERROR_SETUP_ONLY_SELF_WRONG_SENDER;
130
+ }
107
131
  if (
108
- contractClass.currentContractClassId.equals(entry.classId) &&
109
- (entry.selector === undefined || entry.selector.equals(functionSelector))
132
+ entry.rejectNullMsgSender &&
133
+ publicCall.request.msgSender.equals(AztecAddress.fromBigInt(NULL_MSG_SENDER_CONTRACT_ADDRESS))
110
134
  ) {
111
- return true;
135
+ return TX_ERROR_SETUP_NULL_MSG_SENDER;
112
136
  }
137
+ return undefined;
113
138
  }
114
139
  }
115
140
 
116
- return false;
141
+ return TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED;
117
142
  }
118
143
  }
@@ -222,14 +222,12 @@ export class LibP2PService extends WithTracer implements P2PService {
222
222
  this.protocolVersion,
223
223
  );
224
224
 
225
- this.blockProposalValidator = new BlockProposalValidator(epochCache, {
225
+ const proposalValidatorOpts = {
226
226
  txsPermitted: !config.disableTransactions,
227
- maxTxsPerBlock: config.maxTxsPerBlock,
228
- });
229
- this.checkpointProposalValidator = new CheckpointProposalValidator(epochCache, {
230
- txsPermitted: !config.disableTransactions,
231
- maxTxsPerBlock: config.maxTxsPerBlock,
232
- });
227
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
228
+ };
229
+ this.blockProposalValidator = new BlockProposalValidator(epochCache, proposalValidatorOpts);
230
+ this.checkpointProposalValidator = new CheckpointProposalValidator(epochCache, proposalValidatorOpts);
233
231
  this.checkpointAttestationValidator = config.fishermanMode
234
232
  ? new FishermanAttestationValidator(epochCache, mempools.attestationPool, telemetry)
235
233
  : new CheckpointAttestationValidator(epochCache);
@@ -1621,7 +1619,10 @@ export class LibP2PService extends WithTracer implements P2PService {
1621
1619
  nextSlotTimestamp: UInt64,
1622
1620
  ): Promise<Record<string, TransactionValidator>> {
1623
1621
  const gasFees = await this.getGasFees(currentBlockNumber);
1624
- const allowedInSetup = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
1622
+ const allowedInSetup = [
1623
+ ...(await getDefaultAllowedSetupFunctions()),
1624
+ ...(this.config.txPublicSetupAllowListExtend ?? []),
1625
+ ];
1625
1626
  const blockNumber = BlockNumber(currentBlockNumber + 1);
1626
1627
 
1627
1628
  return createFirstStageTxValidationsForGossipedTransactions(
@@ -340,6 +340,7 @@ process.on('message', async msg => {
340
340
  const config: P2PConfig = {
341
341
  ...rawConfig,
342
342
  peerIdPrivateKey: rawConfig.peerIdPrivateKey ? new SecretValue(rawConfig.peerIdPrivateKey) : undefined,
343
+ priceBumpPercentage: 10n,
343
344
  } as P2PConfig;
344
345
 
345
346
  workerConfig = config;