@aztec-labs/p2p 6.0.0-rc.1 → 6.0.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.
- package/dest/bootstrap/bootstrap.d.ts +2 -2
- package/dest/bootstrap/bootstrap.d.ts.map +1 -1
- package/dest/bootstrap/bootstrap.js +4 -2
- package/dest/client/factory.d.ts +3 -3
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +6 -6
- package/dest/client/interface.d.ts +1 -1
- package/dest/client/p2p_client.d.ts +1 -1
- package/dest/client/p2p_client.js +1 -1
- package/dest/config.d.ts +11 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +16 -0
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +15 -3
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool.js +15 -0
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +21 -0
- package/dest/mem_pools/tx_pool_v2/eviction/eviction_manager.d.ts +6 -2
- 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 +9 -0
- package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.d.ts +10 -8
- package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.js +22 -11
- package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts +6 -2
- package/dest/mem_pools/tx_pool_v2/eviction/interfaces.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/interfaces.js +2 -1
- package/dest/mem_pools/tx_pool_v2/interfaces.d.ts +11 -5
- 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 +111 -44
- 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 +3 -24
- package/dest/msg_validators/epoch_cache_lookup.d.ts +18 -0
- package/dest/msg_validators/epoch_cache_lookup.d.ts.map +1 -0
- package/dest/msg_validators/epoch_cache_lookup.js +29 -0
- 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 +80 -82
- 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 +4 -3
- package/dest/msg_validators/tx_validator/factory.d.ts +40 -11
- package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/factory.js +36 -5
- package/dest/msg_validators/tx_validator/gas_validator.d.ts +18 -10
- package/dest/msg_validators/tx_validator/gas_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/gas_validator.js +37 -27
- 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 +3 -1
- package/dest/services/libp2p/libp2p_service.d.ts +18 -9
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +127 -29
- 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 +14 -0
- package/dest/services/tx_provider.d.ts +1 -1
- package/dest/services/tx_provider.d.ts.map +1 -1
- package/dest/services/tx_provider.js +3 -0
- package/dest/test-helpers/make-test-p2p-clients.d.ts +1 -1
- package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
- package/dest/test-helpers/make-test-p2p-clients.js +3 -1
- package/dest/test-helpers/mock-pubsub.js +1 -1
- 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 +3 -1
- package/dest/test-helpers/testbench-utils.d.ts +4 -2
- package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
- package/dest/test-helpers/testbench-utils.js +6 -0
- package/dest/testbench/p2p_client_testbench_worker.js +6 -2
- package/package.json +14 -14
- package/src/bootstrap/bootstrap.ts +3 -2
- package/src/client/factory.ts +7 -7
- package/src/client/interface.ts +1 -1
- package/src/client/p2p_client.ts +1 -1
- package/src/config.ts +11 -0
- package/src/mem_pools/attestation_pool/attestation_pool.ts +25 -0
- package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +31 -0
- package/src/mem_pools/tx_pool_v2/README.md +1 -1
- package/src/mem_pools/tx_pool_v2/eviction/eviction_manager.ts +13 -1
- package/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts +17 -9
- package/src/mem_pools/tx_pool_v2/eviction/interfaces.ts +6 -1
- package/src/mem_pools/tx_pool_v2/interfaces.ts +10 -4
- package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +95 -40
- package/src/msg_validators/attestation_validator/attestation_validator.ts +3 -23
- package/src/msg_validators/epoch_cache_lookup.ts +33 -0
- package/src/msg_validators/proposal_validator/README.md +3 -3
- package/src/msg_validators/proposal_validator/proposal_validator.ts +78 -77
- package/src/msg_validators/tx_validator/README.md +12 -8
- package/src/msg_validators/tx_validator/data_validator.ts +4 -3
- package/src/msg_validators/tx_validator/factory.ts +60 -14
- package/src/msg_validators/tx_validator/gas_validator.ts +46 -34
- package/src/services/discv5/discV5_service.ts +2 -1
- package/src/services/libp2p/libp2p_service.ts +138 -34
- package/src/services/peer-manager/peer_manager.ts +14 -0
- package/src/services/tx_provider.ts +3 -0
- package/src/test-helpers/make-test-p2p-clients.ts +5 -1
- package/src/test-helpers/mock-pubsub.ts +3 -3
- package/src/test-helpers/reqresp-nodes.ts +5 -1
- package/src/test-helpers/testbench-utils.ts +13 -1
- package/src/testbench/p2p_client_testbench_worker.ts +10 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SlotNumber } from '@aztec-labs/foundation/branded-types';
|
|
2
2
|
import type { TypedEventEmitter } from '@aztec-labs/foundation/types';
|
|
3
3
|
import type { L2Block, L2BlockId, L2BlockSource } from '@aztec-labs/stdlib/block';
|
|
4
|
-
import type {
|
|
4
|
+
import type { NextBlockMinFeesProvider } from '@aztec-labs/stdlib/gas';
|
|
5
5
|
import type { WorldStateSynchronizer } from '@aztec-labs/stdlib/interfaces/server';
|
|
6
6
|
import type { BlockHeader, Tx, TxHash, TxValidator } from '@aztec-labs/stdlib/tx';
|
|
7
7
|
|
|
@@ -81,8 +81,11 @@ export type TxPoolV2Dependencies = {
|
|
|
81
81
|
createTxValidator: () => Promise<TxValidator<TxMetaData>>;
|
|
82
82
|
/** Checks whether a tx's setup-phase calls are on the allow list. Precomputed at receipt time. */
|
|
83
83
|
checkAllowedSetupCalls: (tx: Tx) => Promise<boolean>;
|
|
84
|
-
/**
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Provides the fee the next block will charge. Used by eviction rules instead of stale block header fees. Only the
|
|
86
|
+
* exact fee: admission pricing is applied by the validator `createTxValidator` builds.
|
|
87
|
+
*/
|
|
88
|
+
nextBlockMinFeesProvider: NextBlockMinFeesProvider;
|
|
86
89
|
};
|
|
87
90
|
|
|
88
91
|
/**
|
|
@@ -163,6 +166,7 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
|
|
|
163
166
|
|
|
164
167
|
/**
|
|
165
168
|
* Handles a mined block - marks transactions as mined and evicts conflicting pending txs.
|
|
169
|
+
* Soft-deleted transactions in the block are re-added as mined from their retained data.
|
|
166
170
|
* Uses nullifiers directly from the block to evict pending transactions with conflicts.
|
|
167
171
|
* @param block - The complete mined block
|
|
168
172
|
*/
|
|
@@ -197,7 +201,9 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
|
|
|
197
201
|
|
|
198
202
|
/**
|
|
199
203
|
* Handles failed transaction execution.
|
|
200
|
-
* Deletes transactions that failed during block building.
|
|
204
|
+
* Deletes transactions that failed during block building. Transactions already mined are left in place, since a
|
|
205
|
+
* block containing them may have been synced while they were being executed, and they must be kept until that
|
|
206
|
+
* block is finalized.
|
|
201
207
|
* @param txHashes - Hashes of transactions that failed
|
|
202
208
|
*/
|
|
203
209
|
handleFailedExecution(txHashes: TxHash[]): Promise<void>;
|
|
@@ -139,7 +139,7 @@ export class TxPoolV2Impl {
|
|
|
139
139
|
|
|
140
140
|
// Post-event eviction rules (run after events to check ALL pending txs)
|
|
141
141
|
this.#evictionManager.registerRule(new InvalidTxsAfterMiningRule());
|
|
142
|
-
this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule(deps.
|
|
142
|
+
this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule(deps.nextBlockMinFeesProvider));
|
|
143
143
|
this.#evictionManager.registerRule(new InvalidTxsAfterReorgRule(deps.worldStateSynchronizer));
|
|
144
144
|
this.#evictionManager.registerRule(new FeePayerBalanceEvictionRule(deps.worldStateSynchronizer));
|
|
145
145
|
// LowPriorityEvictionRule handles cases where txs become pending via prepareForSlot (unprotect)
|
|
@@ -466,27 +466,21 @@ export class TxPoolV2Impl {
|
|
|
466
466
|
const missing: TxHash[] = [];
|
|
467
467
|
let softDeletedHits = 0;
|
|
468
468
|
let missingPreviouslyEvicted = 0;
|
|
469
|
+
// A soft-deleted tx is never in the indices, so every tx loaded here is resurrected below.
|
|
470
|
+
const softDeleted = await this.#loadSoftDeletedTxs(txHashes);
|
|
469
471
|
|
|
470
472
|
await this.#store.transactionAsync(async () => {
|
|
471
473
|
for (const txHash of txHashes) {
|
|
472
474
|
const txHashStr = txHash.toString();
|
|
475
|
+
const resurrectable = softDeleted.get(txHashStr);
|
|
473
476
|
|
|
474
477
|
if (this.#indices.has(txHashStr)) {
|
|
475
478
|
// Update protection for existing tx
|
|
476
479
|
this.#indices.updateProtection(txHashStr, slotNumber);
|
|
477
|
-
} else if (
|
|
478
|
-
// Resurrect soft-deleted tx as protected
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (buffer) {
|
|
482
|
-
const tx = await this.#loadTxWithProof(txHashStr, buffer);
|
|
483
|
-
await this.#addTx(tx, { protected: slotNumber });
|
|
484
|
-
softDeletedHits++;
|
|
485
|
-
} else {
|
|
486
|
-
// Data missing despite soft-delete flag — treat as truly missing
|
|
487
|
-
this.#indices.setProtection(txHashStr, slotNumber);
|
|
488
|
-
missing.push(txHash);
|
|
489
|
-
}
|
|
480
|
+
} else if (resurrectable) {
|
|
481
|
+
// Resurrect soft-deleted tx as protected
|
|
482
|
+
await this.#addTx(resurrectable.tx, { protected: slotNumber }, {}, resurrectable.meta);
|
|
483
|
+
softDeletedHits++;
|
|
490
484
|
} else {
|
|
491
485
|
// Truly missing — pre-record protection for tx we don't have yet
|
|
492
486
|
this.#indices.setProtection(txHashStr, slotNumber);
|
|
@@ -546,41 +540,56 @@ export class TxPoolV2Impl {
|
|
|
546
540
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
547
541
|
const nullifiers = block.body.txEffects.flatMap(tx => tx.nullifiers.map(n => n.toString()));
|
|
548
542
|
|
|
549
|
-
//
|
|
550
|
-
const
|
|
543
|
+
// A soft-deleted tx is never in the indices, so every tx loaded here is resurrected below.
|
|
544
|
+
const softDeleted = await this.#loadSoftDeletedTxs(txHashes);
|
|
551
545
|
const found: TxMetaData[] = [];
|
|
552
|
-
|
|
553
|
-
const meta = this.#indices.getMetadata(txHash.toString());
|
|
554
|
-
if (meta) {
|
|
555
|
-
feePayers.push(meta.feePayer);
|
|
556
|
-
found.push(meta);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
546
|
+
const resurrected: TxMetaData[] = [];
|
|
559
547
|
|
|
560
548
|
await this.#store.transactionAsync(async () => {
|
|
561
|
-
// Step
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
549
|
+
// Step 3: Mark txs as mined, resurrecting soft-deleted ones. A mined tx must be kept until its block is
|
|
550
|
+
// finalized, whereas a slot-soft-deleted one is hard-deleted on the next slot.
|
|
551
|
+
for (const txHash of txHashes) {
|
|
552
|
+
const txHashStr = txHash.toString();
|
|
553
|
+
const meta = this.#indices.getMetadata(txHashStr);
|
|
554
|
+
const resurrectable = softDeleted.get(txHashStr);
|
|
555
|
+
if (meta) {
|
|
556
|
+
this.#indices.markAsMined(meta, blockId);
|
|
557
|
+
found.push(meta);
|
|
558
|
+
} else if (resurrectable) {
|
|
559
|
+
resurrected.push(await this.#addTx(resurrectable.tx, { mined: blockId }, {}, resurrectable.meta));
|
|
560
|
+
} else {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
await this.#deletedPool.clearIfMinedHigher(txHashStr, blockId.number);
|
|
565
564
|
}
|
|
566
565
|
|
|
567
|
-
// Step
|
|
568
|
-
|
|
566
|
+
// Step 4: Run post-event eviction rules (inside transaction for atomicity)
|
|
567
|
+
const feePayers = new Set([...found, ...resurrected].map(meta => meta.feePayer));
|
|
568
|
+
await this.#evictionManager.evictAfterNewBlock(block.header, nullifiers, [...feePayers]);
|
|
569
569
|
});
|
|
570
570
|
|
|
571
|
-
if (
|
|
571
|
+
if (resurrected.length > 0) {
|
|
572
|
+
this.#instrumentation.recordSoftDeletedHits(resurrected.length);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (found.length > 0 || resurrected.length > 0) {
|
|
572
576
|
// receivedAt is 0 for txs hydrated from the DB on restart (true receive time lost) — leave
|
|
573
|
-
// their delay undefined so the metric isn't polluted by epoch-sized values.
|
|
577
|
+
// their delay undefined so the metric isn't polluted by epoch-sized values. A resurrected tx
|
|
578
|
+
// lost its receive time along with its metadata when it was deleted.
|
|
574
579
|
const now = this.#dateProvider.now();
|
|
575
|
-
this.#callbacks.onTxsMined(
|
|
576
|
-
found.map(m => ({
|
|
580
|
+
this.#callbacks.onTxsMined([
|
|
581
|
+
...found.map(m => ({
|
|
577
582
|
txHash: m.txHash,
|
|
578
583
|
minedDelayMs: m.receivedAt > 0 ? now - m.receivedAt : undefined,
|
|
579
584
|
})),
|
|
580
|
-
|
|
585
|
+
...resurrected.map(m => ({ txHash: m.txHash, minedDelayMs: undefined })),
|
|
586
|
+
]);
|
|
581
587
|
}
|
|
582
588
|
|
|
583
|
-
this.#log.info(`Marked ${found.length} txs as mined in block ${blockId.number}
|
|
589
|
+
this.#log.info(`Marked ${found.length + resurrected.length} txs as mined in block ${blockId.number}`, {
|
|
590
|
+
blockNumber: blockId.number,
|
|
591
|
+
resurrectedTxHashes: resurrected.map(m => m.txHash),
|
|
592
|
+
});
|
|
584
593
|
}
|
|
585
594
|
|
|
586
595
|
async prepareForSlot(slotNumber: SlotNumber): Promise<void> {
|
|
@@ -598,11 +607,14 @@ export class TxPoolV2Impl {
|
|
|
598
607
|
const txsToRestore = this.#indices.filterRestorable(expiredProtected);
|
|
599
608
|
if (txsToRestore.length === 0) {
|
|
600
609
|
this.#log.debug(`Preparing for slot ${slotNumber}, no txs to unprotect`);
|
|
601
|
-
|
|
610
|
+
} else {
|
|
611
|
+
this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`);
|
|
612
|
+
await this.#restoreUnprotectedToPending(txsToRestore, 'during prepareForSlot');
|
|
602
613
|
}
|
|
603
614
|
|
|
604
|
-
|
|
605
|
-
|
|
615
|
+
// Step 4: Sweep pending txs that can no longer pay the next block's fee. The fee is stable
|
|
616
|
+
// across the slot, so this runs here instead of after every mined block.
|
|
617
|
+
await this.#evictionManager.evictAfterSlotPrepared(slotNumber);
|
|
606
618
|
});
|
|
607
619
|
}
|
|
608
620
|
|
|
@@ -719,11 +731,24 @@ export class TxPoolV2Impl {
|
|
|
719
731
|
}
|
|
720
732
|
|
|
721
733
|
async handleFailedExecution(txHashes: TxHash[]): Promise<void> {
|
|
734
|
+
// A tx can be mined by a block that arrives while the sequencer is still executing it. A mined tx must be kept
|
|
735
|
+
// until its block is finalized, whereas deleting it could slot-soft-delete it and lose it on the next slot.
|
|
736
|
+
const toDelete: string[] = [];
|
|
737
|
+
const skippedMined: string[] = [];
|
|
738
|
+
for (const txHash of txHashes) {
|
|
739
|
+
const txHashStr = txHash.toString();
|
|
740
|
+
const isMined = this.#indices.getMetadata(txHashStr)?.minedL2BlockId !== undefined;
|
|
741
|
+
(isMined ? skippedMined : toDelete).push(txHashStr);
|
|
742
|
+
}
|
|
743
|
+
|
|
722
744
|
await this.#store.transactionAsync(async () => {
|
|
723
|
-
await this.#deleteTxsBatch(
|
|
745
|
+
await this.#deleteTxsBatch(toDelete);
|
|
724
746
|
});
|
|
725
747
|
|
|
726
|
-
this.#log.info(`Deleted ${
|
|
748
|
+
this.#log.info(`Deleted ${toDelete.length} failed txs, skipped ${skippedMined.length} already mined`, {
|
|
749
|
+
txHashes: toDelete,
|
|
750
|
+
skippedMinedTxHashes: skippedMined,
|
|
751
|
+
});
|
|
727
752
|
}
|
|
728
753
|
|
|
729
754
|
/**
|
|
@@ -1021,6 +1046,36 @@ export class TxPoolV2Impl {
|
|
|
1021
1046
|
return meta;
|
|
1022
1047
|
}
|
|
1023
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* Loads the retained data of those given txs that are soft-deleted and rebuilds their metadata, so they can be
|
|
1051
|
+
* re-added to the pool. Call it before the store transaction: it does all the throwable I/O, and leaves out a tx
|
|
1052
|
+
* whose data is gone or unreadable, as if it were missing.
|
|
1053
|
+
*/
|
|
1054
|
+
async #loadSoftDeletedTxs(txHashes: TxHash[]): Promise<Map<string, { tx: Tx; meta: TxMetaData }>> {
|
|
1055
|
+
const loaded = new Map<string, { tx: Tx; meta: TxMetaData }>();
|
|
1056
|
+
// Loaded sequentially: usually none of the given txs are soft-deleted, and each one that is costs only local reads.
|
|
1057
|
+
for (const txHash of txHashes) {
|
|
1058
|
+
const txHashStr = txHash.toString();
|
|
1059
|
+
if (!this.#deletedPool.isSoftDeleted(txHashStr)) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
const buffer = await this.#txsDB.getAsync(txHashStr);
|
|
1064
|
+
if (!buffer) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
// Load the proof too: #addTx rewrites both entries, so re-adding a proofless tx would wipe the stored proof.
|
|
1068
|
+
const tx = await this.#loadTxWithProof(txHashStr, buffer);
|
|
1069
|
+
// The metadata was dropped on deletion, so recompute the setup-call flag instead of defaulting it to allowed.
|
|
1070
|
+
const meta = await buildTxMetaData(tx, await this.#checkAllowedSetupCalls(tx));
|
|
1071
|
+
loaded.set(txHashStr, { tx, meta });
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
this.#log.warn(`Failed to load soft-deleted tx ${txHashStr}`, { txHash: txHashStr, err });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return loaded;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1024
1079
|
/**
|
|
1025
1080
|
* Deletes a transaction from both indices and DB.
|
|
1026
1081
|
* Emits onTxsRemoved callback immediately after DB delete.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
|
|
2
|
-
import { NoCommitteeError } from '@aztec-labs/ethereum/contracts';
|
|
3
2
|
import { type Logger, createLogger } from '@aztec-labs/foundation/log';
|
|
4
3
|
import {
|
|
5
4
|
type CheckpointAttestation,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
} from '@aztec-labs/stdlib/p2p';
|
|
12
11
|
import type { ConsensusTimetable } from '@aztec-labs/stdlib/timetable';
|
|
13
12
|
|
|
13
|
+
import { mapEpochCacheLookupFailure } from '../epoch_cache_lookup.js';
|
|
14
14
|
import { classifyReceiveWindowArrival } from '../receive_window.js';
|
|
15
15
|
|
|
16
16
|
export class CheckpointAttestationValidator implements P2PValidator<CheckpointAttestation> {
|
|
@@ -38,26 +38,6 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
38
38
|
async validate(message: CheckpointAttestation): Promise<ValidationResult> {
|
|
39
39
|
const slotNumber = message.payload.header.slotNumber;
|
|
40
40
|
|
|
41
|
-
// Only the epochCache committee/proposer lookups can fail on receiver-local causes (L1 RPC down,
|
|
42
|
-
// or this node behind), and that says nothing about the relaying peer. Map such a failure to
|
|
43
|
-
// ignore rather than letting it throw, since a thrown validation defaults to reject + penalize
|
|
44
|
-
// and would punish an honest relayer for our own failure. A missing committee is still the peer's
|
|
45
|
-
// fault (they shouldn't be sending it), so it stays a reject. This is scoped to just the lookups:
|
|
46
|
-
// any other error in validation still throws and surfaces.
|
|
47
|
-
const onLookupFailure = (e: unknown): ValidationResult => {
|
|
48
|
-
if (e instanceof NoCommitteeError) {
|
|
49
|
-
this.logger.warn(`No committee exists for checkpoint attestation for slot ${slotNumber}`);
|
|
50
|
-
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
51
|
-
}
|
|
52
|
-
this.logger.warn(
|
|
53
|
-
`Ignoring checkpoint attestation for slot ${slotNumber} after a local committee lookup failure`,
|
|
54
|
-
{
|
|
55
|
-
error: e instanceof Error ? e.message : String(e),
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
return { result: 'ignore' };
|
|
59
|
-
};
|
|
60
|
-
|
|
61
41
|
// Cross-chain replay check: reject attestations that carry a foreign signing domain.
|
|
62
42
|
if (!hasValidSignatureContext(message.payload, this.signatureContext)) {
|
|
63
43
|
this.logger.warn(`Rejecting checkpoint attestation with foreign signature context for slot ${slotNumber}`, {
|
|
@@ -108,7 +88,7 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
108
88
|
try {
|
|
109
89
|
inCommittee = await this.epochCache.isInCommittee(slotNumber, attester);
|
|
110
90
|
} catch (e) {
|
|
111
|
-
return
|
|
91
|
+
return mapEpochCacheLookupFailure(e, this.logger, 'checkpoint attestation', slotNumber);
|
|
112
92
|
}
|
|
113
93
|
if (!inCommittee) {
|
|
114
94
|
this.logger.warn(`Attester ${attester.toString()} is not in committee for slot ${slotNumber}`);
|
|
@@ -123,7 +103,7 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
|
|
|
123
103
|
try {
|
|
124
104
|
expectedProposer = await this.epochCache.getProposerAttesterAddressInSlot(slotNumber);
|
|
125
105
|
} catch (e) {
|
|
126
|
-
return
|
|
106
|
+
return mapEpochCacheLookupFailure(e, this.logger, 'checkpoint attestation', slotNumber);
|
|
127
107
|
}
|
|
128
108
|
if (!expectedProposer) {
|
|
129
109
|
this.logger.warn(`No proposer defined for slot ${slotNumber}`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NoCommitteeError } from '@aztec-labs/ethereum/contracts';
|
|
2
|
+
import type { SlotNumber } from '@aztec-labs/foundation/branded-types';
|
|
3
|
+
import type { Logger } from '@aztec-labs/foundation/log';
|
|
4
|
+
import { PeerErrorSeverity, type ValidationResult } from '@aztec-labs/stdlib/p2p';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps a failure of an epoch-cache lookup during p2p validation to a verdict, shared by the proposal
|
|
8
|
+
* and attestation validators so the "a local epoch-cache failure is never the relaying peer's fault"
|
|
9
|
+
* rule lives in one place.
|
|
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").
|
|
18
|
+
*/
|
|
19
|
+
export function mapEpochCacheLookupFailure(
|
|
20
|
+
e: unknown,
|
|
21
|
+
logger: Logger,
|
|
22
|
+
subject: string,
|
|
23
|
+
slotNumber: SlotNumber,
|
|
24
|
+
): ValidationResult {
|
|
25
|
+
if (e instanceof NoCommitteeError) {
|
|
26
|
+
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
27
|
+
}
|
|
28
|
+
logger.warn(`Ignoring ${subject} for slot ${slotNumber} after a local epoch-cache lookup failure`, {
|
|
29
|
+
slotNumber,
|
|
30
|
+
error: e instanceof Error ? e.message : String(e),
|
|
31
|
+
});
|
|
32
|
+
return { result: 'ignore' };
|
|
33
|
+
}
|
|
@@ -28,7 +28,7 @@ Deserialization guards: `BlockProposal.fromBuffer` and `SignedTxs.fromBuffer` bo
|
|
|
28
28
|
| # | Rule | Consequence |
|
|
29
29
|
|---|------|-------------|
|
|
30
30
|
| 9 | **Duplicate**: same archive root already stored | IGNORE (no penalty) |
|
|
31
|
-
| 10 | **Per-position cap**: max 2 proposals per (slot, indexWithinCheckpoint) |
|
|
31
|
+
| 10 | **Per-position cap**: max 2 proposals per (slot, indexWithinCheckpoint) | IGNORE (receiver-local drop: no penalty, no re-broadcast) |
|
|
32
32
|
| 11 | **Equivocation**: >1 distinct proposal for same (slot, index) | ACCEPT (rebroadcast for detection). At count=2: `duplicateProposalCallback` fires -> slash event (`OffenseType.DUPLICATE_PROPOSAL`, configured via `slashDuplicateProposalPenalty`) |
|
|
33
33
|
|
|
34
34
|
### Stage 3: Validator-Client Processing (BlockProposalHandler)
|
|
@@ -84,7 +84,7 @@ The checkpoint's embedded `lastBlock` is extracted via `getBlockProposal()` and
|
|
|
84
84
|
| Rule | Consequence | File |
|
|
85
85
|
|------|-------------|------|
|
|
86
86
|
| Block proposal must pass `BlockProposalValidator.validate()` | If REJECT: entire checkpoint REJECTED | `libp2p_service.ts` |
|
|
87
|
-
| Block proposal must not exceed per-position cap (2) | Checkpoint
|
|
87
|
+
| Block proposal must not exceed per-position cap (2) | Checkpoint IGNORED (terminal block dropped as a receiver-local drop: no store, no re-broadcast, no penalty) | same |
|
|
88
88
|
| Block equivocation detected (>1 proposals for same slot+index) | Checkpoint REJECTED (block itself is ACCEPT for re-broadcast) | same |
|
|
89
89
|
|
|
90
90
|
### Stage 3: Mempool (Attestation Pool)
|
|
@@ -92,7 +92,7 @@ The checkpoint's embedded `lastBlock` is extracted via `getBlockProposal()` and
|
|
|
92
92
|
| Rule | Consequence | File |
|
|
93
93
|
|------|-------------|------|
|
|
94
94
|
| Duplicate (same archive ID) | IGNORE (no penalty). Embedded block still processed if valid. | `attestation_pool.ts` |
|
|
95
|
-
| Per-slot cap: `MAX_CHECKPOINT_PROPOSALS_PER_SLOT` = 2 |
|
|
95
|
+
| Per-slot cap: `MAX_CHECKPOINT_PROPOSALS_PER_SLOT` = 2 | IGNORE (receiver-local drop: no penalty, no re-broadcast). Embedded block still processed. | same |
|
|
96
96
|
|
|
97
97
|
### Stage 4: Equivocation Detection
|
|
98
98
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec-labs/epoch-cache';
|
|
2
|
-
import { NoCommitteeError } from '@aztec-labs/ethereum/contracts';
|
|
3
2
|
import { type Logger, createLogger } from '@aztec-labs/foundation/log';
|
|
4
3
|
import { MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT, MAX_TXS_PER_CHECKPOINT } from '@aztec-labs/stdlib/deserialization';
|
|
5
4
|
import {
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
} from '@aztec-labs/stdlib/p2p';
|
|
13
12
|
import type { ConsensusTimetable } from '@aztec-labs/stdlib/timetable';
|
|
14
13
|
|
|
14
|
+
import { mapEpochCacheLookupFailure } 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. */
|
|
@@ -50,91 +50,92 @@ export class ProposalValidator {
|
|
|
50
50
|
|
|
51
51
|
/** Validates header-level fields: slot, signature, and proposer. */
|
|
52
52
|
public async validate(proposal: BlockProposal | CheckpointProposalCore): Promise<ValidationResult> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
53
|
+
// Cross-chain replay check: reject proposals that carry a foreign signing domain.
|
|
54
|
+
if (!hasValidSignatureContext(proposal, this.signatureContext)) {
|
|
55
|
+
this.logger.warn(`Penalizing peer for proposal with foreign signature context`, {
|
|
56
|
+
chainId: proposal.signatureContext.chainId,
|
|
57
|
+
rollupAddress: proposal.signatureContext.rollupAddress.toString(),
|
|
58
|
+
expectedChainId: this.signatureContext.chainId,
|
|
59
|
+
expectedRollupAddress: this.signatureContext.rollupAddress.toString(),
|
|
60
|
+
});
|
|
61
|
+
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
62
|
+
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
// Slot check: the tight checkpoint proposal receive window (`[receiveStart - δ, target_slot_start -
|
|
65
|
+
// E - D + δ]`) is the sole acceptance gate, applied to both block and checkpoint proposals. The
|
|
66
|
+
// window itself bounds which slots are valid, so far/wrong slots fall outside it. Every block
|
|
67
|
+
// proposal for slot N is sent before the checkpoint proposal for slot N, so nothing legitimate can
|
|
68
|
+
// arrive after the checkpoint receive deadline; gating block proposals on the same window rejects
|
|
69
|
+
// late block proposals at p2p ingress. The attestation deadline remains their re-execution/
|
|
70
|
+
// validation deadline downstream, not their arrival gate.
|
|
71
|
+
const slotNumber = proposal.slotNumber;
|
|
72
|
+
if (!this.skipSlotValidation) {
|
|
73
|
+
// Proposal receive window: [checkpoint_proposal_receive_start, checkpoint_proposal_receive_deadline],
|
|
74
|
+
// widened by the configured clock-disparity tolerance on both ends.
|
|
75
|
+
const startSeconds = this.timetable.getCheckpointProposalReceiveStart(slotNumber);
|
|
76
|
+
const deadlineSeconds = this.timetable.getCheckpointProposalReceiveDeadline(slotNumber);
|
|
77
|
+
const nowMs = Number(this.epochCache.getEpochAndSlotNow().nowMs);
|
|
78
|
+
const windowMiss = classifyReceiveWindowArrival(
|
|
79
|
+
nowMs,
|
|
80
|
+
startSeconds * 1000 - this.clockDisparityMs,
|
|
81
|
+
deadlineSeconds * 1000 + this.clockDisparityMs,
|
|
82
|
+
);
|
|
83
|
+
if (windowMiss) {
|
|
84
|
+
this.logger.warn(
|
|
85
|
+
`Proposal for slot ${slotNumber} is outside its receive window (${windowMiss.outcome.result})`,
|
|
86
|
+
{
|
|
87
|
+
slotNumber,
|
|
88
|
+
nowMs,
|
|
89
|
+
missMs: windowMiss.missMs,
|
|
90
|
+
windowStartSeconds: startSeconds,
|
|
91
|
+
windowDeadlineSeconds: deadlineSeconds,
|
|
92
|
+
},
|
|
83
93
|
);
|
|
84
|
-
|
|
85
|
-
this.logger.warn(
|
|
86
|
-
`Proposal for slot ${slotNumber} is outside its receive window (${windowMiss.outcome.result})`,
|
|
87
|
-
{
|
|
88
|
-
slotNumber,
|
|
89
|
-
nowMs,
|
|
90
|
-
missMs: windowMiss.missMs,
|
|
91
|
-
windowStartSeconds: startSeconds,
|
|
92
|
-
windowDeadlineSeconds: deadlineSeconds,
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
return windowMiss.outcome;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Signature validity
|
|
100
|
-
const proposer = proposal.getSender();
|
|
101
|
-
if (!proposer) {
|
|
102
|
-
this.logger.warn(`Penalizing peer for proposal with invalid signature`);
|
|
103
|
-
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
|
|
94
|
+
return windowMiss.outcome;
|
|
104
95
|
}
|
|
96
|
+
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expectedProposer,
|
|
113
|
-
proposer: proposer.toString(),
|
|
114
|
-
});
|
|
115
|
-
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
|
|
116
|
-
}
|
|
98
|
+
// Signature validity
|
|
99
|
+
const proposer = proposal.getSender();
|
|
100
|
+
if (!proposer) {
|
|
101
|
+
this.logger.warn(`Penalizing peer for proposal with invalid signature`);
|
|
102
|
+
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
|
|
103
|
+
}
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
105
|
+
// A block proposal whose index lands at or beyond the hard attestable ceiling is structurally
|
|
106
|
+
// impossible garbage, so reject it immediately at ingress. This is peer-attributable and needs no
|
|
107
|
+
// epoch-cache lookup, so it runs before the proposer lookup: a structurally-invalid index must be
|
|
108
|
+
// rejected even when a local lookup fails. Indices in
|
|
109
|
+
// `[maxBlocksPerCheckpoint, MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT)` are over the consensus limit
|
|
110
|
+
// but structurally valid proposer misbehavior; they pass gossip validation here so the offending
|
|
111
|
+
// proposal can be retained and re-broadcast as slashing evidence (handled downstream in the p2p
|
|
112
|
+
// service), rather than penalizing the relaying peer.
|
|
113
|
+
if ('indexWithinCheckpoint' in proposal) {
|
|
114
|
+
const indexResult = this.validateBlockIndexWithinCheckpoint(proposal);
|
|
115
|
+
if (indexResult.result !== 'accept') {
|
|
116
|
+
return indexResult;
|
|
129
117
|
}
|
|
118
|
+
}
|
|
130
119
|
|
|
131
|
-
|
|
120
|
+
// An undefined proposer means an empty committee (anyone may propose), so skip the
|
|
121
|
+
// proposer-equality check and keep validating. Only this lookup can fail on a receiver-local
|
|
122
|
+
// cause (L1 RPC down, or this node behind); mapEpochCacheLookupFailure ignores that (it is not the
|
|
123
|
+
// relaying peer's fault) and rejects a missing committee (which is).
|
|
124
|
+
let expectedProposer;
|
|
125
|
+
try {
|
|
126
|
+
expectedProposer = await this.epochCache.getProposerAttesterAddressInSlot(slotNumber);
|
|
132
127
|
} catch (e) {
|
|
133
|
-
|
|
134
|
-
return { result: 'reject', severity: PeerErrorSeverity.LowToleranceError };
|
|
135
|
-
}
|
|
136
|
-
throw e;
|
|
128
|
+
return mapEpochCacheLookupFailure(e, this.logger, 'proposal', slotNumber);
|
|
137
129
|
}
|
|
130
|
+
if (expectedProposer !== undefined && !proposer.equals(expectedProposer)) {
|
|
131
|
+
this.logger.warn(`Penalizing peer for invalid proposer for current slot ${slotNumber}`, {
|
|
132
|
+
expectedProposer,
|
|
133
|
+
proposer: proposer.toString(),
|
|
134
|
+
});
|
|
135
|
+
return { result: 'reject', severity: PeerErrorSeverity.MidToleranceError };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { result: 'accept' };
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/**
|
|
@@ -22,12 +22,14 @@ Unsolicited transactions from any peer. Fully validated in two stages with a poo
|
|
|
22
22
|
|
|
23
23
|
| Step | What runs | On failure |
|
|
24
24
|
|------|-----------|------------|
|
|
25
|
-
| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, MinGasLimits, MaxGasLimits,
|
|
25
|
+
| **Stage 1** (fast) | TxPermitted, Data, Metadata, Timestamp, DoubleSpend, MinGasLimits, MaxGasLimits, MaxFeePerGas, MaxFeePerGasFloor, FeePayerBalance, Phases, BlockHeader | Penalize peer, reject tx (except MaxFeePerGas: ignore tx, no penalty) |
|
|
26
26
|
| **Pool pre-check** | `canAddPendingTx` — checks for duplicates, pool capacity | Ignore tx (no penalty) |
|
|
27
27
|
| **Stage 2** (slow) | Proof verification | Penalize peer, reject tx |
|
|
28
28
|
| **Pool add** | `addPendingTxs` | Accept, ignore, or reject |
|
|
29
29
|
|
|
30
|
-
Each stage-1 and stage-2 validator is paired with a `PeerErrorSeverity
|
|
30
|
+
Each stage-1 and stage-2 validator is paired with a `PeerErrorSeverity`, and the sending peer is penalized with that severity when it fails. When several validators fail, the harshest consequence wins. The `doubleSpendValidator` has special handling: its severity is determined by how recently the nullifier appeared (recent = high tolerance, old = low tolerance).
|
|
31
|
+
|
|
32
|
+
`maxFeePerGasValidator` is the exception, marked `IgnoreWithoutPenalty`: a tx below this node's admission fee (the next-block fee, or the L1-forward fee while the next block cannot be priced) may be perfectly valid for a peer ahead of us, so it is dropped without propagating and the peer keeps its score. That excuse stops at a penalty floor, half the lower of the admission and L1-forward fees: `maxFeePerGasFloorValidator` checks it with a mid-tolerance penalty. The slack covers a peer that has seen a checkpoint we have not, or a fresher L1 block, while zero-fee and negligible-fee spam still costs the sender. Being the mildest consequence, the ignore never masks a penalized failure on the same tx — which is also why the fee-payer balance check is a separate entry (`feePayerBalanceValidator`) that stays penalized.
|
|
31
33
|
|
|
32
34
|
### 2. JSON-RPC
|
|
33
35
|
|
|
@@ -96,10 +98,11 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD
|
|
|
96
98
|
| `MetadataTxValidator` | Chain ID, rollup version, protocol contracts hash, VK tree root | 4.18 us |
|
|
97
99
|
| `TimestampTxValidator` | Transaction has not expired (expiration timestamp vs next slot) | 1.56 us |
|
|
98
100
|
| `DoubleSpendTxValidator` | Nullifiers do not already exist in the nullifier tree | 106.08 us |
|
|
99
|
-
| `GasTxValidator` | Max fee per gas meets current block fees (delegates to `MaxFeePerGasValidator`), and fee payer has sufficient FeeJuice balance | 1.02 ms |
|
|
101
|
+
| `GasTxValidator` | Max fee per gas meets current block fees (delegates to `MaxFeePerGasValidator`), and fee payer has sufficient FeeJuice balance (delegates to `FeePayerBalanceValidator`) | 1.02 ms |
|
|
102
|
+
| `FeePayerBalanceValidator` | Fee payer's FeeJuice balance, plus any setup-phase claim, covers the tx's fee limit. Used standalone in gossip, alongside `MaxFeePerGasValidator`; also called internally by `GasTxValidator` | ~1 ms |
|
|
100
103
|
| `MinGasLimitsValidator` | Gas limits are >= the fixed protocol overheads. Applies on every entry point, with no exemptions | 3–10 us |
|
|
101
104
|
| `MaxGasLimitsValidator` | Gas limits are <= AVM max processable L2 gas (optionally clamped further by network admission limits). Exempted on the gas estimation path | 3–10 us |
|
|
102
|
-
| `MaxFeePerGasValidator` | Max fee per gas >= current block gas fees on both dimensions (DA and L2). Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us |
|
|
105
|
+
| `MaxFeePerGasValidator` | Max fee per gas >= current block gas fees on both dimensions (DA and L2). Used standalone in pool migration and in gossip; also called internally by `GasTxValidator` | 3–10 us |
|
|
103
106
|
| `PhasesTxValidator` | Public function calls in setup phase are on the allow list | 10.12–13.12 us |
|
|
104
107
|
| `AllowedSetupCallsMetaValidator` | Checks the precomputed `allowedSetupCalls` flag on `TxMetaData`. Used in pool migration instead of the full `PhasesTxValidator` | — |
|
|
105
108
|
| `BlockHeaderTxValidator` | Transaction's anchor block hash exists in the archive tree | 98.88 us |
|
|
@@ -115,16 +118,17 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD
|
|
|
115
118
|
| Metadata | Stage 1 | Yes | Yes | — | — |
|
|
116
119
|
| Timestamp | Stage 1 | Yes | — | Yes | Yes |
|
|
117
120
|
| DoubleSpend | Stage 1 | Yes | — | Yes | Yes |
|
|
118
|
-
| Gas (fee balance) |
|
|
121
|
+
| Gas (fee balance) | — | Optional* | — | Yes | — |
|
|
119
122
|
| MinGasLimits | Stage 1 | Yes | — | Yes | Yes |
|
|
120
123
|
| MaxGasLimits | Stage 1 | Yes*** | — | Yes | Yes |
|
|
121
|
-
| MaxFeePerGas (standalone) |
|
|
124
|
+
| MaxFeePerGas (standalone) | Stage 1 | — | — | — | Yes |
|
|
125
|
+
| FeePayerBalance (standalone) | Stage 1 | — | — | — | — |
|
|
122
126
|
| Phases | Stage 1 | Yes | — | Yes | — |
|
|
123
127
|
| AllowedSetupCalls | — | — | — | — | Yes |
|
|
124
128
|
| BlockHeader | Stage 1 | Yes | — | Yes | Yes |
|
|
125
129
|
| Proof | Stage 2 | Optional** | Yes | — | — |
|
|
126
130
|
|
|
127
|
-
\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit validation is owned solely by `MinGasLimitsValidator` and `MaxGasLimitsValidator`.
|
|
131
|
+
\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `MaxFeePerGasValidator` as its first step, so fee-per-gas is checked wherever `GasTxValidator` runs. Gossip runs both halves standalone instead, so each can carry its own peer penalty. Pool migration uses `MaxFeePerGasValidator` standalone because it doesn't need the balance check. Declared gas-limit validation is owned solely by `MinGasLimitsValidator` and `MaxGasLimitsValidator`.
|
|
128
132
|
\** Proof verification is skipped for simulations (no verifier provided).
|
|
129
133
|
\*** Only the ceiling is skipped for simulations: gas estimation submits limits above the per-tx maximum, and the wallet clamps the real tx to the admission limit afterward. The floor still applies, since a tx below it can never be mined.
|
|
130
134
|
|
|
@@ -132,6 +136,6 @@ The gas-limit bounds `MaxGasLimitsValidator` enforces here — the per-tx protoc
|
|
|
132
136
|
|
|
133
137
|
## Fee-Per-Gas Rejection Strategy
|
|
134
138
|
|
|
135
|
-
The `MaxFeePerGasValidator` and `InsufficientFeePerGasEvictionRule` reject and evict transactions whose `maxFeesPerGas` falls below the
|
|
139
|
+
The `MaxFeePerGasValidator` and `InsufficientFeePerGasEvictionRule` reject and evict transactions whose `maxFeesPerGas` falls below the fee the next block will charge. Admission falls back to the L1-forward fee while the next block cannot be priced; eviction only acts on the exact next-block fee and skips its sweep otherwise. This is a simple strategy: if a tx can't pay the current fees, it gets rejected on entry and evicted when the node prepares for the next slot.
|
|
136
140
|
|
|
137
141
|
**Caveat**: This may evict transactions that would become valid again if block fees drop. A more nuanced approach would be to define a threshold (e.g., 50%) and only reject/evict when the tx's max fee falls below that fraction of the current fees. The current approach is simpler and ensures the pool doesn't accumulate transactions with low max fees that are unlikely to be mined soon.
|
|
@@ -121,12 +121,13 @@ export class DataTxValidator implements TxValidator<Tx> {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// The tx base requires exactly the trimmed length; any other length makes the tx unprovable.
|
|
125
|
+
const expectedLength = 1 + tx.contractClassLogFields[i].fields.findLastIndex(f => !f.isZero());
|
|
126
|
+
if (logHash.logHash.length !== expectedLength) {
|
|
126
127
|
this.#log.verbose(
|
|
127
128
|
`Rejecting tx ${tx
|
|
128
129
|
.getTxHash()
|
|
129
|
-
.toString()} because of incorrect contract class log length. Expected
|
|
130
|
+
.toString()} because of incorrect contract class log length. Expected ${expectedLength}. Got ${
|
|
130
131
|
logHash.logHash.length
|
|
131
132
|
}.`,
|
|
132
133
|
);
|