@aztec/p2p 0.0.1-commit.3895657bc → 0.0.1-commit.3e3d0c9cd

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 (86) hide show
  1. package/README.md +129 -3
  2. package/dest/client/factory.d.ts +1 -1
  3. package/dest/client/factory.d.ts.map +1 -1
  4. package/dest/client/factory.js +19 -7
  5. package/dest/client/p2p_client.d.ts +1 -1
  6. package/dest/client/p2p_client.d.ts.map +1 -1
  7. package/dest/client/p2p_client.js +16 -6
  8. package/dest/config.d.ts +7 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +10 -0
  11. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  12. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  13. package/dest/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.js +2 -1
  14. package/dest/mem_pools/tx_pool/priority.d.ts +2 -2
  15. package/dest/mem_pools/tx_pool/priority.d.ts.map +1 -1
  16. package/dest/mem_pools/tx_pool/priority.js +4 -4
  17. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts +1 -1
  18. package/dest/mem_pools/tx_pool/tx_pool_test_suite.d.ts.map +1 -1
  19. package/dest/mem_pools/tx_pool/tx_pool_test_suite.js +3 -1
  20. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts +1 -1
  21. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.d.ts.map +1 -1
  22. package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +2 -1
  23. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +3 -1
  24. package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
  25. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts +9 -2
  26. package/dest/mem_pools/tx_pool_v2/tx_metadata.d.ts.map +1 -1
  27. package/dest/mem_pools/tx_pool_v2/tx_metadata.js +7 -1
  28. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts +4 -2
  29. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.d.ts.map +1 -1
  30. package/dest/mem_pools/tx_pool_v2/tx_pool_v2.js +3 -0
  31. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
  32. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  33. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +19 -5
  34. package/dest/msg_validators/tx_validator/factory.d.ts +23 -4
  35. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  36. package/dest/msg_validators/tx_validator/factory.js +28 -8
  37. package/dest/msg_validators/tx_validator/gas_validator.d.ts +13 -4
  38. package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
  39. package/dest/msg_validators/tx_validator/gas_validator.js +39 -9
  40. package/dest/msg_validators/tx_validator/phases_validator.d.ts +21 -1
  41. package/dest/msg_validators/tx_validator/phases_validator.d.ts.map +1 -1
  42. package/dest/msg_validators/tx_validator/phases_validator.js +27 -0
  43. package/dest/services/libp2p/libp2p_service.d.ts +1 -1
  44. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  45. package/dest/services/libp2p/libp2p_service.js +30 -6
  46. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +8 -2
  47. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  48. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +26 -9
  49. package/dest/services/reqresp/batch-tx-requester/interface.d.ts +3 -1
  50. package/dest/services/reqresp/batch-tx-requester/interface.d.ts.map +1 -1
  51. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts +2 -1
  52. package/dest/services/reqresp/batch-tx-requester/missing_txs.d.ts.map +1 -1
  53. package/dest/services/reqresp/batch-tx-requester/missing_txs.js +6 -0
  54. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts +3 -1
  55. package/dest/services/reqresp/batch-tx-requester/peer_collection.d.ts.map +1 -1
  56. package/dest/services/reqresp/batch-tx-requester/peer_collection.js +3 -0
  57. package/dest/services/reqresp/reqresp.js +1 -1
  58. package/dest/test-helpers/testbench-utils.d.ts +1 -1
  59. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  60. package/dest/test-helpers/testbench-utils.js +2 -1
  61. package/package.json +14 -14
  62. package/src/client/factory.ts +34 -11
  63. package/src/client/p2p_client.ts +16 -8
  64. package/src/config.ts +16 -0
  65. package/src/mem_pools/tx_pool/eviction/fee_payer_balance_eviction_rule.ts +2 -1
  66. package/src/mem_pools/tx_pool/priority.ts +4 -4
  67. package/src/mem_pools/tx_pool/tx_pool_test_suite.ts +3 -1
  68. package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +2 -1
  69. package/src/mem_pools/tx_pool_v2/interfaces.ts +2 -0
  70. package/src/mem_pools/tx_pool_v2/tx_metadata.ts +11 -1
  71. package/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +13 -1
  72. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +20 -5
  73. package/src/msg_validators/attestation_validator/README.md +49 -0
  74. package/src/msg_validators/proposal_validator/README.md +123 -0
  75. package/src/msg_validators/tx_validator/README.md +5 -1
  76. package/src/msg_validators/tx_validator/factory.ts +36 -3
  77. package/src/msg_validators/tx_validator/gas_validator.ts +41 -8
  78. package/src/msg_validators/tx_validator/phases_validator.ts +30 -0
  79. package/src/services/libp2p/libp2p_service.ts +28 -6
  80. package/src/services/reqresp/README.md +229 -0
  81. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +29 -9
  82. package/src/services/reqresp/batch-tx-requester/interface.ts +2 -0
  83. package/src/services/reqresp/batch-tx-requester/missing_txs.ts +7 -0
  84. package/src/services/reqresp/batch-tx-requester/peer_collection.ts +5 -0
  85. package/src/services/reqresp/reqresp.ts +1 -1
  86. package/src/test-helpers/testbench-utils.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import {
2
+ MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT,
2
3
  MAX_PROCESSABLE_L2_GAS,
3
4
  PRIVATE_TX_L2_GAS_OVERHEAD,
4
5
  PUBLIC_TX_L2_GAS_OVERHEAD,
@@ -49,16 +50,31 @@ export interface HasGasLimitData {
49
50
  */
50
51
  export class GasLimitsValidator<T extends HasGasLimitData> implements TxValidator<T> {
51
52
  #log: Logger;
52
-
53
- constructor(bindings?: LoggerBindings) {
54
- this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings);
53
+ #effectiveMaxL2Gas: number;
54
+ #effectiveMaxDAGas: number;
55
+ #rollupManaLimit: number;
56
+ #maxBlockL2Gas: number;
57
+ #maxBlockDAGas: number;
58
+
59
+ constructor(opts?: {
60
+ rollupManaLimit?: number;
61
+ maxBlockL2Gas?: number;
62
+ maxBlockDAGas?: number;
63
+ bindings?: LoggerBindings;
64
+ }) {
65
+ this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings);
66
+ this.#rollupManaLimit = opts?.rollupManaLimit ?? Infinity;
67
+ this.#maxBlockL2Gas = opts?.maxBlockL2Gas ?? Infinity;
68
+ this.#maxBlockDAGas = opts?.maxBlockDAGas ?? Infinity;
69
+ this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, this.#rollupManaLimit, this.#maxBlockL2Gas);
70
+ this.#effectiveMaxDAGas = Math.min(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, this.#maxBlockDAGas);
55
71
  }
56
72
 
57
73
  validateTx(tx: T): Promise<TxValidationResult> {
58
74
  return Promise.resolve(this.validateGasLimit(tx));
59
75
  }
60
76
 
61
- /** Checks gas limits are >= fixed minimums and <= AVM max processable L2 gas. */
77
+ /** Checks gas limits are >= fixed minimums and <= effective max gas (L2 and DA). */
62
78
  validateGasLimit(tx: T): TxValidationResult {
63
79
  const gasLimits = tx.data.constants.txContext.gasSettings.gasLimits;
64
80
  const minGasLimits = new Gas(
@@ -74,10 +90,21 @@ export class GasLimitsValidator<T extends HasGasLimitData> implements TxValidato
74
90
  return { result: 'invalid', reason: [TX_ERROR_INSUFFICIENT_GAS_LIMIT] };
75
91
  }
76
92
 
77
- if (gasLimits.l2Gas > MAX_PROCESSABLE_L2_GAS) {
78
- this.#log.verbose(`Rejecting transaction due to the gas limit(s) being higher than the maximum processable gas`, {
93
+ if (gasLimits.l2Gas > this.#effectiveMaxL2Gas) {
94
+ this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, {
79
95
  gasLimits,
80
- minGasLimits,
96
+ effectiveMaxL2Gas: this.#effectiveMaxL2Gas,
97
+ rollupManaLimit: this.#rollupManaLimit,
98
+ maxBlockL2Gas: this.#maxBlockL2Gas,
99
+ });
100
+ return { result: 'invalid', reason: [TX_ERROR_GAS_LIMIT_TOO_HIGH] };
101
+ }
102
+
103
+ if (gasLimits.daGas > this.#effectiveMaxDAGas) {
104
+ this.#log.verbose(`Rejecting transaction due to the DA gas limit being higher than the effective maximum`, {
105
+ gasLimits,
106
+ effectiveMaxDAGas: this.#effectiveMaxDAGas,
107
+ maxBlockDAGas: this.#maxBlockDAGas,
81
108
  });
82
109
  return { result: 'invalid', reason: [TX_ERROR_GAS_LIMIT_TOO_HIGH] };
83
110
  }
@@ -106,21 +133,27 @@ export class GasTxValidator implements TxValidator<Tx> {
106
133
  #publicDataSource: PublicStateSource;
107
134
  #feeJuiceAddress: AztecAddress;
108
135
  #gasFees: GasFees;
136
+ #gasLimitOpts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number };
109
137
 
110
138
  constructor(
111
139
  publicDataSource: PublicStateSource,
112
140
  feeJuiceAddress: AztecAddress,
113
141
  gasFees: GasFees,
114
142
  private bindings?: LoggerBindings,
143
+ opts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number },
115
144
  ) {
116
145
  this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings);
117
146
  this.#publicDataSource = publicDataSource;
118
147
  this.#feeJuiceAddress = feeJuiceAddress;
119
148
  this.#gasFees = gasFees;
149
+ this.#gasLimitOpts = opts;
120
150
  }
