@aztec-labs/p2p 6.0.0-rc.3 → 6.0.0-rc.4
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.
- package/dest/bootstrap/bootstrap.d.ts +2 -2
- package/dest/bootstrap/bootstrap.d.ts.map +1 -1
- package/dest/bootstrap/bootstrap.js +1 -3
- package/dest/client/p2p_client.js +1 -2
- package/dest/config.d.ts +1 -11
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +0 -16
- package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts +1 -1
- package/dest/mem_pools/tx_pool_v2/deleted_pool.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/deleted_pool.js +1 -0
- package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts +2 -1
- package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.js +25 -1
- package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +3 -3
- package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts +2 -1
- package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.js +10 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +6 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +39 -8
- package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +1 -1
- package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
- package/dest/msg_validators/attestation_validator/attestation_validator.js +25 -13
- package/dest/msg_validators/epoch_cache_lookup.d.ts +15 -12
- package/dest/msg_validators/epoch_cache_lookup.d.ts.map +1 -1
- package/dest/msg_validators/epoch_cache_lookup.js +27 -18
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.js +23 -15
- package/dest/msg_validators/tx_validator/data_validator.d.ts +1 -1
- package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/data_validator.js +61 -13
- package/dest/services/discv5/discV5_service.d.ts +2 -2
- package/dest/services/discv5/discV5_service.d.ts.map +1 -1
- package/dest/services/discv5/discV5_service.js +2 -3
- package/dest/services/dummy_service.d.ts +2 -1
- package/dest/services/dummy_service.d.ts.map +1 -1
- package/dest/services/dummy_service.js +1 -0
- package/dest/services/encoding.d.ts +2 -1
- package/dest/services/encoding.d.ts.map +1 -1
- package/dest/services/encoding.js +1 -0
- package/dest/services/libp2p/libp2p_service.d.ts +6 -2
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +38 -9
- package/dest/services/peer-manager/peer_manager.d.ts +1 -1
- package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
- package/dest/services/peer-manager/peer_manager.js +5 -1
- package/dest/services/peer-manager/peer_scoring.d.ts +13 -1
- package/dest/services/peer-manager/peer_scoring.d.ts.map +1 -1
- package/dest/services/peer-manager/peer_scoring.js +38 -0
- package/dest/services/reqresp/protocols/auth.d.ts +2 -1
- package/dest/services/reqresp/protocols/auth.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/auth.js +1 -0
- package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts +2 -1
- package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/block_txs/bitvector.js +1 -0
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts +1 -1
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.js +13 -4
- package/dest/services/reqresp/protocols/goodbye.d.ts +2 -1
- package/dest/services/reqresp/protocols/goodbye.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/goodbye.js +1 -0
- package/dest/services/reqresp/protocols/ping.d.ts +2 -1
- package/dest/services/reqresp/protocols/ping.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/ping.js +1 -0
- package/dest/services/reqresp/reqresp.d.ts +2 -1
- package/dest/services/reqresp/reqresp.d.ts.map +1 -1
- package/dest/services/reqresp/reqresp.js +1 -0
- package/dest/test-helpers/mock-pubsub.d.ts +2 -1
- package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
- package/dest/test-helpers/mock-pubsub.js +1 -0
- package/dest/test-helpers/reqresp-nodes.d.ts +1 -1
- package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
- package/dest/test-helpers/reqresp-nodes.js +1 -0
- package/dest/test-helpers/testbench-utils.js +18 -6
- package/dest/testbench/p2p_client_testbench_worker.d.ts +1 -1
- package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
- package/dest/testbench/p2p_client_testbench_worker.js +1 -0
- package/dest/testbench/worker_client_manager.d.ts +1 -1
- package/dest/testbench/worker_client_manager.d.ts.map +1 -1
- package/dest/testbench/worker_client_manager.js +2 -1
- package/dest/util.d.ts +1 -1
- package/dest/util.d.ts.map +1 -1
- package/dest/util.js +1 -0
- package/dest/versioning.d.ts +1 -1
- package/dest/versioning.d.ts.map +1 -1
- package/dest/versioning.js +1 -0
- package/package.json +16 -16
- package/src/bootstrap/bootstrap.ts +1 -2
- package/src/client/p2p_client.ts +1 -1
- package/src/config.ts +0 -11
- package/src/mem_pools/tx_pool_v2/README.md +2 -0
- package/src/mem_pools/tx_pool_v2/deleted_pool.ts +1 -0
- package/src/mem_pools/tx_pool_v2/eviction/eviction_manager.ts +28 -1
- package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +2 -2
- package/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.ts +11 -1
- package/src/mem_pools/tx_pool_v2/interfaces.ts +5 -0
- package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +46 -11
- package/src/msg_validators/attestation_validator/README.md +4 -3
- package/src/msg_validators/attestation_validator/attestation_validator.ts +23 -11
- package/src/msg_validators/epoch_cache_lookup.ts +33 -15
- package/src/msg_validators/proposal_validator/README.md +5 -4
- package/src/msg_validators/proposal_validator/proposal_validator.ts +23 -14
- package/src/msg_validators/tx_validator/data_validator.ts +63 -18
- package/src/services/discv5/discV5_service.ts +2 -2
- package/src/services/dummy_service.ts +1 -0
- package/src/services/encoding.ts +1 -0
- package/src/services/libp2p/libp2p_service.ts +37 -8
- package/src/services/peer-manager/peer_manager.ts +5 -1
- package/src/services/peer-manager/peer_scoring.ts +43 -0
- package/src/services/reqresp/protocols/auth.ts +1 -0
- package/src/services/reqresp/protocols/block_txs/bitvector.ts +1 -0
- package/src/services/reqresp/protocols/block_txs/block_txs_handler.ts +17 -4
- package/src/services/reqresp/protocols/goodbye.ts +1 -0
- package/src/services/reqresp/protocols/ping.ts +2 -0
- package/src/services/reqresp/reqresp.ts +1 -0
- package/src/test-helpers/mock-pubsub.ts +1 -0
- package/src/test-helpers/reqresp-nodes.ts +1 -0
- package/src/test-helpers/testbench-utils.ts +6 -6
- package/src/testbench/p2p_client_testbench_worker.ts +1 -0
- package/src/testbench/worker_client_manager.ts +2 -1
- package/src/util.ts +1 -0
- package/src/versioning.ts +1 -0
|
@@ -131,6 +131,8 @@ export class TxPoolV2Impl {
|
|
|
131
131
|
this.#evictionManager = new EvictionManager(this.#createPoolOperations(), log);
|
|
132
132
|
|
|
133
133
|
// Pre-add rules (run during addPendingTxs) - work with TxMetaData
|
|
134
|
+
// Order matters: each rule sees the pool without the txs that earlier rules picked for eviction.
|
|
135
|
+
// Each eviction pick is final: later rules cannot roll back an eviction.
|
|
134
136
|
this.#evictionManager.registerPreAddRule(new NullifierConflictRule());
|
|
135
137
|
this.#evictionManager.registerPreAddRule(new FeePayerBalancePreAddRule());
|
|
136
138
|
this.#evictionManager.registerPreAddRule(
|
|
@@ -240,11 +242,17 @@ export class TxPoolV2Impl {
|
|
|
240
242
|
const ignored: TxHash[] = [];
|
|
241
243
|
const rejected: TxHash[] = [];
|
|
242
244
|
const errors = new Map<string, TxPoolRejectionError>();
|
|
245
|
+
// Per-rejected-tx failure reasons, kept so receiver-local state drift can be told apart from a
|
|
246
|
+
// sender-attributable rejection: a tx that went stale against local state is not the relayer's fault.
|
|
247
|
+
const rejectionReasons = new Map<string, string[]>();
|
|
243
248
|
const acceptedPending = new Set<string>();
|
|
244
249
|
|
|
245
250
|
// Phase 1: Pre-compute all throwable I/O outside the transaction.
|
|
246
251
|
// If any pre-computation throws, the entire call fails before mutations happen.
|
|
247
|
-
const precomputed = new Map<
|
|
252
|
+
const precomputed = new Map<
|
|
253
|
+
string,
|
|
254
|
+
{ meta: TxMetaData; minedBlockId: L2BlockId | undefined; isValid: boolean; reason?: string[] }
|
|
255
|
+
>();
|
|
248
256
|
|
|
249
257
|
const validator = await this.#createTxValidator();
|
|
250
258
|
|
|
@@ -257,11 +265,14 @@ export class TxPoolV2Impl {
|
|
|
257
265
|
|
|
258
266
|
// Validate non-mined txs (mined and pre-protected txs bypass validation inside the transaction)
|
|
259
267
|
let isValid = true;
|
|
268
|
+
let reason: string[] | undefined;
|
|
260
269
|
if (!minedBlockId) {
|
|
261
|
-
|
|
270
|
+
const validation = await this.#validateMeta(meta, validator);
|
|
271
|
+
isValid = validation.valid;
|
|
272
|
+
reason = validation.reason;
|
|
262
273
|
}
|
|
263
274
|
|
|
264
|
-
precomputed.set(txHashStr, { meta, minedBlockId, isValid });
|
|
275
|
+
precomputed.set(txHashStr, { meta, minedBlockId, isValid, reason });
|
|
265
276
|
}
|
|
266
277
|
|
|
267
278
|
// Phase 2: Apply mutations inside the transaction using only pre-computed results,
|
|
@@ -283,7 +294,7 @@ export class TxPoolV2Impl {
|
|
|
283
294
|
continue;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
|
-
const { meta, minedBlockId, isValid } = precomputed.get(txHashStr)!;
|
|
297
|
+
const { meta, minedBlockId, isValid, reason } = precomputed.get(txHashStr)!;
|
|
287
298
|
const preProtectedSlot = this.#indices.getProtectionSlot(txHashStr);
|
|
288
299
|
|
|
289
300
|
if (minedBlockId) {
|
|
@@ -297,6 +308,9 @@ export class TxPoolV2Impl {
|
|
|
297
308
|
} else if (!isValid) {
|
|
298
309
|
// Failed pre-computed validation
|
|
299
310
|
rejected.push(txHash);
|
|
311
|
+
if (reason && reason.length > 0) {
|
|
312
|
+
rejectionReasons.set(txHashStr, reason);
|
|
313
|
+
}
|
|
300
314
|
} else {
|
|
301
315
|
// Regular pending tx - run pre-add rules using pre-computed metadata
|
|
302
316
|
const result = await this.#tryAddRegularPendingTx(
|
|
@@ -338,7 +352,13 @@ export class TxPoolV2Impl {
|
|
|
338
352
|
this.#instrumentation.recordRejected(rejected.length);
|
|
339
353
|
}
|
|
340
354
|
|
|
341
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
accepted,
|
|
357
|
+
ignored,
|
|
358
|
+
rejected,
|
|
359
|
+
...(errors.size > 0 ? { errors } : {}),
|
|
360
|
+
...(rejectionReasons.size > 0 ? { rejectionReasons } : {}),
|
|
361
|
+
};
|
|
342
362
|
}
|
|
343
363
|
|
|
344
364
|
/** Adds a validated pending tx, running pre-add rules and evicting conflicts. */
|
|
@@ -863,7 +883,18 @@ export class TxPoolV2Impl {
|
|
|
863
883
|
if (!buffer) {
|
|
864
884
|
return undefined;
|
|
865
885
|
}
|
|
866
|
-
|
|
886
|
+
if (opts.includeProof === false) {
|
|
887
|
+
return Tx.fromBuffer(buffer);
|
|
888
|
+
}
|
|
889
|
+
// The body and its separately-stored proof are read in two steps, not atomically: an eviction
|
|
890
|
+
// can delete the proof row after the body read. Serving a proofless tx makes the honest requester
|
|
891
|
+
// penalise us, so drop it. A present-but-empty proof is a valid sentinel; only a missing row is
|
|
892
|
+
// the evicted case.
|
|
893
|
+
const proofBuffer = await this.#proofsDB.getAsync(txHashStr);
|
|
894
|
+
if (!proofBuffer) {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
return Tx.fromBuffers(buffer, proofBuffer);
|
|
867
898
|
}
|
|
868
899
|
|
|
869
900
|
async getTxsByHash(txHashes: TxHash[], opts: { includeProof?: boolean } = {}): Promise<(Tx | undefined)[]> {
|
|
@@ -1122,15 +1153,19 @@ export class TxPoolV2Impl {
|
|
|
1122
1153
|
// ============================================================================
|
|
1123
1154
|
|
|
1124
1155
|
/** Validates transaction metadata, returning true if valid */
|
|
1125
|
-
async #validateMeta(
|
|
1156
|
+
async #validateMeta(
|
|
1157
|
+
meta: TxMetaData,
|
|
1158
|
+
validator?: TxValidator<TxMetaData>,
|
|
1159
|
+
context?: string,
|
|
1160
|
+
): Promise<{ valid: boolean; reason?: string[] }> {
|
|
1126
1161
|
const txValidator = validator ?? (await this.#createTxValidator());
|
|
1127
1162
|
const result = await txValidator.validateTx(meta);
|
|
1128
1163
|
if (result.result !== 'valid') {
|
|
1129
1164
|
const contextStr = context ? ` ${context}` : '';
|
|
1130
1165
|
this.#log.info(`Tx ${meta.txHash}${contextStr} failed validation: ${result.reason?.join(', ')}`);
|
|
1131
|
-
return false;
|
|
1166
|
+
return { valid: false, reason: result.reason };
|
|
1132
1167
|
}
|
|
1133
|
-
return true;
|
|
1168
|
+
return { valid: true };
|
|
1134
1169
|
}
|
|
1135
1170
|
|
|
1136
1171
|
/** Validates metadata directly */
|
|
@@ -1142,7 +1177,7 @@ export class TxPoolV2Impl {
|
|
|
1142
1177
|
const invalid: string[] = [];
|
|
1143
1178
|
const validator = await this.#createTxValidator();
|
|
1144
1179
|
for (const meta of metas) {
|
|
1145
|
-
if (await this.#validateMeta(meta, validator, context)) {
|
|
1180
|
+
if ((await this.#validateMeta(meta, validator, context)).valid) {
|
|
1146
1181
|
valid.push(meta);
|
|
1147
1182
|
} else {
|
|
1148
1183
|
invalid.push(meta.txHash);
|
|
@@ -1291,7 +1326,7 @@ export class TxPoolV2Impl {
|
|
|
1291
1326
|
},
|
|
1292
1327
|
getFeePayerPendingTxs: (feePayer: string) => this.#indices.getFeePayerPendingTxs(feePayer),
|
|
1293
1328
|
getPendingTxCount: () => this.#indices.getPendingTxCount(),
|
|
1294
|
-
|
|
1329
|
+
iteratePendingByPriority: (order: 'asc' | 'desc') => this.#indices.iteratePendingByPriority(order),
|
|
1295
1330
|
};
|
|
1296
1331
|
}
|
|
1297
1332
|
}
|
|
@@ -10,11 +10,12 @@ This module validates `CheckpointAttestation` gossipsub messages. Attestations a
|
|
|
10
10
|
|---|------|-------------|----------|------|
|
|
11
11
|
| 1 | **Slot timeliness**: `currentSlot` or `nextSlot`. Previous slot within 500ms: IGNORE. Older: REJECT. | REJECT or IGNORE | HighToleranceError | `attestation_validator.ts` |
|
|
12
12
|
| 2 | **Attester signature**: `getSender()` must recover valid address | REJECT | LowToleranceError | same |
|
|
13
|
-
| 3 | **Attester in committee**: recovered address in committee
|
|
14
|
-
| 4 | **Proposer exists**: `
|
|
13
|
+
| 3 | **Attester in committee**: recovered address in the slot's committee (an `open` or `none` committee has no members) | REJECT | HighToleranceError | same |
|
|
14
|
+
| 4 | **Proposer exists**: `lookupProposer` must not be `open` | REJECT | HighToleranceError | same |
|
|
15
15
|
| 5 | **Proposer signature**: `getProposer()` must recover valid address | REJECT | LowToleranceError | same |
|
|
16
16
|
| 6 | **Proposer matches expected**: recovered proposer = expected for slot | REJECT | HighToleranceError | same |
|
|
17
|
-
| 7 | **
|
|
17
|
+
| 7 | **No committee**: proposer lookup is `none` | REJECT | LowToleranceError | same |
|
|
18
|
+
| 8 | **Committee not stable**: either lookup is `unstable` (seed not fixed on L1), or it throws locally | IGNORE | none | same |
|
|
18
19
|
|
|
19
20
|
**Fisherman mode extension** (`FishermanAttestationValidator`): if a checkpoint proposal for the same archive exists in pool, the attestation's `ConsensusPayload` must `.equals()` the stored proposal's payload. On mismatch: REJECT + LowToleranceError.
|
|
20
21
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
|
|
1
|
+
import type { CommitteeMembershipLookup, EpochCacheInterface, ProposerLookup } from '@aztec-labs/epoch-cache';
|
|
2
2
|
import { type Logger, createLogger } from '@aztec-labs/foundation/log';
|
|
3
3
|
import {
|
|
4
4
|
type CheckpointAttestation,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '@aztec-labs/stdlib/p2p';
|
|
11
11
|
import type { ConsensusTimetable } from '@aztec-labs/stdlib/timetable';
|
|
12
12
|
|
|
13
|
-
import { mapEpochCacheLookupFailure } from '../epoch_cache_lookup.js';
|
|
13
|
+
import { ignoreUnstableCommittee, mapEpochCacheLookupFailure, rejectNoCommittee } from '../epoch_cache_lookup.js';
|
|
14
14
|
import { classifyReceiveWindowArrival } from '../receive_window.js';
|
|
15
15
|
|
|
16
16
|
export class CheckpointAttestationValidator implements P2PValidator<CheckpointAttestation> {
|
|
@@ -84,13 +84,17 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Verify the attester is in the committee for this slot
|
|
87
|
-
let
|
|
87
|
+
let membership: CommitteeMembershipLookup<boolean>;
|
|
88
88
|
try {
|
|
89
|
-
|
|
89
|
+
membership = await this.epochCache.lookupMembership(slotNumber, attester);
|
|
90
90
|
} catch (e) {
|
|
91
91
|
return mapEpochCacheLookupFailure(e, this.logger, 'checkpoint attestation', slotNumber);
|
|
92
92
|
}
|
|
93
|
-
if (
|
|
93
|
+
if (membership.state === 'unstable') {
|
|
94
|
+
return ignoreUnstableCommittee(membership, this.logger, 'checkpoint attestation', slotNumber);
|
|
95
|
+
}
|
|
96
|
+
// An open committee and a missing one have no members, so the attester cannot be one.
|
|
97
|
+
if (membership.state !== 'members' || !membership.inCommittee) {
|
|
94
98
|
this.logger.warn(`Attester ${attester.toString()} is not in committee for slot ${slotNumber}`);
|
|
95
99
|
return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
|
|
96
100
|
}
|
|
@@ -99,16 +103,24 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
99
103
|
// We look up the proposer for the specific slot rather than using currentSlot/nextSlot
|
|
100
104
|
// since timing differences could cause mismatches
|
|
101
105
|
const proposer = message.getProposer();
|
|
102
|
-
let
|
|
106
|
+
let proposerLookup: ProposerLookup;
|
|
103
107
|
try {
|
|
104
|
-
|
|
108
|
+
proposerLookup = await this.epochCache.lookupProposer(slotNumber);
|
|
105
109
|
} catch (e) {
|
|
106
110
|
return mapEpochCacheLookupFailure(e, this.logger, 'checkpoint attestation', slotNumber);
|
|
107
111
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
switch (proposerLookup.state) {
|
|
113
|
+
case 'unstable':
|
|
114
|
+
return ignoreUnstableCommittee(proposerLookup, this.logger, 'checkpoint attestation', slotNumber);
|
|
115
|
+
case 'none':
|
|
116
|
+
return rejectNoCommittee();
|
|
117
|
+
case 'open':
|
|
118
|
+
this.logger.warn(`No proposer defined for slot ${slotNumber}`);
|
|
119
|
+
return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
|
|
120
|
+
case 'members':
|
|
121
|
+
break;
|
|
111
122
|
}
|
|
123
|
+
const expectedProposer = proposerLookup.proposer;
|
|
112
124
|
if (!proposer) {
|
|
113
125
|
this.logger.warn(`Invalid proposer signature in checkpoint attestation for slot ${slotNumber}`);
|
|
114
126
|
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
@@ -116,7 +128,7 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
116
128
|
if (!proposer.equals(expectedProposer)) {
|
|
117
129
|
this.logger.warn(
|
|
118
130
|
`Proposer signature mismatch in checkpoint attestation. ` +
|
|
119
|
-
`Expected ${expectedProposer
|
|
131
|
+
`Expected ${expectedProposer.toString()} but got ${proposer.toString()} for slot ${slotNumber}`,
|
|
120
132
|
);
|
|
121
133
|
return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
|
|
122
134
|
}
|
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { UnstableCommittee } from '@aztec-labs/epoch-cache';
|
|
2
2
|
import type { SlotNumber } from '@aztec-labs/foundation/branded-types';
|
|
3
3
|
import type { Logger } from '@aztec-labs/foundation/log';
|
|
4
4
|
import { PeerErrorSeverity, type ValidationResult } from '@aztec-labs/stdlib/p2p';
|
|
5
5
|
|
|
6
|
+
// Verdicts shared by the proposal and attestation validators, so the "a local epoch-cache failure is never the
|
|
7
|
+
// relaying peer's fault" rule lives in one place.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A slot with no committee on L1 is the peer's fault — they should not be sending us a proposal or attestation for it
|
|
11
|
+
* — so it rejects. It is a routine peer-fault reject and needs no log line.
|
|
12
|
+
*/
|
|
13
|
+
export function rejectNoCommittee(): ValidationResult {
|
|
14
|
+
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A committee whose seed is not fixed on L1 yet says nothing about the relaying peer, and is expected around epoch
|
|
19
|
+
* boundaries, so it ignores without a warning.
|
|
20
|
+
*/
|
|
21
|
+
export function ignoreUnstableCommittee(
|
|
22
|
+
lookup: UnstableCommittee,
|
|
23
|
+
logger: Logger,
|
|
24
|
+
subject: string,
|
|
25
|
+
slotNumber: SlotNumber,
|
|
26
|
+
): ValidationResult {
|
|
27
|
+
logger.debug(`Ignoring ${subject} for slot ${slotNumber}: its committee is not stable on L1 yet`, {
|
|
28
|
+
slotNumber,
|
|
29
|
+
epoch: lookup.epoch,
|
|
30
|
+
l1BlockNumber: lookup.l1BlockNumber,
|
|
31
|
+
});
|
|
32
|
+
return { result: 'ignore' };
|
|
33
|
+
}
|
|
34
|
+
|
|
6
35
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* A missing committee (`NoCommitteeError`) is the peer's fault — they should not be sending us a
|
|
12
|
-
* proposal/attestation for a slot with no committee — so it rejects. Any other failure is
|
|
13
|
-
* receiver-local (an L1 RPC outage, or this node being behind) and says nothing about the relaying
|
|
14
|
-
* peer, so it ignores rather than penalizes. Only the local failure is warned; a missing committee is
|
|
15
|
-
* a routine peer-fault reject and needs no log line.
|
|
16
|
-
*
|
|
17
|
-
* `subject` names the gossip object for the log ("proposal" / "checkpoint attestation").
|
|
36
|
+
* A lookup that throws failed for a receiver-local reason (an L1 RPC outage, or this node being behind), so it ignores
|
|
37
|
+
* rather than penalizes, and warns. `subject` names the gossip object for the log ("proposal" / "checkpoint
|
|
38
|
+
* attestation").
|
|
18
39
|
*/
|
|
19
40
|
export function mapEpochCacheLookupFailure(
|
|
20
41
|
e: unknown,
|
|
@@ -22,9 +43,6 @@ export function mapEpochCacheLookupFailure(
|
|
|
22
43
|
subject: string,
|
|
23
44
|
slotNumber: SlotNumber,
|
|
24
45
|
): ValidationResult {
|
|
25
|
-
if (e instanceof NoCommitteeError) {
|
|
26
|
-
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
27
|
-
}
|
|
28
46
|
logger.warn(`Ignoring ${subject} for slot ${slotNumber} after a local epoch-cache lookup failure`, {
|
|
29
47
|
slotNumber,
|
|
30
48
|
error: e instanceof Error ? e.message : String(e),
|
|
@@ -17,9 +17,10 @@ File: `proposal_validator.ts`
|
|
|
17
17
|
| 3 | **Txs permitted**: if `disableTransactions`, must have 0 txHashes and 0 embedded txs | REJECT | MidToleranceError |
|
|
18
18
|
| 4 | **Max txs**: `txHashes.length <= MAX_TXS_PER_CHECKPOINT` (based on blob capacity), further restricted by `maxTxsPerBlock` when configured. Also applies to checkpoint-embedded blocks. | REJECT | MidToleranceError |
|
|
19
19
|
| 5 | **Embedded txs in txHashes**: every embedded tx's hash must appear in `txHashes` | REJECT | MidToleranceError |
|
|
20
|
-
| 6 | **Proposer check**: signer must match expected proposer for slot (skipped
|
|
20
|
+
| 6 | **Proposer check**: signer must match expected proposer for slot (skipped when the committee is `open`, i.e. size 0) | REJECT | MidToleranceError |
|
|
21
21
|
| 7 | **Tx hash integrity**: each embedded tx's recomputed hash must match declared hash | REJECT | LowToleranceError |
|
|
22
|
-
| 8 | **
|
|
22
|
+
| 8 | **No committee**: proposer lookup is `none` | REJECT | LowToleranceError |
|
|
23
|
+
| 9 | **Committee not stable**: proposer lookup is `unstable` (seed not fixed on L1), or it throws locally | IGNORE | none |
|
|
23
24
|
|
|
24
25
|
Deserialization guards: `BlockProposal.fromBuffer` and `SignedTxs.fromBuffer` both enforce `txCount <= MAX_TXS_PER_BLOCK` (65536). Violation -> REJECT + LowToleranceError.
|
|
25
26
|
|
|
@@ -29,7 +30,7 @@ Deserialization guards: `BlockProposal.fromBuffer` and `SignedTxs.fromBuffer` bo
|
|
|
29
30
|
|---|------|-------------|
|
|
30
31
|
| 9 | **Duplicate**: same archive root already stored | IGNORE (no penalty) |
|
|
31
32
|
| 10 | **Per-position cap**: max 2 proposals per (slot, indexWithinCheckpoint) | IGNORE (receiver-local drop: no penalty, no re-broadcast) |
|
|
32
|
-
| 11 | **Equivocation**: >1 distinct proposal for same (slot, index) | ACCEPT (rebroadcast for detection). At count=2: `duplicateProposalCallback` fires
|
|
33
|
+
| 11 | **Equivocation**: >1 distinct proposal for same (slot, index) | ACCEPT (rebroadcast for detection), not processed. At count=2: `duplicateProposalCallback` fires and the slot is marked as equivocated on the spot. No slash here: the count is per position, not per signer. The slasher's `BroadcastedProposalWatcher` later attributes `OffenseType.DUPLICATE_PROPOSAL` (configured via `slashDuplicateProposalPenalty`) to a signer only if that signer signed two different payloads at one position |
|
|
33
34
|
|
|
34
35
|
### Stage 3: Validator-Client Processing (BlockProposalHandler)
|
|
35
36
|
|
|
@@ -51,7 +52,7 @@ Only runs on validator nodes. Non-validator nodes use a default handler that tri
|
|
|
51
52
|
| 23 | **Re-execution**: no failed txs | `failed_txs` (ReExFailedTxsError) -- **SLASHABLE** |
|
|
52
53
|
| 24 | **Re-execution**: archive root and header match proposal | `state_mismatch` (ReExStateMismatchError) -- **SLASHABLE** |
|
|
53
54
|
|
|
54
|
-
**Escape hatch**: during escape hatch periods (`
|
|
55
|
+
**Escape hatch**: during escape hatch periods (`lookupEscapeHatchAtSlot`), re-execution and slashing are both disabled, and the proposal is rejected locally.
|
|
55
56
|
|
|
56
57
|
**Conditional re-execution**: rules 22-24 only run when at least one condition is true: `fishermanMode` enabled, `slashBroadcastedInvalidBlockPenalty > 0`, committee membership, `alwaysReexecuteBlockProposals`, or `blobClient.canUpload()`.
|
|
57
58
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
|
|
1
|
+
import type { EpochCacheInterface, ProposerLookup } from '@aztec-labs/epoch-cache';
|
|
2
2
|
import { type Logger, createLogger } from '@aztec-labs/foundation/log';
|
|
3
3
|
import { MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT, MAX_TXS_PER_CHECKPOINT } from '@aztec-labs/stdlib/deserialization';
|
|
4
4
|
import {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from '@aztec-labs/stdlib/p2p';
|
|
12
12
|
import type { ConsensusTimetable } from '@aztec-labs/stdlib/timetable';
|
|
13
13
|
|
|
14
|
-
import { mapEpochCacheLookupFailure } from '../epoch_cache_lookup.js';
|
|
14
|
+
import { ignoreUnstableCommittee, mapEpochCacheLookupFailure, rejectNoCommittee } from '../epoch_cache_lookup.js';
|
|
15
15
|
import { classifyReceiveWindowArrival } from '../receive_window.js';
|
|
16
16
|
|
|
17
17
|
/** Validates header-level and tx-level fields of block and checkpoint proposals. */
|
|
@@ -117,22 +117,31 @@ export class ProposalValidator {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
// relaying peer's fault) and rejects a missing committee (which is).
|
|
124
|
-
let expectedProposer;
|
|
120
|
+
// Only this lookup can fail on a receiver-local cause (L1 RPC down, or this node behind), which
|
|
121
|
+
// mapEpochCacheLookupFailure ignores as it is not the relaying peer's fault.
|
|
122
|
+
let expectedProposer: ProposerLookup;
|
|
125
123
|
try {
|
|
126
|
-
expectedProposer = await this.epochCache.
|
|
124
|
+
expectedProposer = await this.epochCache.lookupProposer(slotNumber);
|
|
127
125
|
} catch (e) {
|
|
128
126
|
return mapEpochCacheLookupFailure(e, this.logger, 'proposal', slotNumber);
|
|
129
127
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
expectedProposer,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
switch (expectedProposer.state) {
|
|
129
|
+
case 'unstable':
|
|
130
|
+
return ignoreUnstableCommittee(expectedProposer, this.logger, 'proposal', slotNumber);
|
|
131
|
+
case 'none':
|
|
132
|
+
return rejectNoCommittee();
|
|
133
|
+
case 'open':
|
|
134
|
+
// Anyone may propose, so there is no proposer to check the signer against.
|
|
135
|
+
break;
|
|
136
|
+
case 'members':
|
|
137
|
+
if (!proposer.equals(expectedProposer.proposer)) {
|
|
138
|
+
this.logger.warn(`Penalizing peer for invalid proposer for current slot ${slotNumber}`, {
|
|
139
|
+
expectedProposer: expectedProposer.proposer,
|
|
140
|
+
proposer: proposer.toString(),
|
|
141
|
+
});
|
|
142
|
+
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
return { result: 'accept' };
|
|
@@ -53,34 +53,68 @@ export class DataTxValidator implements TxValidator<Tx> {
|
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Checks the supplied public calldata entries against the calldata hashes of the public call requests. Every request
|
|
58
|
+
* must have an entry, and every entry must hash to its own hash and be used by some request. Several requests may
|
|
59
|
+
* share one entry, so a hash may appear at most as many times as requests use it. Both the calldata the calls receive
|
|
60
|
+
* and the supplied entries are capped. All of this is checked before hashing any entry.
|
|
61
|
+
*/
|
|
56
62
|
async #hasCorrectCalldata(tx: Tx): Promise<string | undefined> {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
63
|
+
const txHash = tx.getTxHash().toString();
|
|
64
|
+
const calldataMap = tx.getCalldataMap();
|
|
65
|
+
|
|
66
|
+
const requestCountByHash = new Map<string, number>();
|
|
67
|
+
for (const [index, { request }] of tx.getPublicCallRequestsWithCalldata().entries()) {
|
|
68
|
+
const hash = request.calldataHash.toString();
|
|
69
|
+
if (!calldataMap.has(hash)) {
|
|
70
|
+
const reason = TX_ERROR_INCORRECT_CALLDATA;
|
|
71
|
+
this.#log.verbose(`Rejecting tx ${txHash}. Reason: ${reason}. Call request has no calldata entry.`, {
|
|
72
|
+
txHash,
|
|
73
|
+
callRequestIndex: index,
|
|
74
|
+
});
|
|
75
|
+
return reason;
|
|
76
|
+
}
|
|
77
|
+
requestCountByHash.set(hash, (requestCountByHash.get(hash) ?? 0) + 1);
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
// A wrongly labeled entry can make the calls receive less calldata than is supplied, so both totals are capped.
|
|
81
|
+
const deliveredCount = tx.getDeliveredPublicCalldataCount();
|
|
82
|
+
const suppliedCount = tx.getTotalPublicCalldataCount();
|
|
83
|
+
if (
|
|
84
|
+
deliveredCount > MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS ||
|
|
85
|
+
suppliedCount > MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS
|
|
86
|
+
) {
|
|
68
87
|
const reason = TX_ERROR_CALLDATA_COUNT_TOO_LARGE;
|
|
69
88
|
this.#log.verbose(
|
|
70
|
-
`Rejecting tx ${
|
|
71
|
-
|
|
72
|
-
.toString()}. Reason: ${reason}. Expected no greater than ${MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS} fields. Got ${tx.getTotalPublicCalldataCount()}.`,
|
|
89
|
+
`Rejecting tx ${txHash}. Reason: ${reason}. Expected no greater than ${MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS} fields.`,
|
|
90
|
+
{ txHash, deliveredCount, suppliedCount },
|
|
73
91
|
);
|
|
74
92
|
return reason;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
const
|
|
78
|
-
for (
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
95
|
+
const entryCountByHash = new Map<string, number>();
|
|
96
|
+
for (const [index, cd] of tx.publicFunctionCalldata.entries()) {
|
|
97
|
+
const hash = cd.hash.toString();
|
|
98
|
+
const entryCount = (entryCountByHash.get(hash) ?? 0) + 1;
|
|
99
|
+
const requestCount = requestCountByHash.get(hash) ?? 0;
|
|
100
|
+
if (entryCount > requestCount) {
|
|
101
|
+
const reason = TX_ERROR_CALLDATA_COUNT_MISMATCH;
|
|
102
|
+
this.#log.verbose(
|
|
103
|
+
`Rejecting tx ${txHash}. Reason: ${reason}. Calldata entry is listed more times than call requests use it.`,
|
|
104
|
+
{ txHash, calldataIndex: index, requestCount },
|
|
105
|
+
);
|
|
106
|
+
return reason;
|
|
107
|
+
}
|
|
108
|
+
entryCountByHash.set(hash, entryCount);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const [index, cd] of tx.publicFunctionCalldata.entries()) {
|
|
112
|
+
if (!(await computeCalldataHash(cd.values)).equals(cd.hash)) {
|
|
82
113
|
const reason = TX_ERROR_INCORRECT_CALLDATA;
|
|
83
|
-
this.#log.verbose(`Rejecting tx ${
|
|
114
|
+
this.#log.verbose(`Rejecting tx ${txHash}. Reason: ${reason}. Calldata entry does not match its hash.`, {
|
|
115
|
+
txHash,
|
|
116
|
+
calldataIndex: index,
|
|
117
|
+
});
|
|
84
118
|
return reason;
|
|
85
119
|
}
|
|
86
120
|
}
|
|
@@ -121,6 +155,17 @@ export class DataTxValidator implements TxValidator<Tx> {
|
|
|
121
155
|
}
|
|
122
156
|
}
|
|
123
157
|
|
|
158
|
+
// The private kernel tail requires every contract class log within the claimed count to have a non-zero length,
|
|
159
|
+
// and the tx base treats a zero-length log as absent, so a tx carrying one can never be proven.
|
|
160
|
+
if (logHash.logHash.length === 0) {
|
|
161
|
+
const txHash = tx.getTxHash().toString();
|
|
162
|
+
this.#log.verbose(`Rejecting tx ${txHash} because of a zero-length contract class log at index ${i}.`, {
|
|
163
|
+
txHash,
|
|
164
|
+
logIndex: i,
|
|
165
|
+
});
|
|
166
|
+
return TX_ERROR_CONTRACT_CLASS_LOG_LENGTH;
|
|
167
|
+
}
|
|
168
|
+
|
|
124
169
|
// The tx base requires exactly the trimmed length; any other length makes the tx unprovable.
|
|
125
170
|
const expectedLength = 1 + tx.contractClassLogFields[i].fields.findLastIndex(f => !f.isZero());
|
|
126
171
|
if (logHash.logHash.length !== expectedLength) {
|
|
@@ -8,9 +8,10 @@ import type { PeerId } from '@libp2p/interface';
|
|
|
8
8
|
import { type Multiaddr, multiaddr } from '@multiformats/multiaddr';
|
|
9
9
|
import { Discv5, type Discv5EventEmitter, type IDiscv5CreateOptions } from '@nethermindeth/discv5';
|
|
10
10
|
import { ENR, SignableENR } from '@nethermindeth/enr';
|
|
11
|
+
import { Buffer } from 'buffer';
|
|
11
12
|
import EventEmitter from 'events';
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
+
import type { P2PConfig } from '../../config.js';
|
|
14
15
|
import { createNodeENR } from '../../enr/generate-enr.js';
|
|
15
16
|
import { AZTEC_ENR_KEY, Discv5Event, PeerEvent } from '../../types/index.js';
|
|
16
17
|
import { convertToMultiaddr } from '../../util.js';
|
|
@@ -126,7 +127,6 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService
|
|
|
126
127
|
...configOverrides.config,
|
|
127
128
|
},
|
|
128
129
|
metricsRegistry,
|
|
129
|
-
rateLimiterOpts: DISCV5_RATE_LIMITER_OPTS,
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
// Hook onto the onEstablished method to check the peer's version from the ENR,
|
|
@@ -4,6 +4,7 @@ import type { Gossipable, PeerErrorSeverity, TopicType } from '@aztec-labs/stdli
|
|
|
4
4
|
import { Tx, TxHash } from '@aztec-labs/stdlib/tx';
|
|
5
5
|
import type { PeerId } from '@libp2p/interface';
|
|
6
6
|
import type { ENR } from '@nethermindeth/enr';
|
|
7
|
+
import { Buffer } from 'buffer';
|
|
7
8
|
import EventEmitter from 'events';
|
|
8
9
|
|
|
9
10
|
import type { PeerManagerInterface } from './peer-manager/interface.js';
|
package/src/services/encoding.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createLogger } from '@aztec-labs/foundation/log';
|
|
|
3
3
|
import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec-labs/stdlib/p2p';
|
|
4
4
|
import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
|
|
5
5
|
import type { Message } from '@libp2p/interface';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
6
7
|
import { webcrypto } from 'node:crypto';
|
|
7
8
|
import { compressSync, uncompressSync } from 'snappy';
|
|
8
9
|
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from '@aztec-labs/stdlib/p2p';
|
|
34
34
|
import { ConsensusTimetable, getDefaultCheckpointProposalSyncGrace } from '@aztec-labs/stdlib/timetable';
|
|
35
35
|
import { MerkleTreeId } from '@aztec-labs/stdlib/trees';
|
|
36
|
-
import { Tx, type TxValidationResult } from '@aztec-labs/stdlib/tx';
|
|
36
|
+
import { Tx, type TxValidationResult, isReceiverLocalStateDrift } from '@aztec-labs/stdlib/tx';
|
|
37
37
|
import type { UInt64 } from '@aztec-labs/stdlib/types';
|
|
38
38
|
import { InvalidBlockProposalTxsError } from '@aztec-labs/stdlib/validators';
|
|
39
39
|
import { compressComponentVersions } from '@aztec-labs/stdlib/versioning';
|
|
@@ -62,6 +62,7 @@ import type { AddressManager, ConnectionManager } from '@libp2p/interface-intern
|
|
|
62
62
|
import { tcp } from '@libp2p/tcp';
|
|
63
63
|
import { multiaddr } from '@multiformats/multiaddr';
|
|
64
64
|
import { ENR } from '@nethermindeth/enr';
|
|
65
|
+
import { Buffer } from 'buffer';
|
|
65
66
|
import { createLibp2p } from 'libp2p';
|
|
66
67
|
|
|
67
68
|
import type { P2PConfig } from '../../config.js';
|
|
@@ -183,7 +184,10 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
183
184
|
private protocolVersion = '';
|
|
184
185
|
private topicStrings: Record<TopicType, string> = {} as Record<TopicType, string>;
|
|
185
186
|
|
|
186
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Callback invoked when a second proposal is seen at one position, so the slot is marked as equivocated. It does not
|
|
189
|
+
* slash: the slasher attributes DUPLICATE_PROPOSAL per signer from the retained proposals.
|
|
190
|
+
*/
|
|
187
191
|
private duplicateProposalCallback?: (info: {
|
|
188
192
|
slot: SlotNumber;
|
|
189
193
|
proposer: EthAddress;
|
|
@@ -818,15 +822,25 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
818
822
|
* empty committee every signer is accepted, so two distinct honest proposers for the same slot look
|
|
819
823
|
* like equivocation; without an expected proposer there is no one to attribute the offense to.
|
|
820
824
|
* Mirrors the guard in checkpoint_equivocation_watcher, which refuses to slash when the slot has no
|
|
821
|
-
* proposer.
|
|
825
|
+
* proposer. While the slot's committee is not stable on L1 there is no expected proposer yet either.
|
|
822
826
|
*/
|
|
823
827
|
private async canAttributeDuplicateProposal(slot: SlotNumber): Promise<boolean> {
|
|
824
|
-
const expectedProposer = await this.epochCache.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
+
const expectedProposer = await this.epochCache.lookupProposer(slot);
|
|
829
|
+
switch (expectedProposer.state) {
|
|
830
|
+
case 'members':
|
|
831
|
+
return true;
|
|
832
|
+
case 'open':
|
|
833
|
+
case 'unstable':
|
|
834
|
+
this.logger.warn(
|
|
835
|
+
`Not attributing duplicate proposal at slot ${slot}: no expected proposer (empty or not yet stable committee)`,
|
|
836
|
+
{ slot, committeeState: expectedProposer.state },
|
|
837
|
+
);
|
|
838
|
+
return false;
|
|
839
|
+
case 'none':
|
|
840
|
+
// The proposal validator rejects a slot without a committee before duplicates are counted, so this needs the
|
|
841
|
+
// committee to vanish in between. Throwing makes the gossip validator reject the message, as it does for that.
|
|
842
|
+
throw new Error(`No committee on L1 for slot ${slot}`);
|
|
828
843
|
}
|
|
829
|
-
return true;
|
|
830
844
|
}
|
|
831
845
|
|
|
832
846
|
/**
|
|
@@ -1222,9 +1236,24 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1222
1236
|
} else if (wasIgnored) {
|
|
1223
1237
|
return { result: TopicValidatorResult.Ignore, obj: tx };
|
|
1224
1238
|
} else {
|
|
1239
|
+
// The final pool insert rebuilt a fresh validator and rejected a tx that passed the earlier
|
|
1240
|
+
// checks and proof. If the rejection is receiver-local state drift, the relayer forwarded a tx
|
|
1241
|
+
// valid from its own view - ignore without penalizing it; only a sender-attributable reject penalizes.
|
|
1242
|
+
const rejectionReasons = addResult.rejectionReasons?.get(txHash.toString()) ?? [];
|
|
1243
|
+
if (isReceiverLocalStateDrift(rejectionReasons)) {
|
|
1244
|
+
this.logger.verbose(
|
|
1245
|
+
`Ignoring gossiped tx ${txHash.toString()}: pool rejected on receiver-local state drift`,
|
|
1246
|
+
{
|
|
1247
|
+
source: source.toString(),
|
|
1248
|
+
reasons: rejectionReasons,
|
|
1249
|
+
},
|
|
1250
|
+
);
|
|
1251
|
+
return { result: TopicValidatorResult.Ignore, obj: tx };
|
|
1252
|
+
}
|
|
1225
1253
|
this.logger.warn(`Gossiped tx ${txHash.toString()} unexpectedly rejected by pool`, {
|
|
1226
1254
|
source: source.toString(),
|
|
1227
1255
|
txHash: txHash.toString(),
|
|
1256
|
+
reasons: rejectionReasons,
|
|
1228
1257
|
});
|
|
1229
1258
|
return { result: TopicValidatorResult.Reject, severity: PeerErrorSeverity.HighToleranceError };
|
|
1230
1259
|
}
|
|
@@ -3,6 +3,7 @@ import { compactArray } from '@aztec-labs/foundation/collection';
|
|
|
3
3
|
import { makeEthSignDigest, tryRecoverAddress } from '@aztec-labs/foundation/crypto/secp256k1-signer';
|
|
4
4
|
import { Fr } from '@aztec-labs/foundation/curves/bn254';
|
|
5
5
|
import type { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
6
|
+
import { inspect } from '@aztec-labs/foundation/inspect';
|
|
6
7
|
import { createLogger } from '@aztec-labs/foundation/log';
|
|
7
8
|
import { bufferToHex } from '@aztec-labs/foundation/string';
|
|
8
9
|
import { DateProvider } from '@aztec-labs/foundation/timer';
|
|
@@ -13,7 +14,7 @@ import type { Connection, PeerId } from '@libp2p/interface';
|
|
|
13
14
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
14
15
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
15
16
|
import { ENR } from '@nethermindeth/enr';
|
|
16
|
-
import {
|
|
17
|
+
import { Buffer } from 'buffer';
|
|
17
18
|
|
|
18
19
|
import type { P2PConfig } from '../../config.js';
|
|
19
20
|
import { PeerEvent } from '../../types/index.js';
|
|
@@ -424,6 +425,9 @@ export class PeerManager implements PeerManagerInterface {
|
|
|
424
425
|
this.authenticatedPeerIdToValidatorAddress.delete(peerIdStr);
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
// Keep a disconnected non-healthy peer's score so a reconnect resumes its penalty rather than starting
|
|
429
|
+
// clean. The score and ban maps are bounded (PeerScoring retention caps), so retaining them cannot grow
|
|
430
|
+
// the heartbeat-scanned state without limit under identity churn. A healthy peer carries no penalty to keep.
|
|
427
431
|
if (this.peerScoring.getScoreState(peerIdStr) === PeerScoreState.Healthy) {
|
|
428
432
|
this.peerScoring.removePeer(peerIdStr);
|
|
429
433
|
}
|