121
151
 
122
152
  async validateTx(tx: Tx): Promise<TxValidationResult> {
123
- const gasLimitValidation = new GasLimitsValidator(this.bindings).validateGasLimit(tx);
153
+ const gasLimitValidation = new GasLimitsValidator({
154
+ ...this.#gasLimitOpts,
155
+ bindings: this.bindings,
156
+ }).validateGasLimit(tx);
124
157
  if (gasLimitValidation.result === 'invalid') {
125
158
  return Promise.resolve(gasLimitValidation);
126
159
  }
@@ -141,3 +141,33 @@ export class PhasesTxValidator implements TxValidator<Tx> {
141
141
  return TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED;
142
142
  }
143
143
  }
144
+
145
+ /** Structural interface for the allowed-setup-calls flag check. */
146
+ export interface HasAllowedSetupCallsData {
147
+ txHash: { toString(): string };
148
+ allowedSetupCalls: boolean;
149
+ }
150
+
151
+ /**
152
+ * Validates that a transaction's setup-phase calls were allowed at receipt time.
153
+ *
154
+ * Checks the precomputed `allowedSetupCalls` flag on TxMetaData. The flag is
155
+ * computed by running the PhasesTxValidator on the full Tx when it first enters
156
+ * the pool. This lightweight validator is used during pending pool migration to
157
+ * reject txs whose setup calls are not on the allow list.
158
+ */
159
+ export class AllowedSetupCallsMetaValidator<T extends HasAllowedSetupCallsData> implements TxValidator<T> {
160
+ #log: Logger;
161
+
162
+ constructor(bindings?: LoggerBindings) {
163
+ this.#log = createLogger('sequencer:tx_validator:tx_phases_meta', bindings);
164
+ }
165
+
166
+ validateTx(tx: T): Promise<TxValidationResult> {
167
+ if (!tx.allowedSetupCalls) {
168
+ this.#log.verbose(`Rejecting tx ${tx.txHash} because its setup calls are not on the allow list`);
169
+ return Promise.resolve({ result: 'invalid', reason: [TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED] });
170
+ }
171
+ return Promise.resolve({ result: 'valid' });
172
+ }
173
+ }
@@ -237,11 +237,11 @@ export class LibP2PService extends WithTracer implements P2PService {
237
237
  this.gossipSubEventHandler = this.handleGossipSubEvent.bind(this);
238
238
 
239
239
  this.blockReceivedCallback = async (block: BlockProposal): Promise<boolean> => {
240
- this.logger.debug(
241
- `Handler not yet registered: Block received callback not set. Received block for slot ${block.slotNumber} from peer.`,
240
+ this.logger.warn(
241
+ `Handler for block received not yet registered on P2P service. Received block ${block.blockNumber} for slot ${block.slotNumber} from peer.`,
242
242
  { p2pMessageIdentifier: await block.p2pMessageLoggingIdentifier() },
243
243
  );
244
- return false;
244
+ return true;
245
245
  };
246
246
 
247
247
  this.checkpointReceivedCallback = (
@@ -754,6 +754,9 @@ export class LibP2PService extends WithTracer implements P2PService {
754
754
  if (!validator || !validator.addMessage(msgId)) {
755
755
  this.instrumentation.incMessagePrevalidationStatus(false, topicType);
756
756
  this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), TopicValidatorResult.Ignore);
757
+ if (topicType === TopicType.tx) {
758
+ this.logger.verbose(`Ignoring already-seen tx gossip message`, { msgId, source: source.toString() });
759
+ }
757
760
  return { result: false, topicType };
758
761
  }
759
762
 
@@ -923,6 +926,11 @@ export class LibP2PService extends WithTracer implements P2PService {
923
926
  severity = await this.handleDoubleSpendFailure(tx, txBlockNumber);
924
927
  }
925
928
 
929
+ this.logger.verbose(`Rejecting gossiped tx ${tx.getTxHash().toString()}: stage 1 validation failed`, {
930
+ validator: name,
931
+ severity,
932
+ source: source.toString(),
933
+ });
926
934
  this.peerManager.penalizePeer(source, severity);
927
935
  return { result: TopicValidatorResult.Reject };
928
936
  }
@@ -930,6 +938,9 @@ export class LibP2PService extends WithTracer implements P2PService {
930
938
  // Pool pre-check: see if the pool would accept this tx before doing expensive proof verification
931
939
  const canAdd = await this.mempools.txPool.canAddPendingTx(tx);
932
940
  if (canAdd === 'ignored') {
941
+ this.logger.verbose(`Ignoring gossiped tx ${tx.getTxHash().toString()}: pool pre-check returned ignored`, {
942
+ source: source.toString(),
943
+ });
933
944
  return { result: TopicValidatorResult.Ignore, obj: tx };
934
945
  }
935
946
 
@@ -937,7 +948,12 @@ export class LibP2PService extends WithTracer implements P2PService {
937
948
  const secondStageValidators = this.createSecondStageMessageValidators();
938
949
  const secondStageOutcome = await this.runValidations(tx, secondStageValidators);
939
950
  if (!secondStageOutcome.allPassed) {
940
- const { severity } = secondStageOutcome.failure;
951
+ const { severity, name } = secondStageOutcome.failure;
952
+ this.logger.verbose(`Rejecting gossiped tx ${tx.getTxHash().toString()}: stage 2 validation failed`, {
953
+ validator: name,
954
+ severity,
955
+ source: source.toString(),
956
+ });
941
957
  this.peerManager.penalizePeer(source, severity);
942
958
  return { result: TopicValidatorResult.Reject };
943
959
  }
@@ -949,7 +965,7 @@ export class LibP2PService extends WithTracer implements P2PService {
949
965
  const wasAccepted = addResult.accepted.some(h => h.equals(txHash));
950
966
  const wasIgnored = addResult.ignored.some(h => h.equals(txHash));
951
967
 
952
- this.logger.trace(`Validate propagated tx`, {
968
+ this.logger.verbose(`Validate propagated tx ${txHash.toString()}`, {
953
969
  wasAccepted,
954
970
  wasIgnored,
955
971
  [Attributes.P2P_ID]: source.toString(),
@@ -1192,7 +1208,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1192
1208
  // Note: Validators do NOT attest to individual blocks, only to checkpoint proposals.
1193
1209
  const isValid = await this.blockReceivedCallback(block, sender);
1194
1210
  if (!isValid) {
1195
- this.logger.warn(`Block proposal validation failed for block ${block.blockNumber}`, block.toBlockInfo());
1211
+ this.logger.info(`Block proposal validation failed for block ${block.blockNumber}`, block.toBlockInfo());
1196
1212
  }
1197
1213
  }
1198
1214
 
@@ -1626,6 +1642,7 @@ export class LibP2PService extends WithTracer implements P2PService {
1626
1642
  ...(this.config.txPublicSetupAllowListExtend ?? []),
1627
1643
  ];
1628
1644
  const blockNumber = BlockNumber(currentBlockNumber + 1);
1645
+ const l1Constants = await this.archiver.getL1Constants();
1629
1646
 
1630
1647
  return createFirstStageTxValidationsForGossipedTransactions(
1631
1648
  nextSlotTimestamp,
@@ -1639,6 +1656,11 @@ export class LibP2PService extends WithTracer implements P2PService {
1639
1656
  !this.config.disableTransactions,
1640
1657
  allowedInSetup,
1641
1658
  this.logger.getBindings(),
1659
+ {
1660
+ rollupManaLimit: l1Constants.rollupManaLimit,
1661
+ maxBlockL2Gas: this.config.validateMaxL2BlockGas,
1662
+ maxBlockDAGas: this.config.validateMaxDABlockGas,
1663
+ },
1642
1664
  );
1643
1665
  }
1644
1666
 
@@ -0,0 +1,229 @@
1
+ # ReqResp Protocols
2
+
3
+ This module implements libp2p request-response protocols for the Aztec P2P network. All protocols share common transport-level validation (rate limiting, timeouts, Snappy decompression, error penalties) with protocol-specific logic layered on top.
4
+
5
+ ## Common Transport Validation
6
+
7
+ ### Rate Limiting (Responder Side)
8
+
9
+ Applied before the protocol handler runs.
10
+
11
+ | Protocol | Peer Limit | Global Limit | File |
12
+ |----------|-----------|-------------|------|
13
+ | PING | 5/s | 10/s | `rate-limiter/rate_limits.ts` |
14
+ | STATUS | 5/s | 10/s | same |
15
+ | AUTH | 5/s | 10/s | same |
16
+ | GOODBYE | 5/s | 10/s | same |
17
+ | BLOCK | 2/s | 5/s | same |
18
+ | BLOCK_TXS | 10/s | 200/s | same |
19
+ | TX | (see rate limits file) | (see rate limits file) | same |
20
+
21
+ - Per-peer limit exceeded: `HighToleranceError` penalty + `RATE_LIMIT_EXCEEDED` status. Penalty fires inside `RequestResponseRateLimiter.allow()`, not the stream handler.
22
+ - Global limit exceeded: `RATE_LIMIT_EXCEEDED` status only (no peer penalty).
23
+
24
+ ### Response Status Byte (Requester Side)
25
+
26
+ | Rule | Consequence | File |
27
+ |------|-------------|------|
28
+ | First chunk must be exactly 1 byte | `ReqRespStatusError(UNKNOWN)` | `status.ts` |
29
+ | Byte must be valid `ReqRespStatus` enum (0-4, 126, 127) | `ReqRespStatusError(UNKNOWN)` | same |
30
+
31
+ Note: `prettyPrintReqRespStatus` is missing a `NOT_FOUND` case (minor logging bug).
32
+
33
+ ### Snappy Decompression (Requester Side)
34
+
35
+ Per-protocol size limits checked via preamble before decompression.
36
+
37
+ ### Timeouts (Requester Side)
38
+
39
+ | Timeout | Default | Penalty |
40
+ |---------|---------|---------|
41
+ | Individual request | 10s | HighToleranceError |
42
+ | Dial | 5s | HighToleranceError |
43
+
44
+ ### Error Penalty Categorization (Requester Side)
45
+
46
+ | Error Type | Severity |
47
+ |------------|----------|
48
+ | GOODBYE subprotocol errors | None |
49
+ | `CollectiveReqRespTimeoutError` / `InvalidResponseError` | None |
50
+ | `AbortError` / connection close / muxer closed | None |
51
+ | `ECONNRESET` / `EPIPE` / `ECONNREFUSED` / `ERR_UNEXPECTED_EOF` | HighToleranceError |
52
+ | `ERR_UNSUPPORTED_PROTOCOL` | HighToleranceError |
53
+ | `IndividualReqRespTimeoutError` / `TimeoutError` | HighToleranceError |
54
+ | Catch-all | HighToleranceError |
55
+
56
+ ### Request Error Penalty (Responder Side)
57
+
58
+ | Error Type | Severity |
59
+ |------------|----------|
60
+ | `BADLY_FORMED_REQUEST` | LowToleranceError |
61
+ | All others | None |
62
+
63
+ ### Notes
64
+
65
+ - Request payloads are NOT snappy-compressed (asymmetric: only responses use snappy).
66
+
67
+ ---
68
+
69
+ ## Handshake Protocols
70
+
71
+ ### Connection-Level Gating (Before Any Handshake)
72
+
73
+ | Rule | Consequence | File |
74
+ |------|-------------|------|
75
+ | Deny inbound connection from IP/peerId with too many failed auth handshakes | Connection denied | `libp2p_service.ts` |
76
+ | Threshold: `p2pMaxFailedAuthAttemptsAllowed` (default 3) | Tracked per peerId AND per IP | `peer_manager.ts` |
77
+ | Failed auth entries expire after 1 hour | Peer can reconnect; no escalating penalty for repeat offenders | same |
78
+
79
+ ### Handshake Trigger Logic (`peer:connect`)
80
+
81
+ 1. `p2pDisableStatusHandshake` = true: no handshake
82
+ 2. `p2pAllowOnlyValidators` = false: STATUS handshake
83
+ 3. Peer is protected (trusted/private/preferred): STATUS handshake
84
+ 4. Otherwise: AUTH handshake (superset of STATUS)
85
+
86
+ Config constraint: `p2pDisableStatusHandshake && p2pAllowOnlyValidators` is disallowed.
87
+
88
+ ### STATUS Protocol (`/aztec/req/status/1.0.0`)
89
+
90
+ **Requester side** (`peer_manager.ts`):
91
+
92
+ | Rule | Consequence |
93
+ |------|-------------|
94
+ | Response status must be SUCCESS | Peer scheduled for disconnect |
95
+ | `compressedComponentsVersion` must match | Peer scheduled for disconnect |
96
+ | Any exception | Peer scheduled for disconnect |
97
+
98
+ `StatusMessage.validate()` currently only checks `compressedComponentsVersion`. Fields `latestBlockNumber`, `latestBlockHash`, `finalizedBlockNumber` are NOT validated (TODO in code).
99
+
100
+ **Responder side**: no validation of incoming request content (always responds with own status). This means the requester leaks its blockchain state to any peer before validation.
101
+
102
+ **Deserialization bounds**: `MAX_VERSION_STRING_LENGTH` = 64 bytes, `MAX_BLOCK_HASH_STRING_LENGTH` = 128 bytes. Expected response size: 1 KB.
103
+
104
+ ### AUTH Protocol (`/aztec/req/auth/1.0.0`)
105
+
106
+ **Requester side** (`peer_manager.ts`):
107
+
108
+ | # | Rule | Consequence |
109
+ |---|------|-------------|
110
+ | 1 | Response status is SUCCESS | `markAuthHandshakeFailed` + disconnect |
111
+ | 2 | `compressedComponentsVersion` match | `markAuthHandshakeFailed` + disconnect |
112
+ | 3 | Valid ECDSA signature recovery from challenge response | `markAuthHandshakeFailed` + disconnect |
113
+ | 4 | Recovered address is a registered validator | `markAuthHandshakeFailed` + disconnect |
114
+ | 5 | Validator address not already authenticated to different peerId | Silent return (no disconnect, no failure marking -- peer stays connected but unauthenticated) |
115
+ | 6 | Any exception | `markAuthHandshakeFailed` + disconnect |
116
+
117
+ Challenge: random `Fr`, payload = `keccak256("Aztec Validator Challenge:" + challenge)`, signed with `eth_sign` style. Challenge is NOT bound to peer identity (transport encryption via Noise is the binding layer).
118
+
119
+ On success: peer added to authenticated maps, prior failures cleared (including IP-based ones -- shared-IP peers benefit from a legitimate validator's success).
120
+
121
+ **Responder side** (`validator-client/src/validator.ts` + `peer_manager.ts`):
122
+
123
+ | # | Rule | Consequence |
124
+ |---|------|-------------|
125
+ | 1 | Peer must be protected (`shouldTrustWithIdentity` in `peer_manager.ts`) | Returns empty buffer (SUCCESS status + empty payload -> requester gets parse error -> `markAuthHandshakeFailed`) |
126
+ | 2 | Node must have registered validator address | Returns empty buffer (same consequence) |
127
+
128
+ **Unauthenticated peer gossip**: when `p2pAllowOnlyValidators` is true, unauthenticated peers get `appSpecificScore = -Infinity`, completely excluding them from all gossip.
129
+
130
+ ### PING Protocol (`/aztec/req/ping/1.0.0`)
131
+
132
+ No validation on either side. Responder returns `Buffer.from('pong')`. Expected response: 1 KB.
133
+
134
+ ### GOODBYE Protocol (`/aztec/req/goodbye/1.0.0`)
135
+
136
+ **Responder**: buffer must be 1 byte (defaults to `UNKNOWN` on invalid length). Goodbye reason byte is NOT validated against the enum -- any byte 0-255 accepted. Peer scheduled for disconnect regardless of reason.
137
+
138
+ **Requester**: response errors are never penalized (GOODBYE subprotocol exempt from error categorization).
139
+
140
+ ### Periodic Re-validation
141
+
142
+ | Rule | Interval | File |
143
+ |------|----------|------|
144
+ | Authenticated validators re-checked against current validator set | Every heartbeat (`peerCheckIntervalMS`) | `peer_manager.ts` |
145
+ | If validator address no longer registered, auth entry removed | Same | same |
146
+
147
+ Protected peers (private/trusted/preferred) are always considered "authenticated" without AUTH handshake.
148
+
149
+ ---
150
+
151
+ ## Block Data Protocols
152
+
153
+ ### BLOCK Protocol (`/aztec/req/block/1.0.0`)
154
+
155
+ **Server side**:
156
+
157
+ | Rule | Consequence | File |
158
+ |------|-------------|------|
159
+ | Request must parse as `Fr` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block.ts` |
160
+ | Block lookup throws | `INTERNAL_ERROR` status | same |
161
+ | Block not found | SUCCESS + empty buffer (design choice; no `NOT_FOUND` status used) | same |
162
+
163
+ **Requester side** (Snappy limit: 3 MB):
164
+
165
+ | Rule | Consequence | File |
166
+ |------|-------------|------|
167
+ | Response block number must match requested | LowToleranceError; rejected | `libp2p_service.ts` (`validateRequestedBlock`) |
168
+ | Local block must exist for hash verification | Rejected (no penalty) | same |
169
+ | Response block hash must equal local block hash | MidToleranceError; rejected | same |
170
+
171
+ **Limitation**: the local-block requirement means BLOCK req/resp is unusable for initial P2P-only sync (before L1 sync provides local copies for verification). A TODO in the code acknowledges this.
172
+
173
+ ### BLOCK_TXS Protocol (`/aztec/req/block_txs/1.0.0`)
174
+
175
+ **Server side**:
176
+
177
+ | Rule | Consequence | File |
178
+ |------|-------------|------|
179
+ | Request must parse as `BlockTxsRequest` (Fr + TxHashArray + BitVector) | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/block_txs/block_txs_handler.ts` |
180
+ | BitVector length: non-negative and <= `MAX_TXS_PER_BLOCK` (65536) | Deserialization throws -> `BADLY_FORMED_REQUEST` | `protocols/block_txs/bitvector.ts` |
181
+ | Archive root not found and no explicit txHashes | `NOT_FOUND` status | handler |
182
+ | Internal error during lookup | Unhandled exception -> stream abort (no `INTERNAL_ERROR` status, unlike BLOCK) | handler |
183
+
184
+ Conditional registration: BLOCK_TXS handler only registered when `config.disableTransactions` is false. Otherwise peers get `ERR_UNSUPPORTED_PROTOCOL`.
185
+
186
+ **Requester side via `sendBatchRequest`** (Snappy limit: `max(N, 1) * 512 + 1` KB):
187
+
188
+ | Rule | Consequence | File |
189
+ |------|-------------|------|
190
+ | Archive root must match request | MidToleranceError | `libp2p_service.ts` (`validateRequestedBlockTxs`) |
191
+ | BitVector length must match request | MidToleranceError | same |
192
+ | No duplicate tx hashes | MidToleranceError | same |
193
+ | Tx count within bounds | MidToleranceError | same |
194
+ | Local block proposal must exist for archive root | Rejected (no penalty) | same |
195
+ | All tx hashes must be in proposal's tx list at allowed indices | LowToleranceError | same |
196
+ | Txs in strictly increasing index order | LowToleranceError | same |
197
+ | Each tx passes well-formedness (Metadata [4 fields], Size, Data, Proof) | LowToleranceError | same |
198
+
199
+ **Requester side via `BatchTxRequester`** (separate validation path):
200
+
201
+ | Rule | Consequence | File |
202
+ |------|-------------|------|
203
+ | Non-SUCCESS status: `FAILURE`/`UNKNOWN` | HighToleranceError + "bad peer" tracking | `batch-tx-requester/batch_tx_requester.ts` |
204
+ | `RATE_LIMIT_EXCEEDED` | Peer marked rate-limited (cooldown) | same |
205
+ | `NOT_FOUND` / `BADLY_FORMED_REQUEST` / `INTERNAL_ERROR` | Falls through silently (no penalty) | same |
206
+ | Each tx validated (Metadata + Size + Data + Proof) | LowToleranceError per invalid tx; valid txs from same response still accepted | same |
207
+ | Archive root match + non-empty txIndices | No penalty on mismatch; peer not promoted to "smart" | same |
208
+
209
+ **Double penalty on transport errors**: when `BatchTxRequester` encounters a transport error (e.g., ECONNRESET), both `sendRequestToPeer`'s internal handler and the `BatchTxRequester`'s catch block penalize the peer, resulting in double HighToleranceError.
210
+
211
+ See [BatchTxRequester README](batch-tx-requester/README.md) for the full architecture (peer classification, worker model, wire protocol).
212
+
213
+ ### TX Protocol (`/aztec/req/tx/1.0.0`)
214
+
215
+ **Server side**:
216
+
217
+ | Rule | Consequence | File |
218
+ |------|-------------|------|
219
+ | Request must parse as `TxHashArray` | `BADLY_FORMED_REQUEST` + LowToleranceError | `protocols/tx.ts` |
220
+
221
+ **Requester side** (validator registered at startup, not the default noop):
222
+
223
+ | Rule | Consequence | File |
224
+ |------|-------------|------|
225
+ | Each returned tx hash must be in the requested set | MidToleranceError | `libp2p_service.ts` (`validateRequestedTxs`) |
226
+ | Each tx passes well-formedness (Metadata + Size + Data + Proof) | LowToleranceError | same |
227
+
228
+ Snappy limit: `max(N, 1) * 512 + 1` KB.
229
+
@@ -463,9 +463,18 @@ export class BatchTxRequester {
463
463
  * this implies we will query these peers couple of more times and give them a chance to "redeem" themselves before completely ignoring them
464
464
  */
465
465
  private handleFailResponseFromPeer(peerId: PeerId, responseStatus: ReqRespStatus) {
466
- //TODO: Should we ban these peers?
467
466
  if (responseStatus === ReqRespStatus.FAILURE || responseStatus === ReqRespStatus.UNKNOWN) {
468
467
  this.peers.penalisePeer(peerId, PeerErrorSeverity.HighToleranceError);
468
+ this.peers.markPeerDumb(peerId);
469
+ this.txsMetadata.clearPeerData(peerId);
470
+ return;
471
+ }
472
+
473
+ // NOT_FOUND means the peer pruned its block proposal — it can no longer serve
474
+ // index-based requests, but this is a legitimate state so we don't penalize.
475
+ if (responseStatus === ReqRespStatus.NOT_FOUND) {
476
+ this.peers.markPeerDumb(peerId);
477
+ this.txsMetadata.clearPeerData(peerId);
469
478
  return;
470
479
  }
471
480
 
@@ -555,10 +564,9 @@ export class BatchTxRequester {
555
564
  return;
556
565
  }
557
566
 
558
- // If block response is invalid we still want to query this peer in the future
559
- // Because they sent successful response, so they might become smart peer in the future
560
- // Or send us needed txs
561
- if (!this.isBlockResponseValid(response)) {
567
+ const hasArchiveRootMismatch = this.blockTxsSource.archive.toString() !== response.archiveRoot.toString();
568
+ if (hasArchiveRootMismatch) {
569
+ this.handleArchiveRootMismatch(peerId, response);
562
570
  return;
563
571
  }
564
572
 
@@ -576,13 +584,25 @@ export class BatchTxRequester {
576
584
  this.smartRequesterSemaphore.release();
577
585
  }
578
586
 
579
- private isBlockResponseValid(response: BlockTxsResponse): boolean {
580
- const archiveRootsMatch = this.blockTxsSource.archive.toString() === response.archiveRoot.toString();
581
- const peerHasSomeTxsFromProposal = !response.txIndices.isEmpty();
582
- return archiveRootsMatch && peerHasSomeTxsFromProposal;
587
+ /**
588
+ * Handles an archive root mismatch between local state and peer response.
589
+ *
590
+ * - Response archive is Fr.ZERO (peer pruned proposal, legitimate): marks peer dumb.
591
+ * - Non-zero archive mismatch (malicious response): penalises + marks dumb.
592
+ */
593
+ private handleArchiveRootMismatch(peerId: PeerId, response: BlockTxsResponse): void {
594
+ if (!response.archiveRoot.isZero()) {
595
+ this.peers.penalisePeer(peerId, PeerErrorSeverity.LowToleranceError);
596
+ }
597
+
598
+ this.peers.markPeerDumb(peerId);
599
+ this.txsMetadata.clearPeerData(peerId);
583
600
  }
584
601
 
585
602
  private peerHasSomeTxsWeAreMissing(_peerId: PeerId, response: BlockTxsResponse): boolean {
603
+ if (response.txIndices.isEmpty()) {
604
+ return false;
605
+ }
586
606
  const txsPeerHas = new Set(this.extractHashesPeerHasFromResponse(response).map(h => h.toString()));
587
607
  return this.txsMetadata.getMissingTxHashes().intersection(txsPeerHas).size > 0;
588
608
  }
@@ -23,6 +23,8 @@ export interface ITxMetadataCollection {
23
23
  alreadyFetched(txHash: TxHash): boolean;
24
24
  // Returns true if tx was marked as fetched, false if it was already marked as fetched
25
25
  markPeerHas(peerId: PeerId, txHashes: TxHash[]): void;
26
+ /** Remove all tx metadata associations for a peer (e.g. on demotion from smart to dumb). */
27
+ clearPeerData(peerId: PeerId): void;
26
28
  }
27
29
 
28
30
  /**
@@ -158,4 +158,11 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection {
158
158
  }
159
159
  });
160
160
  }
161
+
162
+ public clearPeerData(peerId: PeerId) {
163
+ const peerIdStr = peerId.toString();
164
+ for (const txMeta of this.txMetadata.values()) {
165
+ txMeta.peers.delete(peerIdStr);
166
+ }
167
+ }
161
168
  }
@@ -12,6 +12,7 @@ export const RATE_LIMIT_EXCEEDED_PEER_CACHE_TTL = 1000; // 1s
12
12
 
13
13
  export interface IPeerCollection {
14
14
  markPeerSmart(peerId: PeerId): void;
15
+ markPeerDumb(peerId: PeerId): void;
15
16
 
16
17
  /** Sample next peer in round-robin fashion. No smart peers if returns undefined */
17
18
  nextSmartPeerToQuery(): PeerId | undefined;
@@ -57,6 +58,10 @@ export class PeerCollection implements IPeerCollection {
57
58
  this.smartPeers.add(peerId.toString());
58
59
  }
59
60
 
61
+ public markPeerDumb(peerId: PeerId): void {
62
+ this.smartPeers.delete(peerId.toString());
63
+ }
64
+
60
65
  // We keep track of all peers that are queried for peer sampling algorithm
61
66
  private queriedSmartPeers: Set<string> = new Set<string>();
62
67
  private queriedDumbPeers: Set<string> = new Set<string>();
@@ -320,7 +320,7 @@ export class ReqResp implements ReqRespInterface {
320
320
  };
321
321
 
322
322
  for (const index of indices) {
323
- this.logger.info(`Sending request ${index} to peer ${peerAsString}`);
323
+ this.logger.trace(`Sending request ${index} to peer ${peerAsString}`);
324
324
  const response = await this.sendRequestToPeer(peer, subProtocol, requestBuffers[index]);
325
325
 
326
326
  // Check the status of the response buffer
@@ -292,6 +292,7 @@ export function createMockEpochCache(): EpochCacheInterface {
292
292
  ethereumSlotDuration: 1,
293
293
  proofSubmissionEpochs: 1,
294
294
  targetCommitteeSize: 48,
295
+ rollupManaLimit: Number.MAX_SAFE_INTEGER,
295
296
  }),
296
297
  };
297
298
  }